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>
This commit is contained in:
384
templates/login.html
Normal file
384
templates/login.html
Normal file
@@ -0,0 +1,384 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user