File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use server';
import { unstable_rethrow } from 'next/navigation';
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<string> {
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<SystemMenuFormState> {
await verifySession();
let values;
try {
values = await readValues(formData);
} catch (error) {
// 이슈: 세션이 밀렸을 때 backendFetch가 던지는 redirect를 이 catch가 삼키면 안 된다.
unstable_rethrow(error);
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) {
unstable_rethrow(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) {
unstable_rethrow(error);
return { status: 'error', message: failureMessage(error, DELETE_FAILED_MESSAGE) };
}
revalidatePath(SYSTEM_MENUS_PATH);
return { status: 'success' };
}