feat: 로그 파일 삭제 다이얼로그 및 일괄 삭제 기능

- alert 대신 다이얼로그로 삭제 확인
- 체크박스로 파일 선택 기능
- 전체 선택/해제 및 일괄 삭제 지원
- 삭제 중 로딩 상태 표시
This commit is contained in:
caadiq 2025-12-27 16:29:03 +09:00
parent 4108e4547d
commit e13f608d88

View file

@ -89,6 +89,8 @@ export default function Admin({ isMobile = false }) {
const [logLoading, setLogLoading] = useState(false); // const [logLoading, setLogLoading] = useState(false); //
const [serverDropdownOpen, setServerDropdownOpen] = useState(false); // const [serverDropdownOpen, setServerDropdownOpen] = useState(false); //
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false); // const [typeDropdownOpen, setTypeDropdownOpen] = useState(false); //
const [deleteLogDialog, setDeleteLogDialog] = useState({ show: false, files: [], loading: false }); //
const [selectedLogFiles, setSelectedLogFiles] = useState(new Set()); // ID
const logEndRef = useRef(null); const logEndRef = useRef(null);
const logContainerRef = useRef(null); const logContainerRef = useRef(null);
const isInitialLoad = useRef(true); const isInitialLoad = useRef(true);
@ -832,23 +834,66 @@ export default function Admin({ isMobile = false }) {
} }
}; };
// //
const deleteLogFile = async (file, e) => { const openDeleteLogDialog = (files, e) => {
if (e) e.stopPropagation(); if (e) e.stopPropagation();
if (!confirm(`${file.fileName} 파일을 삭제하시겠습니까?`)) return; const fileArray = Array.isArray(files) ? files : [files];
setDeleteLogDialog({ show: true, files: fileArray, loading: false });
};
//
const executeDeleteLog = async () => {
setDeleteLogDialog(prev => ({ ...prev, loading: true }));
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const response = await fetch(`/api/admin/logfile?id=${file.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) { for (const file of deleteLogDialog.files) {
fetchLogFiles(); // await fetch(`/api/admin/logfile?id=${file.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
} }
setToast(`${deleteLogDialog.files.length}개 로그 파일 삭제 완료`);
fetchLogFiles();
setSelectedLogFiles(new Set());
} catch (error) { } catch (error) {
console.error('로그 파일 삭제 실패:', error); console.error('로그 파일 삭제 실패:', error);
setToast('삭제 실패', true);
} finally {
setDeleteLogDialog({ show: false, files: [], loading: false });
}
};
//
const toggleLogFileSelect = (fileId, e) => {
e.stopPropagation();
setSelectedLogFiles(prev => {
const next = new Set(prev);
if (next.has(fileId)) {
next.delete(fileId);
} else {
next.add(fileId);
}
return next;
});
};
// /
const toggleSelectAllLogs = () => {
if (selectedLogFiles.size === logFiles.length) {
setSelectedLogFiles(new Set());
} else {
setSelectedLogFiles(new Set(logFiles.map(f => f.id)));
}
};
//
const deleteSelectedLogs = () => {
const selectedFiles = logFiles.filter(f => selectedLogFiles.has(f.id));
if (selectedFiles.length > 0) {
openDeleteLogDialog(selectedFiles);
} }
}; };
@ -1542,6 +1587,34 @@ export default function Admin({ isMobile = false }) {
</div> </div>
</div> </div>
{/* 일괄 삭제 컨트롤 */}
{logFiles.length > 0 && (
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<button
onClick={toggleSelectAllLogs}
className="text-xs text-zinc-400 hover:text-white transition-colors"
>
{selectedLogFiles.size === logFiles.length ? '전체 해제' : '전체 선택'}
</button>
{selectedLogFiles.size > 0 && (
<span className="text-xs text-zinc-500">
{selectedLogFiles.size} 선택됨
</span>
)}
</div>
{selectedLogFiles.size > 0 && (
<button
onClick={deleteSelectedLogs}
className="flex items-center gap-1 px-2 py-1 text-xs bg-red-600/20 text-red-400 hover:bg-red-600/30 rounded-lg transition-colors"
>
<Trash2 size={12} />
선택 삭제 ({selectedLogFiles.size})
</button>
)}
</div>
)}
<div className="space-y-2 max-h-[300px] overflow-y-auto custom-scrollbar pr-2"> <div className="space-y-2 max-h-[300px] overflow-y-auto custom-scrollbar pr-2">
{logFiles.length === 0 ? ( {logFiles.length === 0 ? (
<p className="text-zinc-500 text-sm text-center py-4">로그 파일이 없습니다</p> <p className="text-zinc-500 text-sm text-center py-4">로그 파일이 없습니다</p>
@ -1549,25 +1622,36 @@ export default function Admin({ isMobile = false }) {
logFiles.map((file) => ( logFiles.map((file) => (
<div <div
key={file.id} key={file.id}
className="flex items-center justify-between p-3 bg-zinc-800/50 rounded-xl hover:bg-zinc-800 transition-colors cursor-pointer group" className={`flex items-center justify-between p-3 rounded-xl hover:bg-zinc-800 transition-colors cursor-pointer group ${
selectedLogFiles.has(file.id) ? 'bg-zinc-700/50' : 'bg-zinc-800/50'
}`}
onClick={() => viewLogContent(file)} onClick={() => viewLogContent(file)}
> >
<div className="flex-1 min-w-0"> <div className="flex items-center gap-2 flex-1 min-w-0">
<p className="text-white text-sm truncate group-hover:text-mc-green transition-colors">{file.fileName}</p> <input
<p className="text-xs text-zinc-500"> type="checkbox"
{file.fileSize} {file.serverId} checked={selectedLogFiles.has(file.id)}
<span className={`ml-1 ${ onChange={(e) => toggleLogFileSelect(file.id, e)}
file.fileType === 'debug' ? 'text-yellow-500' : onClick={(e) => e.stopPropagation()}
file.fileType === 'latest' ? 'text-blue-400' : 'text-zinc-400' className="w-4 h-4 rounded border-zinc-600 bg-zinc-700 text-emerald-500 focus:ring-emerald-500 focus:ring-offset-0"
}`}> />
{file.fileType} <div className="flex-1 min-w-0">
</span> <p className="text-white text-sm truncate group-hover:text-mc-green transition-colors">{file.fileName}</p>
</p> <p className="text-xs text-zinc-500">
{file.fileSize} {file.serverId}
<span className={`ml-1 ${
file.fileType === 'debug' ? 'text-yellow-500' :
file.fileType === 'latest' ? 'text-blue-400' : 'text-zinc-400'
}`}>
{file.fileType}
</span>
</p>
</div>
</div> </div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Tooltip content="삭제"> <Tooltip content="삭제">
<button <button
onClick={(e) => deleteLogFile(file, e)} onClick={(e) => openDeleteLogDialog(file, e)}
className="p-2 text-zinc-400 hover:text-red-500 hover:bg-zinc-700 rounded-lg transition-colors" className="p-2 text-zinc-400 hover:text-red-500 hover:bg-zinc-700 rounded-lg transition-colors"
> >
<Trash2 size={16} /> <Trash2 size={16} />
@ -2573,6 +2657,66 @@ export default function Admin({ isMobile = false }) {
</motion.div> </motion.div>
)} )}
{/* 로그 삭제 확인 다이얼로그 */}
{deleteLogDialog.show && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
onClick={() => !deleteLogDialog.loading && setDeleteLogDialog({ show: false, files: [], loading: false })}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6 w-full max-w-sm mx-4"
onClick={e => e.stopPropagation()}
>
<h3 className="text-white text-lg font-medium mb-2">로그 파일 삭제</h3>
<p className="text-zinc-400 text-sm mb-4">
{deleteLogDialog.files.length === 1 ? (
<>
<span className="text-red-400 font-medium">{deleteLogDialog.files[0]?.fileName}</span> 파일을 삭제하시겠습니까?
</>
) : (
<>
<span className="text-red-400 font-medium">{deleteLogDialog.files.length}</span> 파일을 삭제하시겠습니까?
</>
)}
</p>
{deleteLogDialog.files.length > 1 && (
<div className="max-h-32 overflow-y-auto custom-scrollbar mb-4 space-y-1">
{deleteLogDialog.files.map(f => (
<div key={f.id} className="text-xs text-zinc-500 truncate"> {f.fileName}</div>
))}
</div>
)}
<div className="flex gap-3">
<button
onClick={() => setDeleteLogDialog({ show: false, files: [], loading: false })}
disabled={deleteLogDialog.loading}
className="flex-1 py-2.5 bg-zinc-800 hover:bg-zinc-700 disabled:opacity-50 text-white font-medium rounded-xl transition-colors"
>
취소
</button>
<button
onClick={executeDeleteLog}
disabled={deleteLogDialog.loading}
className="flex-1 py-2.5 bg-red-600 hover:bg-red-500 disabled:bg-red-800 text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2"
>
{deleteLogDialog.loading ? (
<>
<Loader2 size={16} className="animate-spin" />
삭제 ...
</>
) : '삭제'}
</button>
</div>
</motion.div>
</motion.div>
)}
{/* 로그 뷰어 다이얼로그 */} {/* 로그 뷰어 다이얼로그 */}
{logViewerOpen && ( {logViewerOpen && (
<motion.div <motion.div