From 5415893f9d191f37374d4a67a2076fbcb88f29a3 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sat, 24 Jan 2026 10:56:12 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8A=B8=EB=9E=99=20=EC=98=81=EC=83=81=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EA=B5=AC=EB=B6=84=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB: music_video_url을 video_url로 변경, video_type 컬럼 추가 - 백엔드: insertTracks에서 video_url, video_type 처리 - 관리자: 영상 타입 선택 드롭다운 추가 (뮤직비디오/스페셜 영상) - CustomSelect: {value, label} 객체 옵션 및 size prop 지원 - 트랙 상세: video_type에 따른 라벨 동적 표시 Co-Authored-By: Claude Opus 4.5 --- backend/src/services/album.js | 5 ++- .../components/pc/admin/album/TrackItem.jsx | 35 +++++++++++----- .../pc/admin/common/CustomSelect.jsx | 40 +++++++++++++------ .../src/pages/mobile/album/TrackDetail.jsx | 7 ++-- .../src/pages/pc/public/album/TrackDetail.jsx | 7 ++-- 5 files changed, 64 insertions(+), 30 deletions(-) diff --git a/backend/src/services/album.js b/backend/src/services/album.js index f755a75..e42338d 100644 --- a/backend/src/services/album.js +++ b/backend/src/services/album.js @@ -181,12 +181,13 @@ async function insertTracks(connection, albumId, tracks) { track.composer || null, track.arranger || null, track.lyrics || null, - track.music_video_url || null, + track.video_url || null, + track.video_type || null, ]); await connection.query( `INSERT INTO album_tracks - (album_id, track_number, title, duration, is_title_track, lyricist, composer, arranger, lyrics, music_video_url) + (album_id, track_number, title, duration, is_title_track, lyricist, composer, arranger, lyrics, video_url, video_type) VALUES ?`, [values] ); diff --git a/frontend/src/components/pc/admin/album/TrackItem.jsx b/frontend/src/components/pc/admin/album/TrackItem.jsx index 7e4c2a1..b7a85c8 100644 --- a/frontend/src/components/pc/admin/album/TrackItem.jsx +++ b/frontend/src/components/pc/admin/album/TrackItem.jsx @@ -4,6 +4,13 @@ import { memo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Trash2, Star, ChevronDown } from 'lucide-react'; +import { CustomSelect } from '../common'; + +const VIDEO_TYPE_OPTIONS = [ + { value: '', label: '선택' }, + { value: 'music_video', label: '뮤직비디오' }, + { value: 'special', label: '스페셜 영상' }, +]; /** * @param {Object} props @@ -120,16 +127,26 @@ const TrackItem = memo(function TrackItem({ track, index, onUpdate, onRemove }) - {/* MV URL */} + {/* 비디오 URL */}
- - onUpdate(index, 'music_video_url', e.target.value)} - className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" - placeholder="https://youtube.com/watch?v=..." - /> + +
+ onUpdate(index, 'video_url', e.target.value)} + className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" + placeholder="https://youtube.com/watch?v=..." + /> + onUpdate(index, 'video_type', value)} + options={VIDEO_TYPE_OPTIONS} + placeholder="선택" + size="sm" + className="w-32" + /> +
{/* 가사 */} diff --git a/frontend/src/components/pc/admin/common/CustomSelect.jsx b/frontend/src/components/pc/admin/common/CustomSelect.jsx index a32001c..29f6b97 100644 --- a/frontend/src/components/pc/admin/common/CustomSelect.jsx +++ b/frontend/src/components/pc/admin/common/CustomSelect.jsx @@ -9,10 +9,12 @@ import { ChevronDown } from 'lucide-react'; * @param {Object} props * @param {string} props.value - 선택된 값 * @param {Function} props.onChange - 값 변경 핸들러 - * @param {string[]} props.options - 옵션 목록 + * @param {Array} props.options - 옵션 목록 (문자열 또는 {value, label} 객체) * @param {string} props.placeholder - 플레이스홀더 + * @param {string} props.className - 추가 클래스명 + * @param {string} props.size - 크기 ('sm' | 'md') */ -function CustomSelect({ value, onChange, options, placeholder }) { +function CustomSelect({ value, onChange, options, placeholder, className = '', size = 'md' }) { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); @@ -26,17 +28,29 @@ function CustomSelect({ value, onChange, options, placeholder }) { return () => document.removeEventListener('mousedown', handleClickOutside); }, []); + // 옵션을 {value, label} 형태로 정규화 + const normalizedOptions = options.map((opt) => + typeof opt === 'string' ? { value: opt, label: opt } : opt + ); + + // 현재 선택된 옵션의 라벨 찾기 + const selectedLabel = normalizedOptions.find((opt) => opt.value === value)?.label; + + const sizeClasses = size === 'sm' ? 'px-3 py-2 text-sm' : 'px-4 py-2.5'; + return ( -
+
@@ -49,19 +63,19 @@ function CustomSelect({ value, onChange, options, placeholder }) { transition={{ duration: 0.15 }} className="absolute z-50 w-full mt-2 bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden" > - {options.map((option) => ( + {normalizedOptions.map((option) => ( ))} diff --git a/frontend/src/pages/mobile/album/TrackDetail.jsx b/frontend/src/pages/mobile/album/TrackDetail.jsx index 888e101..2e76f45 100644 --- a/frontend/src/pages/mobile/album/TrackDetail.jsx +++ b/frontend/src/pages/mobile/album/TrackDetail.jsx @@ -37,7 +37,8 @@ function MobileTrackDetail() { enabled: !!albumName && !!trackTitle, }); - const youtubeVideoId = useMemo(() => getYoutubeVideoId(track?.music_video_url), [track?.music_video_url]); + const youtubeVideoId = useMemo(() => getYoutubeVideoId(track?.video_url), [track?.video_url]); + const videoLabel = track?.video_type === 'special' ? '스페셜 영상' : '뮤직비디오'; // 가사 펼침 상태 const [showFullLyrics, setShowFullLyrics] = useState(false); @@ -140,12 +141,12 @@ function MobileTrackDetail() { >

- 뮤직비디오 + {videoLabel}