- FastAPI 라우터에서 슬래시 문제로 인한 307 리다이렉트 수정 - Nginx 프록시 설정에서 경로 중복 문제 해결 - 계정 관리 시스템 구현 (로그인, 사용자 관리, 권한 설정) - 노트북 연결 기능 수정 (notebook_id 필드 추가) - 메모 트리 UI 개선 (수평 레이아웃, 드래그 기능 제거) - 헤더 UI 개선 및 고정 위치 설정 - 백업/복원 스크립트 추가 - PDF 미리보기 토큰 인증 지원
317 lines
14 KiB
HTML
317 lines
14 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">
|
|
|
|
<style>
|
|
/* 배경 이미지 스타일 */
|
|
.login-background {
|
|
background: url('static/images/login-bg.jpg') center/cover;
|
|
background-attachment: fixed;
|
|
}
|
|
|
|
/* 기본 배경 (이미지가 없을 때) */
|
|
.login-background-fallback {
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(147, 51, 234, 0.3));
|
|
}
|
|
|
|
/* 글래스모피즘 효과 */
|
|
.glass-effect {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(15px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
/* 애니메이션 */
|
|
.fade-in {
|
|
animation: fadeIn 0.8s ease-out;
|
|
}
|
|
|
|
.slide-up {
|
|
animation: slideUp 0.6s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* 입력 필드 포커스 효과 */
|
|
.input-glow:focus {
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
/* 로그인 버튼 호버 효과 */
|
|
.btn-login:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.4);
|
|
}
|
|
|
|
/* 파티클 애니메이션 */
|
|
.particle {
|
|
position: absolute;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 50%;
|
|
animation: float 6s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
|
50% { transform: translateY(-20px) rotate(180deg); }
|
|
}
|
|
|
|
/* 로고 영역 개선 */
|
|
.logo-container {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen login-background-fallback" x-data="loginApp()">
|
|
<!-- 배경 파티클 -->
|
|
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
|
<div class="particle w-2 h-2" style="left: 10%; top: 20%; animation-delay: 0s;"></div>
|
|
<div class="particle w-3 h-3" style="left: 20%; top: 80%; animation-delay: 2s;"></div>
|
|
<div class="particle w-1 h-1" style="left: 80%; top: 30%; animation-delay: 4s;"></div>
|
|
<div class="particle w-2 h-2" style="left: 90%; top: 70%; animation-delay: 1s;"></div>
|
|
<div class="particle w-1 h-1" style="left: 30%; top: 10%; animation-delay: 3s;"></div>
|
|
<div class="particle w-2 h-2" style="left: 70%; top: 90%; animation-delay: 5s;"></div>
|
|
</div>
|
|
|
|
<!-- 메인 컨테이너 -->
|
|
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative">
|
|
<!-- 로그인 영역 -->
|
|
<div class="max-w-md w-full space-y-8">
|
|
<!-- 로고 및 제목 -->
|
|
<div class="text-center fade-in">
|
|
<div class="mx-auto h-24 w-24 glass-effect rounded-full flex items-center justify-center mb-6 shadow-2xl">
|
|
<i class="fas fa-book text-white text-4xl"></i>
|
|
</div>
|
|
<h2 class="text-4xl font-bold text-white mb-2 drop-shadow-lg">Document Server</h2>
|
|
<p class="text-blue-100 text-lg">지식을 관리하고 공유하세요</p>
|
|
</div>
|
|
|
|
<!-- 로그인 폼 -->
|
|
<div class="glass-effect rounded-2xl shadow-2xl p-8 slide-up">
|
|
<div class="mb-6">
|
|
<h3 class="text-2xl font-semibold text-white text-center mb-2">로그인</h3>
|
|
<p class="text-blue-100 text-center text-sm">계정에 로그인하여 시작하세요</p>
|
|
</div>
|
|
|
|
<!-- 알림 메시지 -->
|
|
<div x-show="notification.show" x-transition class="mb-6 p-4 rounded-lg" :class="notification.type === 'success' ? 'bg-green-500/20 text-green-100 border border-green-400/30' : 'bg-red-500/20 text-red-100 border border-red-400/30'">
|
|
<div class="flex items-center">
|
|
<i :class="notification.type === 'success' ? 'fas fa-check-circle text-green-400' : 'fas fa-exclamation-circle text-red-400'" class="mr-2"></i>
|
|
<span x-text="notification.message"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<form @submit.prevent="login()" class="space-y-6">
|
|
<div>
|
|
<label for="email" class="block text-sm font-medium text-blue-100 mb-2">
|
|
<i class="fas fa-envelope mr-2"></i>이메일
|
|
</label>
|
|
<input type="email" id="email" x-model="loginForm.email" required
|
|
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-blue-200 input-glow focus:outline-none focus:border-blue-400 transition-all duration-300"
|
|
placeholder="이메일을 입력하세요">
|
|
</div>
|
|
|
|
<div>
|
|
<label for="password" class="block text-sm font-medium text-blue-100 mb-2">
|
|
<i class="fas fa-lock mr-2"></i>비밀번호
|
|
</label>
|
|
<div class="relative">
|
|
<input :type="showPassword ? 'text' : 'password'" id="password" x-model="loginForm.password" required
|
|
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-blue-200 input-glow focus:outline-none focus:border-blue-400 transition-all duration-300 pr-12"
|
|
placeholder="비밀번호를 입력하세요">
|
|
<button type="button" @click="showPassword = !showPassword"
|
|
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-blue-200 hover:text-white transition-colors">
|
|
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로그인 유지 -->
|
|
<div class="flex items-center justify-between">
|
|
<label class="flex items-center text-sm text-blue-100">
|
|
<input type="checkbox" x-model="loginForm.remember"
|
|
class="mr-2 rounded bg-white/10 border-white/20 text-blue-500 focus:ring-blue-500 focus:ring-offset-0">
|
|
로그인 상태 유지
|
|
</label>
|
|
<a href="#" class="text-sm text-blue-200 hover:text-white transition-colors">
|
|
비밀번호를 잊으셨나요?
|
|
</a>
|
|
</div>
|
|
|
|
<button type="submit" :disabled="loading"
|
|
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed btn-login transition-all duration-300">
|
|
<i class="fas fa-spinner fa-spin mr-2" x-show="loading"></i>
|
|
<i class="fas fa-sign-in-alt mr-2" x-show="!loading"></i>
|
|
<span x-text="loading ? '로그인 중...' : '로그인'"></span>
|
|
</button>
|
|
</form>
|
|
|
|
<!-- 추가 옵션 -->
|
|
<div class="mt-6 text-center">
|
|
<div class="relative">
|
|
<div class="absolute inset-0 flex items-center">
|
|
<div class="w-full border-t border-white/20"></div>
|
|
</div>
|
|
<div class="relative flex justify-center text-sm">
|
|
<span class="px-2 bg-transparent text-blue-200">또는</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<button @click="goToSetup()" class="text-blue-200 hover:text-white text-sm transition-colors">
|
|
<i class="fas fa-cog mr-1"></i>시스템 초기 설정
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 푸터 -->
|
|
<div class="text-center text-blue-200 text-sm fade-in mt-8">
|
|
<p>© 2024 Document Server. All rights reserved.</p>
|
|
<p class="mt-1">
|
|
<i class="fas fa-shield-alt mr-1"></i>
|
|
안전하고 신뢰할 수 있는 문서 관리 시스템
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 배경 이미지 로드 스크립트 -->
|
|
<script>
|
|
// 배경 이미지 로드 시도
|
|
const img = new Image();
|
|
img.onload = function() {
|
|
document.body.classList.remove('login-background-fallback');
|
|
document.body.classList.add('login-background');
|
|
};
|
|
img.onerror = function() {
|
|
console.log('배경 이미지를 찾을 수 없어 기본 그라디언트를 사용합니다.');
|
|
};
|
|
img.src = 'static/images/login-bg.jpg';
|
|
</script>
|
|
|
|
<!-- API 스크립트 -->
|
|
<script src="static/js/api.js"></script>
|
|
|
|
<!-- 로그인 앱 스크립트 -->
|
|
<script>
|
|
function loginApp() {
|
|
return {
|
|
loading: false,
|
|
showPassword: false,
|
|
|
|
loginForm: {
|
|
email: '',
|
|
password: '',
|
|
remember: false
|
|
},
|
|
|
|
notification: {
|
|
show: false,
|
|
type: 'success',
|
|
message: ''
|
|
},
|
|
|
|
async init() {
|
|
console.log('🔐 로그인 앱 초기화');
|
|
|
|
// 이미 로그인된 경우 메인 페이지로 리다이렉트
|
|
const token = localStorage.getItem('access_token');
|
|
if (token) {
|
|
try {
|
|
await api.getCurrentUser();
|
|
window.location.href = 'index.html';
|
|
return;
|
|
} catch (error) {
|
|
// 토큰이 유효하지 않으면 제거
|
|
localStorage.removeItem('access_token');
|
|
}
|
|
}
|
|
|
|
// URL 파라미터에서 리다이렉트 URL 확인
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
this.redirectUrl = urlParams.get('redirect') || 'index.html';
|
|
},
|
|
|
|
async login() {
|
|
if (!this.loginForm.email || !this.loginForm.password) {
|
|
this.showNotification('이메일과 비밀번호를 입력해주세요.', 'error');
|
|
return;
|
|
}
|
|
|
|
this.loading = true;
|
|
try {
|
|
console.log('🔐 로그인 시도:', this.loginForm.email);
|
|
|
|
const result = await api.login(this.loginForm.email, this.loginForm.password);
|
|
|
|
if (result.success) {
|
|
this.showNotification('로그인 성공! 페이지를 이동합니다...', 'success');
|
|
|
|
// 잠시 후 리다이렉트
|
|
setTimeout(() => {
|
|
window.location.href = this.redirectUrl || 'index.html';
|
|
}, 1000);
|
|
} else {
|
|
this.showNotification(result.message || '로그인에 실패했습니다.', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ 로그인 오류:', error);
|
|
this.showNotification('로그인 중 오류가 발생했습니다.', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
goToSetup() {
|
|
window.location.href = 'setup.html';
|
|
},
|
|
|
|
showNotification(message, type = 'success') {
|
|
this.notification = {
|
|
show: true,
|
|
type,
|
|
message
|
|
};
|
|
|
|
setTimeout(() => {
|
|
this.notification.show = false;
|
|
}, 5000);
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|