임동욱 임동욱 08-20
merge: 콘텐츠 저장 오류 진단
@109971068326bfded9b9266d8d3c54e473a43b91
app/(protected)/(basic)/contents/_actions.ts
--- app/(protected)/(basic)/contents/_actions.ts
+++ app/(protected)/(basic)/contents/_actions.ts
@@ -3,6 +3,7 @@
 import { revalidatePath } from 'next/cache';
 import { redirect } from 'next/navigation';
 import { verifySession } from '@/lib/auth/dal';
+import { BackendRequestError } from '@/lib/http/backend-fetch';
 import { fetchSchoolGradeCodes } from '@/lib/data/repositories/common-code-repository';
 import { uploadAttachment } from '@/lib/data/repositories/file-repository';
 import {
@@ -20,6 +21,7 @@
 } from '@/lib/data/repositories/content-quiz-repository';
 import { CONTENT_TYPE_CODE, type ContentKind } from '@/lib/domain/content';
 import {
+  ContentInputError,
   validateComicForm,
   validateQuizForm,
   validateShortsForm,
@@ -29,6 +31,7 @@
 import {
   FILE_MODULE,
   MAX_ATTACHMENT_BYTES,
+  allowedExtensions,
   isAllowedFileExtension,
   type FileModule,
 } from '@/lib/domain/file-module';
@@ -90,11 +93,11 @@
     return retainedId;
   }
   if (file.size > MAX_ATTACHMENT_BYTES) {
-    throw new Error(OVERSIZE_MESSAGE);
+    throw new ContentInputError(OVERSIZE_MESSAGE);
   }
   if (!isAllowedFileExtension(module, file.name)) {
-    throw new Error(
-      `${module.description}에는 ${module.extensions.join(', ')} 파일만 올릴 수 있습니다.`
+    throw new ContentInputError(
+      `${module.description}에는 ${allowedExtensions(module).join(', ')} 파일만 올릴 수 있습니다.`
     );
   }
   return uploadAttachment(file, module.id);
@@ -111,14 +114,22 @@
   try {
     return await run();
   } catch (error) {
+    // 진짜 원인은 여기에만 남는다 — 화면에는 사람이 고칠 수 있는 사유만 나간다.
+    console.error('[contents] 저장 실패', error);
     return failure(error, SAVE_FAILED_MESSAGE);
   }
 }
 
+/**
+ * 사유를 화면에 낼지 정한다. 백엔드가 돌려준 메시지와 입력 오류만 그대로 보이고, 나머지는
+ * 개발자용 문구(예: TypeError)라 일반 문구로 바꾼다 — 그대로 내면 사용자가 할 수 있는 일이 없다.
+ */
 function failure(error: unknown, fallback: string): ContentFormState {
+  const showable =
+    error instanceof ContentInputError || error instanceof BackendRequestError;
   return {
     status: 'error',
-    message: error instanceof Error && error.message ? error.message : fallback,
+    message: showable && error.message ? error.message : fallback,
   };
 }
 
lib/domain/content-form.ts
--- lib/domain/content-form.ts
+++ lib/domain/content-form.ts
@@ -32,6 +32,17 @@
 
 export const INITIAL_CONTENT_FORM_STATE: ContentFormState = { status: 'idle' };
 
+/**
+ * 사용자에게 그대로 보여도 되는 오류 — 용량·확장자처럼 사람이 고칠 수 있는 사유다.
+ * 그 밖의 예외는 개발자용 문구라 화면에 내지 않고 서버 로그로만 남긴다.
+ */
+export class ContentInputError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = 'ContentInputError';
+  }
+}
+
 export type GradeSelection = {
   /** `COM_SCHUL_CD` 코드값. */
   schoolCode: string;
Add a comment
List