File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
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<string, unknown> {
return value !== null && typeof value === 'object';
}
function readString(source: Record<string, unknown>, key: string): string {
const value = source[key];
return typeof value === 'string' ? value : '';
}
function readNumber(source: Record<string, unknown>, 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<AdminMenu[]> => {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<unknown>(`${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<void> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<null>(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<void> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<null>(
`${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<void> {
const accessToken = await getSessionAccessToken();
const result = await backendFetch<null>(
`${ADMIN_MENU_PATH}/${encodeURIComponent(menuId)}`,
{
method: 'DELETE',
accessToken: accessToken ?? undefined,
cache: 'no-store',
canHaveNullData: true,
}
);
if (!result.ok) {
throw new BackendRequestError(result);
}
}