feat: 로그 파일 삭제 다이얼로그 및 일괄 삭제 기능
- alert 대신 다이얼로그로 삭제 확인 - 체크박스로 파일 선택 기능 - 전체 선택/해제 및 일괄 삭제 지원 - 삭제 중 로딩 상태 표시
This commit is contained in:
parent
4108e4547d
commit
e13f608d88
1 changed files with 166 additions and 22 deletions
|
|
@ -89,6 +89,8 @@ export default function Admin({ isMobile = false }) {
|
|||
const [logLoading, setLogLoading] = useState(false); // 로그 로딩
|
||||
const [serverDropdownOpen, setServerDropdownOpen] = 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 logContainerRef = useRef(null);
|
||||
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 (!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 {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`/api/admin/logfile?id=${file.id}`, {
|
||||
|
||||
for (const file of deleteLogDialog.files) {
|
||||
await fetch(`/api/admin/logfile?id=${file.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
fetchLogFiles(); // 목록 새로고침
|
||||
}
|
||||
|
||||
setToast(`${deleteLogDialog.files.length}개 로그 파일 삭제 완료`);
|
||||
fetchLogFiles();
|
||||
setSelectedLogFiles(new Set());
|
||||
} catch (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>
|
||||
|
||||
{/* 일괄 삭제 컨트롤 */}
|
||||
{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">
|
||||
{logFiles.length === 0 ? (
|
||||
<p className="text-zinc-500 text-sm text-center py-4">로그 파일이 없습니다</p>
|
||||
|
|
@ -1549,9 +1622,19 @@ export default function Admin({ isMobile = false }) {
|
|||
logFiles.map((file) => (
|
||||
<div
|
||||
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)}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedLogFiles.has(file.id)}
|
||||
onChange={(e) => toggleLogFileSelect(file.id, e)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-4 h-4 rounded border-zinc-600 bg-zinc-700 text-emerald-500 focus:ring-emerald-500 focus:ring-offset-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white text-sm truncate group-hover:text-mc-green transition-colors">{file.fileName}</p>
|
||||
<p className="text-xs text-zinc-500">
|
||||
|
|
@ -1564,10 +1647,11 @@ export default function Admin({ isMobile = false }) {
|
|||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Tooltip content="삭제">
|
||||
<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"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
|
|
@ -2573,6 +2657,66 @@ export default function Admin({ isMobile = false }) {
|
|||
</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 && (
|
||||
<motion.div
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue