From 169c584d31804e33ba2ae2b39aa7b7952c515c30 Mon Sep 17 00:00:00 2001 From: caadiq Date: Thu, 29 Jan 2026 22:20:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=BD=98=EC=84=9C=ED=8A=B8=20=ED=8F=BC?= =?UTF-8?q?=EC=97=90=20=EA=B5=BF=EC=A6=88=20=EC=84=B9=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 굿즈 이미지 다중 업로드 및 드래그 순서 변경 - 삭제 시 확인 다이얼로그 표시 Co-Authored-By: Claude Opus 4.5 --- .../form/concert/MerchandiseSection.jsx | 169 ++++++++++++++++++ .../pc/admin/schedules/form/concert/index.jsx | 10 ++ 2 files changed, 179 insertions(+) create mode 100644 frontend/src/pages/pc/admin/schedules/form/concert/MerchandiseSection.jsx diff --git a/frontend/src/pages/pc/admin/schedules/form/concert/MerchandiseSection.jsx b/frontend/src/pages/pc/admin/schedules/form/concert/MerchandiseSection.jsx new file mode 100644 index 0000000..acec621 --- /dev/null +++ b/frontend/src/pages/pc/admin/schedules/form/concert/MerchandiseSection.jsx @@ -0,0 +1,169 @@ +import { useRef, useState } from "react"; +import { motion, AnimatePresence, Reorder } from "framer-motion"; +import { Image, Plus, Trash2, GripVertical } from "lucide-react"; + +import ConfirmDialog from "@/components/pc/admin/common/ConfirmDialog"; + +/** + * 굿즈 섹션 + * - 다수의 굿즈 이미지 업로드 + * - 드래그로 순서 변경 + */ +function MerchandiseSection({ items, setItems }) { + const fileInputRef = useRef(null); + const [deleteConfirm, setDeleteConfirm] = useState({ + isOpen: false, + itemId: null, + itemName: null, + }); + + // 이미지 추가 + const handleFileChange = (e) => { + const files = Array.from(e.target.files); + if (files.length === 0) return; + + const newItems = files.map((file, i) => { + const url = URL.createObjectURL(file); + return { + id: `md-${Date.now()}-${i}`, + file, + preview: url, + }; + }); + + setItems((prev) => [...prev, ...newItems]); + + // input 초기화 + e.target.value = ""; + }; + + // 이미지 삭제 시도 + const handleRemoveItem = (id) => { + const item = items.find((it) => it.id === id); + setDeleteConfirm({ + isOpen: true, + itemId: id, + itemName: item?.file?.name || "이미지", + }); + }; + + // 이미지 삭제 실행 + const confirmRemoveItem = () => { + if (deleteConfirm.itemId !== null) { + setItems((prev) => { + const item = prev.find((it) => it.id === deleteConfirm.itemId); + if (item?.preview) { + URL.revokeObjectURL(item.preview); + } + return prev.filter((it) => it.id !== deleteConfirm.itemId); + }); + } + setDeleteConfirm({ isOpen: false, itemId: null, itemName: null }); + }; + + return ( + <> + + setDeleteConfirm({ isOpen: false, itemId: null, itemName: null }) + } + onConfirm={confirmRemoveItem} + title="이미지 삭제" + message={ +

+ {deleteConfirm.itemName} + 을(를) 삭제하시겠습니까? +

+ } + confirmText="삭제" + cancelText="취소" + /> + +
+

굿즈

+ + + + {items.length === 0 ? ( +
fileInputRef.current?.click()} + className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-gray-200 rounded-xl cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors" + > + +

+ 클릭하여 굿즈 이미지를 추가하세요 +

+

여러 장 선택 가능

+
+ ) : ( + + + {items.map((item, index) => ( + +
+ +
+ +
+ {`굿즈 +
+ +
+

+ {item.file?.name} +

+

+ {index + 1}번째 +

+
+ + +
+ ))} +
+
+ )} + + {items.length > 0 && ( +

+ 드래그하여 순서를 변경할 수 있습니다. 순서대로 표시됩니다. +

+ )} +
+ + ); +} + +export default MerchandiseSection; diff --git a/frontend/src/pages/pc/admin/schedules/form/concert/index.jsx b/frontend/src/pages/pc/admin/schedules/form/concert/index.jsx index da216e1..2fd196b 100644 --- a/frontend/src/pages/pc/admin/schedules/form/concert/index.jsx +++ b/frontend/src/pages/pc/admin/schedules/form/concert/index.jsx @@ -11,6 +11,7 @@ import { getMembers } from "@/api/public/members"; import ConcertInfoSection from "./ConcertInfoSection"; import ScheduleSection from "./ScheduleSection"; +import MerchandiseSection from "./MerchandiseSection"; /** * 콘서트 일정 추가 폼 @@ -40,6 +41,9 @@ function ConcertForm() { { id: 1, date: "", time: "", venue: null }, ]); + // 굿즈 이미지 + const [merchandiseItems, setMerchandiseItems] = useState([]); + // 로딩 상태 const [saving, setSaving] = useState(false); @@ -113,6 +117,12 @@ function ConcertForm() { {/* 공연 일정 */} + {/* 굿즈 */} + + {/* 버튼 */}