- FastAPI 라우터에서 슬래시 문제로 인한 307 리다이렉트 수정 - Nginx 프록시 설정에서 경로 중복 문제 해결 - 계정 관리 시스템 구현 (로그인, 사용자 관리, 권한 설정) - 노트북 연결 기능 수정 (notebook_id 필드 추가) - 메모 트리 UI 개선 (수평 레이아웃, 드래그 기능 제거) - 헤더 UI 개선 및 고정 위치 설정 - 백업/복원 스크립트 추가 - PDF 미리보기 토큰 인증 지원
364 lines
19 KiB
HTML
364 lines
19 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>
|
|
|
|
<!-- 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>
|