import { useState, useEffect, useRef } from 'react'; import { useNavigate, useParams, Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Save, Home, ChevronRight, LogOut, Music, Trash2, Plus, Image, Star, ChevronDown, ChevronLeft, Calendar } from 'lucide-react'; import Toast from '../../../components/Toast'; // 커스텀 드롭다운 컴포넌트 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 CustomDatePicker({ value, onChange }) { const [isOpen, setIsOpen] = useState(false); const [viewMode, setViewMode] = useState('days'); // 'days' | 'months' | 'years' const [viewDate, setViewDate] = useState(() => { if (value) return new Date(value); return new Date(); }); const ref = useRef(null); useEffect(() => { const handleClickOutside = (e) => { if (ref.current && !ref.current.contains(e.target)) { setIsOpen(false); setViewMode('days'); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const year = viewDate.getFullYear(); const month = viewDate.getMonth(); const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const days = []; for (let i = 0; i < firstDay; i++) { days.push(null); } for (let i = 1; i <= daysInMonth; i++) { days.push(i); } // 년도 범위 (현재 년도 기준 -10 ~ +10) const startYear = Math.floor(year / 10) * 10 - 1; const years = Array.from({ length: 12 }, (_, i) => startYear + i); const prevMonth = () => setViewDate(new Date(year, month - 1, 1)); const nextMonth = () => setViewDate(new Date(year, month + 1, 1)); const prevYearRange = () => setViewDate(new Date(year - 10, month, 1)); const nextYearRange = () => setViewDate(new Date(year + 10, month, 1)); const selectDate = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; onChange(dateStr); setIsOpen(false); setViewMode('days'); }; const selectYear = (y) => { setViewDate(new Date(y, month, 1)); setViewMode('months'); }; const selectMonth = (m) => { setViewDate(new Date(year, m, 1)); setViewMode('days'); }; const formatDisplayDate = (dateStr) => { if (!dateStr) return ''; const [y, m, d] = dateStr.split('-'); return `${y}년 ${parseInt(m)}월 ${parseInt(d)}일`; }; const isSelected = (day) => { if (!value || !day) return false; const [y, m, d] = value.split('-'); return parseInt(y) === year && parseInt(m) === month + 1 && parseInt(d) === day; }; const isToday = (day) => { if (!day) return false; const today = new Date(); return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day; }; const isCurrentYear = (y) => { return new Date().getFullYear() === y; }; const isCurrentMonth = (m) => { const today = new Date(); return today.getFullYear() === year && today.getMonth() === m; }; const months = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; return (
{isOpen && ( {/* 헤더 */}
{viewMode === 'years' && ( {/* 년도 라벨 */}
년도
{/* 년도 그리드 */}
{years.map((y) => ( ))}
{/* 월 라벨 */}
{/* 월 그리드 */}
{months.map((m, i) => ( ))}
)} {viewMode === 'months' && ( {/* 월 라벨 */}
월 선택
{/* 월 그리드 */}
{months.map((m, i) => ( ))}
)} {viewMode === 'days' && ( {/* 요일 */}
{['일', '월', '화', '수', '목', '금', '토'].map((d, i) => (
{d}
))}
{/* 날짜 */}
{days.map((day, i) => ( ))}
)}
)}
); } 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] = useState(null); // Toast 자동 숨김 useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(null), 3000); return () => clearTimeout(timer); } }, [toast]); const [formData, setFormData] = useState({ title: '', album_type: '', album_type_short: '', release_date: '', cover_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_url: data.cover_url || '', folder_name: data.folder_name || '', description: data.description || '', }); if (data.cover_url) { setCoverPreview(data.cover_url); } setTracks(data.tracks || []); setLoading(false); }) .catch(error => { console.error('앨범 로드 오류:', error); setLoading(false); }); } }, [id, isEditMode, navigate]); const handleLogout = () => { localStorage.removeItem('adminToken'); localStorage.removeItem('adminUser'); navigate('/admin'); }; const handleInputChange = (e) => { const { name, value } = e.target; 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 = ['정규', '미니', '싱글']; const pageVariants = { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 } }; return ( {/* Toast */} setToast(null)} /> {/* 헤더 */}
fromis_9 Admin
안녕하세요, {user?.username}
{/* 메인 콘텐츠 */}
{/* 브레드크럼 */} 앨범 관리 {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 }))} />
{/* 설명 */}