fix: 추가 이미지 순서 변경·삭제 저장이 500으로 막히던 원인(재업로드 파일명)을 고친다
저장된 장을 다시 올릴 때 이름을 MIME(image/jpeg;charset=UTF-8)에서 그대로 만들어 확장자가 'jpeg;charset=utf-8'이 됐고, 백엔드가 이름의 확장자로 허용 여부를 가려(EgovFileMngUtil. checkAllowFileExtension) 거절했다(dev 로그 #585). 묶음 파일 목록의 원본 이름·확장자를 쓰고 없으면 MIME 매개변수를 뗀다. 덧붙이기 저장 뒤 화면이 옛 칸을 유지하던 것도 장수를 key에 넣어 다시 마운트한다. Co-Authored-By: Claude Fable 5.1
@726f941ccfe9356a7791e381c01c418d57d4d263
--- app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
+++ app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
... | ... | @@ -378,9 +378,10 @@ |
| 378 | 378 |
description="대표 1장 필수 · 추가 최대 11장 (앱 슬라이더 최대 12장)" |
| 379 | 379 |
className={styles.section}
|
| 380 | 380 |
> |
| 381 |
- {/* 저장으로 파일 id가 바뀌면 다시 마운트해 칸 상태를 서버 값으로 되돌린다. */}
|
|
| 381 |
+ {/* 저장으로 파일 id나 장수가 바뀌면 다시 마운트해 칸 상태를 서버 값으로 되돌린다 —
|
|
| 382 |
+ 덧붙이기 저장은 id가 그대로라 장수로 알아챈다. */} |
|
| 382 | 383 |
<MerchantImageField |
| 383 |
- key={`${detail?.thumbnailFileId ?? ''}/${detail?.imageFileId ?? ''}`}
|
|
| 384 |
+ key={`${detail?.thumbnailFileId ?? ''}/${detail?.imageFileId ?? ''}/${detail?.imageUrls.length ?? 0}`}
|
|
| 384 | 385 |
defaultThumbnailUrl={detail?.thumbnailUrl ?? null}
|
| 385 | 386 |
retainedThumbnailId={detail?.thumbnailFileId ?? null}
|
| 386 | 387 |
defaultImageUrls={detail?.imageUrls ?? []}
|
--- app/(protected)/(basic)/system/merchant-details/_actions.ts
+++ app/(protected)/(basic)/system/merchant-details/_actions.ts
... | ... | @@ -7,8 +7,10 @@ |
| 7 | 7 |
import { fetchCommonCodes } from '@/lib/data/repositories/common-code-repository';
|
| 8 | 8 |
import {
|
| 9 | 9 |
buildFileImageUrl, |
| 10 |
+ fetchAttachmentFiles, |
|
| 10 | 11 |
uploadAttachment, |
| 11 | 12 |
uploadAttachments, |
| 13 |
+ type AttachmentFile, |
|
| 12 | 14 |
} from '@/lib/data/repositories/file-repository'; |
| 13 | 15 |
import {
|
| 14 | 16 |
createMerchantDetail, |
... | ... | @@ -152,8 +154,18 @@ |
| 152 | 154 |
return uploadAttachment(file, FILE_MODULE.merchantImage.id); |
| 153 | 155 |
} |
| 154 | 156 |
|
| 155 |
-/** 저장된 이미지 한 장을 다시 파일로 — 묶음을 새 순서로 다시 올릴 때 쓴다. */ |
|
| 156 |
-async function fetchSavedImage(fileId: string, fileSn: number): Promise<File> {
|
|
| 157 |
+/** |
|
| 158 |
+ * 저장된 이미지 한 장을 다시 파일로 — 묶음을 새 순서로 다시 올릴 때 쓴다. |
|
| 159 |
+ * |
|
| 160 |
+ * 파일 이름은 원본 이름(묶음 파일 목록)을 쓴다 — 백엔드가 이름의 확장자로 허용 여부를 가르기 때문이다. |
|
| 161 |
+ * 목록에 없으면 MIME에서 만들되 `;charset=…` 같은 매개변수는 뗀다(안 떼면 확장자가 |
|
| 162 |
+ * `jpeg;charset=utf-8`이 되어 거절당한다 — 실제로 났던 500). |
|
| 163 |
+ */ |
|
| 164 |
+async function fetchSavedImage( |
|
| 165 |
+ fileId: string, |
|
| 166 |
+ fileSn: number, |
|
| 167 |
+ saved: AttachmentFile | undefined |
|
| 168 |
+): Promise<File> {
|
|
| 157 | 169 |
const url = buildFileImageUrl(fileId, fileSn); |
| 158 | 170 |
if (!url) {
|
| 159 | 171 |
throw new InputError(INVALID_REQUEST_MESSAGE); |
... | ... | @@ -166,8 +178,10 @@ |
| 166 | 178 |
`저장된 추가 이미지 ${fileSn}번을 다시 읽지 못했습니다. 이 서버에 파일이 없을 수 있으니 이미지를 다시 올려 주세요.`
|
| 167 | 179 |
); |
| 168 | 180 |
} |
| 169 |
- const extension = blob.type.split('/')[1] || 'jpg';
|
|
| 170 |
- return new File([blob], `image-${fileSn}.${extension}`, { type: blob.type });
|
|
| 181 |
+ const mimeType = blob.type.split(';')[0].trim();
|
|
| 182 |
+ const extension = saved?.extension || mimeType.split('/')[1] || 'jpg';
|
|
| 183 |
+ const name = saved?.name && /\.[A-Za-z0-9]+$/.test(saved.name) ? saved.name : `image-${fileSn}.${extension}`;
|
|
| 184 |
+ return new File([blob], name, { type: mimeType || `image/${extension}` });
|
|
| 171 | 185 |
} |
| 172 | 186 |
|
| 173 | 187 |
/** |
... | ... | @@ -226,6 +240,7 @@ |
| 226 | 240 |
|
| 227 | 241 |
let newBytes = 0; |
| 228 | 242 |
const bundle: File[] = []; |
| 243 |
+ const savedFiles = retainedId && savedSns.length > 0 ? await fetchAttachmentFiles(retainedId) : []; |
|
| 229 | 244 |
for (const [index, slot] of slots.entries()) {
|
| 230 | 245 |
const picked = files[index]; |
| 231 | 246 |
if (picked) {
|
... | ... | @@ -233,7 +248,10 @@ |
| 233 | 248 |
newBytes += picked.size; |
| 234 | 249 |
bundle.push(picked); |
| 235 | 250 |
} else if (slot.startsWith('saved:') && retainedId) {
|
| 236 |
- bundle.push(await fetchSavedImage(retainedId, Number(slot.slice(6)))); |
|
| 251 |
+ const fileSn = Number(slot.slice(6)); |
|
| 252 |
+ bundle.push( |
|
| 253 |
+ await fetchSavedImage(retainedId, fileSn, savedFiles.find((file) => file.fileSn === fileSn)) |
|
| 254 |
+ ); |
|
| 237 | 255 |
} |
| 238 | 256 |
} |
| 239 | 257 |
if (newBytes > MAX_ATTACHMENT_BYTES) {
|
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?