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 {
createCodeDetail,
createCodeGroup,
deleteCodeDetail,
deleteCodeGroup,
updateCodeDetail,
updateCodeGroup,
} from '@/lib/data/repositories/common-code-repository';
import {
validateCommonCodeDetail,
validateCommonCodeGroup,
type CommonCodeFormState,
} from '@/lib/domain/common-code-form';
import { COMMON_CODES_PATH } from '@/lib/domain/common-code-query';
/**
* 코드관리 등록/수정/삭제 Server Action(기획 SYS_COD_001).
*
* **모든 Action이 `verifySession()`으로 시작한다** — Server Action은 UI를 거치지 않고 직접
* POST될 수 있어 이 확인이 유일한 최종 방어선이다.
*
* 검증은 화면이 아니라 여기서 확정한다(`lib/domain/common-code-form.ts`의 규칙을 호출) —
* 화면의 required 속성과 읽기 전용 표시는 편의일 뿐 신뢰 경계가 아니다.
*
* **수정 대상(코드ID)은 읽기 전용 입력이 아니라 hidden 필드에서 읽는다** — 읽기 전용 칸은
* 위조될 수 있고, 위조되더라도 대상이 바뀌면 안 되기 때문이다.
*/
const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.';
const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.';
function readString(formData: FormData, key: string): string {
const value = formData.get(key);
return typeof value === 'string' ? value : '';
}
/**
* 폼 숫자 입력을 파싱한다. `Number('')`이 조용히 `0`이 되는 함정을 피하려고 빈 문자열은
* 명시적으로 `NaN`으로 둔다 — 그래야 검증이 "값이 비었음"을 잡아낸다.
*/
function parseFormNumber(formData: FormData, key: string): number {
const trimmed = readString(formData, key).trim();
return trimmed === '' ? NaN : Number(trimmed);
}
/* ── 공통코드 ─────────────────────────────────────────────────────────────── */
/** 기획 [공통코드 등록] 팝업. */
export async function createCodeGroupAction(
_prevState: CommonCodeFormState,
formData: FormData
): Promise<CommonCodeFormState> {
await verifySession();
const validation = validateCommonCodeGroup({
comCd: readString(formData, 'comCd'),
name: readString(formData, 'name'),
description: readString(formData, 'description'),
});
if (!validation.ok) {
return { status: 'error', errors: validation.errors };
}
try {
await createCodeGroup(validation.values);
} catch (error) {
// 이슈: 세션이 밀렸을 때 backendFetch가 던지는 redirect를 이 catch가 삼키면 안 된다.
unstable_rethrow(error);
return { status: 'error', message: SAVE_FAILED_MESSAGE };
}
revalidatePath(COMMON_CODES_PATH);
return { status: 'success' };
}
/** 기획 [공통코드 수정] 팝업 — 코드ID는 대상을 가리키는 값이라 바뀌지 않는다. */
export async function updateCodeGroupAction(
_prevState: CommonCodeFormState,
formData: FormData
): Promise<CommonCodeFormState> {
await verifySession();
const targetComCd = readString(formData, 'targetComCd').trim();
if (!targetComCd) {
return { status: 'error', message: INVALID_REQUEST_MESSAGE };
}
const validation = validateCommonCodeGroup({
comCd: targetComCd,
name: readString(formData, 'name'),
description: readString(formData, 'description'),
});
if (!validation.ok) {
return { status: 'error', errors: validation.errors };
}
try {
await updateCodeGroup(targetComCd, validation.values);
} catch (error) {
unstable_rethrow(error);
return { status: 'error', message: SAVE_FAILED_MESSAGE };
}
revalidatePath(COMMON_CODES_PATH);
return { status: 'success' };
}
/**
* 기획 ④ 삭제 — 확인 얼럿은 화면이 띄우고 여기서는 인증과 입력만 확인한다.
*
* 폼 제출이 아니라 얼럿의 [삭제] 클릭에 반응하는 단발 호출이라 `useActionState`의
* (prevState, formData) 규약 대신 식별자를 직접 받는다.
*/
export async function deleteCodeGroupAction(
comCd: string
): Promise<CommonCodeFormState> {
await verifySession();
if (!comCd) {
return { status: 'error', message: INVALID_REQUEST_MESSAGE };
}
try {
await deleteCodeGroup(comCd);
} catch (error) {
unstable_rethrow(error);
return { status: 'error', message: DELETE_FAILED_MESSAGE };
}
revalidatePath(COMMON_CODES_PATH);
return { status: 'success' };
}
/* ── 상세코드 ─────────────────────────────────────────────────────────────── */
/** 기획 [상세코드 등록] 팝업 — 코드ID는 좌측에서 고른 공통코드다. */
export async function createCodeDetailAction(
_prevState: CommonCodeFormState,
formData: FormData
): Promise<CommonCodeFormState> {
await verifySession();
const validation = validateCommonCodeDetail({
comCd: readString(formData, 'comCd'),
comDtlCd: readString(formData, 'comDtlCd'),
name: readString(formData, 'name'),
description: readString(formData, 'description'),
sortSeq: parseFormNumber(formData, 'sortSeq'),
});
if (!validation.ok) {
return { status: 'error', errors: validation.errors };
}
try {
await createCodeDetail(validation.values);
} catch (error) {
unstable_rethrow(error);
return { status: 'error', message: SAVE_FAILED_MESSAGE };
}
revalidatePath(COMMON_CODES_PATH);
return { status: 'success' };
}
/** 기획 [상세코드 수정] 팝업 — 코드ID·코드는 대상을 가리키는 값이라 바뀌지 않는다. */
export async function updateCodeDetailAction(
_prevState: CommonCodeFormState,
formData: FormData
): Promise<CommonCodeFormState> {
await verifySession();
const targetComCd = readString(formData, 'targetComCd').trim();
const targetComDtlCd = readString(formData, 'targetComDtlCd').trim();
if (!targetComCd || !targetComDtlCd) {
return { status: 'error', message: INVALID_REQUEST_MESSAGE };
}
const validation = validateCommonCodeDetail({
comCd: targetComCd,
comDtlCd: readString(formData, 'comDtlCd'),
name: readString(formData, 'name'),
description: readString(formData, 'description'),
sortSeq: parseFormNumber(formData, 'sortSeq'),
});
if (!validation.ok) {
return { status: 'error', errors: validation.errors };
}
try {
await updateCodeDetail(targetComCd, targetComDtlCd, validation.values);
} catch (error) {
unstable_rethrow(error);
return { status: 'error', message: SAVE_FAILED_MESSAGE };
}
revalidatePath(COMMON_CODES_PATH);
return { status: 'success' };
}
export async function deleteCodeDetailAction(
comCd: string,
comDtlCd: string
): Promise<CommonCodeFormState> {
await verifySession();
if (!comCd || !comDtlCd) {
return { status: 'error', message: INVALID_REQUEST_MESSAGE };
}
try {
await deleteCodeDetail(comCd, comDtlCd);
} catch (error) {
unstable_rethrow(error);
return { status: 'error', message: DELETE_FAILED_MESSAGE };
}
revalidatePath(COMMON_CODES_PATH);
return { status: 'success' };
}