/** * 관리자 활동 로그 페이지 */ import { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Home, ChevronRight, Search, ChevronLeft, ChevronDown, User, Bot, ScrollText, X, } from 'lucide-react'; import { AdminLayout, DatePicker } from '@/components/pc/admin'; import { useAdminAuth } from '@/hooks/pc/admin'; // 더미 데이터 const DUMMY_LOGS = [ { id: 1, actor: 'admin', action: 'create', category: 'album', target_type: 'album', target_id: 12, summary: '앨범 생성: Unlock My World', details: null, created_at: '2026-03-02T14:30:00' }, { id: 2, actor: 'admin', action: 'upload', category: 'album', target_type: 'photo', target_id: 45, summary: '사진 업로드: Unlock My World (3장)', details: { count: 3 }, created_at: '2026-03-02T14:25:00' }, { id: 3, actor: 'youtube-3', action: 'sync_complete', category: 'sync', target_type: 'youtube_bot', target_id: 3, summary: '동기화 완료: 스프 채널 (2개 추가)', details: { addedCount: 2, channelName: '스프' }, created_at: '2026-03-02T14:20:00' }, { id: 4, actor: 'admin', action: 'create', category: 'schedule', target_type: 'youtube_schedule', target_id: 789, summary: 'YouTube 일정 생성: fromis_9 컴백 티저', details: { videoId: 'abc123' }, created_at: '2026-03-02T14:15:00' }, { id: 5, actor: 'admin', action: 'update', category: 'member', target_type: 'member', target_id: 1, summary: '멤버 수정: 이서연 프로필 업데이트', details: null, created_at: '2026-03-02T14:10:00' }, { id: 6, actor: 'x-1', action: 'sync_complete', category: 'sync', target_type: 'x_bot', target_id: 1, summary: '동기화 완료: fromis_9 공식 (1개 추가)', details: { addedCount: 1 }, created_at: '2026-03-02T14:05:00' }, { id: 7, actor: 'admin', action: 'delete', category: 'schedule', target_type: 'youtube_schedule', target_id: 456, summary: 'YouTube 일정 삭제: 이전 영상', details: null, created_at: '2026-03-02T14:00:00' }, { id: 8, actor: 'admin', action: 'create', category: 'concert', target_type: 'concert', target_id: 5, summary: '콘서트 일정 생성: fromis_9 팬미팅', details: null, created_at: '2026-03-02T13:55:00' }, { id: 9, actor: 'admin', action: 'update', category: 'category', target_type: 'category', target_id: 3, summary: '카테고리 수정: 음악방송', details: null, created_at: '2026-03-02T13:50:00' }, { id: 10, actor: 'admin', action: 'update', category: 'category', target_type: 'category', target_id: null, summary: '카테고리 순서 변경', details: null, created_at: '2026-03-02T13:45:00' }, { id: 11, actor: 'youtube-1', action: 'error', category: 'sync', target_type: 'youtube_bot', target_id: 1, summary: '동기화 에러: API 할당량 초과', details: { error: 'quotaExceeded' }, created_at: '2026-03-02T13:40:00' }, { id: 12, actor: 'admin', action: 'start', category: 'bot', target_type: 'youtube_bot', target_id: 3, summary: 'YouTube 봇 시작: 스프', details: null, created_at: '2026-03-02T13:35:00' }, { id: 13, actor: 'admin', action: 'stop', category: 'bot', target_type: 'youtube_bot', target_id: 2, summary: 'YouTube 봇 정지: 채널 비활성화', details: null, created_at: '2026-03-02T13:30:00' }, { id: 14, actor: 'admin', action: 'create', category: 'dict', target_type: 'dict', target_id: 10, summary: '사전 저장: fromis_9 → 프로미스나인', details: null, created_at: '2026-03-02T13:25:00' }, { id: 15, actor: 'admin', action: 'delete', category: 'album', target_type: 'teaser', target_id: 8, summary: '티저 삭제: Unlock My World 티저 1', details: null, created_at: '2026-03-02T13:20:00' }, { id: 16, actor: 'admin', action: 'create', category: 'schedule', target_type: 'x_schedule', target_id: 100, summary: 'X 일정 생성: fromis_9 공식 트윗', details: null, created_at: '2026-03-02T13:15:00' }, { id: 17, actor: 'youtube-3', action: 'sync_complete', category: 'sync', target_type: 'youtube_bot', target_id: 3, summary: '동기화 완료: 스프 채널 (1개 추가)', details: { addedCount: 1 }, created_at: '2026-03-02T13:10:00' }, { id: 18, actor: 'admin', action: 'update', category: 'schedule', target_type: 'youtube_schedule', target_id: 780, summary: 'YouTube 일정 수정: 제목 변경', details: null, created_at: '2026-03-02T13:05:00' }, { id: 19, actor: 'admin', action: 'create', category: 'bot', target_type: 'youtube_bot', target_id: 4, summary: 'YouTube 봇 생성: 새 채널', details: null, created_at: '2026-03-02T13:00:00' }, { id: 20, actor: 'admin', action: 'delete', category: 'bot', target_type: 'x_bot', target_id: 2, summary: 'X 봇 삭제: 비활성 계정', details: null, created_at: '2026-03-02T12:55:00' }, { id: 21, actor: 'x-1', action: 'sync_complete', category: 'sync', target_type: 'x_bot', target_id: 1, summary: '동기화 완료: fromis_9 공식 (3개 추가)', details: { addedCount: 3 }, created_at: '2026-03-02T12:50:00' }, { id: 22, actor: 'admin', action: 'upload', category: 'album', target_type: 'photo', target_id: 50, summary: '사진 업로드: My Little Society (5장)', details: { count: 5 }, created_at: '2026-03-02T12:45:00' }, { id: 23, actor: 'admin', action: 'update', category: 'album', target_type: 'album', target_id: 5, summary: '앨범 수정: My Little Society 정보 변경', details: null, created_at: '2026-03-02T12:40:00' }, { id: 24, actor: 'youtube-1', action: 'sync_complete', category: 'sync', target_type: 'youtube_bot', target_id: 1, summary: '동기화 완료: 공식 채널 (1개 추가)', details: { addedCount: 1 }, created_at: '2026-03-02T12:35:00' }, { id: 25, actor: 'admin', action: 'create', category: 'schedule', target_type: 'youtube_schedule', target_id: 791, summary: 'YouTube 일정 생성: 연습 영상', details: null, created_at: '2026-03-02T12:30:00' }, ]; // 카테고리 목록 const CATEGORIES = [ { value: 'album', label: '앨범' }, { value: 'schedule', label: '일정' }, { value: 'member', label: '멤버' }, { value: 'bot', label: '봇' }, { value: 'category', label: '카테고리' }, { value: 'dict', label: '사전' }, { value: 'concert', label: '콘서트' }, { value: 'sync', label: '동기화' }, ]; // 액션 뱃지 색상 const ACTION_STYLES = { create: 'bg-emerald-100 text-emerald-700', upload: 'bg-emerald-100 text-emerald-700', update: 'bg-blue-100 text-blue-700', delete: 'bg-red-100 text-red-700', sync_complete: 'bg-purple-100 text-purple-700', error: 'bg-red-100 text-red-700', start: 'bg-amber-100 text-amber-700', stop: 'bg-amber-100 text-amber-700', }; // 액션 한글 라벨 const ACTION_LABELS = { create: '생성', upload: '업로드', update: '수정', delete: '삭제', sync_complete: '동기화', error: '에러', start: '시작', stop: '정지', }; const ITEMS_PER_PAGE = 15; function Logs() { const { user } = useAdminAuth(); // 필터 상태 const [searchQuery, setSearchQuery] = useState(''); const [selectedCategories, setSelectedCategories] = useState([]); const [actorFilter, setActorFilter] = useState('all'); // all, admin, bot const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [actorDropdownOpen, setActorDropdownOpen] = useState(false); // 카테고리 토글 const toggleCategory = (cat) => { setSelectedCategories((prev) => prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat] ); setCurrentPage(1); }; // 필터링된 로그 const filteredLogs = useMemo(() => { return DUMMY_LOGS.filter((log) => { // 카테고리 필터 if (selectedCategories.length > 0 && !selectedCategories.includes(log.category)) { return false; } // 행위자 필터 if (actorFilter === 'admin' && log.actor !== 'admin') return false; if (actorFilter === 'bot' && log.actor === 'admin') return false; // 텍스트 검색 if (searchQuery && !log.summary.toLowerCase().includes(searchQuery.toLowerCase())) { return false; } // 날짜 필터 if (dateFrom) { const logDate = log.created_at.split('T')[0]; if (logDate < dateFrom) return false; } if (dateTo) { const logDate = log.created_at.split('T')[0]; if (logDate > dateTo) return false; } return true; }); }, [searchQuery, selectedCategories, actorFilter, dateFrom, dateTo]); // 페이지네이션 const totalPages = Math.ceil(filteredLogs.length / ITEMS_PER_PAGE); const paginatedLogs = filteredLogs.slice( (currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE ); // 날짜/시간 포맷 const formatDateTime = (dateStr) => { const date = new Date(dateStr); const y = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${y}.${month}.${day} ${hours}:${minutes}`; }; // 행위자 아이콘 const renderActorBadge = (actor) => { if (actor === 'admin') { return ( 관리자 ); } return ( {actor} ); }; // 필터 초기화 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) => ( ))} )}
{/* 날짜 필터 */}
{ setDateFrom(v); setCurrentPage(1); }} placeholder="시작일" />
~
{ setDateTo(v); setCurrentPage(1); }} placeholder="종료일" />
{/* 필터 초기화 */} {hasActiveFilters && ( )}
{/* 하단: 카테고리 칩 */}
카테고리 {CATEGORIES.map((cat) => ( ))}
{/* 결과 개수 */}

{filteredLogs.length}개의 로그

{/* 로그 테이블 */} {paginatedLogs.map((log, index) => ( ))}
시간 행위자 액션 카테고리 내용
{formatDateTime(log.created_at)} {renderActorBadge(log.actor)} {ACTION_LABELS[log.action] || log.action} {CATEGORIES.find((c) => c.value === log.category)?.label || log.category} {log.summary}
{paginatedLogs.length === 0 && (

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

)}
{/* 페이지네이션 */} {totalPages > 1 && (
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( ))}
)}
); } export default Logs;