diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx
index a588d58..0ee1669 100644
--- a/frontend/src/pages/Admin.jsx
+++ b/frontend/src/pages/Admin.jsx
@@ -155,6 +155,8 @@ export default function Admin({ isMobile = false }) {
const [isIconDragging, setIsIconDragging] = useState(false);
const [deleteIconDialog, setDeleteIconDialog] = useState({ show: false, modId: null });
const [isIconListExpanded, setIsIconListExpanded] = useState(false);
+ const [pendingIconFiles, setPendingIconFiles] = useState([]); // 아이콘 파일 대기열
+ const [clearingIconFiles, setClearingIconFiles] = useState(false);
// 권한 확인
useEffect(() => {
@@ -324,52 +326,91 @@ export default function Admin({ isMobile = false }) {
}
}, []);
- // 아이콘 ZIP 업로드 (여러 파일 지원)
- const handleIconZipUpload = async (files) => {
+ // 아이콘 ZIP 파일 추가 (대기열에만 추가)
+ const addIconFiles = (files) => {
const fileArray = Array.isArray(files) ? files : [files];
const zipFiles = fileArray.filter(f => f && f.name.endsWith('.zip'));
if (zipFiles.length === 0) {
- setToast('ZIP 파일만 업로드 가능합니다', true);
+ setToast('ZIP 파일만 추가 가능합니다', true);
return;
}
- setIconUploading(true);
- try {
- const token = localStorage.getItem('token');
- const formData = new FormData();
- zipFiles.forEach(file => formData.append('files', file));
-
- const res = await fetch('/api/admin/icons/upload', {
- method: 'POST',
- headers: { 'Authorization': `Bearer ${token}` },
- body: formData
- });
-
- const data = await res.json();
- if (data.success) {
- const totalUploaded = data.results?.reduce((sum, r) => sum + r.uploaded, 0) || 0;
- const totalUpdated = data.results?.reduce((sum, r) => sum + r.updated, 0) || 0;
- const modIds = data.results?.map(r => r.modId).join(', ') || '';
- setToast(`아이콘 업로드 완료: ${modIds} (${totalUploaded}개 업로드, ${totalUpdated}개 DB 업데이트)`);
-
- // 에러가 있는 경우 추가 알림
- if (data.errors?.length > 0) {
- console.warn('아이콘 업로드 일부 실패:', data.errors);
- }
- fetchIconMods();
- } else {
- const errorMsg = data.errors?.map(e => `${e.file}: ${e.error}`).join('\n') || data.error || '업로드 실패';
- setToast(errorMsg, true);
- }
- } catch (error) {
- console.error('아이콘 업로드 실패:', error);
- setToast('업로드 중 오류 발생', true);
- } finally {
- setIconUploading(false);
+ // 중복 체크 후 대기열에 추가
+ const newFiles = zipFiles
+ .filter(f => !pendingIconFiles.some(pf => pf.name === f.name))
+ .map(f => ({ name: f.name, file: f, status: 'pending' }));
+
+ if (newFiles.length > 0) {
+ setPendingIconFiles(prev => [...prev, ...newFiles]);
}
};
+ // 아이콘 파일 제거
+ const removeIconFile = (fileName) => {
+ setPendingIconFiles(prev => prev.filter(f => f.name !== fileName));
+ };
+
+ // 완료된 아이콘 파일 지우기
+ const clearCompletedIconFiles = () => {
+ setClearingIconFiles(true);
+ setTimeout(() => {
+ setPendingIconFiles(prev => prev.filter(f => f.status === 'pending'));
+ setClearingIconFiles(false);
+ }, 300);
+ };
+
+ // 아이콘 업로드 시작
+ const startIconUpload = async () => {
+ const filesToUpload = pendingIconFiles.filter(f => f.status === 'pending');
+ if (filesToUpload.length === 0) return;
+
+ setIconUploading(true);
+
+ for (const fileObj of filesToUpload) {
+ // 상태를 'processing'으로 변경
+ setPendingIconFiles(prev =>
+ prev.map(f => f.name === fileObj.name ? { ...f, status: 'processing' } : f)
+ );
+
+ try {
+ const token = localStorage.getItem('token');
+ const formData = new FormData();
+ formData.append('files', fileObj.file);
+
+ const res = await fetch('/api/admin/icons/upload', {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${token}` },
+ body: formData
+ });
+
+ const data = await res.json();
+ if (data.success && data.results?.length > 0) {
+ const result = data.results[0];
+ setPendingIconFiles(prev =>
+ prev.map(f => f.name === fileObj.name ? {
+ ...f,
+ status: 'success',
+ result: `${result.uploaded}개 업로드, ${result.updated}개 DB 업데이트`
+ } : f)
+ );
+ } else {
+ const errorMsg = data.errors?.[0]?.error || data.error || '업로드 실패';
+ setPendingIconFiles(prev =>
+ prev.map(f => f.name === fileObj.name ? { ...f, status: 'error', error: errorMsg } : f)
+ );
+ }
+ } catch (error) {
+ setPendingIconFiles(prev =>
+ prev.map(f => f.name === fileObj.name ? { ...f, status: 'error', error: error.message } : f)
+ );
+ }
+ }
+
+ setIconUploading(false);
+ fetchIconMods();
+ };
+
// 아이콘 모드 삭제
const deleteIconMod = async (modId) => {
try {
@@ -2136,14 +2177,14 @@ export default function Admin({ isMobile = false }) {
isIconDragging
? 'border-emerald-500 bg-emerald-500/10'
: 'border-zinc-700 hover:border-emerald-500 hover:bg-zinc-800/50'
- } ${iconUploading ? 'pointer-events-none opacity-50' : ''}`}
+ }`}
onDragOver={(e) => { e.preventDefault(); setIsIconDragging(true); }}
onDragLeave={() => setIsIconDragging(false)}
onDrop={(e) => {
e.preventDefault();
setIsIconDragging(false);
const files = Array.from(e.dataTransfer.files);
- if (files.length > 0) handleIconZipUpload(files);
+ if (files.length > 0) addIconFiles(files);
}}
>
{
const files = Array.from(e.target.files);
- if (files.length > 0) handleIconZipUpload(files);
+ if (files.length > 0) addIconFiles(files);
e.target.value = '';
}}
/>
- {iconUploading ? (
-