'use server'; import { revalidatePath } from 'next/cache'; import { verifySession } from '@/lib/auth/dal'; import { BackendRequestError } from '@/lib/http/backend-fetch'; import { uploadAttachment } from '@/lib/data/repositories/file-repository'; import { createSystemMenu, deleteSystemMenu, updateSystemMenu, } from '@/lib/data/repositories/system-menu-repository'; import { FILE_MODULE } from '@/lib/domain/file-module'; import { validateSystemMenu, type SystemMenuFormState, } from '@/lib/domain/system-menu-form'; const SYSTEM_MENUS_PATH = '/system/menus'; const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.'; /** 백엔드가 돌려준 사유는 그대로 보여준다 — "최상위 메뉴가 없습니다"처럼 사용자가 알아야 할 * 내용이고, 감추면 화면만 보고는 원인을 알 수 없다. */ function failureMessage(error: unknown, fallback: string): string { return error instanceof BackendRequestError ? error.message : fallback; } const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.'; const UPLOAD_FAILED_MESSAGE = '아이콘을 올리지 못했습니다. 잠시 후 다시 시도해 주세요.'; function readString(formData: FormData, key: string): string { const value = formData.get(key); return typeof value === 'string' ? value : ''; } function parseFormNumber(formData: FormData, key: string): number { const trimmed = readString(formData, key).trim(); return trimmed === '' ? NaN : Number(trimmed); } /** * 아이콘 칸의 결과 파일 아이디. 새 파일을 고르면 올려서 그 아이디를, 아니면 기존 아이디를 * 쓴다. 비운 경우 빈 문자열을 보내 컬럼을 지운다. */ async function resolveIconId( formData: FormData, fileKey: string, currentKey: string ): Promise { const picked = formData.get(fileKey); if (picked instanceof File && picked.size > 0) { return uploadAttachment(picked, FILE_MODULE.menuIcon.id); } return readString(formData, currentKey); } async function readValues(formData: FormData) { return { id: readString(formData, 'id'), parentId: readString(formData, 'parentId'), parentIdPath: readString(formData, 'parentIdPath'), name: readString(formData, 'name'), sortOrder: parseFormNumber(formData, 'sortOrder'), roleId: readString(formData, 'roleId'), isTopActive: readString(formData, 'isTopActive') === 'Y', url: readString(formData, 'url'), description: readString(formData, 'description'), largeIconId: await resolveIconId(formData, 'largeIcon', 'largeIconId'), smallIconId: await resolveIconId(formData, 'smallIcon', 'smallIconId'), isActive: readString(formData, 'isActive') === 'Y', }; } export async function saveSystemMenuAction( _prevState: SystemMenuFormState, formData: FormData ): Promise { await verifySession(); let values; try { values = await readValues(formData); } catch { return { status: 'error', message: UPLOAD_FAILED_MESSAGE }; } const validation = validateSystemMenu(values); if (!validation.ok) { return { status: 'error', errors: validation.errors }; } try { if (validation.values.id) { await updateSystemMenu(validation.values); } else { await createSystemMenu(validation.values); } } catch (error) { return { status: 'error', message: failureMessage(error, SAVE_FAILED_MESSAGE) }; } revalidatePath(SYSTEM_MENUS_PATH); return { status: 'success' }; } export async function deleteSystemMenuAction( menuId: string ): Promise<{ status: 'success' } | { status: 'error'; message: string }> { await verifySession(); try { await deleteSystemMenu(menuId); } catch (error) { return { status: 'error', message: failureMessage(error, DELETE_FAILED_MESSAGE) }; } revalidatePath(SYSTEM_MENUS_PATH); return { status: 'success' }; }