fromis_9/frontend/src/pages/pc/admin/AdminAlbumPhotos.jsx

824 lines
40 KiB
React
Raw Normal View History

import { useState, useEffect, useRef } from 'react';
import { useNavigate, Link, useParams } from 'react-router-dom';
import { motion, AnimatePresence, Reorder } from 'framer-motion';
import {
Upload, Trash2, Image, X, Check, Plus,
Home, ChevronRight, LogOut, ArrowLeft, Grid, List,
ZoomIn, AlertTriangle, GripVertical, Users, User, Users2,
Tag, FolderOpen, Save
} from 'lucide-react';
import Toast from '../../../components/Toast';
// 멤버 목록
const MEMBERS = ['이서연', '송하영', '장규리', '박지원', '이나경', '이채영', '백지헌'];
function AdminAlbumPhotos() {
const { albumId } = useParams();
const navigate = useNavigate();
const fileInputRef = useRef(null);
const [album, setAlbum] = useState(null);
const [photos, setPhotos] = useState([]);
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
const [toast, setToast] = useState(null);
const [selectedPhotos, setSelectedPhotos] = useState([]);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [deleteDialog, setDeleteDialog] = useState({ show: false, photos: [] });
const [deleting, setDeleting] = useState(false);
const [previewPhoto, setPreviewPhoto] = useState(null);
const [dragOver, setDragOver] = useState(false);
// 업로드 대기 중인 파일들
const [pendingFiles, setPendingFiles] = useState([]);
const [photoType, setPhotoType] = useState('concept'); // 'concept' | 'teaser'
const [conceptName, setConceptName] = useState('');
const [saving, setSaving] = useState(false);
const [pendingDeleteId, setPendingDeleteId] = useState(null); // 삭제 대기 파일 ID
// Toast 자동 숨김
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}
}, [toast]);
useEffect(() => {
// 로그인 확인
const token = localStorage.getItem('adminToken');
const userData = localStorage.getItem('adminUser');
if (!token || !userData) {
navigate('/admin');
return;
}
setUser(JSON.parse(userData));
fetchAlbumData();
}, [navigate, albumId]);
const fetchAlbumData = async () => {
try {
// 앨범 정보 로드
const albumRes = await fetch(`/api/albums/${albumId}`);
if (!albumRes.ok) throw new Error('앨범을 찾을 수 없습니다');
const albumData = await albumRes.json();
setAlbum(albumData);
// TODO: 기존 사진 목록 로드 (API 구현 후)
setPhotos([]);
setLoading(false);
} catch (error) {
console.error('앨범 로드 오류:', error);
setToast({ message: error.message, type: 'error' });
setLoading(false);
}
};
const handleLogout = () => {
localStorage.removeItem('adminToken');
localStorage.removeItem('adminUser');
navigate('/admin');
};
// 파일 선택
const handleFileSelect = (e) => {
const files = Array.from(e.target.files);
addFilesToPending(files);
e.target.value = ''; // 같은 파일 재선택 가능하도록
};
// 드래그 앤 드롭 (깜빡임 방지를 위해 카운터 사용)
const dragCounterRef = useRef(0);
const handleDragEnter = (e) => {
e.preventDefault();
e.stopPropagation();
dragCounterRef.current++;
if (e.dataTransfer.types.includes('Files')) {
setDragOver(true);
}
};
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
dragCounterRef.current--;
if (dragCounterRef.current === 0) {
setDragOver(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
dragCounterRef.current = 0;
setDragOver(false);
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
if (files.length > 0) {
addFilesToPending(files);
}
};
// 대기 목록에 파일 추가 (중복 방지)
const addFilesToPending = (files) => {
// 기존 파일명 목록으로 중복 체크 (파일명 + 크기로 비교)
const existingKeys = new Set(
pendingFiles.map(f => `${f.filename}_${f.file.size}`)
);
const uniqueFiles = files.filter(file => {
const key = `${file.name}_${file.size}`;
if (existingKeys.has(key)) {
return false; // 중복
}
existingKeys.add(key);
return true;
});
if (uniqueFiles.length < files.length) {
const duplicateCount = files.length - uniqueFiles.length;
setToast({
message: `${duplicateCount}개의 중복 파일이 제외되었습니다.`,
type: 'warning'
});
}
if (uniqueFiles.length === 0) return;
const newFiles = uniqueFiles.map((file, index) => ({
id: Date.now() + index,
file,
preview: URL.createObjectURL(file),
filename: file.name,
groupType: 'group', // 'group' | 'solo' | 'unit'
members: [], // 태깅된 멤버들 (단체일 경우 빈 배열)
conceptName: '', // 개별 컨셉명
}));
setPendingFiles(prev => [...prev, ...newFiles]);
};
// 대기 파일 순서 변경 (Reorder)
const handleReorder = (newOrder) => {
setPendingFiles(newOrder);
};
// 직접 순서 변경 (입력으로)
const moveToPosition = (fileId, newPosition) => {
const pos = parseInt(newPosition, 10);
if (isNaN(pos) || pos < 1) return;
setPendingFiles(prev => {
const targetIndex = Math.min(pos - 1, prev.length - 1);
const currentIndex = prev.findIndex(f => f.id === fileId);
if (currentIndex === -1 || currentIndex === targetIndex) return prev;
const newFiles = [...prev];
const [removed] = newFiles.splice(currentIndex, 1);
newFiles.splice(targetIndex, 0, removed);
return newFiles;
});
};
// 대기 파일 삭제 (확인 후)
const confirmDeletePendingFile = () => {
if (!pendingDeleteId) return;
setPendingFiles(prev => {
const file = prev.find(f => f.id === pendingDeleteId);
if (file) URL.revokeObjectURL(file.preview);
return prev.filter(f => f.id !== pendingDeleteId);
});
setPendingDeleteId(null);
};
// 대기 파일 메타 정보 수정
const updatePendingFile = (id, field, value) => {
setPendingFiles(prev => prev.map(f =>
f.id === id ? { ...f, [field]: value } : f
));
};
// 멤버 토글 (솔로일 경우 한 명만, 유닛일 경우 다중 선택)
const toggleMember = (fileId, member) => {
setPendingFiles(prev => prev.map(f => {
if (f.id !== fileId) return f;
// 솔로일 경우 한 명만 선택 가능
if (f.groupType === 'solo') {
return { ...f, members: f.members.includes(member) ? [] : [member] };
}
// 유닛일 경우 다중 선택
const members = f.members.includes(member)
? f.members.filter(m => m !== member)
: [...f.members, member];
return { ...f, members };
}));
};
// 타입 변경 시 멤버 초기화
const changeGroupType = (fileId, newType) => {
setPendingFiles(prev => prev.map(f => {
if (f.id !== fileId) return f;
// 단체 선택 시 멤버 비움 (단체는 멤버 태깅 안 함)
if (newType === 'group') {
return { ...f, groupType: newType, members: [] };
}
// 솔로/유닛 변경 시 멤버 유지 (솔로인 경우 첫 번째만)
if (newType === 'solo' && f.members.length > 1) {
return { ...f, groupType: newType, members: [f.members[0]] };
}
return { ...f, groupType: newType };
}));
};
// 업로드 처리 (임시)
const handleUpload = async () => {
if (pendingFiles.length === 0) {
setToast({ message: '업로드할 사진을 선택해주세요.', type: 'warning' });
return;
}
// 컨셉명 검증 (각 파일별로)
const missingConcept = pendingFiles.some(f => !f.conceptName.trim());
if (missingConcept) {
setToast({ message: '모든 사진의 컨셉/티저 이름을 입력해주세요.', type: 'warning' });
return;
}
// 솔로/유닛인데 멤버 선택 안 한 경우
const missingMembers = pendingFiles.some(f =>
(f.groupType === 'solo' || f.groupType === 'unit') && f.members.length === 0
);
if (missingMembers) {
setToast({ message: '솔로/유닛 사진에는 멤버를 선택해주세요.', type: 'warning' });
return;
}
setSaving(true);
setUploadProgress(0);
try {
// 임시 진행률 시뮬레이션
for (let i = 0; i <= 100; i += 10) {
await new Promise(r => setTimeout(r, 100));
setUploadProgress(i);
}
// TODO: 실제 업로드 API 호출
// const formData = new FormData();
// pendingFiles.forEach((pf, idx) => {
// formData.append('photos', pf.file);
// formData.append(`meta_${idx}`, JSON.stringify({
// order: idx + 1,
// groupType: pf.groupType,
// members: pf.members,
// }));
// });
// formData.append('photoType', photoType);
// formData.append('conceptName', conceptName);
setToast({ message: `${pendingFiles.length}개의 사진이 업로드되었습니다.`, type: 'success' });
// 미리보기 URL 해제
pendingFiles.forEach(f => URL.revokeObjectURL(f.preview));
setPendingFiles([]);
setConceptName('');
} catch (error) {
console.error('업로드 오류:', error);
setToast({ message: '업로드 중 오류가 발생했습니다.', type: 'error' });
} finally {
setSaving(false);
setUploadProgress(0);
}
};
// 삭제 처리 (기존 사진)
const handleDelete = async () => {
setDeleting(true);
// TODO: 실제 삭제 API 호출
await new Promise(r => setTimeout(r, 1000));
setPhotos(prev => prev.filter(p => !deleteDialog.photos.includes(p.id)));
setSelectedPhotos([]);
setToast({ message: `${deleteDialog.photos.length}개의 사진이 삭제되었습니다.`, type: 'success' });
setDeleteDialog({ show: false, photos: [] });
setDeleting(false);
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
{/* Toast */}
<Toast toast={toast} onClose={() => setToast(null)} />
{/* 삭제 확인 다이얼로그 */}
<AnimatePresence>
{deleteDialog.show && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={() => !deleting && setDeleteDialog({ show: false, photos: [] })}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-white rounded-2xl p-6 max-w-md w-full mx-4 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
<AlertTriangle className="text-red-500" size={20} />
</div>
<h3 className="text-lg font-bold text-gray-900">사진 삭제</h3>
</div>
<p className="text-gray-600 mb-6">
<span className="font-medium text-gray-900">{deleteDialog.photos.length}</span> 사진을 삭제하시겠습니까?
<br />
<span className="text-sm text-red-500"> 작업은 되돌릴 없습니다.</span>
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setDeleteDialog({ show: false, photos: [] })}
disabled={deleting}
className="px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
>
취소
</button>
<button
onClick={handleDelete}
disabled={deleting}
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50 flex items-center gap-2"
>
{deleting ? (
<>
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
삭제 ...
</>
) : (
<>
<Trash2 size={16} />
삭제
</>
)}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
{/* 이미지 미리보기 */}
<AnimatePresence>
{previewPhoto && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
onClick={() => setPreviewPhoto(null)}
>
<button
onClick={() => setPreviewPhoto(null)}
className="absolute top-4 right-4 p-2 text-white/70 hover:text-white transition-colors"
>
<X size={24} />
</button>
<motion.img
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
exit={{ scale: 0.9 }}
src={previewPhoto.preview || previewPhoto.url}
alt={previewPhoto.filename}
className="max-w-[90vw] max-h-[90vh] object-contain"
onClick={(e) => e.stopPropagation()}
/>
</motion.div>
)}
</AnimatePresence>
{/* 헤더 */}
<header className="bg-white shadow-sm border-b border-gray-100">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<Link to="/" className="text-2xl font-bold text-primary hover:opacity-80 transition-opacity">
fromis_9
</Link>
<span className="px-3 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full">
Admin
</span>
</div>
<div className="flex items-center gap-4">
<span className="text-gray-500 text-sm">
안녕하세요, <span className="text-gray-900 font-medium">{user?.username}</span>
</span>
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<LogOut size={18} />
<span>로그아웃</span>
</button>
</div>
</div>
</header>
{/* 메인 콘텐츠 */}
<main className="max-w-7xl mx-auto px-6 py-8">
{/* 브레드크럼 */}
<motion.div
className="flex items-center gap-2 text-sm text-gray-400 mb-8"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
>
<Link to="/admin/dashboard" className="hover:text-primary transition-colors">
<Home size={16} />
</Link>
<ChevronRight size={14} />
<Link to="/admin/albums" className="hover:text-primary transition-colors">앨범 관리</Link>
<ChevronRight size={14} />
<span className="text-gray-700">{album?.title} - 사진 관리</span>
</motion.div>
{/* 타이틀 */}
<motion.div
className="flex items-center gap-4 mb-8"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
>
<img
src={album?.cover_url}
alt={album?.title}
className="w-14 h-14 rounded-xl object-cover"
/>
<div>
<h1 className="text-2xl font-bold text-gray-900">{album?.title}</h1>
<p className="text-gray-500">사진 업로드 관리</p>
</div>
</motion.div>
{/* 업로드 설정 */}
<motion.div
className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6 mb-6"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
<h2 className="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
<FolderOpen size={20} />
업로드 설정
</h2>
{/* 타입 선택 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">사진 타입 *</label>
<div className="flex gap-2 max-w-xs">
<button
onClick={() => setPhotoType('concept')}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${
photoType === 'concept'
? 'bg-primary text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
컨셉 포토
</button>
<button
onClick={() => setPhotoType('teaser')}
className={`flex-1 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${
photoType === 'teaser'
? 'bg-primary text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
티저 이미지
</button>
</div>
<p className="text-xs text-gray-400 mt-2">
컨셉/티저 이름은 사진별로 입력합니다.
</p>
</div>
</motion.div>
{/* 업로드 진행률 */}
{saving && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-6 bg-white rounded-xl p-4 border border-gray-100 shadow-sm"
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">업로드 ...</span>
<span className="text-sm font-medium text-primary">{uploadProgress}%</span>
</div>
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${uploadProgress}%` }}
className="h-full bg-primary rounded-full"
/>
</div>
</motion.div>
)}
{/* 드래그 앤 드롭 영역 + 파일 목록 */}
<motion.div
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.25 }}
className={`relative rounded-2xl border-2 border-dashed transition-colors bg-white ${
dragOver
? 'border-primary bg-primary/5'
: 'border-gray-200'
}`}
>
{/* 드래그 오버레이 */}
{dragOver && (
<div className="absolute inset-0 flex items-center justify-center bg-primary/10 rounded-2xl z-10">
<div className="text-center">
<Upload size={48} className="mx-auto text-primary mb-2" />
<p className="text-primary font-medium">여기에 사진을 놓으세요</p>
</div>
</div>
)}
{pendingFiles.length === 0 ? (
/* 빈 상태 */
<div className="py-20 text-center">
<Image size={48} className="mx-auto text-gray-300 mb-4" />
<p className="text-gray-500 mb-2">사진을 드래그하여 업로드하세요</p>
<p className="text-gray-400 text-sm mb-4">JPG, PNG, WebP 지원</p>
<button
onClick={() => fileInputRef.current?.click()}
className="inline-flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
<Plus size={18} />
파일 선택
</button>
</div>
) : (
/* 파일 목록 (드래그 정렬 가능) */
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-gray-500">
드래그하여 순서를 변경할 있습니다. 순서대로 <code className="bg-gray-100 px-1 rounded">01.webp</code>, <code className="bg-gray-100 px-1 rounded">02.webp</code>... 저장됩니다.
</p>
<button
onClick={() => fileInputRef.current?.click()}
className="text-sm text-primary hover:text-primary-dark transition-colors"
>
+ 추가
</button>
</div>
<Reorder.Group
axis="y"
values={pendingFiles}
onReorder={handleReorder}
className="space-y-3"
>
{pendingFiles.map((file, index) => (
<Reorder.Item
key={file.id}
value={file}
className="bg-gray-50 rounded-xl p-4 cursor-grab active:cursor-grabbing"
>
<div className="flex gap-4 items-center">
{/* 드래그 핸들 + 순서 번호 (직접 입력 가능) */}
<div className="flex items-center gap-2">
<GripVertical size={18} className="text-gray-300" />
<input
type="text"
inputMode="numeric"
defaultValue={String(index + 1).padStart(2, '0')}
key={`order-${file.id}-${index}`}
onBlur={(e) => {
const val = e.target.value.trim();
if (val && !isNaN(val)) {
moveToPosition(file.id, val);
}
e.target.value = String(pendingFiles.findIndex(f => f.id === file.id) + 1).padStart(2, '0');
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.target.blur();
}
}}
className="w-10 h-8 bg-primary/10 text-primary rounded-lg text-center text-sm font-bold border-0 focus:outline-none focus:ring-2 focus:ring-primary [appearance:textfield]"
title="순서를 직접 입력할 수 있습니다"
/>
</div>
{/* 썸네일 (정사각형) */}
<img
src={file.preview}
alt={file.filename}
draggable="false"
className="w-36 h-36 rounded-xl object-cover cursor-pointer hover:opacity-80 transition-opacity flex-shrink-0 select-none"
onClick={() => setPreviewPhoto(file)}
/>
{/* 메타 정보 */}
<div className="flex-1 space-y-3">
{/* 파일명 */}
<p className="text-base font-medium text-gray-900 truncate">{file.filename}</p>
{/* 단체/솔로/유닛 선택 */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500 w-16">타입:</span>
<div className="flex gap-1.5">
{[
{ value: 'group', icon: Users, label: '단체' },
{ value: 'solo', icon: User, label: '솔로' },
{ value: 'unit', icon: Users2, label: '유닛' },
].map(({ value, icon: Icon, label }) => (
<button
key={value}
onClick={() => changeGroupType(file.id, value)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors ${
file.groupType === value
? 'bg-primary text-white'
: 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'
}`}
>
<Icon size={14} />
{label}
</button>
))}
</div>
</div>
{/* 멤버 태깅 (단체는 비활성화) */}
<div className="flex items-center gap-2 flex-wrap min-h-8">
<span className="text-sm text-gray-500 w-16">멤버:</span>
{file.groupType === 'group' ? (
<span className="text-sm text-gray-400">단체 사진은 멤버 태깅이 필요 없습니다</span>
) : (
MEMBERS.map(member => (
<button
key={member}
onClick={() => toggleMember(file.id, member)}
className={`px-3 py-1 rounded-full text-sm transition-colors ${
file.members.includes(member)
? 'bg-primary text-white'
: 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'
}`}
>
{member}
</button>
))
)}
{file.groupType === 'solo' && (
<span className="text-xs text-gray-400 ml-2">( 명만 선택)</span>
)}
</div>
{/* 컨셉/티저 이름 (개별 입력) */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500 w-16">컨셉명:</span>
<input
type="text"
value={file.conceptName}
onChange={(e) => updatePendingFile(file.id, 'conceptName', e.target.value)}
className="flex-1 px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary"
placeholder="컨셉명을 입력하세요"
/>
</div>
</div>
{/* 삭제 버튼 */}
<button
onClick={() => setPendingDeleteId(file.id)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors self-start"
>
<Trash2 size={18} />
</button>
</div>
</Reorder.Item>
))}
</Reorder.Group>
</div>
)}
</motion.div>
{/* 하단 액션 버튼 */}
{pendingFiles.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mt-6 flex justify-end gap-4"
>
<button
onClick={() => {
pendingFiles.forEach(f => URL.revokeObjectURL(f.preview));
setPendingFiles([]);
}}
className="px-6 py-2.5 text-gray-600 hover:text-gray-900 transition-colors"
>
취소
</button>
<button
onClick={handleUpload}
disabled={saving}
className="flex items-center gap-2 px-6 py-2.5 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors disabled:opacity-50"
>
{saving ? (
<>
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
업로드 ...
</>
) : (
<>
<Save size={18} />
{pendingFiles.length} 사진 업로드
</>
)}
</button>
</motion.div>
)}
{/* 삭제 확인 다이얼로그 */}
<AnimatePresence>
{pendingDeleteId && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={() => setPendingDeleteId(null)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
onClick={(e) => e.stopPropagation()}
className="bg-white rounded-2xl p-6 max-w-sm w-full mx-4 shadow-xl"
>
<div className="text-center">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Trash2 className="text-red-500" size={24} />
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">사진을 삭제할까요?</h3>
<p className="text-gray-500 text-sm mb-6">
사진을 목록에서 제거합니다.
</p>
<div className="flex gap-3">
<button
onClick={() => setPendingDeleteId(null)}
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
취소
</button>
<button
onClick={confirmDeletePendingFile}
className="flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
삭제
</button>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
<input
ref={fileInputRef}
type="file"
multiple
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
</main>
</div>
);
}
export default AdminAlbumPhotos;