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