- Add main application files (main.py, models.py, schemas.py, etc.) - Add routers for all features (waiting, attendance, members, etc.) - Add HTML templates for admin and user interfaces - Add migration scripts and utility files - Add Docker configuration - Add documentation files - Add .gitignore to exclude database and cache files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
384 lines
15 KiB
HTML
384 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WaitFlow Login</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
background-color: #f3f4f6;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.login-wrapper {
|
|
width: 100%;
|
|
max-width: 520px;
|
|
/* Increased from 420px */
|
|
padding: 20px;
|
|
}
|
|
|
|
.brand-section {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: 42px;
|
|
/* Increased from 36px */
|
|
font-weight: 800;
|
|
color: #4f46e5;
|
|
letter-spacing: -1px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.brand-subtitle {
|
|
font-size: 18px;
|
|
/* Increased from 16px */
|
|
color: #6b7280;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.login-card {
|
|
background: white;
|
|
padding: 50px;
|
|
/* Increased from 40px */
|
|
border-radius: 16px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-size: 16px;
|
|
/* Increased from 14px */
|
|
font-weight: 500;
|
|
color: #374151;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 16px 20px;
|
|
/* Increased from 12px 16px */
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
/* Increased from 15px */
|
|
outline: none;
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.form-input:focus {
|
|
border-color: #4f46e5;
|
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
|
}
|
|
|
|
.btn-login {
|
|
width: 100%;
|
|
padding: 16px;
|
|
/* Increased from 14px */
|
|
background-color: #4f46e5;
|
|
color: white;
|
|
font-size: 18px;
|
|
/* Increased from 16px */
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.btn-login:hover {
|
|
background-color: #4338ca;
|
|
}
|
|
|
|
.btn-login:disabled {
|
|
background-color: #a5b4fc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.alert {
|
|
padding: 14px;
|
|
border-radius: 8px;
|
|
margin-bottom: 24px;
|
|
font-size: 14px;
|
|
display: none;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.alert.error {
|
|
background-color: #fef2f2;
|
|
color: #991b1b;
|
|
border: 1px solid #fecaca;
|
|
}
|
|
|
|
.alert.success {
|
|
background-color: #ecfdf5;
|
|
color: #065f46;
|
|
border: 1px solid #a7f3d0;
|
|
}
|
|
|
|
.alert.info {
|
|
background-color: #eff6ff;
|
|
color: #1e40af;
|
|
border: 1px solid #bfdbfe;
|
|
}
|
|
|
|
.credentials-info {
|
|
margin-top: 30px;
|
|
text-align: center;
|
|
font-size: 13px;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.credentials-details {
|
|
margin-top: 10px;
|
|
padding: 15px;
|
|
background-color: #f9fafb;
|
|
border-radius: 8px;
|
|
text-align: left;
|
|
border: 1px solid #e5e7eb;
|
|
display: none;
|
|
/* Hidden by default for simple look, toggled via JS if needed or just remove */
|
|
}
|
|
|
|
/* Footer */
|
|
.footer-copy {
|
|
margin-top: 40px;
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="login-wrapper">
|
|
<div class="brand-section">
|
|
<h1 class="brand-title">WaitFlow</h1>
|
|
<p class="brand-subtitle">Smart Waiting Management System</p>
|
|
</div>
|
|
|
|
<div class="login-card">
|
|
<div id="alert" class="alert"></div>
|
|
|
|
<form id="loginForm">
|
|
<div class="form-group">
|
|
<label for="username" class="form-label">아이디</label>
|
|
<input type="text" id="username" name="username" class="form-input" required autocomplete="username"
|
|
placeholder="아이디를 입력하세요">
|
|
<div style="margin-top: 10px; display: flex; align-items: center;">
|
|
<input type="checkbox" id="saveId"
|
|
style="width: auto; margin-right: 8px; transform: scale(1.2);">
|
|
<label for="saveId" style="font-size: 15px; color: #6b7280; cursor: pointer;">아이디 저장</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password" class="form-label">비밀번호</label>
|
|
<input type="password" id="password" name="password" class="form-input" required
|
|
autocomplete="current-password" placeholder="비밀번호를 입력하세요">
|
|
<div style="margin-top: 10px; display: flex; align-items: center;">
|
|
<input type="checkbox" id="savePw"
|
|
style="width: auto; margin-right: 8px; transform: scale(1.2);">
|
|
<label for="savePw" style="font-size: 15px; color: #6b7280; cursor: pointer;">비밀번호 저장</label>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-login" id="loginBtn">로그인</button>
|
|
</form>
|
|
|
|
<div class="footer-copy">
|
|
© 2025 WaitFlow. All rights reserved.
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const loginForm = document.getElementById('loginForm');
|
|
const loginBtn = document.getElementById('loginBtn');
|
|
const alertBox = document.getElementById('alert');
|
|
|
|
function showAlert(message, type = 'info') {
|
|
alertBox.textContent = message;
|
|
alertBox.className = `alert ${type}`;
|
|
alertBox.style.display = 'block';
|
|
|
|
if (type === 'success') {
|
|
setTimeout(() => {
|
|
alertBox.style.display = 'none';
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
const username = usernameInput.value;
|
|
const password = passwordInput.value;
|
|
const saveId = document.getElementById('saveId').checked;
|
|
const savePw = document.getElementById('savePw').checked;
|
|
|
|
if (!username || !password) {
|
|
showAlert('사용자명과 비밀번호를 입력해주세요.', 'error');
|
|
return;
|
|
}
|
|
|
|
loginBtn.disabled = true;
|
|
loginBtn.textContent = '로그인 중...';
|
|
alertBox.style.display = 'none';
|
|
|
|
try {
|
|
const formData = new URLSearchParams();
|
|
formData.append('username', username);
|
|
formData.append('password', password);
|
|
|
|
const response = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
// 토큰 저장 (세션용)
|
|
localStorage.setItem('access_token', data.access_token);
|
|
localStorage.setItem('username', username);
|
|
|
|
// 아이디/비밀번호 저장 (옵션)
|
|
if (saveId) {
|
|
localStorage.setItem('saved_username', username);
|
|
localStorage.setItem('remember_id', 'true');
|
|
} else {
|
|
localStorage.removeItem('saved_username');
|
|
localStorage.removeItem('remember_id');
|
|
}
|
|
|
|
if (savePw) {
|
|
localStorage.setItem('saved_password', password);
|
|
localStorage.setItem('remember_pw', 'true');
|
|
} else {
|
|
localStorage.removeItem('saved_password');
|
|
localStorage.removeItem('remember_pw');
|
|
}
|
|
|
|
showAlert('로그인 성공! 페이지를 이동합니다...', 'success');
|
|
|
|
// 사용자 정보 가져오기 및 리다이렉트 (기존 로직 유지)
|
|
setTimeout(async () => {
|
|
try {
|
|
const userResponse = await fetch('/api/auth/me', {
|
|
headers: {
|
|
'Authorization': `Bearer ${data.access_token}`
|
|
}
|
|
});
|
|
|
|
if (userResponse.ok) {
|
|
const currentUser = await userResponse.json();
|
|
|
|
if (currentUser) {
|
|
localStorage.setItem('user_role', currentUser.role);
|
|
|
|
// 역할에 따라 다른 페이지로 이동
|
|
if (currentUser.role === 'system_admin') {
|
|
window.location.href = '/superadmin';
|
|
} else if (currentUser.role === 'franchise_admin' || currentUser.role === 'franchise_manager') {
|
|
if (currentUser.franchise_id) {
|
|
window.location.href = `/admin?franchise_id=${currentUser.franchise_id}`;
|
|
} else {
|
|
window.location.href = '/admin';
|
|
}
|
|
} else if (currentUser.role === 'store_admin') {
|
|
if (currentUser.store_id) {
|
|
try {
|
|
const storeResponse = await fetch(`/api/stores/${currentUser.store_id}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${data.access_token}`
|
|
}
|
|
});
|
|
if (storeResponse.ok) {
|
|
const store = await storeResponse.json();
|
|
if (store && store.code) {
|
|
window.location.href = `/?store=${store.code}`;
|
|
} else {
|
|
window.location.href = '/';
|
|
}
|
|
} else {
|
|
window.location.href = '/';
|
|
}
|
|
} catch (e) {
|
|
console.error('매장 정보 조회 실패:', e);
|
|
window.location.href = '/';
|
|
}
|
|
} else {
|
|
window.location.href = '/';
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
window.location.href = '/';
|
|
}
|
|
} catch (error) {
|
|
window.location.href = '/';
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
showAlert(data.detail || '로그인에 실패했습니다.', 'error');
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = '로그인';
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
showAlert('서버 연결에 실패했습니다.', 'error');
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = '로그인';
|
|
}
|
|
});
|
|
|
|
// 페이지 로드 시 초기화 및 저장된 정보 로드
|
|
window.addEventListener('load', () => {
|
|
// 1. 기존 세션 정보 삭제 (자동 로그인 방지)
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('username');
|
|
localStorage.removeItem('user_role');
|
|
|
|
// 2. 저장된 아이디/비밀번호 불러오기
|
|
const savedUsername = localStorage.getItem('saved_username');
|
|
const rememberId = localStorage.getItem('remember_id');
|
|
const savedPassword = localStorage.getItem('saved_password');
|
|
const rememberPw = localStorage.getItem('remember_pw');
|
|
|
|
if (rememberId === 'true' && savedUsername) {
|
|
document.getElementById('username').value = savedUsername;
|
|
document.getElementById('saveId').checked = true;
|
|
}
|
|
|
|
if (rememberPw === 'true' && savedPassword) {
|
|
document.getElementById('password').value = savedPassword;
|
|
document.getElementById('savePw').checked = true;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |