임동욱 임동욱 08-13
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
+++ app/(protected)/(basic)/boards/_actions.ts
@@ -20,8 +20,8 @@
 import {
   validateBoardPostForm,
   validateInquiryAnswerForm,
-  type BoardPostFormErrors,
-  type InquiryAnswerFormErrors,
+  type BoardPostFormState,
+  type InquiryAnswerFormState,
 } from '@/lib/domain/board-post-form';
 import { BOARD_PATHS } from '@/lib/domain/board-post-query';
 
@@ -36,29 +36,15 @@
  *
  * 검증은 화면이 아니라 여기서 확정한다(`lib/domain/board-post-form.ts`의 규칙을 호출).
  *
+ * `*FormState` 타입과 그 초깃값(`INITIAL_*`)은 이 파일이 아니라 `lib/domain/board-post-form.ts`에
+ * 있다 — `'use server'` 파일은 async 함수 외의 값을 export할 수 없다(그 쪽 주석에 근거를 적어 두었다).
+ *
  * 첨부파일은 **같은 폼에 실려 온 File을 이 Action이 직접 업로드**한다. 브라우저에서 먼저 업로드해
  * id를 받아 두는 방식보다 단순하고(왕복 1회), 업로드 API가 요구하는 관리자 토큰이 서버에만
  * 있으므로 자연스럽다 — 토큰은 httpOnly 세션 안에 있어 브라우저가 꺼낼 수 없다.
  */
 
 const BOARD_TYPES: readonly BoardType[] = ['notice', 'inquiry', 'faq'];
-
-export type BoardPostFormState =
-  | { status: 'idle' }
-  | { status: 'error'; message?: string; errors?: BoardPostFormErrors }
-  | { status: 'success' };
-
-export type InquiryAnswerFormState =
-  | { status: 'idle' }
-  | { status: 'error'; message?: string; errors?: InquiryAnswerFormErrors }
-  | { status: 'success' };
-
-export const INITIAL_BOARD_POST_FORM_STATE: BoardPostFormState = {
-  status: 'idle',
-};
-export const INITIAL_INQUIRY_ANSWER_FORM_STATE: InquiryAnswerFormState = {
-  status: 'idle',
-};
 
 const INVALID_REQUEST_MESSAGE = '요청이 올바르지 않습니다.';
 const UPLOAD_FAILED_MESSAGE =
app/(protected)/(basic)/boards/_components/board-post-form-modal.tsx
--- app/(protected)/(basic)/boards/_components/board-post-form-modal.tsx
+++ app/(protected)/(basic)/boards/_components/board-post-form-modal.tsx
@@ -17,14 +17,11 @@
 } from '@/lib/domain/board-post';
 import {
   CONTENT_MAX_LENGTH,
+  INITIAL_BOARD_POST_FORM_STATE,
   TITLE_MAX_LENGTH,
 } from '@/lib/domain/board-post-form';
 import { BOARD_TYPE_OPTIONS } from '@/lib/domain/board-post-query';
-import {
-  INITIAL_BOARD_POST_FORM_STATE,
-  createBoardPostAction,
-  updateBoardPostAction,
-} from '../_actions';
+import { createBoardPostAction, updateBoardPostAction } from '../_actions';
 import { BoardAttachmentField } from './board-attachment-field';
 import { BoardContentField } from './board-content-field';
 
app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
--- app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
+++ app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
@@ -13,11 +13,11 @@
   formatOptionalValue,
   type BoardPost,
 } from '@/lib/domain/board-post';
-import { ANSWER_MAX_LENGTH } from '@/lib/domain/board-post-form';
 import {
+  ANSWER_MAX_LENGTH,
   INITIAL_INQUIRY_ANSWER_FORM_STATE,
-  saveInquiryAnswerAction,
-} from '../../_actions';
+} from '@/lib/domain/board-post-form';
+import { saveInquiryAnswerAction } from '../../_actions';
 import { BoardAttachmentField } from '../../_components/board-attachment-field';
 import { BoardContentField } from '../../_components/board-content-field';
 import { FieldError } from '../../_components/board-post-form-modal';
lib/domain/board-post-form.ts
--- lib/domain/board-post-form.ts
+++ lib/domain/board-post-form.ts
@@ -55,6 +55,33 @@
   | { ok: true; values: V }
   | { ok: false; errors: E };
 
+/**
+ * 게시판 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_*` 같은 객체 상수를 함께 export하면 그 파일의 Server Action을 호출하는 즉시 500으로
+ * 깨진다. 빌드·타입체크는 통과하고 실제로 폼을 제출해야만 드러나는 런타임 전용 제약이라 여기로
+ * 옮겼다 — domain 계층은 `'use server'`가 없어 값 export에 제약이 없다
+ * (`admin-member-form.ts`·`decoration-item-form.ts`의 동일 패턴 참조).
+ */
+export type BoardPostFormState =
+  | { status: 'idle' }
+  | { status: 'error'; message?: string; errors?: BoardPostFormErrors }
+  | { status: 'success' };
+
+export const INITIAL_BOARD_POST_FORM_STATE: BoardPostFormState = {
+  status: 'idle',
+};
+
+export type InquiryAnswerFormState =
+  | { status: 'idle' }
+  | { status: 'error'; message?: string; errors?: InquiryAnswerFormErrors }
+  | { status: 'success' };
+
+export const INITIAL_INQUIRY_ANSWER_FORM_STATE: InquiryAnswerFormState = {
+  status: 'idle',
+};
+
 /** `YYYY-MM-DD` 형식인지. 달력 위젯 없이 입력될 수 있으므로 형식을 직접 본다. */
 function isIsoDate(value: string): boolean {
   if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
Add a comment
List