File name
Commit message
Commit date
File name
Commit message
Commit date
09-04
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'),
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' };
}