임동욱 임동욱 08-20
merge: 게시판 첨부 저장소
@dda5cb6dc136d410419a569209c735652b77a9b7
app/(protected)/(basic)/boards/_actions.ts
--- app/(protected)/(basic)/boards/_actions.ts
+++ app/(protected)/(basic)/boards/_actions.ts
@@ -25,8 +25,10 @@
 } from '@/lib/domain/board-post-form';
 import { BOARD_PATHS } from '@/lib/domain/board-post-query';
 import {
-  BOARD_ATTACHMENT_MODULE_ID,
+  FILE_MODULE,
   MAX_ATTACHMENT_BYTES,
+  allowedExtensions,
+  isAllowedFileExtension,
 } from '@/lib/domain/file-module';
 
 /**
@@ -117,8 +119,13 @@
   if (file.size > MAX_ATTACHMENT_BYTES) {
     throw new Error(OVERSIZE_MESSAGE);
   }
+  if (!isAllowedFileExtension(FILE_MODULE.board, file.name)) {
+    throw new Error(
+      `${allowedExtensions(FILE_MODULE.board).join(', ')} 파일만 올릴 수 있습니다.`
+    );
+  }
 
-  return uploadAttachment(file, BOARD_ATTACHMENT_MODULE_ID);
+  return uploadAttachment(file, FILE_MODULE.board.id);
 }
 
 /** 등록·수정이 공유하는 입력 읽기. */
app/(protected)/(basic)/boards/_components/board-attachment-field.tsx
--- app/(protected)/(basic)/boards/_components/board-attachment-field.tsx
+++ app/(protected)/(basic)/boards/_components/board-attachment-field.tsx
@@ -4,7 +4,13 @@
 import { FoxHelperText } from '@fox/core/components/fox-helper-text';
 import { Field } from '@/components/ui/field';
 import styles from './board-attachment-field.module.scss';
-import { MAX_ATTACHMENT_BYTES } from '@/lib/domain/file-module';
+import {
+  FILE_MODULE,
+  MAX_ATTACHMENT_BYTES,
+  allowedExtensions,
+  fileModuleAccept,
+  isAllowedFileExtension,
+} from '@/lib/domain/file-module';
 
 interface BoardAttachmentFieldProps {
   label?: string;
@@ -30,6 +36,8 @@
 const OVERSIZE_MESSAGE = `첨부파일은 ${Math.floor(
   MAX_ATTACHMENT_BYTES / (1024 * 1024)
 )}MB 이하만 올릴 수 있습니다.`;
+
+const EXTENSION_MESSAGE = `${allowedExtensions(FILE_MODULE.board).join(', ')} 파일만 올릴 수 있습니다.`;
 
 export function BoardAttachmentField({
   label = '첨부파일',
@@ -60,14 +68,24 @@
       <input
         type="file"
         name="attachment"
+        accept={fileModuleAccept(FILE_MODULE.board)}
         className={styles.input}
         onChange={(event) => {
           const file = event.target.files?.[0];
-          if (file && file.size > MAX_ATTACHMENT_BYTES) {
+          if (!file) {
+            setError(undefined);
+            return;
+          }
+          if (file.size > MAX_ATTACHMENT_BYTES) {
             event.target.value = '';
             setError(OVERSIZE_MESSAGE);
             return;
           }
+          if (!isAllowedFileExtension(FILE_MODULE.board, file.name)) {
+            event.target.value = '';
+            setError(EXTENSION_MESSAGE);
+            return;
+          }
           setError(undefined);
         }}
       />
lib/domain/file-module.ts
--- lib/domain/file-module.ts
+++ lib/domain/file-module.ts
@@ -17,6 +17,20 @@
   extensions: readonly string[];
 };
 
+/**
+ * 업로드 전체에 걸린 확장자 화이트리스트 — 백엔드 `Globals.fileUpload.Extensions`.
+ * `EgovMultipartResolver`가 저장소 설정보다 **먼저** 이 목록으로 거른다.
+ *
+ * 이슈: 저장소 설정이 허용해도 여기 없으면 못 올린다. `MODULE_VOD_CONTENT`는 mov·avi·mkv·
+ *   webm·wmv를 허용하지만 실제로 통과하는 것은 mp4뿐이고, svg도 마찬가지로 막힌다.
+ *   백엔드에 이 목록 확장을 요청하면 그때 여기만 늘리면 된다.
+ */
+export const GLOBAL_UPLOAD_EXTENSIONS: readonly string[] = [
+  'gif', 'jpg', 'jpeg', 'png',
+  'xls', 'xlsx', 'ppt', 'pptx', 'doc', 'docx', 'hwp', 'pdf',
+  'mp4', 'mp3', 'txt', 'html', 'htm',
+];
+
 const IMAGE = ['jpg', 'png', 'gif'] as const;
 
 export const FILE_MODULE = {
@@ -64,25 +78,16 @@
     description: '회원 프로필 저장소',
     extensions: IMAGE,
   },
+  /** 게시판(공지사항·FAQ·1:1문의) 첨부. */
+  board: {
+    id: 'MODULE_BBS',
+    description: '게시글 스토리지',
+    // TODO: 이 저장소만의 허용 확장자를 받지 못했다. 전역 화이트리스트를 그대로 두어 화면이
+    // 먼저 막지 않게 한다 — 저장소 목록이 더 좁으면 백엔드가 사유를 돌려준다.
+    extensions: GLOBAL_UPLOAD_EXTENSIONS,
+  },
 } as const satisfies Record<string, FileModule>;
 
-// TODO: 게시판(공지사항·FAQ·1:1문의) 첨부용 저장소가 목록에 없다. 지금은 이 값이 설정에 없어
-// 기본 저장소로 폴백해 저장된다 — 백엔드에 모듈 추가를 요청하고 그 id로 교체한다.
-export const BOARD_ATTACHMENT_MODULE_ID = 'MODULE_BBS';
-
-/**
- * 업로드 전체에 걸린 확장자 화이트리스트 — 백엔드 `Globals.fileUpload.Extensions`.
- * `EgovMultipartResolver`가 저장소 설정보다 **먼저** 이 목록으로 거른다.
- *
- * 이슈: 저장소 설정이 허용해도 여기 없으면 못 올린다. `MODULE_VOD_CONTENT`는 mov·avi·mkv·
- *   webm·wmv를 허용하지만 실제로 통과하는 것은 mp4뿐이고, svg도 마찬가지로 막힌다.
- *   백엔드에 이 목록 확장을 요청하면 그때 여기만 늘리면 된다.
- */
-const GLOBAL_UPLOAD_EXTENSIONS: readonly string[] = [
-  'gif', 'jpg', 'jpeg', 'png',
-  'xls', 'xlsx', 'ppt', 'pptx', 'doc', 'docx', 'hwp', 'pdf',
-  'mp4', 'mp3', 'txt', 'html', 'htm',
-];
 
 /** 실제로 통과하는 확장자 — 저장소 설정과 전역 화이트리스트의 교집합이다. */
 export function allowedExtensions(module: FileModule): string[] {
Add a comment
List