Files
waiting-system/templates/login.html
Jun-dev f699a29a85 Add waiting system application files
- 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>
2025-12-14 00:29:39 +09:00

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">
&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>