/** * 관리자 활동 로그 페이지 */ import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useQuery, keepPreviousData } from '@tanstack/react-query'; import { motion, AnimatePresence } from 'framer-motion'; import { Home, ChevronRight, Search, ChevronLeft, ChevronDown, X, Loader2, Check, ScrollText, } from 'lucide-react'; import { AdminLayout, DatePicker, CATEGORY_LABELS, ACTION_STYLES, ACTION_LABELS, ITEMS_PER_PAGE, formatDateTime, LogDetailDialog, ActorBadge, Summary, } from '@/components/pc/admin'; import { useAdminAuth } from '@/hooks/pc/admin'; import { adminLogApi } from '@/api/admin'; function Logs() { const { user } = useAdminAuth(); // 필터 상태 const [searchQuery, setSearchQuery] = useState(''); const [selectedCategories, setSelectedCategories] = useState([]); const [actorFilter, setActorFilter] = useState('all'); const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [pageInput, setPageInput] = useState('1'); const [actorDropdownOpen, setActorDropdownOpen] = useState(false); const [categoryDropdownOpen, setCategoryDropdownOpen] = useState(false); const [selectedLog, setSelectedLog] = useState(null); // 검색어 디바운스 const [debouncedSearch, setDebouncedSearch] = useState(''); useEffect(() => { const timer = setTimeout(() => setDebouncedSearch(searchQuery), 300); return () => clearTimeout(timer); }, [searchQuery]); // 카테고리 목록 조회 const { data: categoryData } = useQuery({ queryKey: ['admin', 'logs', 'categories'], queryFn: () => adminLogApi.getLogCategories(), staleTime: 5 * 60 * 1000, }); const categories = categoryData?.categories || []; // 로그 API 호출 const { data, isLoading } = useQuery({ queryKey: ['admin', 'logs', { page: currentPage, category: selectedCategories.join(','), actor: actorFilter === 'all' ? '' : actorFilter, search: debouncedSearch, from: dateFrom, to: dateTo }], queryFn: () => adminLogApi.getLogs({ page: currentPage, limit: ITEMS_PER_PAGE, category: selectedCategories.join(',') || undefined, actor: actorFilter === 'all' ? undefined : actorFilter, search: debouncedSearch || undefined, from: dateFrom || undefined, to: dateTo || undefined, }), placeholderData: keepPreviousData, }); const logs = data?.logs || []; const total = data?.total || 0; const totalPages = data?.totalPages || 0; // 페이지 변경 시 입력 필드 동기화 useEffect(() => { setPageInput(String(currentPage)); }, [currentPage]); const goToPageFromInput = () => { const n = parseInt(pageInput, 10); if (!Number.isFinite(n) || n < 1) { setPageInput(String(currentPage)); return; } const clamped = Math.min(totalPages, n); setCurrentPage(clamped); setPageInput(String(clamped)); }; // 카테고리 토글 const toggleCategory = (cat) => { setSelectedCategories((prev) => prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat] ); setCurrentPage(1); }; // 카테고리 드롭다운 버튼 텍스트 const getCategoryButtonText = () => { if (selectedCategories.length === 0) return '전체 카테고리'; if (selectedCategories.length === 1) return CATEGORY_LABELS[selectedCategories[0]] || selectedCategories[0]; return `카테고리 (${selectedCategories.length})`; }; // 필터 초기화 const clearFilters = () => { setSearchQuery(''); setSelectedCategories([]); setActorFilter('all'); setDateFrom(''); setDateTo(''); setCurrentPage(1); }; const hasActiveFilters = searchQuery || selectedCategories.length > 0 || actorFilter !== 'all' || dateFrom || dateTo; return (
{/* 브레드크럼 */}
활동 로그
{/* 타이틀 */}

활동 로그

모든 관리자 및 봇 활동 기록을 확인합니다

{/* 필터 영역 */}
{/* 검색 */}
{ setSearchQuery(e.target.value); setCurrentPage(1); }} placeholder="로그 검색..." className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm" />
{/* 행위자 드롭다운 */}
{actorDropdownOpen && ( <>
setActorDropdownOpen(false)} /> {[ { value: 'all', label: '전체 행위자' }, { value: 'admin', label: '관리자' }, { value: 'bot', label: '봇' }, ].map((opt) => ( ))} )}
{/* 카테고리 드롭다운 */}
{categoryDropdownOpen && ( <>
setCategoryDropdownOpen(false)} /> {categories.map((cat) => ( ))} {selectedCategories.length > 0 && ( <>
)} )}
{/* 날짜 필터 */}
{ setDateFrom(v); setCurrentPage(1); }} placeholder="시작일" max={dateTo || undefined} compact />
~
{ setDateTo(v); setCurrentPage(1); }} placeholder="종료일" min={dateFrom || undefined} compact />
{/* 필터 초기화 */} {hasActiveFilters && ( )}
{/* 결과 개수 */}

{total}개의 로그

{/* 로그 테이블 */} {logs.map((log, index) => ( ))}
시간 행위자 액션 카테고리 내용
{formatDateTime(log.created_at)} {ACTION_LABELS[log.action] || log.action} {CATEGORY_LABELS[log.category] || log.category}
setSelectedLog(log)} className="truncate cursor-pointer hover:text-gray-900 transition-colors" >
{isLoading && logs.length === 0 && (

로그를 불러오는 중...

)} {!isLoading && logs.length === 0 && (

{hasActiveFilters ? '검색 결과가 없습니다.' : '활동 로그가 없습니다.'}

)}
{/* 페이지네이션 */} {totalPages > 1 && (
{Array.from({ length: totalPages }, (_, i) => i + 1) .filter((page) => { if (totalPages <= 7) return true; if (page === 1 || page === totalPages) return true; if (Math.abs(page - currentPage) <= 2) return true; return false; }) .reduce((acc, page, i, arr) => { if (i > 0 && page - arr[i - 1] > 1) { acc.push({ type: 'ellipsis', key: `e-${page}` }); } acc.push({ type: 'page', value: page, key: page }); return acc; }, []) .map((item) => item.type === 'ellipsis' ? ( ... ) : ( ) )}
setPageInput(e.target.value.replace(/\D/g, ''))} onBlur={goToPageFromInput} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); goToPageFromInput(); e.currentTarget.blur(); } }} className="w-12 h-9 text-center tabular-nums border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" aria-label="페이지 번호 입력" /> / {totalPages}
)}
{/* 로그 상세 다이얼로그 */} setSelectedLog(null)} /> ); } export default Logs;