import 'server-only'; import { cache } from 'react'; import { getSessionAccessToken } from '@/lib/auth/dal'; import { BackendRequestError, backendFetch } from '@/lib/http/backend-fetch'; import type { AdminMenu } from '@/lib/domain/admin-menu'; import { toYesNo } from '@/lib/domain/admin-menu'; import type { AdminMenuValues } from '@/lib/domain/admin-menu-form'; /* * GET /api/v1/mngr/menu/pagination 목록 (TB_SYS_MENU) * POST /api/v1/mngr/menu 등록 — 어노테이션 없음 → form * PUT /api/v1/mngr/menu/{menuId} 수정 — @RequestBody(JSON) * DELETE /api/v1/mngr/menu/{menuId} 삭제 * * 이슈(백엔드, 보고함): * 1. `DELETE`의 `menuId`에 `@PathVariable`이 없어 경로값이 바인딩되지 않는다 → 항상 실패한다. * 2. 등록의 menuId 채번이 `MAX(menu_id) WHERE UP_MENU_ID = ?`라 상위별 최대값이다 → 자식이 없는 * 메뉴에 처음 추가하면 이미 존재하는 번호가 나와 PK 충돌로 실패한다. * 3. `GET /{menuId}`는 경로변수 이름이 `userId`로 어긋나 있고 빈 VO로 조회한다 → 쓰지 않는다. * 수정 팝업의 초기값은 목록 응답에서 넘겨 채운다. * 4. `totalCount`가 현재 페이지 행 수다(다른 목록과 같은 PaginationUtil 결함). * 5. 새창여부에 해당하는 컬럼이 없다 — 화면은 기획대로 그리되 저장되지 않는다. */ const ADMIN_MENU_PATH = '/api/v1/mngr/menu'; // 기획이 "페이징 없이 전체 노출"이라 한 장으로 받는다. const FETCH_LIMIT = 10_000; function isRecord(value: unknown): value is Record { return value !== null && typeof value === 'object'; } function readString(source: Record, key: string): string { const value = source[key]; return typeof value === 'string' ? value : ''; } function readNumber(source: Record, key: string): number { const value = source[key]; return typeof value === 'number' ? value : 0; } function toAdminMenu(raw: unknown): AdminMenu { if (!isRecord(raw)) { throw new Error('메뉴 응답 항목의 형식이 올바르지 않습니다.'); } const id = readString(raw, 'menuId'); if (!id) { throw new Error('메뉴 응답에 menuId가 없습니다.'); } const parentId = readString(raw, 'upMenuId'); return { id, parentId: parentId || null, name: readString(raw, 'menuName'), url: readString(raw, 'menuUrl'), description: readString(raw, 'menuExplan'), sortOrder: readNumber(raw, 'sortOrdr'), roleId: readString(raw, 'roleId'), roleName: readString(raw, 'roleNm'), isActive: readString(raw, 'useYn') !== 'N', isTopActive: readString(raw, 'topYn') === 'Y', }; } export const fetchAdminMenus = cache(async (): Promise => { const accessToken = await getSessionAccessToken(); const result = await backendFetch(`${ADMIN_MENU_PATH}/pagination`, { method: 'GET', query: { pageIndex: 1, recordCountPerPage: 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(toAdminMenu); }); function buildPayload(values: AdminMenuValues) { return { menuName: values.name, menuUrl: values.url, menuExplan: values.description, sortOrdr: values.sortOrder, roleId: values.roleId, useYn: toYesNo(values.isActive), topYn: toYesNo(values.isTopActive), }; } export async function createAdminMenu( parentId: string, values: AdminMenuValues ): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch(ADMIN_MENU_PATH, { method: 'POST', form: { ...buildPayload(values), upMenuId: parentId }, accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, }); if (!result.ok) { throw new BackendRequestError(result); } } export async function updateAdminMenu( menuId: string, values: AdminMenuValues ): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${ADMIN_MENU_PATH}/${encodeURIComponent(menuId)}`, { method: 'PUT', body: buildPayload(values), accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, } ); if (!result.ok) { throw new BackendRequestError(result); } } export async function deleteAdminMenu(menuId: string): Promise { const accessToken = await getSessionAccessToken(); const result = await backendFetch( `${ADMIN_MENU_PATH}/${encodeURIComponent(menuId)}`, { method: 'DELETE', accessToken: accessToken ?? undefined, cache: 'no-store', canHaveNullData: true, } ); if (!result.ok) { throw new BackendRequestError(result); } }