diff --git a/frontend/src/components/pc/admin/common/ConfirmDialog.jsx b/frontend/src/components/pc/admin/common/ConfirmDialog.jsx
index 1b4627a..06fff6c 100644
--- a/frontend/src/components/pc/admin/common/ConfirmDialog.jsx
+++ b/frontend/src/components/pc/admin/common/ConfirmDialog.jsx
@@ -14,6 +14,7 @@
* - loadingText: 로딩 중 텍스트 (기본: "삭제 중...")
* - variant: 버튼 색상 (기본: "danger", "primary" 가능)
*/
+import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle, Trash2 } from 'lucide-react';
@@ -46,7 +47,7 @@ function ConfirmDialog({
primary: 'text-primary',
};
- return (
+ return createPortal(
시간과 장소는 선택사항입니다. 미정인 경우 비워두세요.
diff --git a/frontend/src/pages/pc/admin/schedules/form/concert/SetlistSection.jsx b/frontend/src/pages/pc/admin/schedules/form/concert/SetlistSection.jsx new file mode 100644 index 0000000..a4cd5c5 --- /dev/null +++ b/frontend/src/pages/pc/admin/schedules/form/concert/SetlistSection.jsx @@ -0,0 +1,323 @@ +import { useState, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Plus, Trash2, Users, Search } from "lucide-react"; + +import ConfirmDialog from "@/components/pc/admin/common/ConfirmDialog"; +import SongSearchDialog from "./SongSearchDialog"; + +/** + * 세트리스트 섹션 + * - 곡 추가/삭제 + * - 곡명, 앨범명, 참여 멤버 + * - 순서 자동 부여 + */ +function SetlistSection({ setlist, setSetlist, members, selectedMemberIds, albums }) { + const containerRef = useRef(null); + const [nextId, setNextId] = useState(() => { + const maxId = setlist.reduce((max, s) => Math.max(max, s.id || 0), 0); + return maxId + 1; + }); + + // 삭제 확인 다이얼로그 + const [deleteConfirm, setDeleteConfirm] = useState({ + isOpen: false, + songId: null, + songName: null, + }); + + // 곡 검색 다이얼로그 + const [songSearchOpen, setSongSearchOpen] = useState(false); + + // 직접 입력 곡 추가 + const addSong = () => { + const newSong = { + id: nextId, + songName: "", + albumName: "", + memberIds: [...selectedMemberIds], + }; + setSetlist((prev) => [...prev, newSong]); + setNextId(nextId + 1); + + setTimeout(() => { + if (containerRef.current) { + const lastChild = containerRef.current.lastElementChild; + if (lastChild) { + lastChild.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } + }, 100); + }; + + // 검색에서 선택한 곡 추가 + const addSongsFromSearch = (songs) => { + let id = nextId; + const newSongs = songs.map((song) => ({ + id: id++, + songName: song.songName, + albumName: song.albumName, + memberIds: [...selectedMemberIds], + })); + setSetlist((prev) => [...prev, ...newSongs]); + setNextId(id); + + setTimeout(() => { + if (containerRef.current) { + const lastChild = containerRef.current.lastElementChild; + if (lastChild) { + lastChild.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } + }, 100); + }; + + // 곡 삭제 시도 + const handleRemoveSong = (id) => { + if (setlist.length <= 1) return; + + const song = setlist.find((s) => s.id === id); + + if (song && (song.songName || song.albumName)) { + setDeleteConfirm({ + isOpen: true, + songId: id, + songName: song.songName || "제목 없음", + }); + } else { + removeSong(id); + } + }; + + // 곡 삭제 실행 + const removeSong = (id) => { + setSetlist((prev) => prev.filter((s) => s.id !== id)); + }; + + // 삭제 확인 + const handleConfirmDelete = () => { + if (deleteConfirm.songId !== null) { + removeSong(deleteConfirm.songId); + } + setDeleteConfirm({ isOpen: false, songId: null, songName: null }); + }; + + // 곡 필드 업데이트 + const updateSong = (id, field, value) => { + setSetlist((prev) => + prev.map((s) => (s.id === id ? { ...s, [field]: value } : s)) + ); + }; + + // 곡별 멤버 토글 + const toggleSongMember = (songId, memberId) => { + setSetlist((prev) => + prev.map((s) => { + if (s.id !== songId) return s; + const has = s.memberIds.includes(memberId); + return { + ...s, + memberIds: has + ? s.memberIds.filter((id) => id !== memberId) + : [...s.memberIds, memberId], + }; + }) + ); + }; + + // 곡별 멤버 전체 선택/해제 + const toggleAllSongMembers = (songId) => { + setSetlist((prev) => + prev.map((s) => { + if (s.id !== songId) return s; + const allSelected = members.every((m) => s.memberIds.includes(m.id)); + return { + ...s, + memberIds: allSelected ? [] : members.map((m) => m.id), + }; + }) + ); + }; + + return ( + <> ++ {deleteConfirm.songName} + 을(를) 삭제하시겠습니까? +
+ } + confirmText="삭제" + cancelText="취소" + /> + ++ 곡 추가 시 콘서트 참여 멤버가 자동으로 선택됩니다. 솔로/유닛 곡은 + 개별 조정하세요. +
++ {searchQuery + ? "검색 결과가 없습니다" + : "등록된 곡이 없습니다"} +
+