From b619d0eeb504276c0d19f4cb6ba8f5ed19973c17 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 13 Aug 2026 01:21:53 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EC=B5=9C=EC=8B=A0=EC=86=8C=EC=8B=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EA=B0=80=20=EC=A0=95=EC=83=81=20=EC=97=85=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정(PATCH) 요청이 파일을 S3에 올리지 않고 File 객체를 그대로 보내고 있어 백엔드가 이미지를 제대로 처리하지 못했다. 등록(v2) 플로우와 동일하게 presigned URL 발급 후 S3 업로드, URL 전달 방식으로 통일했다. --- .../_components/Modal/EditNewsModal.tsx | 1 + .../org/OrgAdmin/HomeSection/api.ts | 31 +++++++++---------- .../org/OrgAdmin/HomeSection/queries.ts | 20 ++++++++---- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/org/OrgAdmin/HomeSection/_components/Modal/EditNewsModal.tsx b/src/components/org/OrgAdmin/HomeSection/_components/Modal/EditNewsModal.tsx index 5ba44cd1..fcee6f49 100644 --- a/src/components/org/OrgAdmin/HomeSection/_components/Modal/EditNewsModal.tsx +++ b/src/components/org/OrgAdmin/HomeSection/_components/Modal/EditNewsModal.tsx @@ -72,6 +72,7 @@ export const EditNewsModal = ({ { id: newsId, file: image.file, + existingImageUrl: news?.image, title, link: link ?? '', }, diff --git a/src/components/org/OrgAdmin/HomeSection/api.ts b/src/components/org/OrgAdmin/HomeSection/api.ts index 3e1b44cf..e5f8004d 100644 --- a/src/components/org/OrgAdmin/HomeSection/api.ts +++ b/src/components/org/OrgAdmin/HomeSection/api.ts @@ -110,18 +110,17 @@ export const getNews = async (id: number) => { return data; }; -export const patchNews = async (id: number, formData: FormData) => { - const res = await fetcher.PATCH('/admin/news/{id}', { +export const patchNewsV2 = async ( + id: number, + data: { imageUrl: string; title: string; link: string }, +) => { + const res = await fetcher.PATCH('/admin/news/{id}/v2', { params: { path: { id, }, }, - body: { - image: formData.get('image') as string, - title: formData.get('title') as string, - link: formData.get('link') as string, - }, + body: data, }); return res; @@ -234,9 +233,9 @@ export const deployHomeTab = async ({ reviewItems, newsItems, }: DeployHomeInput) => { - const newsDetails = await Promise.all( - newsItems.map((item) => getNews(item.id)), - ); + const newsDetails = ( + await Promise.all(newsItems.map((item) => getNews(item.id))) + ).filter((news): news is NonNullable => Boolean(news)); const deployResponse = await postHomeTab({ generation: Number(ACTIVITY_GENERATION), @@ -246,13 +245,11 @@ export const deployHomeTab = async ({ content: content ?? '', authorInfo: authorInfo ?? '', })), - news: newsDetails - .filter((news): news is NonNullable => Boolean(news)) - .map((news) => ({ - imageFileName: extractFileNameFromUrl(news.image), - title: news.title, - link: news.link, - })), + news: newsDetails.map((news) => ({ + imageFileName: extractFileNameFromUrl(news.image), + title: news.title, + link: news.link, + })), }); if (homeHeaderImageFile && deployResponse.homeHeaderImage) { diff --git a/src/components/org/OrgAdmin/HomeSection/queries.ts b/src/components/org/OrgAdmin/HomeSection/queries.ts index 64d9d82a..a234b928 100644 --- a/src/components/org/OrgAdmin/HomeSection/queries.ts +++ b/src/components/org/OrgAdmin/HomeSection/queries.ts @@ -14,7 +14,7 @@ import { getNews, getPresignedUrl, getReviews, - patchNews, + patchNewsV2, patchReview, postNewsV2, postReview, @@ -113,19 +113,27 @@ export const useEditNewsMutation = () => { mutationFn: async (data: { id: number; file?: File; + existingImageUrl?: string; title: string; link: string; }) => { - const formData = new FormData(); + let imageUrl = data.existingImageUrl; if (data.file) { - formData.append('image', data.file); + const presignedData = await getPresignedUrl(data.file); + await uploadToS3(presignedData.presignedUrl, data.file); + imageUrl = presignedData.fileUrl; } - formData.append('title', data.title); - formData.append('link', data.link); + if (!imageUrl) { + throw new Error('이미지가 필요합니다.'); + } - return await patchNews(data.id, formData); + return await patchNewsV2(data.id, { + imageUrl, + title: data.title, + link: data.link, + }); }, onSuccess: (_, data) => { queryClient.invalidateQueries({ From e47a6d1ddec4a1adb5ec0fd13cce7bb245573ec7 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 13 Aug 2026 01:22:16 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=ED=99=88=ED=83=AD=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=8B=9C=20=EC=B5=9C=EC=8B=A0=EC=86=8C=EC=8B=9D=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=9E=AC=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /admin/home 응답은 뉴스 이미지마다 새 presigned URL을 발급하는데, 이 URL에 실제 업로드하는 로직이 없어 confirm 시 이미지가 빈 채로 반영되고 있었다. 기존 이미지를 받아 해당 URL로 재업로드하도록 추가했다. --- .../org/OrgAdmin/HomeSection/api.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/components/org/OrgAdmin/HomeSection/api.ts b/src/components/org/OrgAdmin/HomeSection/api.ts index e5f8004d..89d335fe 100644 --- a/src/components/org/OrgAdmin/HomeSection/api.ts +++ b/src/components/org/OrgAdmin/HomeSection/api.ts @@ -263,5 +263,30 @@ export const deployHomeTab = async ({ } } + // /admin/home은 뉴스마다 새 presigned URL을 발급한다(기존 이미지를 그대로 + // 재사용하지 않음). 이 URL에 실제로 업로드하지 않으면 confirm 단계에서 + // 이미지가 비어있는 채로 반영되어 최신소식 이미지가 깨지거나 누락된다. + await Promise.all( + newsDetails.map(async (news, index) => { + const presignedUrl = deployResponse.news?.[index]?.imagePresignedUrl; + + if (!presignedUrl) return; + + const imageResponse = await fetch(news.image); + const imageBlob = await imageResponse.blob(); + + const uploadResponse = await fetch(presignedUrl, { + method: 'PUT', + body: imageBlob, + }); + + if (!uploadResponse.ok) { + throw new Error( + `최신소식(${news.title}) 이미지 업로드에 실패했습니다.`, + ); + } + }), + ); + return postHomeTabConfirm(); }; From 756cbe6148788aa77675101aca1d2400fbd72a85 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 13 Aug 2026 01:22:22 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=ED=8E=B8=EC=A7=91=20=EC=A4=91=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80/=EC=82=AD=EC=A0=9C=EB=90=9C=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=EC=9D=B4=20=EB=B0=B0=ED=8F=AC=20=EC=8B=9C=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 편집모드 중 draft를 서버 데이터로 동기화하지 않도록 막은 기존 로직 때문에, 편집 중 새로 추가/삭제한 리뷰·최신소식이 draft에 반영되지 않아 배포 시 누락되는 문제가 있었다. 로컬 재정렬 순서는 유지하면서 추가/삭제된 항목만 반영하는 병합 로직(reconcileWithServerData)으로 교체했다. --- .../org/OrgAdmin/HomeSection/HomeSection.tsx | 9 +++++++++ .../HomeSection/_utils/reconcileWithServerData.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/components/org/OrgAdmin/HomeSection/_utils/reconcileWithServerData.ts diff --git a/src/components/org/OrgAdmin/HomeSection/HomeSection.tsx b/src/components/org/OrgAdmin/HomeSection/HomeSection.tsx index c2655891..9d204873 100644 --- a/src/components/org/OrgAdmin/HomeSection/HomeSection.tsx +++ b/src/components/org/OrgAdmin/HomeSection/HomeSection.tsx @@ -14,6 +14,7 @@ import NewsSection from '@/components/org/OrgAdmin/HomeSection/_components/News/ import ReviewSection from '@/components/org/OrgAdmin/HomeSection/_components/Review/ReviewSection'; import type { Review } from '@/components/org/OrgAdmin/HomeSection/_types/types'; import { isSameOrder } from '@/components/org/OrgAdmin/HomeSection/_utils/isSameOrder'; +import { reconcileWithServerData } from '@/components/org/OrgAdmin/HomeSection/_utils/reconcileWithServerData'; import { extractFileNameFromUrl } from '@/components/org/OrgAdmin/HomeSection/api'; import { useAdminInfoQuery, @@ -64,6 +65,14 @@ const HomeSectionContent = ({ onEditModeChange }: HomeSectionProps) => { useEffect(() => { if (isEditMode) { + // 편집 중엔 로컬 재정렬(드래그 순서)을 그대로 유지하되, 그 사이 추가/삭제된 + // 항목만 반영한다. 서버 데이터로 통째로 덮어쓰면 로컬 순서가 날아가고 + // (리뷰 수정 시 순서 초기화 버그), 반대로 아예 무시하면 편집 중 새로 + // 추가한 항목이 draft에 안 실려 배포 시 누락된다. + setDraft((prev) => ({ + reviews: reconcileWithServerData(prev.reviews, initialReviews), + news: reconcileWithServerData(prev.news, latestNews), + })); return; } diff --git a/src/components/org/OrgAdmin/HomeSection/_utils/reconcileWithServerData.ts b/src/components/org/OrgAdmin/HomeSection/_utils/reconcileWithServerData.ts new file mode 100644 index 00000000..36f2307d --- /dev/null +++ b/src/components/org/OrgAdmin/HomeSection/_utils/reconcileWithServerData.ts @@ -0,0 +1,15 @@ +export const reconcileWithServerData = ( + draftItems: T[], + serverItems: T[], +): T[] => { + const serverById = new Map(serverItems.map((item) => [item.id, item])); + + const reconciledExisting = draftItems + .filter((item) => serverById.has(item.id)) + .map((item) => serverById.get(item.id) as T); + + const draftIds = new Set(draftItems.map((item) => item.id)); + const newItems = serverItems.filter((item) => !draftIds.has(item.id)); + + return [...reconciledExisting, ...newItems]; +}; From 231bd10017b25cbce7c1e1606847247de5c65291 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Thu, 13 Aug 2026 01:22:25 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EC=9E=84=EC=9B=90=EC=A7=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=9A=A9=EB=9F=89=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EB=AC=B8=EA=B5=AC=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "00mb"로 잘못 표기되어 있던 용량 제한 안내를 "10mb"로 수정했다. --- .../org/OrgAdmin/AboutSection/Executives/ExecInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/org/OrgAdmin/AboutSection/Executives/ExecInfo.tsx b/src/components/org/OrgAdmin/AboutSection/Executives/ExecInfo.tsx index c2b75e45..fe0c79d6 100644 --- a/src/components/org/OrgAdmin/AboutSection/Executives/ExecInfo.tsx +++ b/src/components/org/OrgAdmin/AboutSection/Executives/ExecInfo.tsx @@ -46,7 +46,7 @@ const ExecInfo = ({ - 사진은 1:1 비율로 올려주세요. 사진 용량은 00mb 아래로 첨부해주세요. + 사진은 1:1 비율로 올려주세요. 사진 용량은 10mb 아래로 첨부해주세요. Date: Thu, 13 Aug 2026 01:22:30 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=20=EC=9D=B4=EC=8A=88=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=9D=BC=EC=A0=95=20=EC=95=88=EB=82=B4=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=9E=84=EC=8B=9C=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 안내 이미지가 정상적으로 뜨지 않는 이슈가 있어 관련 버튼과 모달을 임시로 주석 처리했다. --- .../org/OrgAdmin/AboutSection/Schedule/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/org/OrgAdmin/AboutSection/Schedule/index.tsx b/src/components/org/OrgAdmin/AboutSection/Schedule/index.tsx index 94638c80..b1ba296a 100644 --- a/src/components/org/OrgAdmin/AboutSection/Schedule/index.tsx +++ b/src/components/org/OrgAdmin/AboutSection/Schedule/index.tsx @@ -95,9 +95,10 @@ const Schedule = ({ isEditable = true }: ScheduleProps) => { 전체 일정 - + {/* TODO: 이미지 제대로 안뜨는 이슈로 잠시 주석처리 */} + {/* - + */}