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
... | ... | @@ -15,7 +15,7 @@ |
| 15 | 15 |
validateAdminMemberCreate, |
| 16 | 16 |
validateAdminMemberUpdate, |
| 17 | 17 |
type AdminMemberEditableValues, |
| 18 |
- type AdminMemberFormErrors, |
|
| 18 |
+ type AdminMemberFormState, |
|
| 19 | 19 |
} from '@/lib/domain/admin-member-form'; |
| 20 | 20 |
import { ADMIN_MEMBERS_PATH } from '@/lib/domain/admin-member-query';
|
| 21 | 21 |
|
... | ... | @@ -31,16 +31,12 @@ |
| 31 | 31 |
* 실제 저장은 Repository에 맡긴다. 백엔드에 등록/수정/삭제 API가 아직 없어 Repository가 mock |
| 32 | 32 |
* 저장소로 위임하고 있지만, **이 파일은 그 사실을 알지 못한다** — 백엔드 API가 생겨도 이 파일은 |
| 33 | 33 |
* 바뀌지 않는다. |
| 34 |
+ * |
|
| 35 |
+ * `AdminMemberFormState` 타입과 그 초깃값(`INITIAL_ADMIN_MEMBER_FORM_STATE`)은 이 파일이 아니라 |
|
| 36 |
+ * `lib/domain/admin-member-form.ts`에 있다 — Next.js가 `'use server'` 파일에서 함수가 아닌 값을 |
|
| 37 |
+ * export하는 것을 런타임에 거부하기 때문이다(해당 타입 주석 참조). 타입만 이 파일에서 다시 쓰는 |
|
| 38 |
+ * 것은 문제 없다 — 타입은 컴파일 시 지워져 런타임 export로 남지 않는다. |
|
| 34 | 39 |
*/ |
| 35 |
- |
|
| 36 |
-export type AdminMemberFormState = |
|
| 37 |
- | { status: 'idle' }
|
|
| 38 |
- | { status: 'error'; message?: string; errors?: AdminMemberFormErrors }
|
|
| 39 |
- | { status: 'success' };
|
|
| 40 |
- |
|
| 41 |
-export const INITIAL_ADMIN_MEMBER_FORM_STATE: AdminMemberFormState = {
|
|
| 42 |
- status: 'idle', |
|
| 43 |
-}; |
|
| 44 | 40 |
|
| 45 | 41 |
const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.'; |
| 46 | 42 |
const SELF_DELETE_MESSAGE = '현재 로그인한 본인 계정은 삭제할 수 없습니다.'; |
--- app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-create-modal.tsx
... | ... | @@ -9,9 +9,9 @@ |
| 9 | 9 |
import {
|
| 10 | 10 |
ADMIN_LOGIN_ID_HELP_TEXT, |
| 11 | 11 |
ADMIN_PASSWORD_HELP_TEXT, |
| 12 |
+ INITIAL_ADMIN_MEMBER_FORM_STATE, |
|
| 12 | 13 |
} from '@/lib/domain/admin-member-form'; |
| 13 | 14 |
import {
|
| 14 |
- INITIAL_ADMIN_MEMBER_FORM_STATE, |
|
| 15 | 15 |
checkAdminLoginId, |
| 16 | 16 |
createAdminMemberAction, |
| 17 | 17 |
type LoginIdCheckResult, |
--- app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
+++ app/(protected)/(basic)/admins/_components/admin-member-edit-modal.tsx
... | ... | @@ -11,11 +11,11 @@ |
| 11 | 11 |
ADMIN_LOCK_STATUS_OPTIONS, |
| 12 | 12 |
type AdminMember, |
| 13 | 13 |
} from '@/lib/domain/admin-member'; |
| 14 |
-import { ADMIN_PASSWORD_HELP_TEXT } from '@/lib/domain/admin-member-form';
|
|
| 15 | 14 |
import {
|
| 15 |
+ ADMIN_PASSWORD_HELP_TEXT, |
|
| 16 | 16 |
INITIAL_ADMIN_MEMBER_FORM_STATE, |
| 17 |
- updateAdminMemberAction, |
|
| 18 |
-} from '../_actions'; |
|
| 17 |
+} from '@/lib/domain/admin-member-form'; |
|
| 18 |
+import { updateAdminMemberAction } from '../_actions';
|
|
| 19 | 19 |
import {
|
| 20 | 20 |
AdminMemberFormFields, |
| 21 | 21 |
FieldError, |
--- lib/domain/admin-member-form.ts
+++ lib/domain/admin-member-form.ts
... | ... | @@ -64,6 +64,24 @@ |
| 64 | 64 |
| { ok: false; errors: AdminMemberFormErrors };
|
| 65 | 65 |
|
| 66 | 66 |
/** |
| 67 |
+ * 등록/수정 Server Action의 `useActionState` 결과 상태. 원래는 `_actions.ts`(`'use server'` |
|
| 68 |
+ * 파일)에 있었으나, Next.js는 **`'use server'` 파일이 async 함수 외의 값을 export하는 것을 |
|
| 69 |
+ * 런타임에 거부한다**("A 'use server' file can only export async functions, found object" —
|
|
| 70 |
+ * `INITIAL_ADMIN_MEMBER_FORM_STATE`처럼 일반 객체 상수를 함께 export하면 그 파일의 Server |
|
| 71 |
+ * Action을 호출하는 즉시 500으로 깨진다. 빌드/타입체크는 통과하고 실제로 폼을 제출해야만 |
|
| 72 |
+ * 드러나는 런타임 전용 제약이라 여기로 옮겼다 — domain 계층은 `'use server'`가 없어 값 export에 |
|
| 73 |
+ * 제약이 없다(`decoration-item-form.ts`의 동일 패턴 참조). |
|
| 74 |
+ */ |
|
| 75 |
+export type AdminMemberFormState = |
|
| 76 |
+ | { status: 'idle' }
|
|
| 77 |
+ | { status: 'error'; message?: string; errors?: AdminMemberFormErrors }
|
|
| 78 |
+ | { status: 'success' };
|
|
| 79 |
+ |
|
| 80 |
+export const INITIAL_ADMIN_MEMBER_FORM_STATE: AdminMemberFormState = {
|
|
| 81 |
+ status: 'idle', |
|
| 82 |
+}; |
|
| 83 |
+ |
|
| 84 |
+/** |
|
| 67 | 85 |
* 비밀번호 조합 종류 수 — 영문 소문자 / 숫자 / 특수문자 중 몇 종류가 섞였는지 센다. |
| 68 | 86 |
* 시안이 "2종류 이상"만 요구하므로 대문자는 별도 종류로 세지 않는다(있어도 무방하다). |
| 69 | 87 |
*/ |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?