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:
2025-12-14 00:29:39 +09:00
parent dd1322625e
commit f699a29a85
120 changed files with 35602 additions and 0 deletions

352
static/css/common.css Normal file
View File

@@ -0,0 +1,352 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f7fa;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #fff;
padding: 20px;
margin-bottom: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.header h1 {
font-size: 28px;
color: #2c3e50;
margin-bottom: 10px;
}
.header .subtitle {
font-size: 14px;
color: #7f8c8d;
}
.card {
background: #fff;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin-bottom: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
outline: none;
}
.btn-primary {
background: #3498db;
color: #fff;
}
.btn-primary:hover {
background: #2980b9;
transform: translateY(-2px);
}
.btn-success {
background: #27ae60;
color: #fff;
}
.btn-success:hover {
background: #229954;
}
.btn-danger {
background: #e74c3c;
color: #fff;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-warning {
background: #f39c12;
color: #fff;
}
.btn-warning:hover {
background: #e67e22;
}
.btn-secondary {
background: #95a5a6;
color: #fff;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-sm {
padding: 8px 16px;
font-size: 14px;
}
.btn-lg {
padding: 16px 32px;
font-size: 18px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid #ecf0f1;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #3498db;
}
.alert {
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal.active {
display: flex;
animation: fadeIn 0.3s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-content {
background: #fff;
padding: 30px;
border-radius: 10px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
transform: scale(0.9);
opacity: 0;
animation: modalSlideIn 0.3s ease forwards;
}
@keyframes modalSlideIn {
from {
transform: scale(0.9) translateY(-20px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
.modal-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #ecf0f1;
}
.modal-header h2 {
font-size: 24px;
color: #2c3e50;
}
.modal-footer {
margin-top: 20px;
padding-top: 15px;
border-top: 2px solid #ecf0f1;
text-align: right;
}
.modal-footer .btn {
margin-left: 10px;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
.table th {
background: #f8f9fa;
font-weight: 600;
color: #2c3e50;
}
.table tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.badge-waiting {
background: #3498db;
color: #fff;
}
.badge-attended {
background: #27ae60;
color: #fff;
}
.badge-cancelled {
background: #e74c3c;
color: #fff;
}
.loading {
text-align: center;
padding: 40px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.nav-menu {
background: #fff;
padding: 15px;
margin-bottom: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.nav-menu a {
display: inline-block;
padding: 10px 20px;
margin: 5px;
color: #2c3e50;
text-decoration: none;
border-radius: 6px;
transition: all 0.3s;
}
.nav-menu a:hover,
.nav-menu a.active {
background: #3498db;
color: #fff;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 22px;
}
.card {
padding: 20px;
}
.btn {
padding: 10px 20px;
font-size: 14px;
}
.table {
font-size: 14px;
}
.table th,
.table td {
padding: 8px;
}
}

View File

@@ -0,0 +1,114 @@
/* 키패드 스타일 - Modern (기본) */
.keypad-style-modern .key {
background: linear-gradient(145deg, #ffffff, #f8f9fa);
border: 2px solid #e8ecef;
border-radius: 18px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12),
0 3px 6px rgba(0, 0, 0, 0.08),
inset 0 -2px 4px rgba(0, 0, 0, 0.05);
}
.keypad-style-modern .key:hover {
background: linear-gradient(145deg, #3498db, #2980b9);
border-color: #2980b9;
transform: translateY(-4px) scale(1.03);
box-shadow: 0 10px 24px rgba(52, 152, 219, 0.35),
0 6px 12px rgba(52, 152, 219, 0.25);
}
/* 키패드 스타일 - Bold (진한 경계선) */
.keypad-style-bold .key {
background: #ffffff;
border: 4px solid #34495e;
border-radius: 16px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.keypad-style-bold .key:hover {
background: #3498db;
border-color: #2c3e50;
transform: translateY(-3px);
box-shadow: 0 8px 16px rgba(52, 152, 219, 0.4);
}
/* 키패드 스타일 - Dark (검정 배경, 흰색 글자) */
.keypad-style-dark .key {
background: linear-gradient(145deg, #2c3e50, #34495e);
border: 2px solid #1a252f;
border-radius: 18px;
color: #ffffff !important;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3),
0 3px 6px rgba(0, 0, 0, 0.2),
inset 0 -2px 4px rgba(0, 0, 0, 0.2);
}
.keypad-style-dark .key:hover {
background: linear-gradient(145deg, #3498db, #2980b9);
border-color: #2980b9;
transform: translateY(-4px) scale(1.03);
box-shadow: 0 10px 24px rgba(52, 152, 219, 0.5),
0 6px 12px rgba(52, 152, 219, 0.3);
}
/* 키패드 스타일 - Colorful (다채로운 그라데이션) */
.keypad-style-colorful .key {
background: linear-gradient(145deg, #667eea, #764ba2);
border: none;
border-radius: 20px;
color: #ffffff !important;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4),
0 4px 10px rgba(118, 75, 162, 0.3);
}
.keypad-style-colorful .key:hover {
background: linear-gradient(145deg, #f093fb, #f5576c);
transform: translateY(-5px) scale(1.05);
box-shadow: 0 12px 28px rgba(240, 147, 251, 0.5),
0 6px 14px rgba(245, 87, 108, 0.4);
}
/* 백스페이스 버튼 스타일 오버라이드 */
.keypad-style-modern .key.backspace,
.keypad-style-bold .key.backspace,
.keypad-style-dark .key.backspace,
.keypad-style-colorful .key.backspace {
background: linear-gradient(145deg, #e74c3c, #c0392b);
border-color: #c0392b;
color: #fff !important;
}
/* 폰트 크기 - Small */
.keypad-font-small .key {
font-size: 32px;
}
.keypad-font-small .phone-number {
font-size: 52px;
}
/* 폰트 크기 - Medium */
.keypad-font-medium .key {
font-size: 38px;
}
.keypad-font-medium .phone-number {
font-size: 60px;
}
/* 폰트 크기 - Large (기본) */
.keypad-font-large .key {
font-size: 44px;
}
.keypad-font-large .phone-number {
font-size: 68px;
}
/* 폰트 크기 - XLarge */
.keypad-font-xlarge .key {
font-size: 52px;
}
.keypad-font-xlarge .phone-number {
font-size: 76px;
}

189
static/js/logout.js Normal file
View File

@@ -0,0 +1,189 @@
/**
* Centralized Logout Logic
* Handles modal injection, display, and API interactions for logging out.
*/
(function () {
// Inject Logout Modal HTML and Styles if they don't exist
function injectLogoutModal() {
if (document.getElementById('common-logout-modal')) return;
const modalHtml = `
<div id="common-logout-modal" class="common-modal-overlay">
<div class="common-modal-content">
<h2 class="common-modal-title">로그아웃</h2>
<p class="common-modal-message">정말 로그아웃 하시겠습니까?</p>
<div class="common-modal-actions">
<button id="common-logout-cancel" class="common-btn common-btn-secondary">취소</button>
<button id="common-logout-confirm" class="common-btn common-btn-primary">로그아웃</button>
</div>
</div>
</div>
`;
const styleHtml = `
<style>
.common-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 99999;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
opacity: 0;
transition: opacity 0.3s ease;
}
.common-modal-overlay.active {
display: flex;
opacity: 1;
}
.common-modal-content {
background: white;
padding: 30px;
border-radius: 16px;
width: 90%;
max-width: 400px;
text-align: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
transform: scale(0.95);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.common-modal-overlay.active .common-modal-content {
transform: scale(1);
}
.common-modal-title {
font-size: 22px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 12px;
}
.common-modal-message {
font-size: 16px;
color: #7f8c8d;
margin-bottom: 25px;
line-height: 1.5;
}
.common-modal-actions {
display: flex;
gap: 12px;
justify-content: center;
}
.common-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
flex: 1;
}
.common-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.common-btn:active {
transform: translateY(0);
}
.common-btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.common-btn-secondary {
background: #f1f3f5;
color: #495057;
}
.common-btn-secondary:hover {
background: #e9ecef;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styleHtml);
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Bind Events
document.getElementById('common-logout-cancel').addEventListener('click', closeLogoutModal);
document.getElementById('common-logout-confirm').addEventListener('click', executeLogout);
// Close on overlay click
document.getElementById('common-logout-modal').addEventListener('click', function (e) {
if (e.target === this) closeLogoutModal();
});
}
// Expose global functions
window.showLogoutModal = function () {
injectLogoutModal(); // Ensure it exists
const modal = document.getElementById('common-logout-modal');
// Small timeout to allow display:flex to apply before opacity transition
modal.style.display = 'flex';
requestAnimationFrame(() => {
modal.classList.add('active');
});
};
window.closeLogoutModal = function () {
const modal = document.getElementById('common-logout-modal');
if (modal) {
modal.classList.remove('active');
setTimeout(() => {
modal.style.display = 'none';
}, 300); // Wait for transition
}
};
// Alias for backward compatibility with existing buttons
window.logout = function (event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
window.showLogoutModal();
};
window.executeLogout = async function () {
try {
// Close SSE connection if exists
if (window.eventSource) {
window.eventSource.close();
}
// Call API
await fetch('/api/auth/logout', { method: 'POST' });
} catch (error) {
console.error('Logout error:', error);
} finally {
// Clear all possible Storage items
const keysToRemove = [
'access_token',
'refresh_token',
'selected_store_id',
'selected_store_name',
'selected_store_code',
'username',
'user_role',
'superadmin_franchise_context',
'store_management_context'
];
keysToRemove.forEach(key => localStorage.removeItem(key));
// Redirect
window.location.replace('/login');
}
};
// Initialize on load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectLogoutModal);
} else {
injectLogoutModal();
}
})();

View File

@@ -0,0 +1,21 @@
// 화면 크기 및 방향 모니터링 (태블릿 최적화 디버깅)
function logScreenInfo() {
const width = window.innerWidth;
const height = window.innerHeight;
const orientation = width > height ? 'landscape' : 'portrait';
const deviceType = width < 600 ? 'mobile' :
width < 768 ? 'small-tablet' :
width < 1024 ? 'tablet' : 'large-tablet/desktop';
console.log(`📱 Screen Info: ${width}x${height} (${orientation}) - ${deviceType}`);
}
// 초기 로드 시 화면 정보 출력
window.addEventListener('DOMContentLoaded', () => {
logScreenInfo();
});
// 화면 크기 변경 시 정보 출력 (회전 등)
window.addEventListener('resize', () => {
logScreenInfo();
});