임동욱 임동욱 08-11
fix: use server 파일의 값 export로 인한 관리자 회원 Server Action 오류 수정
admins/_actions.ts('use server')가 AdminMemberFormState 타입의 초깃값
INITIAL_ADMIN_MEMBER_FORM_STATE를 값으로 export하고 있어, 관리자 등록/수정/
삭제/ID 중복확인 Server Action을 호출할 때마다 "A 'use server' file can
only export async functions, found object" 런타임 에러로 500이 발생했다.
빌드·타입체크는 통과하고 실제로 폼을 제출해야만 드러나는 문제였다.

decoration-items가 이미 적용해 둔 동일 패턴을 따라 AdminMemberFormState
타입과 INITIAL_ADMIN_MEMBER_FORM_STATE 상수를 'use server'가 없는
lib/domain/admin-member-form.ts로 옮기고, _actions.ts는 타입만 다시
import하도록 정리했다. 두 소비처(admin-member-create-modal.tsx,
admin-member-edit-modal.tsx)의 import 경로도 함께 갱신했다.

Co-Authored-By: Claude Opus 5 
@abb6259dae12b4a46a5a1228c489f708ce631d9c
app/(protected)/(basic)/admins/_actions.ts
--- app/(protected)/(basic)/admins/_actions.ts
+++ app/(protected)/(basic)/admins/_actions.ts
@@ -15,7 +15,7 @@
   validateAdminMemberCreate,
   validateAdminMemberUpdate,
   type AdminMemberEditableValues,
-  type AdminMemberFormErrors,
+  type AdminMemberFormState,
 } from '@/lib/domain/admin-member-form';
 import { ADMIN_MEMBERS_PATH } from '@/lib/domain/admin-member-query';
 
@@ -31,16 +31,12 @@
  * 실제 저장은 Repository에 맡긴다. 백엔드에 등록/수정/삭제 API가 아직 없어 Repository가 mock
  * 저장소로 위임하고 있지만, **이 파일은 그 사실을 알지 못한다** — 백엔드 API가 생겨도 이 파일은
  * 바뀌지 않는다.
+ *
+ * `AdminMemberFormState` 타입과 그 초깃값(`INITIAL_ADMIN_MEMBER_FORM_STATE`)은 이 파일이 아니라
+ * `lib/domain/admin-member-form.ts`에 있다 — Next.js가 `'use server'` 파일에서 함수가 아닌 값을
+ * export하는 것을 런타임에 거부하기 때문이다(해당 타입 주석 참조). 타입만 이 파일에서 다시 쓰는
+ * 것은 문제 없다 — 타입은 컴파일 시 지워져 런타임 export로 남지 않는다.
  */
-
-export type AdminMemberFormState =
-  | { status: 'idle' }
-  | { status: 'error'; message?: string; errors?: AdminMemberFormErrors }
-  | { status: 'success' };
-
-export const INITIAL_ADMIN_MEMBER_FORM_STATE: AdminMemberFormState = {
-  status: 'idle',
-};
 
 const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
 const SELF_DELETE_MESSAGE = '현재 로그인한 본인 계정은 삭제할 수 없습니다.';
app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
--- app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
@@ -9,9 +9,9 @@
 import {
   ADMIN_LOGIN_ID_HELP_TEXT,
   ADMIN_PASSWORD_HELP_TEXT,
+  INITIAL_ADMIN_MEMBER_FORM_STATE,
 } from '@/lib/domain/admin-member-form';
 import {
-  INITIAL_ADMIN_MEMBER_FORM_STATE,
   checkAdminLoginId,
   createAdminMemberAction,
   type LoginIdCheckResult,
app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
--- app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
@@ -11,11 +11,11 @@
   ADMIN_LOCK_STATUS_OPTIONS,
   type AdminMember,
 } from '@/lib/domain/admin-member';
-import { ADMIN_PASSWORD_HELP_TEXT } from '@/lib/domain/admin-member-form';
 import {
+  ADMIN_PASSWORD_HELP_TEXT,
   INITIAL_ADMIN_MEMBER_FORM_STATE,
-  updateAdminMemberAction,
-} from '../_actions';
+} from '@/lib/domain/admin-member-form';
+import { updateAdminMemberAction } from '../_actions';
 import {
   AdminMemberFormFields,
   FieldError,
lib/domain/admin-member-form.ts
--- lib/domain/admin-member-form.ts
+++ lib/domain/admin-member-form.ts
@@ -64,6 +64,24 @@
   | { ok: false; errors: AdminMemberFormErrors };
 
 /**
+ * 등록/수정 Server Action의 `useActionState` 결과 상태. 원래는 `_actions.ts`(`'use server'`
+ * 파일)에 있었으나, Next.js는 **`'use server'` 파일이 async 함수 외의 값을 export하는 것을
+ * 런타임에 거부한다**("A 'use server' file can only export async functions, found object" —
+ * `INITIAL_ADMIN_MEMBER_FORM_STATE`처럼 일반 객체 상수를 함께 export하면 그 파일의 Server
+ * Action을 호출하는 즉시 500으로 깨진다. 빌드/타입체크는 통과하고 실제로 폼을 제출해야만
+ * 드러나는 런타임 전용 제약이라 여기로 옮겼다 — domain 계층은 `'use server'`가 없어 값 export에
+ * 제약이 없다(`decoration-item-form.ts`의 동일 패턴 참조).
+ */
+export type AdminMemberFormState =
+  | { status: 'idle' }
+  | { status: 'error'; message?: string; errors?: AdminMemberFormErrors }
+  | { status: 'success' };
+
+export const INITIAL_ADMIN_MEMBER_FORM_STATE: AdminMemberFormState = {
+  status: 'idle',
+};
+
+/**
  * 비밀번호 조합 종류 수 — 영문 소문자 / 숫자 / 특수문자 중 몇 종류가 섞였는지 센다.
  * 시안이 "2종류 이상"만 요구하므로 대문자는 별도 종류로 세지 않는다(있어도 무방하다).
  */
Add a comment
List