임동욱 임동욱 09-18
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
+++ app/(protected)/(basic)/system/merchant-details/[frcsNo]/_components/merchant-detail-form.tsx
@@ -378,9 +378,10 @@
           description="대표 1장 필수 · 추가 최대 11장 (앱 슬라이더 최대 12장)"
           className={styles.section}
         >
-          {/* 저장으로 파일 id가 바뀌면 다시 마운트해 칸 상태를 서버 값으로 되돌린다. */}
+          {/* 저장으로 파일 id나 장수가 바뀌면 다시 마운트해 칸 상태를 서버 값으로 되돌린다 —
+              덧붙이기 저장은 id가 그대로라 장수로 알아챈다. */}
           <MerchantImageField
-            key={`${detail?.thumbnailFileId ?? ''}/${detail?.imageFileId ?? ''}`}
+            key={`${detail?.thumbnailFileId ?? ''}/${detail?.imageFileId ?? ''}/${detail?.imageUrls.length ?? 0}`}
             defaultThumbnailUrl={detail?.thumbnailUrl ?? null}
             retainedThumbnailId={detail?.thumbnailFileId ?? null}
             defaultImageUrls={detail?.imageUrls ?? []}
app/(protected)/(basic)/system/merchant-details/_actions.ts
--- app/(protected)/(basic)/system/merchant-details/_actions.ts
+++ app/(protected)/(basic)/system/merchant-details/_actions.ts
@@ -7,8 +7,10 @@
 import { fetchCommonCodes } from '@/lib/data/repositories/common-code-repository';
 import {
   buildFileImageUrl,
+  fetchAttachmentFiles,
   uploadAttachment,
   uploadAttachments,
+  type AttachmentFile,
 } from '@/lib/data/repositories/file-repository';
 import {
   createMerchantDetail,
@@ -152,8 +154,18 @@
   return uploadAttachment(file, FILE_MODULE.merchantImage.id);
 }
 
-/** 저장된 이미지 한 장을 다시 파일로 — 묶음을 새 순서로 다시 올릴 때 쓴다. */
-async function fetchSavedImage(fileId: string, fileSn: number): Promise<File> {
+/**
+ * 저장된 이미지 한 장을 다시 파일로 — 묶음을 새 순서로 다시 올릴 때 쓴다.
+ *
+ * 파일 이름은 원본 이름(묶음 파일 목록)을 쓴다 — 백엔드가 이름의 확장자로 허용 여부를 가르기 때문이다.
+ * 목록에 없으면 MIME에서 만들되 `;charset=…` 같은 매개변수는 뗀다(안 떼면 확장자가
+ * `jpeg;charset=utf-8`이 되어 거절당한다 — 실제로 났던 500).
+ */
+async function fetchSavedImage(
+  fileId: string,
+  fileSn: number,
+  saved: AttachmentFile | undefined
+): Promise<File> {
   const url = buildFileImageUrl(fileId, fileSn);
   if (!url) {
     throw new InputError(INVALID_REQUEST_MESSAGE);
@@ -166,8 +178,10 @@
       `저장된 추가 이미지 ${fileSn}번을 다시 읽지 못했습니다. 이 서버에 파일이 없을 수 있으니 이미지를 다시 올려 주세요.`
     );
   }
-  const extension = blob.type.split('/')[1] || 'jpg';
-  return new File([blob], `image-${fileSn}.${extension}`, { type: blob.type });
+  const mimeType = blob.type.split(';')[0].trim();
+  const extension = saved?.extension || mimeType.split('/')[1] || 'jpg';
+  const name = saved?.name && /\.[A-Za-z0-9]+$/.test(saved.name) ? saved.name : `image-${fileSn}.${extension}`;
+  return new File([blob], name, { type: mimeType || `image/${extension}` });
 }
 
 /**
@@ -226,6 +240,7 @@
 
   let newBytes = 0;
   const bundle: File[] = [];
+  const savedFiles = retainedId && savedSns.length > 0 ? await fetchAttachmentFiles(retainedId) : [];
   for (const [index, slot] of slots.entries()) {
     const picked = files[index];
     if (picked) {
@@ -233,7 +248,10 @@
       newBytes += picked.size;
       bundle.push(picked);
     } else if (slot.startsWith('saved:') && retainedId) {
-      bundle.push(await fetchSavedImage(retainedId, Number(slot.slice(6))));
+      const fileSn = Number(slot.slice(6));
+      bundle.push(
+        await fetchSavedImage(retainedId, fileSn, savedFiles.find((file) => file.fileSn === fileSn))
+      );
     }
   }
   if (newBytes > MAX_ATTACHMENT_BYTES) {
Add a comment
List