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`; // 이슈: 백엔드 `totalCount`가 전체가 아니라 현재 페이지 행 수라 페이징을 맡길 수 없다. // 이름순 정렬 파라미터도 없어 전체를 받아 서버에서 자른다. 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, 'admUserId'), name: readRequiredString(raw, 'admNm'), loginId: readRequiredString(raw, 'loginId'), phoneNumber: readOptionalString(raw, 'admTelNo'), email: readOptionalString(raw, 'admEmlAddr'), roleCode: readOptionalString(raw, 'admRoleCd') ?? '', // 이슈: 백엔드에 관리자별 메뉴 권한이 없어 저장도 조회도 되지 않는다. menuCodes: [], createdAt: readOptionalString(raw, 'frstRegDtStr'), isLocked: parseYesNo(raw.acctLockYn), isActive: parseYesNo(raw.useYn), loginFailCount: typeof loginFailCnt === 'number' ? loginFailCnt : null, }; } async function fetchAllAdminMembers(): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch(ADMIN_MEMBER_PAGINATION_PATH, { method: 'GET', query: { pageIndex: 1, recordCountPerPage: ADMIN_MEMBER_FETCH_LIMIT }, 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('관리자 회원 목록 응답의 형식이 올바르지 않습니다.'); } return data.list.map(toAdminMember); } const SEARCH_VALUE_BY_FIELD: Record< AdminMemberSearchField, (member: AdminMember) => string > = { name: (member) => member.name, loginId: (member) => member.loginId, phoneNumber: (member) => member.phoneNumber ?? '', }; function filterByKeyword( items: AdminMember[], query: AdminMemberQuery ): AdminMember[] { const keyword = query.keyword.trim().toLowerCase(); if (!keyword) { return items; } const readValue = SEARCH_VALUE_BY_FIELD[query.searchField]; return items.filter((item) => readValue(item).toLowerCase().includes(keyword)); } 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; }; export async function fetchAdminMembers( query: AdminMemberQuery ): Promise { const all = await fetchAllAdminMembers(); const matched = sortItems(filterByKeyword(all, query), query); const offset = (query.page - 1) * query.pageSize; return { items: matched.slice(offset, offset + query.pageSize), totalCount: matched.length, }; } 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: { admNm: input.name, loginId: input.loginId, loginPw: input.password, admEmlAddr: input.email, admTelNo: input.phoneNumber, admRoleCd: 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, admEmlAddr: input.email, admTelNo: input.phoneNumber, admRoleCd: 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); } }