Feature: 관리자 메뉴 개선 및 새 관리 페이지 추가
- 프로필 드롭다운에서 중복 메뉴 제거 (프로필 관리 + 계정 설정 → 계정 설정) - 관리자 전용 메뉴 추가: - 백업/복원 관리 페이지 (backup-restore.html) - 시스템 로그 페이지 (logs.html) - 관리자 메뉴에 색상 구분 아이콘 적용 - account-settings.html 삭제 (profile.html로 통합)
This commit is contained in:
@@ -1,363 +0,0 @@
|
|||||||
<!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>
|
|
||||||
|
|
||||||
<!-- Tailwind CSS -->
|
|
||||||
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
|
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
||||||
|
|
||||||
<!-- Alpine.js -->
|
|
||||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
||||||
|
|
||||||
<!-- 공통 스타일 -->
|
|
||||||
<link rel="stylesheet" href="static/css/common.css">
|
|
||||||
|
|
||||||
<!-- 인증 가드 -->
|
|
||||||
<script src="static/js/auth-guard.js"></script>
|
|
||||||
|
|
||||||
<!-- 헤더 로더 -->
|
|
||||||
<script src="static/js/header-loader.js"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50 min-h-screen">
|
|
||||||
<!-- 메인 앱 -->
|
|
||||||
<div x-data="accountSettingsApp()" x-init="init()">
|
|
||||||
<!-- 헤더 -->
|
|
||||||
<div id="header-container"></div>
|
|
||||||
|
|
||||||
<!-- 메인 컨텐츠 -->
|
|
||||||
<main class="container mx-auto px-4 py-8 max-w-4xl">
|
|
||||||
<!-- 페이지 헤더 -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<div class="flex items-center space-x-3 mb-2">
|
|
||||||
<i class="fas fa-cog text-2xl text-blue-600"></i>
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900">계정 설정</h1>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600">계정 정보와 환경 설정을 관리하세요</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 알림 메시지 -->
|
|
||||||
<div x-show="notification.show" x-transition class="mb-6 p-4 rounded-lg" :class="notification.type === 'success' ? 'bg-green-100 text-green-800 border border-green-200' : 'bg-red-100 text-red-800 border border-red-200'">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i :class="notification.type === 'success' ? 'fas fa-check-circle text-green-600' : 'fas fa-exclamation-circle text-red-600'" class="mr-2"></i>
|
|
||||||
<span x-text="notification.message"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 설정 섹션들 -->
|
|
||||||
<div class="space-y-8">
|
|
||||||
<!-- 프로필 정보 -->
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
||||||
<div class="flex items-center space-x-3 mb-6">
|
|
||||||
<i class="fas fa-user text-xl text-blue-600"></i>
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900">프로필 정보</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="updateProfile()" class="space-y-4">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
|
||||||
<input type="email" x-model="profile.email" disabled
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed">
|
|
||||||
<p class="mt-1 text-xs text-gray-500">이메일은 변경할 수 없습니다</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">이름</label>
|
|
||||||
<input type="text" x-model="profile.full_name"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
placeholder="이름을 입력하세요">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button type="submit" :disabled="loading"
|
|
||||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2">
|
|
||||||
<i class="fas fa-save"></i>
|
|
||||||
<span x-text="loading ? '저장 중...' : '프로필 저장'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 비밀번호 변경 -->
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
||||||
<div class="flex items-center space-x-3 mb-6">
|
|
||||||
<i class="fas fa-lock text-xl text-blue-600"></i>
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900">비밀번호 변경</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="changePassword()" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">현재 비밀번호</label>
|
|
||||||
<input type="password" x-model="passwordForm.currentPassword" required
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
placeholder="현재 비밀번호를 입력하세요">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">새 비밀번호</label>
|
|
||||||
<input type="password" x-model="passwordForm.newPassword" required
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
placeholder="새 비밀번호를 입력하세요">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">새 비밀번호 확인</label>
|
|
||||||
<input type="password" x-model="passwordForm.confirmPassword" required
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
placeholder="새 비밀번호를 다시 입력하세요">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button type="submit" :disabled="loading"
|
|
||||||
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2">
|
|
||||||
<i class="fas fa-key"></i>
|
|
||||||
<span x-text="loading ? '변경 중...' : '비밀번호 변경'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 환경 설정 -->
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
||||||
<div class="flex items-center space-x-3 mb-6">
|
|
||||||
<i class="fas fa-palette text-xl text-blue-600"></i>
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900">환경 설정</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="updateSettings()" class="space-y-4">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">테마</label>
|
|
||||||
<select x-model="settings.theme"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
||||||
<option value="light">라이트</option>
|
|
||||||
<option value="dark">다크</option>
|
|
||||||
<option value="auto">자동</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">언어</label>
|
|
||||||
<select x-model="settings.language"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
||||||
<option value="ko">한국어</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">시간대</label>
|
|
||||||
<select x-model="settings.timezone"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
||||||
<option value="Asia/Seoul">서울 (UTC+9)</option>
|
|
||||||
<option value="UTC">UTC (UTC+0)</option>
|
|
||||||
<option value="America/New_York">뉴욕 (UTC-5)</option>
|
|
||||||
<option value="Europe/London">런던 (UTC+0)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button type="submit" :disabled="loading"
|
|
||||||
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2">
|
|
||||||
<i class="fas fa-cog"></i>
|
|
||||||
<span x-text="loading ? '저장 중...' : '설정 저장'"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 계정 정보 -->
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
||||||
<div class="flex items-center space-x-3 mb-6">
|
|
||||||
<i class="fas fa-info-circle text-xl text-blue-600"></i>
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900">계정 정보</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-700">계정 ID:</span>
|
|
||||||
<span class="text-gray-600 ml-2" x-text="profile.id"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-700">역할:</span>
|
|
||||||
<span class="text-gray-600 ml-2" x-text="getRoleText(profile.role)"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-700">계정 생성일:</span>
|
|
||||||
<span class="text-gray-600 ml-2" x-text="formatDate(profile.created_at)"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-700">마지막 로그인:</span>
|
|
||||||
<span class="text-gray-600 ml-2" x-text="formatDate(profile.last_login)"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-700">세션 타임아웃:</span>
|
|
||||||
<span class="text-gray-600 ml-2" x-text="profile.session_timeout_minutes === 0 ? '무제한' : profile.session_timeout_minutes + '분'"></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-700">계정 상태:</span>
|
|
||||||
<span class="ml-2" :class="profile.is_active ? 'text-green-600' : 'text-red-600'" x-text="profile.is_active ? '활성' : '비활성'"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API 스크립트 -->
|
|
||||||
<script src="static/js/api.js"></script>
|
|
||||||
|
|
||||||
<!-- 계정 설정 스크립트 -->
|
|
||||||
<script>
|
|
||||||
function accountSettingsApp() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
profile: {
|
|
||||||
id: '',
|
|
||||||
email: '',
|
|
||||||
full_name: '',
|
|
||||||
role: '',
|
|
||||||
is_active: false,
|
|
||||||
created_at: '',
|
|
||||||
last_login: '',
|
|
||||||
session_timeout_minutes: 5
|
|
||||||
},
|
|
||||||
passwordForm: {
|
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
theme: 'light',
|
|
||||||
language: 'ko',
|
|
||||||
timezone: 'Asia/Seoul'
|
|
||||||
},
|
|
||||||
notification: {
|
|
||||||
show: false,
|
|
||||||
message: '',
|
|
||||||
type: 'success'
|
|
||||||
},
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
console.log('🔧 계정 설정 앱 초기화');
|
|
||||||
await this.loadProfile();
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadProfile() {
|
|
||||||
try {
|
|
||||||
const user = await api.getCurrentUser();
|
|
||||||
this.profile = { ...user };
|
|
||||||
this.settings = {
|
|
||||||
theme: user.theme || 'light',
|
|
||||||
language: user.language || 'ko',
|
|
||||||
timezone: user.timezone || 'Asia/Seoul'
|
|
||||||
};
|
|
||||||
console.log('✅ 프로필 로드 완료:', user);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 프로필 로드 실패:', error);
|
|
||||||
this.showNotification('프로필을 불러올 수 없습니다.', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateProfile() {
|
|
||||||
this.loading = true;
|
|
||||||
try {
|
|
||||||
const response = await api.put(`/users/${this.profile.id}`, {
|
|
||||||
full_name: this.profile.full_name
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showNotification('프로필이 성공적으로 업데이트되었습니다.', 'success');
|
|
||||||
await this.loadProfile(); // 프로필 다시 로드
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 프로필 업데이트 실패:', error);
|
|
||||||
this.showNotification('프로필 업데이트에 실패했습니다.', 'error');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async changePassword() {
|
|
||||||
if (this.passwordForm.newPassword !== this.passwordForm.confirmPassword) {
|
|
||||||
this.showNotification('새 비밀번호가 일치하지 않습니다.', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
try {
|
|
||||||
await api.post('/auth/change-password', {
|
|
||||||
current_password: this.passwordForm.currentPassword,
|
|
||||||
new_password: this.passwordForm.newPassword
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showNotification('비밀번호가 성공적으로 변경되었습니다.', 'success');
|
|
||||||
this.passwordForm = {
|
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 비밀번호 변경 실패:', error);
|
|
||||||
this.showNotification('비밀번호 변경에 실패했습니다.', 'error');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateSettings() {
|
|
||||||
this.loading = true;
|
|
||||||
try {
|
|
||||||
await api.put(`/users/${this.profile.id}`, {
|
|
||||||
theme: this.settings.theme,
|
|
||||||
language: this.settings.language,
|
|
||||||
timezone: this.settings.timezone
|
|
||||||
});
|
|
||||||
|
|
||||||
this.showNotification('환경 설정이 성공적으로 저장되었습니다.', 'success');
|
|
||||||
await this.loadProfile(); // 프로필 다시 로드
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 환경 설정 저장 실패:', error);
|
|
||||||
this.showNotification('환경 설정 저장에 실패했습니다.', 'error');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showNotification(message, type = 'success') {
|
|
||||||
this.notification = {
|
|
||||||
show: true,
|
|
||||||
message: message,
|
|
||||||
type: type
|
|
||||||
};
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.notification.show = false;
|
|
||||||
}, 5000);
|
|
||||||
},
|
|
||||||
|
|
||||||
getRoleText(role) {
|
|
||||||
const roleMap = {
|
|
||||||
'root': '시스템 관리자',
|
|
||||||
'admin': '관리자',
|
|
||||||
'user': '사용자'
|
|
||||||
};
|
|
||||||
return roleMap[role] || '사용자';
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDate(dateString) {
|
|
||||||
if (!dateString) return '-';
|
|
||||||
return new Date(dateString).toLocaleString('ko-KR');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
317
frontend/backup-restore.html
Normal file
317
frontend/backup-restore.html
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<!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="backupRestoreApp()">
|
||||||
|
<div class="max-w-4xl 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-8">
|
||||||
|
<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-download text-blue-500 mr-3"></i>
|
||||||
|
데이터 백업
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- 전체 백업 -->
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h3 class="font-semibold text-gray-900 mb-2">전체 백업</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">데이터베이스와 업로드된 파일을 모두 백업합니다.</p>
|
||||||
|
<button @click="createBackup('full')"
|
||||||
|
:disabled="loading"
|
||||||
|
class="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<i class="fas fa-database mr-2"></i>
|
||||||
|
<span x-show="!loading">전체 백업 생성</span>
|
||||||
|
<span x-show="loading">백업 중...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 데이터베이스만 백업 -->
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h3 class="font-semibold text-gray-900 mb-2">데이터베이스 백업</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">데이터베이스만 백업합니다 (파일 제외).</p>
|
||||||
|
<button @click="createBackup('db')"
|
||||||
|
:disabled="loading"
|
||||||
|
class="w-full bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<i class="fas fa-table mr-2"></i>
|
||||||
|
<span x-show="!loading">DB 백업 생성</span>
|
||||||
|
<span x-show="loading">백업 중...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 백업 상태 -->
|
||||||
|
<div x-show="backupStatus" class="mt-4 p-4 rounded-lg" :class="backupStatus.type === 'success' ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i :class="backupStatus.type === 'success' ? 'fas fa-check-circle text-green-500' : 'fas fa-exclamation-circle text-red-500'" class="mr-2"></i>
|
||||||
|
<span x-text="backupStatus.message"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 백업 목록 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-8">
|
||||||
|
<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-history text-green-500 mr-3"></i>
|
||||||
|
백업 목록
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div x-show="backups.length === 0" class="text-center py-8 text-gray-500">
|
||||||
|
<i class="fas fa-inbox text-4xl mb-4"></i>
|
||||||
|
<p>백업 파일이 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="backups.length > 0" class="space-y-4">
|
||||||
|
<template x-for="backup in backups" :key="backup.id">
|
||||||
|
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||||
|
<i class="fas fa-file-archive text-blue-600"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium text-gray-900" x-text="backup.name"></h4>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
<span x-text="backup.type === 'full' ? '전체 백업' : 'DB 백업'"></span>
|
||||||
|
• <span x-text="backup.size"></span>
|
||||||
|
• <span x-text="backup.date"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button @click="downloadBackup(backup)"
|
||||||
|
class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||||
|
<i class="fas fa-download mr-1"></i>
|
||||||
|
다운로드
|
||||||
|
</button>
|
||||||
|
<button @click="deleteBackup(backup)"
|
||||||
|
class="px-3 py-1 text-sm bg-red-600 text-white rounded hover:bg-red-700">
|
||||||
|
<i class="fas fa-trash mr-1"></i>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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-upload text-orange-500 mr-3"></i>
|
||||||
|
데이터 복원
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2"></i>
|
||||||
|
<span class="text-yellow-800 font-medium">주의사항</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-yellow-700 text-sm mt-2">
|
||||||
|
복원 작업은 현재 데이터를 완전히 덮어씁니다. 복원 전에 반드시 현재 데이터를 백업하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">백업 파일 선택</label>
|
||||||
|
<input type="file"
|
||||||
|
@change="handleFileSelect"
|
||||||
|
accept=".sql,.tar.gz,.zip"
|
||||||
|
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="selectedFile">
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2">선택된 파일</h4>
|
||||||
|
<p class="text-sm text-gray-600" x-text="selectedFile?.name"></p>
|
||||||
|
<p class="text-sm text-gray-500" x-text="selectedFile ? formatFileSize(selectedFile.size) : ''"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="restoreBackup()"
|
||||||
|
:disabled="!selectedFile || loading"
|
||||||
|
class="w-full bg-orange-600 text-white px-4 py-3 rounded-lg hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium">
|
||||||
|
<i class="fas fa-upload mr-2"></i>
|
||||||
|
<span x-show="!loading">복원 실행</span>
|
||||||
|
<span x-show="loading">복원 중...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 복원 상태 -->
|
||||||
|
<div x-show="restoreStatus" class="mt-4 p-4 rounded-lg" :class="restoreStatus.type === 'success' ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i :class="restoreStatus.type === 'success' ? 'fas fa-check-circle text-green-500' : 'fas fa-exclamation-circle text-red-500'" class="mr-2"></i>
|
||||||
|
<span x-text="restoreStatus.message"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- JavaScript -->
|
||||||
|
<script src="static/js/api.js"></script>
|
||||||
|
<script src="static/js/header-loader.js"></script>
|
||||||
|
<script>
|
||||||
|
function backupRestoreApp() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
backups: [],
|
||||||
|
selectedFile: null,
|
||||||
|
backupStatus: null,
|
||||||
|
restoreStatus: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.loadBackups();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadBackups() {
|
||||||
|
try {
|
||||||
|
// 실제 구현에서는 백엔드 API 호출
|
||||||
|
// this.backups = await window.api.getBackups();
|
||||||
|
|
||||||
|
// 임시 데모 데이터
|
||||||
|
this.backups = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'backup_2025_09_03_full.tar.gz',
|
||||||
|
type: 'full',
|
||||||
|
size: '125.4 MB',
|
||||||
|
date: '2025년 9월 3일 15:30'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'backup_2025_09_02_db.sql',
|
||||||
|
type: 'db',
|
||||||
|
size: '2.1 MB',
|
||||||
|
date: '2025년 9월 2일 10:15'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('백업 목록 로드 실패:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createBackup(type) {
|
||||||
|
this.loading = true;
|
||||||
|
this.backupStatus = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 실제 구현에서는 백엔드 API 호출
|
||||||
|
// const result = await window.api.createBackup(type);
|
||||||
|
|
||||||
|
// 임시 시뮬레이션
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
|
this.backupStatus = {
|
||||||
|
type: 'success',
|
||||||
|
message: `${type === 'full' ? '전체' : 'DB'} 백업이 성공적으로 생성되었습니다.`
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadBackups();
|
||||||
|
} catch (error) {
|
||||||
|
this.backupStatus = {
|
||||||
|
type: 'error',
|
||||||
|
message: '백업 생성 중 오류가 발생했습니다: ' + error.message
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadBackup(backup) {
|
||||||
|
// 실제 구현에서는 백엔드에서 파일 다운로드
|
||||||
|
alert(`${backup.name} 다운로드를 시작합니다.`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteBackup(backup) {
|
||||||
|
if (!confirm(`${backup.name}을(를) 삭제하시겠습니까?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 실제 구현에서는 백엔드 API 호출
|
||||||
|
// await window.api.deleteBackup(backup.id);
|
||||||
|
|
||||||
|
this.backups = this.backups.filter(b => b.id !== backup.id);
|
||||||
|
alert('백업이 삭제되었습니다.');
|
||||||
|
} catch (error) {
|
||||||
|
alert('백업 삭제 중 오류가 발생했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFileSelect(event) {
|
||||||
|
this.selectedFile = event.target.files[0];
|
||||||
|
this.restoreStatus = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async restoreBackup() {
|
||||||
|
if (!this.selectedFile) return;
|
||||||
|
|
||||||
|
if (!confirm('현재 데이터가 모두 삭제되고 백업 데이터로 복원됩니다. 계속하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.restoreStatus = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 실제 구현에서는 백엔드 API 호출
|
||||||
|
// const formData = new FormData();
|
||||||
|
// formData.append('backup_file', this.selectedFile);
|
||||||
|
// await window.api.restoreBackup(formData);
|
||||||
|
|
||||||
|
// 임시 시뮬레이션
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
this.restoreStatus = {
|
||||||
|
type: 'success',
|
||||||
|
message: '백업이 성공적으로 복원되었습니다. 페이지를 새로고침해주세요.'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.restoreStatus = {
|
||||||
|
type: 'error',
|
||||||
|
message: '복원 중 오류가 발생했습니다: ' + error.message
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -177,25 +177,32 @@
|
|||||||
<!-- 메뉴 항목들 -->
|
<!-- 메뉴 항목들 -->
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<a href="profile.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
<a href="profile.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||||
<i class="fas fa-user-edit w-4 h-4 mr-3 text-gray-400"></i>
|
<i class="fas fa-user-cog w-4 h-4 mr-3 text-gray-400"></i>
|
||||||
프로필 관리
|
|
||||||
</a>
|
|
||||||
<a href="account-settings.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
|
||||||
<i class="fas fa-cog w-4 h-4 mr-3 text-gray-400"></i>
|
|
||||||
계정 설정
|
계정 설정
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- 관리자 메뉴 (관리자만 표시) -->
|
<!-- 관리자 메뉴 (관리자만 표시) -->
|
||||||
<div class="hidden" id="admin-menu-section">
|
<div class="hidden" id="admin-menu-section">
|
||||||
<div class="border-t border-gray-100 my-1"></div>
|
<div class="border-t border-gray-100 my-1"></div>
|
||||||
|
<div class="px-4 py-2">
|
||||||
|
<div class="text-xs font-medium text-gray-500 uppercase tracking-wide">관리자</div>
|
||||||
|
</div>
|
||||||
<a href="user-management.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
<a href="user-management.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||||
<i class="fas fa-users w-4 h-4 mr-3 text-gray-400"></i>
|
<i class="fas fa-users w-4 h-4 mr-3 text-indigo-500"></i>
|
||||||
사용자 관리
|
사용자 관리
|
||||||
</a>
|
</a>
|
||||||
<a href="system-settings.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
<a href="system-settings.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||||
<i class="fas fa-server w-4 h-4 mr-3 text-gray-400"></i>
|
<i class="fas fa-server w-4 h-4 mr-3 text-green-500"></i>
|
||||||
시스템 설정
|
시스템 설정
|
||||||
</a>
|
</a>
|
||||||
|
<a href="backup-restore.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||||
|
<i class="fas fa-database w-4 h-4 mr-3 text-blue-500"></i>
|
||||||
|
백업/복원
|
||||||
|
</a>
|
||||||
|
<a href="logs.html" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
|
||||||
|
<i class="fas fa-file-alt w-4 h-4 mr-3 text-orange-500"></i>
|
||||||
|
시스템 로그
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 로그아웃 -->
|
<!-- 로그아웃 -->
|
||||||
|
|||||||
331
frontend/logs.html
Normal file
331
frontend/logs.html
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
<!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>
|
||||||
Reference in New Issue
Block a user