- 프로필 드롭다운에서 중복 메뉴 제거 (프로필 관리 + 계정 설정 → 계정 설정) - 관리자 전용 메뉴 추가: - 백업/복원 관리 페이지 (backup-restore.html) - 시스템 로그 페이지 (logs.html) - 관리자 메뉴에 색상 구분 아이콘 적용 - account-settings.html 삭제 (profile.html로 통합)
332 lines
16 KiB
HTML
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>
|