import 'server-only'; import { getSessionAccessToken } from '@/lib/auth/dal'; import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch'; import type { AdminMember } from '@/lib/domain/admin-member'; import type { AdminMemberQuery, AdminMemberSearchField, } from '@/lib/domain/admin-member-query'; const ADMIN_MEMBER_BASE_PATH = '/api/v1/mngr/admin'; const ADMIN_MEMBER_PAGINATION_PATH = `${ADMIN_MEMBER_BASE_PATH}/pagination`; /** * 검색 대상 → 백엔드 `searchCondition`. 매퍼가 이 값으로 컬럼을 고른다 * (1=USER_NM · 2=LOGIN_ID · 3=USER_TEL_NO). */ const SEARCH_CONDITION_BY_FIELD: Record = { name: '1', loginId: '2', phoneNumber: '3', }; /** * 이름순 정렬에만 쓰는 상한. 백엔드에 정렬 파라미터가 없어 그때만 전체를 받아 여기서 * 정렬하고 자른다 — 검색·페이징은 백엔드가 한다. */ const ADMIN_MEMBER_FETCH_LIMIT = 10_000; function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object'; } function readRequiredString( source: Record, key: string ): string { const value = source[key]; if (typeof value !== 'string' || value.length === 0) { throw new Error(`관리자 회원 응답에 ${key}가 없습니다.`); } return value; } function readOptionalString( source: Record, key: string ): string | null { const value = source[key]; return typeof value === 'string' && value.length > 0 ? value : null; } function parseYesNo(value: unknown): boolean | null { if (value === 'Y') return true; if (value === 'N') return false; return null; } function toAdminMember(raw: unknown): AdminMember { if (!isRecord(raw)) { throw new Error('관리자 회원 응답 항목의 형식이 올바르지 않습니다.'); } const loginFailCnt = raw.loginFailCnt; return { id: readRequiredString(raw, 'userId'), name: readRequiredString(raw, 'userNm'), loginId: readRequiredString(raw, 'loginId'), phoneNumber: readOptionalString(raw, 'userTelNo'), email: readOptionalString(raw, 'userEmlAddr'), roleCode: readOptionalString(raw, 'roleId') ?? '', createdAt: readOptionalString(raw, 'frstRegDtStr'), isLocked: parseYesNo(raw.acctLockYn), isActive: parseYesNo(raw.useYn), loginFailCount: typeof loginFailCnt === 'number' ? loginFailCnt : null, }; } /** 검색 조건을 백엔드 파라미터로 옮긴다. 검색어가 비면 조건 자체를 보내지 않는다. */ function toSearchParams(query: AdminMemberQuery): Record { const keyword = query.keyword.trim(); if (!keyword) { return {}; } return { searchCondition: SEARCH_CONDITION_BY_FIELD[query.searchField], searchKeyword: keyword, }; } async function requestPage( params: Record ): Promise<{ items: AdminMember[]; totalCount: number }> { const accessToken = await getSessionAccessToken(); const result = await backendFetch(ADMIN_MEMBER_PAGINATION_PATH, { method: 'GET', query: params, accessToken: accessToken ?? undefined, cache: 'no-store', }); if (!result.ok) { throw new BackendRequestError(result); } const data = result.data; if (!isRecord(data) || !Array.isArray(data.list)) { throw new Error('관리자 회원 목록 응답의 형식이 올바르지 않습니다.'); } const totalCount = typeof data.totalCount === 'number' ? data.totalCount : data.list.length; return { items: data.list.map(toAdminMember), totalCount }; } function sortItems( items: AdminMember[], query: AdminMemberQuery ): AdminMember[] { if (query.sort !== 'name') { return items; } return [...items].sort((a, b) => a.name.localeCompare(b.name, 'ko')); } export type AdminMemberPage = { items: AdminMember[]; totalCount: number; }; /** * 검색·페이징은 백엔드가 한다 — 매퍼가 `searchCondition`으로 컬럼을 고르고 별도 카운트 * 쿼리로 전체 건수를 센다. * * **이름순만 예외다.** 백엔드에 정렬 파라미터가 없어 그때는 전체를 받아 여기서 정렬하고 * 자른다. 정렬이 붙으면 이 갈래를 지우면 된다. */ export async function fetchAdminMembers( query: AdminMemberQuery ): Promise { const search = toSearchParams(query); if (query.sort === 'name') { const { items } = await requestPage({ pageIndex: 1, recordCountPerPage: ADMIN_MEMBER_FETCH_LIMIT, ...search, }); const sorted = sortItems(items, query); const offset = (query.page - 1) * query.pageSize; return { items: sorted.slice(offset, offset + query.pageSize), totalCount: sorted.length, }; } return requestPage({ pageIndex: query.page, recordCountPerPage: query.pageSize, ...search, }); } export async function findAdminMemberById( id: string ): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${ADMIN_MEMBER_BASE_PATH}/${encodeURIComponent(id)}`, { method: 'GET', accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, } ); if (!result.ok) { throw new BackendRequestError(result); } return result.data === null ? null : toAdminMember(result.data); } export async function isAdminLoginIdTaken(loginId: string): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${ADMIN_MEMBER_BASE_PATH}/duplication/${encodeURIComponent(loginId.trim())}`, { method: 'GET', accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, } ); if (!result.ok) { throw new BackendRequestError(result); } return result.data !== null; } export type CreateAdminMemberInput = { name: string; loginId: string; password: string; phoneNumber: string; email: string; roleCode: string; }; export type UpdateAdminMemberInput = { password: string; phoneNumber: string; email: string; roleCode: string; isLocked: boolean; }; function toYesNo(value: boolean): string { return value ? 'Y' : 'N'; } export async function createAdminMember( input: CreateAdminMemberInput ): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch(ADMIN_MEMBER_BASE_PATH, { method: 'POST', // 이슈: 등록은 form(@ParameterObject), 수정은 JSON(@RequestBody)로 갈려 있다. form: { userNm: input.name, loginId: input.loginId, loginPw: input.password, userEmlAddr: input.email, userTelNo: input.phoneNumber, roleId: input.roleCode, }, accessToken: accessToken ?? undefined, canHaveNullData: true, }); if (!result.ok) { throw new BackendRequestError(result); } } export async function updateAdminMember( id: string, input: UpdateAdminMemberInput ): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${ADMIN_MEMBER_BASE_PATH}/${encodeURIComponent(id)}`, { method: 'PUT', body: { loginPw: input.password, userEmlAddr: input.email, userTelNo: input.phoneNumber, roleId: input.roleCode, acctLockYn: toYesNo(input.isLocked), }, accessToken: accessToken ?? undefined, canHaveNullData: true, } ); if (!result.ok) { throw new BackendRequestError(result); } } export async function deleteAdminMember(id: string): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${ADMIN_MEMBER_BASE_PATH}/${encodeURIComponent(id)}`, { method: 'DELETE', accessToken: accessToken ?? undefined, canHaveNullData: true, } ); if (!result.ok) { throw new BackendRequestError(result); } }