import 'server-only'; import type { AdminMember } from '@/lib/domain/admin-member'; /** * 관리자 회원 등록/수정/삭제의 **mock 저장소** — 백엔드에 해당 API가 없어 임시로 두는 계층이다. * * 백엔드(edupay-backend develop b742bb4)의 관리자 도메인에는 목록 조회 * (`GET /api/v1/mngr/admin/pagination`)와 로그인 조회밖에 없다 — 등록·수정·삭제·단건 조회 * 어느 것도 컨트롤러·서비스·매퍼에 존재하지 않는다(패키지 전체를 읽어 확인). 그래서 시안 * (ADM_ADM_101 ③⑤ / 102_p / 103_p)의 쓰기 흐름은 이 파일이 대신 처리한다. * * **동작 방식은 "덮어쓰기 오버레이"다.** 목록의 원본은 여전히 백엔드이고, 이 저장소는 그 위에 * 얹을 변경분(추가된 행 / 필드 패치 / 삭제된 id)만 들고 있다. Repository가 백엔드 응답을 받은 뒤 * `applyMockOverlay`로 합쳐 화면에 넘긴다. * * **한계를 분명히 해 둔다 — 이건 데모용이지 저장소가 아니다.** * - 서버 프로세스 메모리에만 있다. 재시작하면 사라지고, 인스턴스가 여럿이면 공유되지 않는다. * - 비밀번호는 **보관하지 않는다.** mock이라 검증할 곳이 없고, 평문 비밀번호를 메모리에 들고 * 있는 형태를 코드에 남기고 싶지 않다. 등록/수정 시 형식 검증만 하고 값은 버린다. * * 백엔드 API가 생기면 이 파일을 삭제하고 Repository의 쓰기 함수 본문만 실제 호출로 교체한다 — * 화면·Server Action은 그대로다. */ /** 신규 등록 행의 id 접두사 — 백엔드 `admUserId`와 섞이지 않게 구분한다. */ const MOCK_ID_PREFIX = 'mock-admin-'; type AdminMemberPatch = Partial< Pick< AdminMember, 'phoneNumber' | 'email' | 'roleCode' | 'menuCodes' | 'isLocked' > >; type MockStoreState = { created: AdminMember[]; patches: Map; deletedIds: Set; nextId: number; }; /** * 개발 중 HMR로 모듈이 다시 평가돼도 상태가 초기화되지 않도록 globalThis에 붙인다 — 저장한 * 내용이 코드 수정 때마다 사라지면 mock으로 흐름을 확인하는 의미가 없다. mock 전용 편법이며, * 실제 데이터 계층에는 쓰지 않는다. */ const globalStore = globalThis as typeof globalThis & { __adminMemberMockStore?: MockStoreState; }; function getState(): MockStoreState { globalStore.__adminMemberMockStore ??= { created: [], patches: new Map(), deletedIds: new Set(), nextId: 1, }; return globalStore.__adminMemberMockStore; } /** 오늘 날짜(YYYY-MM-DD) — 신규 등록 행의 생성일. */ function today(): string { return new Date().toISOString().slice(0, 10); } export type CreateAdminMemberInput = { name: string; loginId: string; phoneNumber: string; email: string; roleCode: string; menuCodes: string[]; }; export type UpdateAdminMemberInput = { phoneNumber: string; email: string; roleCode: string; menuCodes: string[]; isLocked: boolean; }; /** * 백엔드 목록에 mock 변경분을 얹는다 — 삭제된 행을 걷어내고, 수정된 행에 패치를 입히고, * 신규 등록 행을 **앞에** 붙인다(기본 정렬이 생성일 최신순이라 방금 만든 행이 맨 위에 온다). * * 정렬·검색·페이징은 이 함수가 하지 않는다 — 합쳐진 전체 목록을 대상으로 Repository가 처리한다. */ export function applyMockOverlay(backendItems: AdminMember[]): AdminMember[] { const { created, patches, deletedIds } = getState(); const merged = [...created, ...backendItems]; return merged .filter((item) => !deletedIds.has(item.id)) .map((item) => { const patch = patches.get(item.id); return patch ? { ...item, ...patch } : item; }); } /** mock으로 등록된 행들의 로그인 ID — 중복 확인이 백엔드 목록과 함께 검사할 수 있게 노출한다. */ export function listMockCreatedLoginIds(): string[] { return getState().created.map((item) => item.loginId); } export function createMockAdminMember( input: CreateAdminMemberInput ): AdminMember { const state = getState(); const id = `${MOCK_ID_PREFIX}${state.nextId++}`; const member: AdminMember = { id, name: input.name, loginId: input.loginId, phoneNumber: input.phoneNumber, email: input.email || null, roleCode: input.roleCode, menuCodes: input.menuCodes, createdAt: today(), isLocked: false, isActive: true, loginFailCount: 0, }; state.created.unshift(member); return member; } /** * 수정 — 신규 등록 행은 원본을 직접 고치고, 백엔드에서 온 행은 패치로 기록해 둔다(원본을 우리가 * 소유하지 않으므로 매 조회마다 덧입힌다). */ export function updateMockAdminMember( id: string, input: UpdateAdminMemberInput ): void { const state = getState(); const patch: AdminMemberPatch = { phoneNumber: input.phoneNumber, email: input.email || null, roleCode: input.roleCode, menuCodes: input.menuCodes, isLocked: input.isLocked, }; const createdIndex = state.created.findIndex((item) => item.id === id); if (createdIndex >= 0) { state.created[createdIndex] = { ...state.created[createdIndex], ...patch }; return; } state.patches.set(id, { ...state.patches.get(id), ...patch }); } export function deleteMockAdminMember(id: string): void { const state = getState(); state.created = state.created.filter((item) => item.id !== id); state.patches.delete(id); // 백엔드에서 오는 행은 매 조회마다 다시 등장하므로 삭제 사실을 따로 기억해야 한다. state.deletedIds.add(id); }