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 [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}`, {
|
|
||||||
|
for (const file of deleteLogDialog.files) {
|
||||||
|
await fetch(`/api/admin/logfile?id=${file.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
fetchLogFiles(); // 목록 새로고침
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,9 +1622,19 @@ 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 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">
|
<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-white text-sm truncate group-hover:text-mc-green transition-colors">{file.fileName}</p>
|
||||||
<p className="text-xs text-zinc-500">
|
<p className="text-xs text-zinc-500">
|
||||||
|
|
@ -1564,10 +1647,11 @@ export default function Admin({ isMobile = false }) {
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue