fix: use server 파일의 값 export로 인한 게시판 Server Action 오류 수정
공지사항 등록 시 "A 'use server' file can only export async functions, found object"로 500이 발생했다. boards/_actions.ts가 INITIAL_* 상태 상수를 함께 export한 것이 원인이다. 관리자 회원에서 같은 원인을 고친 방식(abb6259)에 맞춰 *FormState 타입과 INITIAL_* 상수를 lib/domain/board-post-form.ts로 옮겼다. 빌드·타입체크는 통과하고 폼을 실제로 제출해야만 드러나는 런타임 전용 제약이라, 근거를 domain 파일 주석에 남겼다. Co-Authored-By: Claude Opus 5
@9086ecbe7a39b99aaa1f7d8f4e7f43654bca4324
--- app/(protected)/(basic)/boards/_actions.ts
+++ app/(protected)/(basic)/boards/_actions.ts
... | ... | @@ -20,8 +20,8 @@ |
| 20 | 20 |
import {
|
| 21 | 21 |
validateBoardPostForm, |
| 22 | 22 |
validateInquiryAnswerForm, |
| 23 |
- type BoardPostFormErrors, |
|
| 24 |
- type InquiryAnswerFormErrors, |
|
| 23 |
+ type BoardPostFormState, |
|
| 24 |
+ type InquiryAnswerFormState, |
|
| 25 | 25 |
} from '@/lib/domain/board-post-form'; |
| 26 | 26 |
import { BOARD_PATHS } from '@/lib/domain/board-post-query';
|
| 27 | 27 |
|
... | ... | @@ -36,29 +36,15 @@ |
| 36 | 36 |
* |
| 37 | 37 |
* 검증은 화면이 아니라 여기서 확정한다(`lib/domain/board-post-form.ts`의 규칙을 호출). |
| 38 | 38 |
* |
| 39 |
+ * `*FormState` 타입과 그 초깃값(`INITIAL_*`)은 이 파일이 아니라 `lib/domain/board-post-form.ts`에 |
|
| 40 |
+ * 있다 — `'use server'` 파일은 async 함수 외의 값을 export할 수 없다(그 쪽 주석에 근거를 적어 두었다). |
|
| 41 |
+ * |
|
| 39 | 42 |
* 첨부파일은 **같은 폼에 실려 온 File을 이 Action이 직접 업로드**한다. 브라우저에서 먼저 업로드해 |
| 40 | 43 |
* id를 받아 두는 방식보다 단순하고(왕복 1회), 업로드 API가 요구하는 관리자 토큰이 서버에만 |
| 41 | 44 |
* 있으므로 자연스럽다 — 토큰은 httpOnly 세션 안에 있어 브라우저가 꺼낼 수 없다. |
| 42 | 45 |
*/ |
| 43 | 46 |
|
| 44 | 47 |
const BOARD_TYPES: readonly BoardType[] = ['notice', 'inquiry', 'faq']; |
| 45 |
- |
|
| 46 |
-export type BoardPostFormState = |
|
| 47 |
- | { status: 'idle' }
|
|
| 48 |
- | { status: 'error'; message?: string; errors?: BoardPostFormErrors }
|
|
| 49 |
- | { status: 'success' };
|
|
| 50 |
- |
|
| 51 |
-export type InquiryAnswerFormState = |
|
| 52 |
- | { status: 'idle' }
|
|
| 53 |
- | { status: 'error'; message?: string; errors?: InquiryAnswerFormErrors }
|
|
| 54 |
- | { status: 'success' };
|
|
| 55 |
- |
|
| 56 |
-export const INITIAL_BOARD_POST_FORM_STATE: BoardPostFormState = {
|
|
| 57 |
- status: 'idle', |
|
| 58 |
-}; |
|
| 59 |
-export const INITIAL_INQUIRY_ANSWER_FORM_STATE: InquiryAnswerFormState = {
|
|
| 60 |
- status: 'idle', |
|
| 61 |
-}; |
|
| 62 | 48 |
|
| 63 | 49 |
const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.'; |
| 64 | 50 |
const UPLOAD_FAILED_MESSAGE = |
--- app/(protected)/(basic)/boards/_components/board-post-form-modal.tsx
+++ app/(protected)/(basic)/boards/_components/board-post-form-modal.tsx
... | ... | @@ -17,14 +17,11 @@ |
| 17 | 17 |
} from '@/lib/domain/board-post'; |
| 18 | 18 |
import {
|
| 19 | 19 |
CONTENT_MAX_LENGTH, |
| 20 |
+ INITIAL_BOARD_POST_FORM_STATE, |
|
| 20 | 21 |
TITLE_MAX_LENGTH, |
| 21 | 22 |
} from '@/lib/domain/board-post-form'; |
| 22 | 23 |
import { BOARD_TYPE_OPTIONS } from '@/lib/domain/board-post-query';
|
| 23 |
-import {
|
|
| 24 |
- INITIAL_BOARD_POST_FORM_STATE, |
|
| 25 |
- createBoardPostAction, |
|
| 26 |
- updateBoardPostAction, |
|
| 27 |
-} from '../_actions'; |
|
| 24 |
+import { createBoardPostAction, updateBoardPostAction } from '../_actions';
|
|
| 28 | 25 |
import { BoardAttachmentField } from './board-attachment-field';
|
| 29 | 26 |
import { BoardContentField } from './board-content-field';
|
| 30 | 27 |
|
--- app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
+++ app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
... | ... | @@ -13,11 +13,11 @@ |
| 13 | 13 |
formatOptionalValue, |
| 14 | 14 |
type BoardPost, |
| 15 | 15 |
} from '@/lib/domain/board-post'; |
| 16 |
-import { ANSWER_MAX_LENGTH } from '@/lib/domain/board-post-form';
|
|
| 17 | 16 |
import {
|
| 17 |
+ ANSWER_MAX_LENGTH, |
|
| 18 | 18 |
INITIAL_INQUIRY_ANSWER_FORM_STATE, |
| 19 |
- saveInquiryAnswerAction, |
|
| 20 |
-} from '../../_actions'; |
|
| 19 |
+} from '@/lib/domain/board-post-form'; |
|
| 20 |
+import { saveInquiryAnswerAction } from '../../_actions';
|
|
| 21 | 21 |
import { BoardAttachmentField } from '../../_components/board-attachment-field';
|
| 22 | 22 |
import { BoardContentField } from '../../_components/board-content-field';
|
| 23 | 23 |
import { FieldError } from '../../_components/board-post-form-modal';
|
--- lib/domain/board-post-form.ts
+++ lib/domain/board-post-form.ts
... | ... | @@ -55,6 +55,33 @@ |
| 55 | 55 |
| { ok: true; values: V }
|
| 56 | 56 |
| { ok: false; errors: E };
|
| 57 | 57 |
|
| 58 |
+/** |
|
| 59 |
+ * 게시판 Server Action의 `useActionState` 결과 상태. 원래는 `_actions.ts`(`'use server'` 파일)에 |
|
| 60 |
+ * 있었으나, Next.js는 **`'use server'` 파일이 async 함수 외의 값을 export하는 것을 런타임에 |
|
| 61 |
+ * 거부한다**("A 'use server' file can only export async functions, found object") —
|
|
| 62 |
+ * `INITIAL_*` 같은 객체 상수를 함께 export하면 그 파일의 Server Action을 호출하는 즉시 500으로 |
|
| 63 |
+ * 깨진다. 빌드·타입체크는 통과하고 실제로 폼을 제출해야만 드러나는 런타임 전용 제약이라 여기로 |
|
| 64 |
+ * 옮겼다 — domain 계층은 `'use server'`가 없어 값 export에 제약이 없다 |
|
| 65 |
+ * (`admin-member-form.ts`·`decoration-item-form.ts`의 동일 패턴 참조). |
|
| 66 |
+ */ |
|
| 67 |
+export type BoardPostFormState = |
|
| 68 |
+ | { status: 'idle' }
|
|
| 69 |
+ | { status: 'error'; message?: string; errors?: BoardPostFormErrors }
|
|
| 70 |
+ | { status: 'success' };
|
|
| 71 |
+ |
|
| 72 |
+export const INITIAL_BOARD_POST_FORM_STATE: BoardPostFormState = {
|
|
| 73 |
+ status: 'idle', |
|
| 74 |
+}; |
|
| 75 |
+ |
|
| 76 |
+export type InquiryAnswerFormState = |
|
| 77 |
+ | { status: 'idle' }
|
|
| 78 |
+ | { status: 'error'; message?: string; errors?: InquiryAnswerFormErrors }
|
|
| 79 |
+ | { status: 'success' };
|
|
| 80 |
+ |
|
| 81 |
+export const INITIAL_INQUIRY_ANSWER_FORM_STATE: InquiryAnswerFormState = {
|
|
| 82 |
+ status: 'idle', |
|
| 83 |
+}; |
|
| 84 |
+ |
|
| 58 | 85 |
/** `YYYY-MM-DD` 형식인지. 달력 위젯 없이 입력될 수 있으므로 형식을 직접 본다. */ |
| 59 | 86 |
function isIsoDate(value: string): boolean {
|
| 60 | 87 |
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
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?