feat: 게시판 첨부를 MODULE_BBS 저장소로 보낸다
`MODULE_BBS`가 저장소 목록에 없어 임시 상수로 빼 두었는데(백엔드가 설정을 못 찾으면 기본 저장소로 조용히 폴백한다) 이제 「게시글 스토리지」로 추가돼 다른 모듈과 같은 자리로 옮긴다. 허용 확장자는 이 저장소만의 목록을 받지 못해 전역 화이트리스트(`Globals.fileUpload.Extensions`)를 그대로 둔다 — 화면이 먼저 막지 않게 하기 위해서다. 저장소 목록이 더 좁으면 백엔드가 사유를 돌려주고 그 문구가 화면에 그대로 나간다. 실제 목록을 받으면 그 한 줄만 바꾼다. 첨부 입력에 `accept`와 확장자 사전 검증이 붙는다 — 지금까지는 아무 파일이나 고를 수 있었고 확장자가 틀리면 백엔드가 사유 없이 실패했다. Co-Authored-By: Claude Opus 5
@ffa13e676010a2d4dc456477c8f5b145e008e86a
--- app/(protected)/(basic)/boards/_actions.ts
+++ app/(protected)/(basic)/boards/_actions.ts
... | ... | @@ -23,8 +23,10 @@ |
| 23 | 23 |
} from '@/lib/domain/board-post-form'; |
| 24 | 24 |
import { BOARD_PATHS } from '@/lib/domain/board-post-query';
|
| 25 | 25 |
import {
|
| 26 |
- BOARD_ATTACHMENT_MODULE_ID, |
|
| 26 |
+ FILE_MODULE, |
|
| 27 | 27 |
MAX_ATTACHMENT_BYTES, |
| 28 |
+ allowedExtensions, |
|
| 29 |
+ isAllowedFileExtension, |
|
| 28 | 30 |
} from '@/lib/domain/file-module'; |
| 29 | 31 |
|
| 30 | 32 |
/** |
... | ... | @@ -113,8 +115,13 @@ |
| 113 | 115 |
if (file.size > MAX_ATTACHMENT_BYTES) {
|
| 114 | 116 |
throw new Error(OVERSIZE_MESSAGE); |
| 115 | 117 |
} |
| 118 |
+ if (!isAllowedFileExtension(FILE_MODULE.board, file.name)) {
|
|
| 119 |
+ throw new Error( |
|
| 120 |
+ `${allowedExtensions(FILE_MODULE.board).join(', ')} 파일만 올릴 수 있습니다.`
|
|
| 121 |
+ ); |
|
| 122 |
+ } |
|
| 116 | 123 |
|
| 117 |
- return uploadAttachment(file, BOARD_ATTACHMENT_MODULE_ID); |
|
| 124 |
+ return uploadAttachment(file, FILE_MODULE.board.id); |
|
| 118 | 125 |
} |
| 119 | 126 |
|
| 120 | 127 |
/** 등록·수정이 공유하는 입력 읽기. */ |
--- app/(protected)/(basic)/boards/_components/board-attachment-field.tsx
+++ app/(protected)/(basic)/boards/_components/board-attachment-field.tsx
... | ... | @@ -4,7 +4,13 @@ |
| 4 | 4 |
import { FoxHelperText } from '@fox/core/components/fox-helper-text';
|
| 5 | 5 |
import { Field } from '@/components/ui/field';
|
| 6 | 6 |
import styles from './board-attachment-field.module.scss'; |
| 7 |
-import { MAX_ATTACHMENT_BYTES } from '@/lib/domain/file-module';
|
|
| 7 |
+import {
|
|
| 8 |
+ FILE_MODULE, |
|
| 9 |
+ MAX_ATTACHMENT_BYTES, |
|
| 10 |
+ allowedExtensions, |
|
| 11 |
+ fileModuleAccept, |
|
| 12 |
+ isAllowedFileExtension, |
|
| 13 |
+} from '@/lib/domain/file-module'; |
|
| 8 | 14 |
|
| 9 | 15 |
interface BoardAttachmentFieldProps {
|
| 10 | 16 |
label?: string; |
... | ... | @@ -30,6 +36,8 @@ |
| 30 | 36 |
const OVERSIZE_MESSAGE = `첨부파일은 ${Math.floor(
|
| 31 | 37 |
MAX_ATTACHMENT_BYTES / (1024 * 1024) |
| 32 | 38 |
)}MB 이하만 올릴 수 있습니다.`; |
| 39 |
+ |
|
| 40 |
+const EXTENSION_MESSAGE = `${allowedExtensions(FILE_MODULE.board).join(', ')} 파일만 올릴 수 있습니다.`;
|
|
| 33 | 41 |
|
| 34 | 42 |
export function BoardAttachmentField({
|
| 35 | 43 |
label = '첨부파일', |
... | ... | @@ -60,14 +68,24 @@ |
| 60 | 68 |
<input |
| 61 | 69 |
type="file" |
| 62 | 70 |
name="attachment" |
| 71 |
+ accept={fileModuleAccept(FILE_MODULE.board)}
|
|
| 63 | 72 |
className={styles.input}
|
| 64 | 73 |
onChange={(event) => {
|
| 65 | 74 |
const file = event.target.files?.[0]; |
| 66 |
- if (file && file.size > MAX_ATTACHMENT_BYTES) {
|
|
| 75 |
+ if (!file) {
|
|
| 76 |
+ setError(undefined); |
|
| 77 |
+ return; |
|
| 78 |
+ } |
|
| 79 |
+ if (file.size > MAX_ATTACHMENT_BYTES) {
|
|
| 67 | 80 |
event.target.value = ''; |
| 68 | 81 |
setError(OVERSIZE_MESSAGE); |
| 69 | 82 |
return; |
| 70 | 83 |
} |
| 84 |
+ if (!isAllowedFileExtension(FILE_MODULE.board, file.name)) {
|
|
| 85 |
+ event.target.value = ''; |
|
| 86 |
+ setError(EXTENSION_MESSAGE); |
|
| 87 |
+ return; |
|
| 88 |
+ } |
|
| 71 | 89 |
setError(undefined); |
| 72 | 90 |
}} |
| 73 | 91 |
/> |
--- lib/domain/file-module.ts
+++ lib/domain/file-module.ts
... | ... | @@ -17,6 +17,20 @@ |
| 17 | 17 |
extensions: readonly string[]; |
| 18 | 18 |
}; |
| 19 | 19 |
|
| 20 |
+/** |
|
| 21 |
+ * 업로드 전체에 걸린 확장자 화이트리스트 — 백엔드 `Globals.fileUpload.Extensions`. |
|
| 22 |
+ * `EgovMultipartResolver`가 저장소 설정보다 **먼저** 이 목록으로 거른다. |
|
| 23 |
+ * |
|
| 24 |
+ * 이슈: 저장소 설정이 허용해도 여기 없으면 못 올린다. `MODULE_VOD_CONTENT`는 mov·avi·mkv· |
|
| 25 |
+ * webm·wmv를 허용하지만 실제로 통과하는 것은 mp4뿐이고, svg도 마찬가지로 막힌다. |
|
| 26 |
+ * 백엔드에 이 목록 확장을 요청하면 그때 여기만 늘리면 된다. |
|
| 27 |
+ */ |
|
| 28 |
+export const GLOBAL_UPLOAD_EXTENSIONS: readonly string[] = [ |
|
| 29 |
+ 'gif', 'jpg', 'jpeg', 'png', |
|
| 30 |
+ 'xls', 'xlsx', 'ppt', 'pptx', 'doc', 'docx', 'hwp', 'pdf', |
|
| 31 |
+ 'mp4', 'mp3', 'txt', 'html', 'htm', |
|
| 32 |
+]; |
|
| 33 |
+ |
|
| 20 | 34 |
const IMAGE = ['jpg', 'png', 'gif'] as const; |
| 21 | 35 |
|
| 22 | 36 |
export const FILE_MODULE = {
|
... | ... | @@ -64,25 +78,16 @@ |
| 64 | 78 |
description: '회원 프로필 저장소', |
| 65 | 79 |
extensions: IMAGE, |
| 66 | 80 |
}, |
| 81 |
+ /** 게시판(공지사항·FAQ·1:1문의) 첨부. */ |
|
| 82 |
+ board: {
|
|
| 83 |
+ id: 'MODULE_BBS', |
|
| 84 |
+ description: '게시글 스토리지', |
|
| 85 |
+ // TODO: 이 저장소만의 허용 확장자를 받지 못했다. 전역 화이트리스트를 그대로 두어 화면이 |
|
| 86 |
+ // 먼저 막지 않게 한다 — 저장소 목록이 더 좁으면 백엔드가 사유를 돌려준다. |
|
| 87 |
+ extensions: GLOBAL_UPLOAD_EXTENSIONS, |
|
| 88 |
+ }, |
|
| 67 | 89 |
} as const satisfies Record<string, FileModule>; |
| 68 | 90 |
|
| 69 |
-// TODO: 게시판(공지사항·FAQ·1:1문의) 첨부용 저장소가 목록에 없다. 지금은 이 값이 설정에 없어 |
|
| 70 |
-// 기본 저장소로 폴백해 저장된다 — 백엔드에 모듈 추가를 요청하고 그 id로 교체한다. |
|
| 71 |
-export const BOARD_ATTACHMENT_MODULE_ID = 'MODULE_BBS'; |
|
| 72 |
- |
|
| 73 |
-/** |
|
| 74 |
- * 업로드 전체에 걸린 확장자 화이트리스트 — 백엔드 `Globals.fileUpload.Extensions`. |
|
| 75 |
- * `EgovMultipartResolver`가 저장소 설정보다 **먼저** 이 목록으로 거른다. |
|
| 76 |
- * |
|
| 77 |
- * 이슈: 저장소 설정이 허용해도 여기 없으면 못 올린다. `MODULE_VOD_CONTENT`는 mov·avi·mkv· |
|
| 78 |
- * webm·wmv를 허용하지만 실제로 통과하는 것은 mp4뿐이고, svg도 마찬가지로 막힌다. |
|
| 79 |
- * 백엔드에 이 목록 확장을 요청하면 그때 여기만 늘리면 된다. |
|
| 80 |
- */ |
|
| 81 |
-const GLOBAL_UPLOAD_EXTENSIONS: readonly string[] = [ |
|
| 82 |
- 'gif', 'jpg', 'jpeg', 'png', |
|
| 83 |
- 'xls', 'xlsx', 'ppt', 'pptx', 'doc', 'docx', 'hwp', 'pdf', |
|
| 84 |
- 'mp4', 'mp3', 'txt', 'html', 'htm', |
|
| 85 |
-]; |
|
| 86 | 91 |
|
| 87 | 92 |
/** 실제로 통과하는 확장자 — 저장소 설정과 전역 화이트리스트의 교집합이다. */ |
| 88 | 93 |
export function allowedExtensions(module: FileModule): string[] {
|
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?