- 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>
645 lines
21 KiB
HTML
645 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>대기접수 - 모바일</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body,
|
|
html {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
/* 스크롤 방지 */
|
|
}
|
|
|
|
body {
|
|
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: #fff;
|
|
}
|
|
|
|
.mobile-container {
|
|
padding: 2vh 20px;
|
|
max-width: 500px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
padding: 1vh 0 2vh 0;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 3.5vh;
|
|
margin-bottom: 0.5vh;
|
|
}
|
|
|
|
.header .date {
|
|
font-size: 1.8vh;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 12px;
|
|
padding: 4px;
|
|
margin-bottom: 2vh;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.tab {
|
|
flex: 1;
|
|
padding: 1.5vh;
|
|
text-align: center;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
font-weight: 600;
|
|
font-size: 2vh;
|
|
}
|
|
|
|
.tab.active {
|
|
background: #fff;
|
|
color: #667eea;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: flex;
|
|
flex-direction: column;
|
|
animation: fadeIn 0.3s;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.card {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 20px;
|
|
padding: 2.5vh 20px;
|
|
color: #2c3e50;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
/* 위아래 분산 */
|
|
max-height: 100%;
|
|
}
|
|
|
|
.phone-input-section {
|
|
margin-bottom: 2vh;
|
|
}
|
|
|
|
.phone-input-section label {
|
|
display: block;
|
|
font-size: 2vh;
|
|
font-weight: 600;
|
|
margin-bottom: 1vh;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.phone-display {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 1.5vh;
|
|
}
|
|
|
|
.phone-prefix {
|
|
font-size: 2.5vh;
|
|
font-weight: 600;
|
|
color: #7f8c8d;
|
|
padding: 1.5vh;
|
|
background: #ecf0f1;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.phone-number {
|
|
flex: 1;
|
|
font-size: 3vh;
|
|
font-weight: 600;
|
|
padding: 1.5vh;
|
|
text-align: center;
|
|
background: #fff;
|
|
border: 2px solid #667eea;
|
|
border-radius: 10px;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.keypad {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 1vh;
|
|
margin-bottom: 2vh;
|
|
flex: 1;
|
|
/* 남은 공간 차지 */
|
|
}
|
|
|
|
.key {
|
|
padding: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 3vh;
|
|
font-weight: 600;
|
|
background: #fff;
|
|
border: 1px solid #ecf0f1;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
color: #2c3e50;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.key:active {
|
|
background: #667eea;
|
|
color: #fff;
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.key.zero {
|
|
grid-column: 2;
|
|
}
|
|
|
|
.key.special {
|
|
background: #e74c3c;
|
|
color: #fff;
|
|
border-color: #e74c3c;
|
|
}
|
|
|
|
.submit-btn {
|
|
width: 100%;
|
|
padding: 2.2vh;
|
|
font-size: 2.5vh;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.submit-btn:active:not(:disabled) {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.submit-btn:disabled {
|
|
background: #95a5a6;
|
|
cursor: not-allowed;
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* Compact Mode for smaller screens or keyboards */
|
|
.mobile-container.compact-mode .header {
|
|
padding: 0;
|
|
margin-bottom: 1vh;
|
|
}
|
|
|
|
.mobile-container.compact-mode .header h1 {
|
|
font-size: 2.5vh;
|
|
}
|
|
|
|
.mobile-container.compact-mode .header .date {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-container.compact-mode .tabs {
|
|
margin-bottom: 1vh;
|
|
padding: 2px;
|
|
}
|
|
|
|
.mobile-container.compact-mode .card {
|
|
padding: 1.5vh;
|
|
}
|
|
|
|
.mobile-container.compact-mode .key {
|
|
font-size: 2.5vh;
|
|
}
|
|
|
|
/* Search Section specific */
|
|
.search-section input {
|
|
width: 100%;
|
|
padding: 2vh;
|
|
font-size: 2.5vh;
|
|
border: 2px solid #ecf0f1;
|
|
border-radius: 10px;
|
|
margin-bottom: 2vh;
|
|
}
|
|
|
|
.search-btn {
|
|
width: 100%;
|
|
padding: 2vh;
|
|
font-size: 2.5vh;
|
|
font-weight: 600;
|
|
background: #3498db;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.result-box {
|
|
background: #f8f9fa;
|
|
padding: 3vh;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
margin-top: 2vh;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.result-box .number {
|
|
font-size: 5vh;
|
|
font-weight: 700;
|
|
color: #667eea;
|
|
margin-bottom: 1.5vh;
|
|
}
|
|
|
|
.result-box .info {
|
|
font-size: 2.5vh;
|
|
margin-bottom: 1vh;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.result-box .detail {
|
|
font-size: 1.8vh;
|
|
color: #7f8c8d;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 5vh 20px;
|
|
color: #7f8c8d;
|
|
}
|
|
|
|
.empty-state .icon {
|
|
font-size: 6vh;
|
|
margin-bottom: 2vh;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="mobile-container">
|
|
<div class="header">
|
|
<h1 id="storeName">대기 시스템</h1>
|
|
<div class="date" id="currentDate"></div>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="switchTab('register')">대기 접수</div>
|
|
<div class="tab" onclick="switchTab('search')">대기 조회</div>
|
|
</div>
|
|
|
|
<!-- 대기 접수 탭 -->
|
|
<div id="registerTab" class="tab-content active">
|
|
<div class="card">
|
|
<!-- waitingStatus 제거됨 -->
|
|
|
|
<div class="phone-input-section">
|
|
<label>핸드폰번호 입력</label>
|
|
<div class="phone-display">
|
|
<div class="phone-prefix">010-</div>
|
|
<div class="phone-number" id="phoneDisplay">____-____</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="keypad">
|
|
<button class="key" onclick="inputNumber('1')">1</button>
|
|
<button class="key" onclick="inputNumber('2')">2</button>
|
|
<button class="key" onclick="inputNumber('3')">3</button>
|
|
<button class="key" onclick="inputNumber('4')">4</button>
|
|
<button class="key" onclick="inputNumber('5')">5</button>
|
|
<button class="key" onclick="inputNumber('6')">6</button>
|
|
<button class="key" onclick="inputNumber('7')">7</button>
|
|
<button class="key" onclick="inputNumber('8')">8</button>
|
|
<button class="key" onclick="inputNumber('9')">9</button>
|
|
<button class="key special" onclick="backspace()">←</button>
|
|
<button class="key zero" onclick="inputNumber('0')">0</button>
|
|
<button class="key" onclick="clearInput()">C</button>
|
|
</div>
|
|
|
|
<button class="submit-btn" id="submitBtn" onclick="submitReception()">
|
|
대기 접수
|
|
</button>
|
|
</div>
|
|
|
|
<div id="registerResult" style="display:none;">
|
|
<div class="card result-box">
|
|
<div class="number" id="resultNumber"></div>
|
|
<div class="info" id="resultClass"></div>
|
|
<div class="detail" id="resultDetail"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 대기 조회 탭 -->
|
|
<div id="searchTab" class="tab-content">
|
|
<div class="card">
|
|
<div class="search-section">
|
|
<label style="display:block; margin-bottom:10px; font-weight:600;">핸드폰번호 조회</label>
|
|
<div class="phone-display" style="margin-bottom:15px;">
|
|
<div class="phone-prefix">010-</div>
|
|
<input type="tel" id="searchPhone" placeholder="1234-5678" maxlength="9"
|
|
style="flex:1; padding:12px; font-size:18px; border:2px solid #ecf0f1; border-radius:10px; text-align:center;">
|
|
</div>
|
|
<button class="search-btn" onclick="searchWaiting()">조회하기</button>
|
|
</div>
|
|
|
|
<div id="searchResult" style="display:none;">
|
|
<div class="result-box">
|
|
<div class="number" id="searchResultNumber"></div>
|
|
<div class="info" id="searchResultClass"></div>
|
|
<div class="detail" id="searchResultDetail"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="searchEmpty" class="empty-state" style="display:none;">
|
|
<div class="icon">🔍</div>
|
|
<p>대기 내역이 없습니다</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Helper function to get headers with store ID
|
|
function getHeaders(additionalHeaders = {}) {
|
|
const headers = { ...additionalHeaders };
|
|
const storeId = localStorage.getItem('selected_store_id');
|
|
if (storeId) {
|
|
headers['X-Store-Id'] = storeId;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
let phoneNumber = '';
|
|
let storeSettings = null;
|
|
|
|
async function loadStoreInfo() {
|
|
try {
|
|
const response = await fetch('/api/store/', { headers: getHeaders() });
|
|
storeSettings = await response.json();
|
|
document.getElementById('storeName').textContent = storeSettings.store_name;
|
|
// waitingStatus 로드 제거 (사용자 요청)
|
|
} catch (error) {
|
|
console.error('매장 정보 조회 실패:', error);
|
|
}
|
|
}
|
|
|
|
async function updateDate() {
|
|
try {
|
|
const response = await fetch('/api/daily/check-status', { headers: getHeaders() });
|
|
const status = await response.json();
|
|
|
|
if (status && status.business_date) {
|
|
const dateObj = new Date(status.business_date);
|
|
const year = dateObj.getFullYear();
|
|
const month = dateObj.getMonth() + 1;
|
|
const day = dateObj.getDate();
|
|
document.getElementById('currentDate').textContent = `${year}년 ${month}월 ${day}일`;
|
|
} else {
|
|
const now = new Date();
|
|
document.getElementById('currentDate').textContent = `${now.getFullYear()}년 ${now.getMonth() + 1}월 ${now.getDate()}일`;
|
|
}
|
|
} catch (error) {
|
|
console.error('영업일 조회 실패:', error);
|
|
const now = new Date();
|
|
document.getElementById('currentDate').textContent = `${now.getFullYear()}년 ${now.getMonth() + 1}월 ${now.getDate()}일`;
|
|
}
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
|
|
if (tab === 'register') {
|
|
document.querySelectorAll('.tab')[0].classList.add('active');
|
|
document.getElementById('registerTab').classList.add('active');
|
|
} else {
|
|
document.querySelectorAll('.tab')[1].classList.add('active');
|
|
document.getElementById('searchTab').classList.add('active');
|
|
}
|
|
}
|
|
|
|
function inputNumber(num) {
|
|
if (phoneNumber.length < 8) {
|
|
phoneNumber += num;
|
|
updateDisplay();
|
|
}
|
|
}
|
|
|
|
function backspace() {
|
|
if (phoneNumber.length > 0) {
|
|
phoneNumber = phoneNumber.slice(0, -1);
|
|
updateDisplay();
|
|
}
|
|
}
|
|
|
|
function clearInput() {
|
|
phoneNumber = '';
|
|
updateDisplay();
|
|
document.getElementById('registerResult').style.display = 'none';
|
|
}
|
|
|
|
function updateDisplay() {
|
|
const display = document.getElementById('phoneDisplay');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
|
|
if (phoneNumber.length === 0) {
|
|
display.textContent = '____-____';
|
|
submitBtn.disabled = true;
|
|
} else if (phoneNumber.length <= 4) {
|
|
const part1 = phoneNumber.padEnd(4, '_');
|
|
display.textContent = `${part1}-____`;
|
|
submitBtn.disabled = true;
|
|
} else {
|
|
const part1 = phoneNumber.substring(0, 4);
|
|
const part2 = phoneNumber.substring(4).padEnd(4, '_');
|
|
display.textContent = `${part1}-${part2}`;
|
|
submitBtn.disabled = phoneNumber.length !== 8;
|
|
}
|
|
}
|
|
|
|
async function submitReception() {
|
|
if (phoneNumber.length !== 8) {
|
|
alert('핸드폰번호 8자리를 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
const fullPhone = '010' + phoneNumber;
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = '접수 중...';
|
|
|
|
try {
|
|
const response = await fetch('/api/waiting/register', {
|
|
method: 'POST',
|
|
headers: getHeaders({ 'Content-Type': 'application/json' }),
|
|
body: JSON.stringify({
|
|
phone: fullPhone
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
showRegisterResult(result);
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.detail || '접수에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('접수 실패:', error);
|
|
alert('접수 중 오류가 발생했습니다.');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = '대기 접수';
|
|
}
|
|
}
|
|
|
|
function showRegisterResult(result) {
|
|
// 조회 탭으로 전환
|
|
switchTab('search');
|
|
|
|
// 조회 결과 표시
|
|
const phoneInput = document.getElementById('phoneDisplay').textContent.replace('010-', '').replace(/-/g, '').replace(/_/g, '');
|
|
document.getElementById('searchPhone').value = phoneInput.substring(0, 4) + (phoneInput.length > 4 ? '-' + phoneInput.substring(4, 8) : '');
|
|
|
|
// 대기 정보 표시
|
|
document.getElementById('searchResultNumber').textContent = `${result.waiting_number}번`;
|
|
document.getElementById('searchResultClass').textContent = result.class_name;
|
|
document.getElementById('searchResultDetail').textContent = `${result.class_order}번째 대기`;
|
|
document.getElementById('searchResult').style.display = 'block';
|
|
document.getElementById('searchEmpty').style.display = 'none';
|
|
|
|
// 접수 폼 초기화
|
|
phoneNumber = '';
|
|
updateDisplay();
|
|
document.getElementById('registerResult').style.display = 'none';
|
|
}
|
|
|
|
async function searchWaiting() {
|
|
let searchPhone = document.getElementById('searchPhone').value.replace(/-/g, '');
|
|
// 숫자만 남기기
|
|
searchPhone = searchPhone.replace(/[^0-9]/g, '');
|
|
|
|
if (searchPhone.length !== 8) {
|
|
alert('핸드폰번호 8자리를 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
const fullPhone = '010' + searchPhone;
|
|
|
|
try {
|
|
const response = await fetch(`/api/waiting/check/${fullPhone}`, { headers: getHeaders() });
|
|
const result = await response.json();
|
|
|
|
if (result.found) {
|
|
document.getElementById('searchResultNumber').textContent = `${result.waiting_number}번`;
|
|
document.getElementById('searchResultClass').textContent = result.class_name;
|
|
document.getElementById('searchResultDetail').textContent = `앞에 ${result.ahead_count}명 대기 중`;
|
|
document.getElementById('searchResult').style.display = 'block';
|
|
document.getElementById('searchEmpty').style.display = 'none';
|
|
} else {
|
|
document.getElementById('searchResult').style.display = 'none';
|
|
document.getElementById('searchEmpty').style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
console.error('조회 실패:', error);
|
|
alert('조회 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
// URL 파라미터에서 매장 정보 가져오기
|
|
async function checkUrlStoreParam() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const storeParam = urlParams.get('store');
|
|
|
|
if (storeParam) {
|
|
try {
|
|
const response = await fetch(`/api/stores/code/${storeParam}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
|
}
|
|
});
|
|
if (response.ok) {
|
|
const store = await response.json();
|
|
localStorage.setItem('selected_store_id', store.id);
|
|
localStorage.setItem('selected_store_name', store.name);
|
|
console.log(`URL 매장 파라미터 적용: ${store.name} (코드: ${storeParam})`);
|
|
} else {
|
|
console.error('매장 코드를 찾을 수 없습니다:', storeParam);
|
|
}
|
|
} catch (e) {
|
|
console.error('매장 정보 조회 실패:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 화면 높이에 따른 자동 레이아웃 조정
|
|
function adjustLayout() {
|
|
const height = window.innerHeight;
|
|
const container = document.querySelector('.mobile-container');
|
|
|
|
// 작은 화면 (예: 키보드가 올라오거나 작은 폰)
|
|
if (height < 600) {
|
|
container.classList.add('compact-mode');
|
|
} else {
|
|
container.classList.remove('compact-mode');
|
|
}
|
|
}
|
|
|
|
// 초기 로드
|
|
async function init() {
|
|
// 레이아웃 조정 리스너 등록
|
|
window.addEventListener('resize', adjustLayout);
|
|
adjustLayout();
|
|
|
|
await checkUrlStoreParam();
|
|
loadStoreInfo();
|
|
updateDate();
|
|
updateDisplay();
|
|
}
|
|
|
|
init();
|
|
|
|
// 조회 탭 전화번호 포맷팅
|
|
document.getElementById('searchPhone').addEventListener('input', function (e) {
|
|
let value = e.target.value.replace(/[^0-9]/g, '');
|
|
if (value.length > 4) {
|
|
value = value.slice(0, 4) + '-' + value.slice(4, 8);
|
|
}
|
|
e.target.value = value;
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |