Files
document-server/frontend/account-settings.html
Hyungi Ahn 6e01dbdeb3 Fix: 업로드 및 API 연결 문제 해결
- FastAPI 라우터에서 슬래시 문제로 인한 307 리다이렉트 수정
- Nginx 프록시 설정에서 경로 중복 문제 해결
- 계정 관리 시스템 구현 (로그인, 사용자 관리, 권한 설정)
- 노트북 연결 기능 수정 (notebook_id 필드 추가)
- 메모 트리 UI 개선 (수평 레이아웃, 드래그 기능 제거)
- 헤더 UI 개선 및 고정 위치 설정
- 백업/복원 스크립트 추가
- PDF 미리보기 토큰 인증 지원
2025-09-03 15:58:10 +09:00

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>