Files
document-server/frontend/logs.html
Hyungi Ahn 43e2614253 Feature: 관리자 메뉴 개선 및 새 관리 페이지 추가
- 프로필 드롭다운에서 중복 메뉴 제거 (프로필 관리 + 계정 설정 → 계정 설정)
- 관리자 전용 메뉴 추가:
  - 백업/복원 관리 페이지 (backup-restore.html)
  - 시스템 로그 페이지 (logs.html)
- 관리자 메뉴에 색상 구분 아이콘 적용
- account-settings.html 삭제 (profile.html로 통합)
2025-09-03 17:46:41 +09:00

332 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>시스템 로그 - Document Server</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="static/js/auth-guard.js"></script>
</head>
<body class="bg-gray-50">
<!-- 헤더 -->
<div id="header-container"></div>
<!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-8" x-data="logsApp()">
<div class="max-w-6xl mx-auto">
<!-- 페이지 헤더 -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">시스템 로그</h1>
<p class="text-gray-600">시스템 활동과 오류 로그를 확인할 수 있습니다.</p>
</div>
<!-- 필터 및 컨트롤 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-6">
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- 로그 레벨 필터 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">로그 레벨</label>
<select x-model="filters.level" @change="filterLogs()" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm">
<option value="">전체</option>
<option value="INFO">정보</option>
<option value="WARNING">경고</option>
<option value="ERROR">오류</option>
<option value="DEBUG">디버그</option>
</select>
</div>
<!-- 날짜 필터 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">날짜</label>
<input type="date" x-model="filters.date" @change="filterLogs()" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm">
</div>
<!-- 검색 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">검색</label>
<input type="text" x-model="filters.search" @input="filterLogs()" placeholder="메시지 검색..." class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm">
</div>
<!-- 컨트롤 버튼 -->
<div class="flex items-end space-x-2">
<button @click="refreshLogs()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">
<i class="fas fa-sync-alt mr-1"></i>
새로고침
</button>
<button @click="clearLogs()" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm">
<i class="fas fa-trash mr-1"></i>
삭제
</button>
</div>
</div>
</div>
</div>
<!-- 로그 통계 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<i class="fas fa-info-circle text-blue-600"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500">정보</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.info"></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-yellow-600"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500">경고</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.warning"></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
<i class="fas fa-times-circle text-red-600"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500">오류</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.error"></p>
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
<i class="fas fa-bug text-gray-600"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-500">디버그</p>
<p class="text-2xl font-semibold text-gray-900" x-text="stats.debug"></p>
</div>
</div>
</div>
</div>
<!-- 로그 목록 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900 flex items-center">
<i class="fas fa-list text-gray-500 mr-3"></i>
로그 목록
<span class="ml-2 text-sm font-normal text-gray-500">(<span x-text="filteredLogs.length"></span>개)</span>
</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">시간</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">레벨</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">소스</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">메시지</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<template x-for="log in paginatedLogs" :key="log.id">
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900" x-text="log.timestamp"></td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
:class="{
'bg-blue-100 text-blue-800': log.level === 'INFO',
'bg-yellow-100 text-yellow-800': log.level === 'WARNING',
'bg-red-100 text-red-800': log.level === 'ERROR',
'bg-gray-100 text-gray-800': log.level === 'DEBUG'
}"
x-text="log.level">
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500" x-text="log.source"></td>
<td class="px-6 py-4 text-sm text-gray-900">
<div class="max-w-md truncate" x-text="log.message" :title="log.message"></div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- 페이지네이션 -->
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<div class="text-sm text-gray-500">
<span x-text="(currentPage - 1) * pageSize + 1"></span>-<span x-text="Math.min(currentPage * pageSize, filteredLogs.length)"></span> / <span x-text="filteredLogs.length"></span>
</div>
<div class="flex space-x-2">
<button @click="currentPage > 1 && currentPage--"
:disabled="currentPage <= 1"
class="px-3 py-1 text-sm bg-gray-200 text-gray-700 rounded hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed">
이전
</button>
<button @click="currentPage < totalPages && currentPage++"
:disabled="currentPage >= totalPages"
class="px-3 py-1 text-sm bg-gray-200 text-gray-700 rounded hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed">
다음
</button>
</div>
</div>
</div>
</div>
</main>
<!-- JavaScript -->
<script src="static/js/api.js"></script>
<script src="static/js/header-loader.js"></script>
<script>
function logsApp() {
return {
logs: [],
filteredLogs: [],
currentPage: 1,
pageSize: 50,
filters: {
level: '',
date: '',
search: ''
},
stats: {
info: 0,
warning: 0,
error: 0,
debug: 0
},
get totalPages() {
return Math.ceil(this.filteredLogs.length / this.pageSize);
},
get paginatedLogs() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.filteredLogs.slice(start, end);
},
init() {
this.loadLogs();
// 자동 새로고침 (30초마다)
setInterval(() => {
this.refreshLogs();
}, 30000);
},
async loadLogs() {
try {
// 실제 구현에서는 백엔드 API 호출
// this.logs = await window.api.getLogs();
// 임시 데모 데이터
this.logs = this.generateDemoLogs();
this.filterLogs();
this.updateStats();
} catch (error) {
console.error('로그 로드 실패:', error);
}
},
generateDemoLogs() {
const levels = ['INFO', 'WARNING', 'ERROR', 'DEBUG'];
const sources = ['auth', 'documents', 'database', 'nginx', 'system'];
const messages = [
'사용자 로그인 성공: admin@test.com',
'문서 업로드 완료: test.pdf',
'데이터베이스 연결 실패',
'API 요청 처리 완료',
'메모리 사용량 경고: 85%',
'백업 작업 시작',
'인증 토큰 만료',
'파일 업로드 오류',
'시스템 재시작 완료',
'캐시 정리 작업 완료'
];
const logs = [];
for (let i = 0; i < 200; i++) {
const date = new Date();
date.setMinutes(date.getMinutes() - i * 5);
logs.push({
id: i + 1,
timestamp: date.toLocaleString('ko-KR'),
level: levels[Math.floor(Math.random() * levels.length)],
source: sources[Math.floor(Math.random() * sources.length)],
message: messages[Math.floor(Math.random() * messages.length)]
});
}
return logs.reverse();
},
filterLogs() {
this.filteredLogs = this.logs.filter(log => {
let matches = true;
if (this.filters.level && log.level !== this.filters.level) {
matches = false;
}
if (this.filters.date) {
const logDate = new Date(log.timestamp).toISOString().split('T')[0];
if (logDate !== this.filters.date) {
matches = false;
}
}
if (this.filters.search && !log.message.toLowerCase().includes(this.filters.search.toLowerCase())) {
matches = false;
}
return matches;
});
this.currentPage = 1;
},
updateStats() {
this.stats = {
info: this.logs.filter(log => log.level === 'INFO').length,
warning: this.logs.filter(log => log.level === 'WARNING').length,
error: this.logs.filter(log => log.level === 'ERROR').length,
debug: this.logs.filter(log => log.level === 'DEBUG').length
};
},
async refreshLogs() {
await this.loadLogs();
},
async clearLogs() {
if (!confirm('모든 로그를 삭제하시겠습니까?')) {
return;
}
try {
// 실제 구현에서는 백엔드 API 호출
// await window.api.clearLogs();
this.logs = [];
this.filteredLogs = [];
this.updateStats();
alert('로그가 삭제되었습니다.');
} catch (error) {
alert('로그 삭제 중 오류가 발생했습니다: ' + error.message);
}
}
}
}
</script>
</body>
</html>