Files
waiting-system/templates/settings.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

1533 lines
75 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>매장 설정</title>
<link rel="stylesheet" href="/static/css/common.css">
<style>
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.tabs-container {
display: flex;
gap: 10px;
margin-bottom: 30px;
border-bottom: 2px solid #ecf0f1;
}
.settings-tab {
padding: 15px 30px;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 16px;
font-weight: 600;
color: #7f8c8d;
transition: all 0.3s;
}
.settings-tab.active {
color: #3498db;
border-bottom-color: #3498db;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s;
}
.class-type-content {
display: none;
}
.class-type-content.active {
display: block;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.class-list {
display: flex;
flex-direction: column;
gap: 20px;
background: #f8f9fa;
padding: 20px;
border-radius: 12px;
max-height: calc(100vh - 350px);
overflow-y: auto;
}
/* Custom Scrollbar for Class List */
.class-list::-webkit-scrollbar {
width: 8px;
}
.class-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.class-list::-webkit-scrollbar-thumb {
background: #bdc3c7;
border-radius: 4px;
}
.class-list::-webkit-scrollbar-thumb:hover {
background: #95a5a6;
}
.class-item {
background: #fff;
padding: 25px 30px;
border-radius: 12px;
display: grid;
grid-template-columns: 60px 1fr auto;
gap: 25px;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border-left: 5px solid #3498db;
transition: all 0.3s;
}
.class-item:hover {
transform: translateX(5px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
border-left-color: #2980b9;
}
.class-number-badge {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #3498db, #2980b9);
color: #fff;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: 700;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
}
.class-item-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.class-item-info .title {
font-size: 26px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 5px;
}
.class-item-info .details {
display: flex;
gap: 30px;
flex-wrap: wrap;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.detail-label {
font-size: 13px;
color: #95a5a6;
font-weight: 500;
}
.detail-value {
font-size: 22px;
font-weight: 700;
color: #2c3e50;
}
.detail-value.time {
color: #3498db;
}
.detail-value.capacity {
color: #27ae60;
}
.detail-value.waiting {
color: #e74c3c;
}
.class-item-actions {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 120px;
}
.class-item-actions .btn {
width: 100%;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.inactive-badge {
display: inline-block;
padding: 4px 12px;
background: #e74c3c;
color: #fff;
border-radius: 12px;
font-size: 12px;
margin-left: 10px;
}
@media (max-width: 768px) {
.settings-header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.tabs-container {
overflow-x: auto;
flex-wrap: nowrap;
}
.class-item {
grid-template-columns: 1fr;
gap: 20px;
padding: 20px;
}
.class-number-badge {
width: 50px;
height: 50px;
font-size: 20px;
}
.class-item-info .title {
font-size: 22px;
}
.detail-value {
font-size: 18px;
}
.class-item-actions {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<div class="settings-header">
<div>
<h1>매장 설정</h1>
<p class="subtitle">매장 정보 및 클래스 관리</p>
</div>
<a href="/" class="btn btn-secondary">← 메인으로</a>
</div>
<div class="tabs-container">
<button class="settings-tab active" onclick="switchTab('store')">매장 정보</button>
<button class="settings-tab" onclick="switchTab('class')">클래스 관리</button>
</div>
<!-- 매장 정보 탭 -->
<div id="storeTab" class="tab-content active">
<!-- 설정 복제 섹션 -->
<div class="card"
style="margin-bottom: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<h3 style="margin-bottom: 15px; color: white;">⚡ 빠른 설정</h3>
<p style="margin-bottom: 20px; opacity: 0.9;">다른 매장의 설정을 복제하여 간편하게 설정할 수 있습니다.</p>
<div style="display: flex; gap: 10px; align-items: center;">
<select id="cloneSourceStore" class="form-control" style="flex: 1; max-width: 400px;">
<option value="">복제할 매장 선택</option>
</select>
<button class="btn" style="background: white; color: #667eea; font-weight: 600;"
onclick="cloneSettings()">
🔄 설정 복제하기
</button>
</div>
<small style="display: block; margin-top: 10px; opacity: 0.85;">
💡 복제 시 매장명을 제외한 모든 설정값이 복사됩니다
</small>
</div>
<div class="card">
<h2 style="margin-bottom:20px;">매장 기본 정보</h2>
<form id="storeForm" onsubmit="saveStoreSettings(event)">
<div class="form-group">
<label>매장명</label>
<input type="text" id="storeName" class="form-control" required>
</div>
<div class="form-row">
<div class="form-group">
<label>대기현황판 표시 클래스 수</label>
<input type="number" id="displayClassesCount" class="form-control" min="1" max="10"
required>
<small style="color:#7f8c8d;">대기현황판에 한 번에 표시할 클래스 개수</small>
</div>
<div class="form-group">
<label>클래스당 줄 수</label>
<input type="number" id="rowsPerClass" class="form-control" min="1" max="5" required>
<small style="color:#7f8c8d;">각 클래스별 대기자 목록을 몇 줄로 표시할지</small>
</div>
</div>
<div class="form-group">
<label>영업 시작 기준 시간 (새벽)</label>
<select id="businessDayStart" class="form-control">
<option value="0">00:00 (자정)</option>
<option value="1">01:00</option>
<option value="2">02:00</option>
<option value="3">03:00</option>
<option value="4">04:00</option>
<option value="5" selected>05:00 (기본값)</option>
<option value="6">06:00</option>
<option value="7">07:00</option>
<option value="8">08:00</option>
</select>
<small style="color:#7f8c8d;">
하루 영업 시작으로 간주할 시간을 선택합니다.<br>
예: 05:00 선택 시, 새벽 4시에 오신 손님은 '전날' 영업일로 기록됩니다.
</small>
</div>
<div class="form-group">
<label class="d-block font-weight-bold mb-2">개점 설정 (Opening Rules)</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="dailyOpeningRule" id="openingRuleStrict"
value="strict">
<label class="form-check-label" for="openingRuleStrict">
당일 개점은 1번만 가능 (1일 1회 제한)
</label>
<small class="form-text text-muted">
이미 개점/마감한 날짜에는 다시 개점할 수 없습니다. "내일 개점해주세요" 알림이 표시됩니다.
</small>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="radio" name="dailyOpeningRule"
id="openingRuleFlexible" value="flexible">
<label class="form-check-label" for="openingRuleFlexible">
2회 이상 개점 가능 (자동 날짜 변경)
</label>
<small class="form-text text-muted">
이미 마감한 날짜에 다시 개점하면, 자동으로 <strong>다음 날짜</strong>로 개점됩니다.
</small>
</div>
</div>
<div class="form-group">
<label class="form-check-label" for="autoClosing">
<input type="checkbox" id="autoClosing" style="width: 20px; height: 20px; cursor: pointer;">
자동 마감 및 리셋 사용
</label>
<small class="form-text text-muted d-block mt-1">
활성화 시(기본): 기준 시간이 되면 자동으로 마감 처리되고 대기자가 리셋됩니다.<br>
비활성화 시: 기준 시간이 지나도 마감되지 않으며, 미처리된 대기자는 다음 영업일로 자동 이월됩니다.
</small>
<!-- 마감 처리 방식 설 (자동 마감 활성화 시 표시) -->
<div id="closingActionContainer" class="mt-3 ml-4" style="display: none;">
<label class="d-block font-weight-bold mb-2">미처리 대기자 처리 방식</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="closingAction" id="actionReset"
value="reset" checked>
<label class="form-check-label" for="actionReset">
초기화 (취소 처리)
</label>
<small class="form-text text-muted">
미처리 대기자를 '취소' 상태로 변경하고 목록에서 제거합니다.
</small>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="radio" name="closingAction" id="actionAttended"
value="attended">
<label class="form-check-label" for="actionAttended">
일괄 출석 처리
</label>
<small class="form-text text-muted">
미처리 대기자를 모두 '출석' 상태로 변경합니다. (데이터 누락 방지)
</small>
</div>
</div>
</div>
<div class="form-group">
<label>대기자 리스트 방향</label>
<select id="listDirection" class="form-control">
<option value="vertical">세로 방향</option>
<option value="horizontal">가로 방향</option>
</select>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="useMaxWaitingLimit"
style="width: 20px; height: 20px; cursor: pointer;">
<span>최대 대기 인원 제한 사용</span>
</label>
<small style="color:#7f8c8d; margin-left: 30px;">
비활성화 시, 최대 대기 인원 제한이 적용되지 않습니다.
</small>
</div>
<div class="form-group" id="maxWaitingLimitGroup">
<label>최대 대기 인원</label>
<input type="number" id="maxWaitingLimit" class="form-control" min="0" required>
<small style="color:#7f8c8d;">최대 대기 등록 가능 인원 (0 = 무제한)</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="blockLastClassRegistration"
style="width: 20px; height: 20px; cursor: pointer;">
<span>마지막 교시 정원 초과 시 대기접수 차단</span>
</label>
<small style="color:#7f8c8d; margin-left: 30px;">
활성화 시, 마지막 교시의 대기 인원이 정원을 초과하면 더 이상 대기접수를 받지 않습니다.
</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="autoRegisterMember"
style="width: 20px; height: 20px; cursor: pointer;">
<span>대기 등록 시 자동 회원가입</span>
</label>
<small style="color:#7f8c8d; margin-left: 30px;">
활성화 시, 비회원이 대기 등록을 하면 자동으로 회원으로 등록됩니다.
</small>
</div>
<div class="form-group">
<label class="d-block font-weight-bold mb-2">출석 횟수 표시 기준</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="attendanceCountType" id="countTypeDays"
value="days" onchange="toggleAttendanceSettings()">
<label class="form-check-label" for="countTypeDays">최근 N일 기준</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="attendanceCountType"
id="countTypeMonthly" value="monthly" onchange="toggleAttendanceSettings()">
<label class="form-check-label" for="countTypeMonthly">이번 달 (월간) 기준</label>
</div>
<div id="attendanceLookbackDaysContainer"
style="margin-top: 10px; margin-left: 20px; display: none;">
<label>조회 기간 (일)</label>
<input type="number" id="attendanceLookbackDays" class="form-control"
style="width: 100px; display: inline-block; margin-left: 10px;" min="1" value="30">
<small class="form-text text-muted d-block mt-1">오늘을 포함한 최근 며칠간의 출석을 집계할지 설정합니다.</small>
</div>
</div>
<hr style="margin: 30px 0; border: none; border-top: 2px solid #ecf0f1;">
<h3 style="margin-bottom: 20px; color: #2c3e50;">대기현황판 표시 설정</h3>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="showWaitingNumber"
style="width: 20px; height: 20px; cursor: pointer;">
<span>대기번호 표시</span>
</label>
<small style="color:#7f8c8d; margin-left: 30px;">
대기번호(예: 대기 5번)를 현황판에 표시합니다.
</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="maskCustomerName"
style="width: 20px; height: 20px; cursor: pointer;">
<span>이름 마스킹 (홍O동)</span>
</label>
<small style="color:#7f8c8d; margin-left: 30px;">
이름을 마스킹하여 표시합니다 (예: 홍길동 → 홍O동).
</small>
</div>
<div class="form-group">
<label>이름 표시 자릿수</label>
<input type="number" id="nameDisplayLength" class="form-control" min="0" max="10" value="0">
<small style="color:#7f8c8d;">
이름을 몇 글자까지 표시할지 설정합니다. (0 = 전체 표시, 1 = 첫 글자만, 2 = 두 글자만 등)
</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="showOrderNumber"
style="width: 20px; height: 20px; cursor: pointer;">
<span>순번 표시 (1번째, 2번째)</span>
</label>
<small style="color:#7f8c8d; margin-left: 30px;">
교시 내 순번(예: 1번째, 2번째)을 표시합니다.
</small>
</div>
<div class="form-group">
<label>표시 순서</label>
<select id="boardDisplayOrder" class="form-control">
<option value="number,name,order">대기번호 → 이름 → 순번</option>
<option value="number,order,name">대기번호 → 순번 → 이름</option>
<option value="name,number,order">이름 → 대기번호 → 순번</option>
<option value="name,order,number">이름 → 순번 → 대기번호</option>
<option value="order,number,name">순번 → 대기번호 → 이름</option>
<option value="order,name,number">순번 → 이름 → 대기번호</option>
</select>
<small style="color:#7f8c8d;">현황판에서 정보를 표시할 순서를 선택합니다.</small>
</div>
<hr style="margin: 30px 0; border: none; border-top: 2px solid #ecf0f1;">
<h3 style="margin-bottom: 20px; color: #2c3e50;">폰트 및 스타일 설정</h3>
<div class="form-group">
<label class="d-block font-weight-bold mb-2">대기관리자 화면 (Manager)</label>
<div class="form-row">
<div class="form-group">
<label>글꼴 (Font)</label>
<select id="managerFontFamily" class="form-control">
<option value="Nanum Gothic">나눔고딕 (Nanum Gothic)</option>
<option value="Gowun Dodum">고운돋움 (Gowun Dodum)</option>
<option value="Noto Sans KR">노토 산스 (Noto Sans KR)</option>
<option value="Spoqa Han Sans Neo">스포카 한 산스 네오 (Spoqa Han Sans Neo)</option>
<option value="Malgun Gothic">맑은 고딕 (Microsoft)</option>
</select>
</div>
<div class="form-group">
<label>글자 크기 (Size)</label>
<select id="managerFontSize" class="form-control">
<option value="14px">14px (작음)</option>
<option value="15px">15px (보통)</option>
<option value="16px">16px (조금 큼)</option>
<option value="18px">18px (큼)</option>
<option value="20px">20px (아주 큼)</option>
<option value="24px">24px (더 큼)</option>
<option value="28px">28px (매우 큼)</option>
<option value="30px">30px (최대)</option>
</select>
</div>
</div>
<div class="form-row" style="margin-top: 15px;">
<div class="form-group">
<label>레이아웃 최대 너비 (px)</label>
<input type="number" id="managerMaxWidth" class="form-control" placeholder="기본값 (95%)"
min="800" step="10">
<small style="color:#7f8c8d;">
화면의 최대 너비를 픽셀(px) 단위로 설정합니다. 비워두면 화면 전체 너비(95%)를 사용합니다.
</small>
</div>
</div>
</div>
<div class="form-group">
<label class="d-block font-weight-bold mb-2">대기현황판 화면 (Board)</label>
<div class="form-row">
<div class="form-group">
<label>글꼴 (Font)</label>
<select id="boardFontFamily" class="form-control">
<option value="Nanum Gothic">나눔고딕 (Nanum Gothic)</option>
<option value="Gowun Dodum">고운돋움 (Gowun Dodum)</option>
<option value="Noto Sans KR">노토 산스 (Noto Sans KR)</option>
<option value="Spoqa Han Sans Neo">스포카 한 산스 네오 (Spoqa Han Sans Neo)</option>
<option value="Malgun Gothic">맑은 고딕 (Microsoft)</option>
</select>
</div>
<div class="form-group">
<label>글자 크기 (Size)</label>
<select id="boardFontSize" class="form-control">
<option value="20px">20px (작음)</option>
<option value="24px">24px (보통)</option>
<option value="28px">28px (조금 큼)</option>
<option value="32px">32px (큼)</option>
<option value="36px">36px (아주 큼)</option>
<option value="40px">40px (초대형)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label class="d-block font-weight-bold mb-2">대기접수 데스크 키패드</label>
<div class="form-row">
<div class="form-group">
<label>키패드 스타일</label>
<select id="keypadStyle" class="form-control">
<option value="modern">모던 (Modern) - 부드러운 그라데이션</option>
<option value="bold">진한 경계선 (Bold) - 명확한 구분</option>
<option value="dark">다크 모드 (Dark) - 검정 배경</option>
<option value="colorful">컬러풀 (Colorful) - 화려한 그라데이션</option>
</select>
<small style="color:#7f8c8d; margin-top: 5px; display: block;">
어르신들은 '진한 경계선' 스타일을 추천합니다.
</small>
</div>
<div class="form-group">
<label>숫자 크기</label>
<select id="keypadFontSize" class="form-control">
<option value="small">작음 (32px)</option>
<option value="medium">보통 (38px)</option>
<option value="large">크게 (44px)</option>
<option value="xlarge">매우 크게 (52px)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label class="d-block font-weight-bold mb-2">대기접수 완료 화면 설정</label>
<div class="form-group">
<label>모달 자동 닫힘 시간 (초)</label>
<input type="number" id="waitingModalTimeout" class="form-control" min="1" max="60"
value="5">
<small style="color:#7f8c8d;">대기접수 완료 모달이 자동으로 닫힐 때까지의 시간</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="showMemberNameInWaitingModal"
style="width: 20px; height: 20px;">
<span>회원 이름 표시 (예: 홍길동님)</span>
</label>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="showNewMemberTextInWaitingModal"
style="width: 20px; height: 20px;">
<span>신규회원 문구 표시 (예: 신규회원님)</span>
</label>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="enableWaitingVoiceAlert" style="width: 20px; height: 20px;">
<span>대기접수 완료 음성 안내 사용 (예: "1교시 대기 접수 되었습니다")</span>
</label>
<div style="margin-left: 30px; margin-top: 10px;">
<input type="text" id="waitingVoiceMessage" class="form-control"
placeholder="음성 안내 문구를 입력하세요 (비워두면 기본 문구 사용)" style="width: 100%;">
<small style="color: #666; display: block; margin-top: 5px;">
* 문구 미입력 시 "{클래스명} 대기 접수 되었습니다" 로 안내됩니다.
</small>
</div>
<div style="margin-left: 30px; margin-top: 10px;">
<label style="display: block; margin-bottom: 5px;">음성 목소리 선택</label>
<div style="display: flex; gap: 10px;">
<select id="waitingVoiceSelect" class="form-control" style="flex: 1;">
<option value="">기본 목소리</option>
</select>
<button type="button" id="testVoiceBtn" class="btn btn-secondary"
style="width: auto;">미리듣기</button>
</div>
</div>
<div style="margin-left: 30px; margin-top: 10px;">
<label style="display: block; margin-bottom: 5px;">음성 스타일</label>
<select id="waitingVoiceStyle" class="form-control" style="width: 100%;">
<option value="standard">기본 (보통 속도/톤)</option>
<option value="senior">어르신 추천 (느리고 또렷하게)</option>
<option value="soft">부드러움 (조금 느리고 부드럽게)</option>
<option value="calm">차분함 (조금 느리고 낮게)</option>
<option value="bright">활기참 (보통 속도, 조금 높게)</option>
</select>
</div>
</div>
</div>
<hr style="margin: 30px 0; border: none; border-top: 2px solid #ecf0f1;">
<div class="form-group">
<label>관리자 비밀번호</label>
<input type="password" id="adminPassword" class="form-control" required>
<small style="color:#7f8c8d;">매장 설정 변경 시 사용됩니다</small>
</div>
<button type="submit" class="btn btn-primary btn-lg">설정 저장</button>
</form>
</div>
</div>
<!-- 클래스 관리 탭 -->
<div id="classTab" class="tab-content">
<!-- 평일/주말 서브 탭 -->
<div class="tabs-container" style="margin-bottom: 20px;">
<button class="settings-tab active" onclick="switchClassTypeTab('weekday')">평일 클래스</button>
<button class="settings-tab" onclick="switchClassTypeTab('weekend')">주말 클래스</button>
</div>
<!-- 평일 클래스 -->
<div id="weekdayClassTab" class="class-type-content active">
<div class="card" style="margin-bottom:20px;">
<button class="btn btn-success" onclick="openAddClassModal('weekday')">+ 평일 클래스 추가</button>
</div>
<div class="class-list" id="weekdayClassList">
<div class="loading">
<div class="spinner"></div>
<p>로딩 중...</p>
</div>
</div>
</div>
<!-- 주말 클래스 -->
<div id="weekendClassTab" class="class-type-content">
<div class="card" style="margin-bottom:20px;">
<button class="btn btn-success" onclick="openAddClassModal('weekend')">+ 주말 클래스 추가</button>
</div>
<div class="class-list" id="weekendClassList">
<div class="loading">
<div class="spinner"></div>
<p>로딩 중...</p>
</div>
</div>
</div>
</div>
</div>
<!-- 클래스 추가/수정 모달 -->
<div id="classModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="classModalTitle">클래스 추가</h2>
</div>
<form id="classForm" onsubmit="saveClass(event)">
<div class="form-group">
<label>클래스 타입</label>
<select id="classType" class="form-control" required>
<option value="weekday">평일 클래스</option>
<option value="weekend">주말 클래스</option>
<option value="all">전체 요일 클래스</option>
</select>
<small style="color:#7f8c8d; margin-top: 5px; display: block;">
평일: 월-금 운영 / 주말: 토-일 운영 / 전체: 모든 요일 운영
</small>
</div>
<div class="form-row">
<div class="form-group">
<label>교시 번호</label>
<input type="number" id="classNumber" class="form-control" min="1" required>
</div>
<div class="form-group">
<label>교시명</label>
<input type="text" id="className" class="form-control" placeholder="예: 1교시" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>시작 시간</label>
<input type="time" id="startTime" class="form-control" required step="60">
</div>
<div class="form-group">
<label>종료 시간</label>
<input type="time" id="endTime" class="form-control" required step="60">
</div>
</div>
<div class="form-group">
<label>최대 수용 인원</label>
<input type="number" id="maxCapacity" class="form-control" min="1" value="10" required>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('classModal')">취소</button>
<button type="submit" class="btn btn-primary">저장</button>
</div>
</form>
</div>
</div>
<!-- 알림 모달 -->
<div id="notificationModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header" style="border-bottom: none; padding-bottom: 0;">
<h2 id="notificationTitle" style="text-align: center; width: 100%; margin: 0;">알림</h2>
</div>
<div class="modal-body" style="padding: 30px; text-align: center;">
<div style="font-size: 64px; margin-bottom: 20px;" id="notificationIcon"></div>
<p id="notificationMessage"
style="font-size: 24px; font-weight: 600; color: #2c3e50; line-height: 1.5; margin: 0;"></p>
</div>
<div class="modal-footer" style="border-top: none; padding-top: 0;">
<button type="button" class="btn btn-primary" onclick="closeNotificationModal()"
style="width: 100%; font-size: 20px; padding: 15px;">확인</button>
</div>
</div>
</div>
<script>
let currentClassId = null;
let classes = [];
let availableStores = [];
// 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;
}
// 알림 모달 표시 함수
function showNotification(message, icon = '✅', title = '알림') {
document.getElementById('notificationTitle').textContent = title;
document.getElementById('notificationMessage').textContent = message;
document.getElementById('notificationIcon').textContent = icon;
document.getElementById('notificationModal').classList.add('active');
}
// 알림 모달 닫기
function closeNotificationModal() {
document.getElementById('notificationModal').classList.remove('active');
}
// ESC 키로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const notificationModal = document.getElementById('notificationModal');
if (notificationModal.classList.contains('active')) {
closeNotificationModal();
}
}
});
function switchTab(tab) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
if (tab === 'store') {
document.querySelectorAll('.settings-tab')[0].classList.add('active');
document.getElementById('storeTab').classList.add('active');
loadStoreSettings();
loadAvailableStores();
} else {
document.querySelectorAll('.settings-tab')[1].classList.add('active');
document.getElementById('classTab').classList.add('active');
loadClasses();
}
}
function switchClassTypeTab(classType) {
// 서브 탭 버튼 활성화 상태 변경
const classTab = document.getElementById('classTab');
const tabButtons = classTab.querySelectorAll('.tabs-container .settings-tab');
tabButtons.forEach(btn => btn.classList.remove('active'));
if (classType === 'weekday') {
tabButtons[0].classList.add('active');
document.getElementById('weekdayClassTab').classList.add('active');
document.getElementById('weekendClassTab').classList.remove('active');
} else {
tabButtons[1].classList.add('active');
document.getElementById('weekendClassTab').classList.add('active');
document.getElementById('weekdayClassTab').classList.remove('active');
}
}
async function loadAvailableStores() {
try {
const token = localStorage.getItem('access_token');
const response = await fetch('/api/stores/', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
availableStores = await response.json();
const currentStoreId = localStorage.getItem('selected_store_id');
const select = document.getElementById('cloneSourceStore');
select.innerHTML = '<option value="">복제할 매장 선택</option>';
// 현재 매장을 제외한 다른 매장들만 표시
availableStores
.filter(store => store.id !== parseInt(currentStoreId) && store.is_active)
.forEach(store => {
const option = document.createElement('option');
option.value = store.id;
option.textContent = `${store.name} (${store.code})`;
select.appendChild(option);
});
}
} catch (error) {
console.error('매장 목록 조회 실패:', error);
}
}
async function cloneSettings() {
const sourceStoreId = document.getElementById('cloneSourceStore').value;
if (!sourceStoreId) {
showNotification('복제할 매장을 선택해주세요.', '⚠️');
return;
}
const sourceStore = availableStores.find(s => s.id === parseInt(sourceStoreId));
const confirmMsg = `${sourceStore.name}의 설정을 복제하시겠습니까?\n\n현재 매장의 모든 설정값(매장명 제외)이 덮어씌워집니다.`;
if (!confirm(confirmMsg)) {
return;
}
try {
const response = await fetch(`/api/store/clone/${sourceStoreId}`, {
method: 'POST',
headers: getHeaders({ 'Content-Type': 'application/json' })
});
if (response.ok) {
showNotification('설정이 성공적으로 복제되었습니다!', '✅');
loadStoreSettings(); // 복제된 설정 다시 로드
} else {
const error = await response.json();
showNotification('복제 실패: ' + (error.detail || '알 수 없는 오류'), '❌', '오류');
}
} catch (error) {
console.error('설정 복제 실패:', error);
showNotification('복제 중 오류가 발생했습니다.', '❌', '오류');
}
}
function toggleClosingAction(isChecked) {
const container = document.getElementById('closingActionContainer');
if (isChecked) {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
}
async function loadStoreSettings() {
try {
const response = await fetch('/api/store/', {
headers: getHeaders()
});
const settings = await response.json();
document.getElementById('storeName').value = settings.store_name;
document.getElementById('displayClassesCount').value = settings.display_classes_count;
document.getElementById('rowsPerClass').value = settings.rows_per_class;
document.getElementById('listDirection').value = settings.list_direction;
document.getElementById('businessDayStart').value = settings.business_day_start !== undefined ? settings.business_day_start : 5;
// 개점 설정
const openingRule = settings.daily_opening_rule || 'strict';
if (openingRule === 'flexible') {
document.getElementById('openingRuleFlexible').checked = true;
} else {
document.getElementById('openingRuleStrict').checked = true;
}
// 자동 마감 설정
const autoClosingCheckbox = document.getElementById('autoClosing');
autoClosingCheckbox.checked = settings.auto_closing !== false; // Default true
// 마감 처리 방식 설정
const closingAction = settings.closing_action || 'reset';
if (closingAction === 'attended') {
document.getElementById('actionAttended').checked = true;
} else {
document.getElementById('actionReset').checked = true;
}
// UI 초기 상태 설정
toggleClosingAction(autoClosingCheckbox.checked);
// 이벤트 리스너 추가
autoClosingCheckbox.addEventListener('change', function () {
toggleClosingAction(this.checked);
});
// 현황판 설정
document.getElementById('useMaxWaitingLimit').checked = settings.use_max_waiting_limit !== undefined ? settings.use_max_waiting_limit : true;
document.getElementById('maxWaitingLimit').value = settings.max_waiting_limit || 50;
document.getElementById('blockLastClassRegistration').checked = settings.block_last_class_registration || false;
document.getElementById('autoRegisterMember').checked = settings.auto_register_member || false;
document.getElementById('adminPassword').value = settings.admin_password;
// 대기현황판 표시 설정
document.getElementById('showWaitingNumber').checked = settings.show_waiting_number !== undefined ? settings.show_waiting_number : true;
document.getElementById('maskCustomerName').checked = settings.mask_customer_name || false;
document.getElementById('nameDisplayLength').value = settings.name_display_length || 0;
document.getElementById('showOrderNumber').checked = settings.show_order_number !== undefined ? settings.show_order_number : true;
document.getElementById('boardDisplayOrder').value = settings.board_display_order || 'number,name,order';
// 출석 횟수 설정
const countType = settings.attendance_count_type || 'days';
if (countType === 'monthly') {
document.getElementById('countTypeMonthly').checked = true;
} else {
document.getElementById('countTypeDays').checked = true;
}
document.getElementById('attendanceLookbackDays').value = settings.attendance_lookback_days || 30;
toggleAttendanceSettings();
// 폰트 설정
document.getElementById('managerFontFamily').value = settings.manager_font_family || 'Nanum Gothic';
document.getElementById('managerFontSize').value = settings.manager_font_size || '15px';
document.getElementById('managerMaxWidth').value = settings.waiting_manager_max_width || '';
document.getElementById('boardFontFamily').value = settings.board_font_family || 'Nanum Gothic';
document.getElementById('boardFontSize').value = settings.board_font_size || '24px';
// 키패드 설정
document.getElementById('keypadStyle').value = settings.keypad_style || 'modern';
if (settings.keypad_font_size) {
document.getElementById('keypadFontSize').value = settings.keypad_font_size;
}
// 대기접수 완료 모달 설정
if (settings.waiting_modal_timeout) {
document.getElementById('waitingModalTimeout').value = settings.waiting_modal_timeout;
}
// 불리언 값 처리 (undefined인 경우 true로 기본값 설정되는 항목 주의)
document.getElementById('showMemberNameInWaitingModal').checked =
(settings.show_member_name_in_waiting_modal !== undefined && settings.show_member_name_in_waiting_modal !== null) ? settings.show_member_name_in_waiting_modal : true;
document.getElementById('showNewMemberTextInWaitingModal').checked =
(settings.show_new_member_text_in_waiting_modal !== undefined && settings.show_new_member_text_in_waiting_modal !== null) ? settings.show_new_member_text_in_waiting_modal : true;
document.getElementById('enableWaitingVoiceAlert').checked =
(settings.enable_waiting_voice_alert !== undefined && settings.enable_waiting_voice_alert !== null) ? settings.enable_waiting_voice_alert : false;
document.getElementById('waitingVoiceMessage').value = settings.waiting_voice_message || '';
// 음성 목록 로드 및 선택
if (window.speechSynthesis) {
const voiceSelect = document.getElementById('waitingVoiceSelect');
let voices = [];
function populateVoices() {
voices = window.speechSynthesis.getVoices().filter(voice => voice.lang.startsWith('ko'));
// 기존 옵션 유지 (기본 목소리)
voiceSelect.innerHTML = '<option value="">기본 목소리</option>';
voices.forEach(voice => {
const option = document.createElement('option');
option.value = voice.name;
option.textContent = `${voice.name} (${voice.lang})`;
voiceSelect.appendChild(option);
});
// 저장된 목소리 선택
if (settings.waiting_voice_name) {
voiceSelect.value = settings.waiting_voice_name;
}
}
populateVoices();
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = populateVoices;
}
// 스타일 프리셋 로직
const styleSelect = document.getElementById('waitingVoiceStyle');
let currentRate = settings.waiting_voice_rate || 1.0;
let currentPitch = settings.waiting_voice_pitch || 1.0;
// 저장된 rate/pitch로 스타일 추정하여 선택
// Floating point comparison needs to be careful, but checking simple equality for presets is fine
if (currentRate === 0.8 && currentPitch === 1.1) styleSelect.value = 'senior';
else if (currentRate === 0.9 && currentPitch === 0.9) styleSelect.value = 'calm';
else if (currentRate === 0.9 && currentPitch === 0.95) styleSelect.value = 'soft';
else if (currentRate === 1.1 && currentPitch === 1.2) styleSelect.value = 'bright';
else styleSelect.value = 'standard';
styleSelect.onchange = function () {
const style = this.value;
if (style === 'senior') { currentRate = 0.8; currentPitch = 1.1; } // Senior: Slower, slightly higher pitch for clarity
else if (style === 'calm') { currentRate = 0.9; currentPitch = 0.9; }
else if (style === 'soft') { currentRate = 0.9; currentPitch = 0.95; } // Soft: Slower, slightly lower pitch
else if (style === 'bright') { currentRate = 1.1; currentPitch = 1.2; }
else { currentRate = 1.0; currentPitch = 1.0; } // standard
};
// 미리듣기 버튼 이벤트
document.getElementById('testVoiceBtn').onclick = function () {
const message = document.getElementById('waitingVoiceMessage').value || "1교시 대기 접수 되었습니다";
const selectedVoiceName = voiceSelect.value;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(message);
utterance.lang = 'ko-KR';
utterance.rate = currentRate;
utterance.pitch = currentPitch;
if (selectedVoiceName) {
const selectedVoice = voices.find(voice => voice.name === selectedVoiceName);
if (selectedVoice) {
utterance.voice = selectedVoice;
}
}
window.speechSynthesis.speak(utterance);
};
}
// 최대 대기 인원 입력 필드 활성화/비활성화
toggleMaxWaitingLimitInput();
} catch (error) {
console.error('설정 조회 실패:', error);
}
}
function toggleAttendanceSettings() {
const isMonthly = document.getElementById('countTypeMonthly').checked;
const container = document.getElementById('attendanceLookbackDaysContainer');
if (isMonthly) {
container.style.display = 'none';
} else {
container.style.display = 'block';
}
}
async function saveStoreSettings(event) {
event.preventDefault();
const storeName = document.getElementById('storeName').value.trim();
const displayCount = document.getElementById('displayClassesCount').value;
const rowsPerClass = document.getElementById('rowsPerClass').value;
const listDirection = document.getElementById('listDirection').value;
const businessDayStart = document.getElementById('businessDayStart').value;
const autoClosing = document.getElementById('autoClosing').checked;
const closingAction = document.querySelector('input[name="closingAction"]:checked').value;
const useMaxWaitingLimit = document.getElementById('useMaxWaitingLimit').checked;
const maxWaitingLimit = document.getElementById('maxWaitingLimit').value;
const blockLastClassRegistration = document.getElementById('blockLastClassRegistration').checked;
const autoRegisterMember = document.getElementById('autoRegisterMember').checked;
const adminPassword = document.getElementById('adminPassword').value;
const showWaitingNumber = document.getElementById('showWaitingNumber').checked;
const maskCustomerName = document.getElementById('maskCustomerName').checked;
const nameDisplayLength = document.getElementById('nameDisplayLength').value;
const showOrderNumber = document.getElementById('showOrderNumber').checked;
const boardDisplayOrder = document.getElementById('boardDisplayOrder').value;
// 출석 횟수 설정
const attendanceCountType = document.querySelector('input[name="attendanceCountType"]:checked').value;
const attendanceLookbackDays = document.getElementById('attendanceLookbackDays').value;
// 폰트 설정
const managerFontFamily = document.getElementById('managerFontFamily').value;
const managerFontSize = document.getElementById('managerFontSize').value;
const boardFontFamily = document.getElementById('boardFontFamily').value;
const boardFontSize = document.getElementById('boardFontSize').value;
// 키패드 설정
const keypadStyle = document.getElementById('keypadStyle').value;
const keypadFontSize = document.getElementById('keypadFontSize').value;
const settings = {
store_name: storeName,
display_classes_count: parseInt(displayCount),
rows_per_class: parseInt(rowsPerClass),
list_direction: listDirection,
business_day_start: parseInt(businessDayStart),
auto_closing: autoClosing,
closing_action: closingAction,
use_max_waiting_limit: useMaxWaitingLimit,
max_waiting_limit: parseInt(maxWaitingLimit),
block_last_class_registration: blockLastClassRegistration,
auto_register_member: autoRegisterMember,
admin_password: adminPassword,
show_waiting_number: showWaitingNumber,
mask_customer_name: maskCustomerName,
name_display_length: parseInt(nameDisplayLength),
show_order_number: showOrderNumber,
board_display_order: boardDisplayOrder,
attendance_count_type: attendanceCountType,
attendance_lookback_days: parseInt(attendanceLookbackDays),
// 폰트 설정 추가
manager_font_family: managerFontFamily,
manager_font_size: managerFontSize,
waiting_manager_max_width: document.getElementById('managerMaxWidth').value ? parseInt(document.getElementById('managerMaxWidth').value) : null,
board_font_family: boardFontFamily,
board_font_size: boardFontSize,
// 키패드 설정 추가
keypad_style: keypadStyle,
keypad_font_size: keypadFontSize,
// 대기접수 완료 모달 설정
waiting_modal_timeout: parseInt(document.getElementById('waitingModalTimeout').value),
show_member_name_in_waiting_modal: document.getElementById('showMemberNameInWaitingModal').checked,
show_new_member_text_in_waiting_modal: document.getElementById('showNewMemberTextInWaitingModal').checked,
enable_waiting_voice_alert: document.getElementById('enableWaitingVoiceAlert').checked,
waiting_voice_message: document.getElementById('waitingVoiceMessage').value,
waiting_voice_name: document.getElementById('waitingVoiceSelect').value,
// 스타일 선택값에서 rate/pitch 도출
waiting_voice_rate: (function () {
const style = document.getElementById('waitingVoiceStyle').value;
if (style === 'senior') return 0.8;
if (style === 'calm') return 0.9;
if (style === 'soft') return 0.9;
if (style === 'bright') return 1.1;
return 1.0;
})(),
waiting_voice_pitch: (function () {
const style = document.getElementById('waitingVoiceStyle').value;
if (style === 'senior') return 1.1;
if (style === 'calm') return 0.9;
if (style === 'soft') return 0.95;
if (style === 'bright') return 1.2;
return 1.0;
})(),
// 개점 설정
daily_opening_rule: document.querySelector('input[name="dailyOpeningRule"]:checked').value
};
try {
const response = await fetch('/api/store/', {
method: 'PUT',
headers: getHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(settings)
});
if (response.ok) {
showNotification('설정이 저장되었습니다.', '✅');
} else {
const error = await response.json();
showNotification(error.detail || '저장에 실패했습니다.', '❌', '오류');
}
} catch (error) {
console.error('저장 실패:', error);
showNotification('저장 중 오류가 발생했습니다.', '❌', '오류');
}
}
async function loadClasses() {
const weekdayList = document.getElementById('weekdayClassList');
const weekendList = document.getElementById('weekendClassList');
weekdayList.innerHTML = '<div class="loading"><div class="spinner"></div><p>로딩 중...</p></div>';
weekendList.innerHTML = '<div class="loading"><div class="spinner"></div><p>로딩 중...</p></div>';
try {
const response = await fetch('/api/classes/?include_inactive=true');
classes = await response.json();
renderClasses();
} catch (error) {
console.error('클래스 조회 실패:', error);
weekdayList.innerHTML = '<div class="empty-state"><p>데이터 로딩 실패</p></div>';
weekendList.innerHTML = '<div class="empty-state"><p>데이터 로딩 실패</p></div>';
}
}
function renderClasses() {
const weekdayList = document.getElementById('weekdayClassList');
const weekendList = document.getElementById('weekendClassList');
// 평일 및 주말 클래스 분리 (각 타입별로 명확히 구분)
const weekdayClasses = classes.filter(cls => cls.class_type === 'weekday');
const weekendClasses = classes.filter(cls => cls.class_type === 'weekend');
const allClasses = classes.filter(cls => cls.class_type === 'all');
// 평일 클래스 렌더링
weekdayList.innerHTML = '';
if (weekdayClasses.length === 0 && allClasses.length === 0) {
weekdayList.innerHTML = '<div class="empty-state"><div class="icon">📚</div><p>등록된 평일 클래스가 없습니다</p></div>';
} else {
weekdayClasses.forEach(cls => {
weekdayList.appendChild(createClassItem(cls));
});
// all 타입 클래스는 회색으로 표시
allClasses.forEach(cls => {
weekdayList.appendChild(createClassItem(cls, true));
});
}
// 주말 클래스 렌더링
weekendList.innerHTML = '';
if (weekendClasses.length === 0 && allClasses.length === 0) {
weekendList.innerHTML = '<div class="empty-state"><div class="icon">📚</div><p>등록된 주말 클래스가 없습니다</p></div>';
} else {
weekendClasses.forEach(cls => {
weekendList.appendChild(createClassItem(cls));
});
// all 타입 클래스는 회색으로 표시
allClasses.forEach(cls => {
weekendList.appendChild(createClassItem(cls, true));
});
}
}
function createClassItem(cls, isAllType = false) {
const item = document.createElement('div');
item.className = 'class-item';
const inactiveBadge = !cls.is_active ? '<span class="inactive-badge">비활성</span>' : '';
const classTypeLabel = cls.class_type === 'weekday' ? '평일 전용' : cls.class_type === 'weekend' ? '주말 전용' : '전체 요일';
// all 타입 클래스는 회색 배경으로 표시
const allTypeStyle = isAllType ? 'opacity: 0.6; background: #f0f0f0;' : '';
const allTypeBadge = isAllType ? '<span style="background: #95a5a6; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px;">전체 요일</span>' : '';
item.innerHTML = `
<div class="class-number-badge" style="${isAllType ? 'background: linear-gradient(135deg, #95a5a6, #7f8c8d);' : ''}">
${cls.class_number}
</div>
<div class="class-item-info">
<div class="title">
${cls.class_name}${inactiveBadge}${allTypeBadge}
</div>
<div class="details">
<div class="detail-item">
<div class="detail-label">수업 시간</div>
<div class="detail-value time">${cls.start_time.substring(0, 5)} - ${cls.end_time.substring(0, 5)}</div>
</div>
<div class="detail-item">
<div class="detail-label">클래스 타입</div>
<div class="detail-value" style="color: ${isAllType ? '#95a5a6' : '#9b59b6'}; font-size: 18px;">${classTypeLabel}</div>
</div>
<div class="detail-item">
<div class="detail-label">최대 인원</div>
<div class="detail-value capacity">${cls.max_capacity}명</div>
</div>
<div class="detail-item">
<div class="detail-label">현재 대기</div>
<div class="detail-value waiting">${cls.current_count || 0}명</div>
</div>
</div>
</div>
<div class="class-item-actions">
<button class="btn btn-sm btn-primary" onclick="openEditClassModal(${cls.id})">수정</button>
${cls.is_active ?
`<button class="btn btn-sm btn-warning" onclick="toggleClassStatus(${cls.id}, false)">비활성화</button>` :
`<button class="btn btn-sm btn-success" onclick="toggleClassStatus(${cls.id}, true)">활성화</button>`
}
</div>
`;
return item;
}
function openAddClassModal(classType) {
currentClassId = null;
// class_type 설정
document.getElementById('classType').value = classType;
// 같은 타입 또는 all 타입의 클래스들을 필터링하여 다음 번호 계산
const relevantClasses = classes.filter(cls =>
cls.class_type === classType || cls.class_type === 'all'
);
const nextNumber = relevantClasses.length > 0
? Math.max(...relevantClasses.map(cls => cls.class_number)) + 1
: 1;
const classTypeLabel = classType === 'weekday' ? '평일' : '주말';
document.getElementById('classModalTitle').textContent = `${classTypeLabel} 클래스 추가`;
document.getElementById('classNumber').value = nextNumber;
document.getElementById('className').value = `${nextNumber}교시`;
document.getElementById('startTime').value = '';
document.getElementById('endTime').value = '';
document.getElementById('maxCapacity').value = 10;
document.getElementById('classModal').classList.add('active');
}
function openEditClassModal(classId) {
const cls = classes.find(c => c.id === classId);
if (!cls) return;
currentClassId = classId;
// class_type 설정
const classType = cls.class_type || 'weekday';
document.getElementById('classType').value = classType;
const classTypeLabel = classType === 'weekday' ? '평일' : classType === 'weekend' ? '주말' : '전체';
document.getElementById('classModalTitle').textContent = `${classTypeLabel} 클래스 수정`;
document.getElementById('classNumber').value = cls.class_number;
document.getElementById('className').value = cls.class_name;
document.getElementById('startTime').value = cls.start_time.substring(0, 5);
document.getElementById('endTime').value = cls.end_time.substring(0, 5);
document.getElementById('maxCapacity').value = cls.max_capacity;
document.getElementById('classModal').classList.add('active');
}
async function saveClass(event) {
event.preventDefault();
const classType = document.getElementById('classType').value;
// weekday_schedule 기본값 설정 (class_type에 따라)
let weekday_schedule;
if (classType === 'weekday') {
weekday_schedule = { mon: true, tue: true, wed: true, thu: true, fri: true, sat: false, sun: false };
} else if (classType === 'weekend') {
weekday_schedule = { mon: false, tue: false, wed: false, thu: false, fri: false, sat: true, sun: true };
} else {
// all 타입
weekday_schedule = { mon: true, tue: true, wed: true, thu: true, fri: true, sat: true, sun: true };
}
const classData = {
class_number: parseInt(document.getElementById('classNumber').value),
class_name: document.getElementById('className').value.trim(),
start_time: document.getElementById('startTime').value + ':00',
end_time: document.getElementById('endTime').value + ':00',
max_capacity: parseInt(document.getElementById('maxCapacity').value),
is_active: true,
class_type: classType,
weekday_schedule: weekday_schedule
};
try {
let response;
if (currentClassId) {
response = await fetch(`/api/classes/${currentClassId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(classData)
});
} else {
response = await fetch('/api/classes/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(classData)
});
}
if (response.ok) {
showNotification('저장되었습니다.', '✅');
closeModal('classModal');
loadClasses();
} else {
const error = await response.json();
showNotification(error.detail || '저장에 실패했습니다.', '❌', '오류');
}
} catch (error) {
console.error('저장 실패:', error);
showNotification('저장 중 오류가 발생했습니다.', '❌', '오류');
}
}
async function toggleClassStatus(classId, activate) {
const cls = classes.find(c => c.id === classId);
const action = activate ? '활성화' : '비활성화';
if (!confirm(`${cls.class_name}을(를) ${action}하시겠습니까?`)) return;
try {
const endpoint = activate ? `/api/classes/${classId}/activate` : `/api/classes/${classId}`;
const method = activate ? 'POST' : 'DELETE';
const response = await fetch(endpoint, { method });
if (response.ok) {
showNotification(`${action}되었습니다.`, '✅');
loadClasses();
} else {
const error = await response.json();
showNotification(error.detail || `${action}에 실패했습니다.`, '❌', '오류');
}
} catch (error) {
console.error('상태 변경 실패:', error);
showNotification('처리 중 오류가 발생했습니다.', '❌', '오류');
}
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
// 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 toggleMaxWaitingLimitInput() {
const useLimit = document.getElementById('useMaxWaitingLimit').checked;
const limitGroup = document.getElementById('maxWaitingLimitGroup');
const limitInput = document.getElementById('maxWaitingLimit');
if (useLimit) {
limitGroup.style.opacity = '1';
limitInput.disabled = false;
} else {
limitGroup.style.opacity = '0.5';
limitInput.disabled = true;
}
}
// 체크박스 변경 시 입력 필드 활성화/비활성화
document.addEventListener('DOMContentLoaded', () => {
const checkbox = document.getElementById('useMaxWaitingLimit');
if (checkbox) {
checkbox.addEventListener('change', toggleMaxWaitingLimitInput);
}
});
// 초기 로드
async function init() {
await checkUrlStoreParam();
loadStoreSettings();
loadAvailableStores();
}
init();
</script>
</body>
</html>