import { useState, useEffect, useRef } from 'react'; import { useNavigate, useParams, Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Save, Home, ChevronRight, Music, Trash2, Plus, Image, Star, ChevronDown } from 'lucide-react'; import Toast from '../../../components/Toast'; import CustomDatePicker from '../../../components/admin/CustomDatePicker'; import AdminLayout from '../../../components/admin/AdminLayout'; import useToast from '../../../hooks/useToast'; // 커스텀 드롭다운 컴포넌트 function CustomSelect({ value, onChange, options, placeholder }) { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); useEffect(() => { const handleClickOutside = (e) => { if (ref.current && !ref.current.contains(e.target)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); return (
{isOpen && ( {options.map((option) => ( ))} )}
); } function AdminAlbumForm() { const navigate = useNavigate(); const { id } = useParams(); const isEditMode = !!id; const coverInputRef = useRef(null); const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [coverPreview, setCoverPreview] = useState(null); const [coverFile, setCoverFile] = useState(null); const { toast, setToast } = useToast(); const [formData, setFormData] = useState({ title: '', album_type: '', album_type_short: '', release_date: '', cover_original_url: '', cover_medium_url: '', cover_thumb_url: '', folder_name: '', description: '', }); const [tracks, setTracks] = useState([]); useEffect(() => { const token = localStorage.getItem('adminToken'); const userData = localStorage.getItem('adminUser'); if (!token || !userData) { navigate('/admin'); return; } setUser(JSON.parse(userData)); if (isEditMode) { setLoading(true); fetch(`/api/albums/${id}`) .then(res => res.json()) .then(data => { setFormData({ title: data.title || '', album_type: data.album_type || '', album_type_short: data.album_type_short || '', release_date: data.release_date ? data.release_date.split('T')[0] : '', cover_original_url: data.cover_original_url || '', cover_medium_url: data.cover_medium_url || '', cover_thumb_url: data.cover_thumb_url || '', folder_name: data.folder_name || '', description: data.description || '', }); if (data.cover_medium_url || data.cover_original_url) { setCoverPreview(data.cover_medium_url || data.cover_original_url); } setTracks(data.tracks || []); setLoading(false); }) .catch(error => { console.error('앨범 로드 오류:', error); setLoading(false); }); } }, [id, isEditMode, navigate]); const handleInputChange = (e) => { const { name, value } = e.target; // 앨범명 변경 시 RustFS 폴더명 자동 생성 if (name === 'title') { const folderName = value .toLowerCase() .replace(/[\s.]+/g, '-') // 띄어쓰기, 점을 하이픈으로 .replace(/[^a-z0-9가-힣-]/g, '') // 특수문자 제거 (영문, 숫자, 한글, 하이픈만 유지) .replace(/-+/g, '-') // 연속 하이픈 하나로 .replace(/^-|-$/g, ''); // 앞뒤 하이픈 제거 setFormData(prev => ({ ...prev, title: value, folder_name: folderName })); } else { setFormData(prev => ({ ...prev, [name]: value })); } }; const handleCoverChange = (e) => { const file = e.target.files[0]; if (file) { setCoverFile(file); const reader = new FileReader(); reader.onloadend = () => { setCoverPreview(reader.result); }; reader.readAsDataURL(file); } }; const addTrack = () => { setTracks(prev => [...prev, { track_number: prev.length + 1, title: '', is_title_track: false, duration: '', }]); }; const removeTrack = (index) => { setTracks(prev => prev.filter((_, i) => i !== index).map((track, i) => ({ ...track, track_number: i + 1 }))); }; const updateTrack = (index, field, value) => { // 작사/작곡/편곡 필드에서 '|' (전각 세로 막대)를 ', '로 자동 변환 let processedValue = value; if (['lyricist', 'composer', 'arranger'].includes(field)) { processedValue = value.replace(/[||]/g, ', '); } setTracks(prev => prev.map((track, i) => i === index ? { ...track, [field]: processedValue } : track )); }; const handleSubmit = async (e) => { e.preventDefault(); // 커스텀 검증 if (!formData.title.trim()) { setToast({ message: '앨범명을 입력해주세요.', type: 'warning' }); return; } if (!formData.folder_name.trim()) { setToast({ message: 'RustFS 폴더명을 입력해주세요.', type: 'warning' }); return; } if (!formData.album_type_short) { setToast({ message: '앨범 타입을 선택해주세요.', type: 'warning' }); return; } if (!formData.release_date) { setToast({ message: '발매일을 선택해주세요.', type: 'warning' }); return; } if (!formData.album_type.trim()) { setToast({ message: '앨범 유형을 입력해주세요.', type: 'warning' }); return; } setSaving(true); try { const token = localStorage.getItem('adminToken'); const url = isEditMode ? `/api/admin/albums/${id}` : '/api/admin/albums'; const method = isEditMode ? 'PUT' : 'POST'; const submitData = new FormData(); submitData.append('data', JSON.stringify({ ...formData, tracks })); if (coverFile) { submitData.append('cover', coverFile); } const response = await fetch(url, { method, headers: { 'Authorization': `Bearer ${token}`, }, body: submitData, }); if (!response.ok) { throw new Error('저장 실패'); } navigate('/admin/albums'); } catch (error) { console.error('저장 오류:', error); setToast({ message: '저장 중 오류가 발생했습니다.', type: 'error' }); } finally { setSaving(false); } }; const albumTypes = ['정규', '미니', '싱글']; return ( {/* Toast */} setToast(null)} /> {/* 메인 콘텐츠 */}
{/* 브레드크럼 */} 앨범 관리 {isEditMode ? '앨범 수정' : '새 앨범 추가'} {/* 타이틀 */}

{isEditMode ? '앨범 수정' : '새 앨범 추가'}

앨범 정보와 트랙을 입력하세요

{loading ? (
) : (
{/* 앨범 기본 정보 */}

앨범 정보

{/* 커버 이미지 */}
coverInputRef.current?.click()} className="w-40 h-40 rounded-xl border-2 border-dashed border-gray-200 flex items-center justify-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors overflow-hidden" > {coverPreview ? ( 커버 미리보기 ) : (

클릭하여 업로드

)}

권장 크기: 1000x1000px

지원 형식: JPG, PNG, WebP

{coverPreview && ( )}
{/* 앨범명 */}
{/* 폴더명 */}
fromis-9/album/

영문 소문자, 숫자, 하이픈만 사용

{/* 앨범 타입 - 커스텀 드롭다운 */}
setFormData(prev => ({ ...prev, album_type_short: val }))} options={albumTypes} placeholder="타입 선택" />
{/* 앨범 유형 (전체) */}
{/* 발매일 - 커스텀 데이트픽커 */}
setFormData(prev => ({ ...prev, release_date: val }))} />
{/* 설명 */}