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;
// 앨범명 변경 시 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 = ['정규', '미니', '싱글'];
const pageVariants = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 }
};
return (
{/* Toast */}
setToast(null)} />
{/* 헤더 */}
{/* 메인 콘텐츠 */}
{/* 브레드크럼 */}
앨범 관리
{isEditMode ? '앨범 수정' : '새 앨범 추가'}
{/* 타이틀 */}
{isEditMode ? '앨범 수정' : '새 앨범 추가'}
앨범 정보와 트랙을 입력하세요
{loading ? (
) : (
)}
);
}
export default AdminAlbumForm;