Files
madeu_diet_home/src/main/resources/templates/web/webevent/makeReservation.html
2026-01-15 19:15:38 +09:00

1421 lines
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/web/layout/layout}">
<th:block layout:fragment="layoutCss">
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', sans-serif;
margin: 0;
background: #fafafa;
color: #222;
height: 100vh;
display: flex;
flex-direction: column;
}
/* Main container */
.reservation-container {
display: flex;
max-width: 1280px !important;
margin: 0 auto;
gap: 20px;
height: 100%;
flex: 1;
padding: 20px 0;
margin-top:65px;
}
.box {
background: #fff;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 24px 16px;
flex: 1 1 0;
min-width: 260px;
display: flex;
flex-direction: column;
height: fit-content;
min-height: 600px;
}
.step-title {
color: #b23c3c;
font-weight: 700;
font-size: 1.1em;
margin-bottom: 12px;
letter-spacing: 0.02em;
transition: color 0.3s ease;
}
.step-title.completed {
color: #008000 !important;
}
/* Service section */
.service-list {
flex: 1;
margin-bottom: 24px;
}
.service-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.1em;
margin-bottom: 10px;
}
.service-item .del {
cursor: pointer;
color: #b23c3c;
margin-left: 8px;
font-size: 1.2em;
transition: color 0.3s ease, opacity 0.3s ease;
}
.service-item .del:hover {
color: #ff0000;
}
.service-item .del.disabled {
color: #ccc;
cursor: not-allowed;
opacity: 0.5;
}
.service-item .del.disabled:hover {
color: #ccc;
}
.service-item .price {
font-weight: bold;
color: #b23c3c;
}
/* Service count indicator */
.service-count-info {
font-size: 0.85em;
color: #888;
margin-bottom: 8px;
text-align: center;
padding: 4px 8px;
background: #f8f9fa;
border-radius: 4px;
}
.service-count-info.single {
color: #ff8c00;
background: #fff3e0;
border: 1px solid #ffcc80;
}
.total {
border-top: 1px solid #eee;
margin-top: auto;
padding-top: 16px;
display: flex;
justify-content: space-between;
font-size: 1.1em;
font-weight: bold;
}
.total .price {
color: #b23c3c;
}
.total small {
font-weight: normal;
color: #888;
font-size: 0.9em;
margin-right: 8px;
}
/* Calendar section */
.calendar-box {
margin-bottom: 20px;
}
.calendar-header {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.calendar-header button {
background: none;
border: none;
font-size: 1.2em;
cursor: pointer;
color: #b23c3c;
padding: 0 6px;
}
.calendar-table {
width: 100%;
border-collapse: collapse;
text-align: center;
margin-bottom: 8px;
}
.calendar-table th,
.calendar-table td {
width: 2em;
height: 2em;
padding: 2px;
font-size: 1em;
border-radius: 50%;
cursor: pointer;
transition: background 0.15s;
}
.calendar-table th {
color: #b23c3c;
font-weight: 500;
background: none;
}
.calendar-table td.selected {
background: #b23c3c;
color: #fff;
}
.calendar-table td.today {
border: 1.5px solid #b23c3c;
}
.calendar-table td:not(.selected):hover {
background: #f5eaea;
}
.calendar-table td.disabled {
color: #ccc;
pointer-events: none;
background: none;
}
/* Time slots */
.time-slots {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.time-btn {
flex: 1 0 30%;
min-width: 80px;
padding: 8px 0;
border: 1px solid #b23c3c;
border-radius: 20px;
background: #fff;
color: #b23c3c;
font-size: 1em;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.time-btn.selected,
.time-btn:active {
background: #b23c3c;
color: #fff;
}
.time-btn:disabled {
color: #ccc;
border-color: #eee;
background: #f5f5f5;
cursor: not-allowed;
}
.person-count {
margin-top: 12px;
font-size: 0.98em;
color: #888;
}
/* Form section */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1em;
resize: none;
transition: border-color 0.3s ease;
}
.form-group textarea {
min-height: 60px;
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 18px;
font-size: 0.98em;
}
.checkbox-group input[type="checkbox"] {
margin-right: 8px;
accent-color: #b23c3c;
}
.submit-btn {
width: 100%;
padding: 14px 0;
background: #ddd;
color: #888;
border: none;
border-radius: 8px;
font-size: 1.1em;
font-weight: bold;
cursor: not-allowed;
transition: all 0.3s ease;
}
.submit-btn.step-progress {
background: linear-gradient(45deg, #ddd, #bbb);
color: #666;
font-size: 0.95em;
}
.submit-btn.ready {
background: #b23c3c;
color: #fff;
cursor: pointer;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
/* Phone message styles (common.js PhoneValidator용) */
.phone-message {
font-size: 12px;
margin-top: 5px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
transition: all 0.3s ease;
}
.phone-message.error {
color: #ff0000;
background-color: #ffebee;
border: 1px solid #ffcdd2;
}
.phone-message.warning {
color: #ff8c00;
background-color: #fff3e0;
border: 1px solid #ffcc80;
}
.phone-message.success {
color: #008000;
background-color: #f1f8e9;
border: 1px solid #c8e6c9;
}
.phone-message.info {
color: #2196f3;
background-color: #e3f2fd;
border: 1px solid #90caf9;
}
/* Birth date message styles */
.birth-date-message {
font-size: 12px;
margin-top: 5px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
transition: all 0.3s ease;
}
.birth-date-message.error {
color: #ff0000;
background-color: #ffebee;
border: 1px solid #ffcdd2;
}
.birth-date-message.warning {
color: #ff8c00;
background-color: #fff3e0;
border: 1px solid #ffcc80;
}
.birth-date-message.success {
color: #008000;
background-color: #f1f8e9;
border: 1px solid #c8e6c9;
}
.birth-date-message.info {
color: #2196f3;
background-color: #e3f2fd;
border: 1px solid #90caf9;
}
/* Responsive Design */
@media (max-width: 1199.98px) {
.reservation-container {
max-width: 960px !important;
gap: 16px;
padding: 20px 15px;
}
.box {
padding: 20px 14px;
}
}
@media (max-width: 991.98px) {
.reservation-container {
max-width: 720px !important;
flex-direction: column;
gap: 24px;
height: auto;
padding: 20px 15px;
}
.box {
min-height: unset;
min-width: unset;
}
}
@media (max-width: 767.98px) {
.reservation-container {
max-width: 540px !important;
padding: 15px;
gap: 20px;
}
.box {
padding: 16px 12px;
}
.calendar-table th,
.calendar-table td {
width: 1.8em;
height: 1.8em;
}
.time-btn {
min-width: 70px;
font-size: 0.95em;
}
}
@media (max-width: 575.98px) {
.reservation-container {
max-width: 100% !important;
padding: 10px;
gap: 16px;
}
.box {
padding: 16px 8px;
}
.calendar-table th,
.calendar-table td {
width: 1.5em;
height: 1.5em;
font-size: 0.9em;
}
.time-btn {
min-width: 60px;
font-size: 0.9em;
padding: 6px 0;
}
.step-title {
font-size: 1em;
}
.service-item {
font-size: 1em;
}
}
</style>
</th:block>
<th:block layout:fragment="layout_top_script">
<script th:inline="javascript">
let category_div_cd = [[${CATEGORY_DIV_CD}]];
let category_no = [[${CATEGORY_NO}]];
let post_no = [[${POST_NO}]];
let procedure_id = [[${PROCEDURE_ID}]];
</script>
</th:block>
<th:block layout:fragment="layoutContent">
<div class="reservation-container">
<!-- 좌측: 시술 예약 -->
<div class="box" id="box-service">
<div class="step-title">STEP 01. 이벤트 예약</div>
<div class="service-count-info" id="service-count-info">선택된 시술: 1개</div>
<div class="service-list" id="service-list">
<div class="service-item">
<span>다이어트약처방</span>
<span>
<span style="text-decoration:line-through; color:#bbb; font-size:0.95em; margin-right:6px;">50,000원</span>
<span class="price">50,000원</span>
<span class="del disabled" title="최소 1개의 시술은 필요합니다" onclick="removeService(this)">×</span>
</span>
</div>
</div>
<div class="total">
<span>총 금액 <small>(부가세 별도)</small></span>
<span class="price" id="total-price">50,000원</span>
</div>
<div style="font-size:0.9em; color:#aaa; margin-top:8px;">결제는 내원시 이루어집니다.</div>
</div>
<!-- 가운데: 예약 시간 선택 -->
<div class="box" id="box-calendar">
<div class="step-title" id="step02-title">STEP 02. 예약 시간 선택</div>
<div class="calendar-box">
<div class="calendar-header">
<button id="prev-month" aria-label="이전 달">&lt;</button>
<span id="calendar-title" style="font-weight:600;"></span>
<button id="next-month" aria-label="다음 달">&gt;</button>
</div>
<table class="calendar-table" id="calendar-table">
<!-- JS로 렌더링 -->
</table>
</div>
<div class="time-slots" id="time-slots">
<!-- JS로 렌더링 -->
</div>
<div class="person-count" style="display:none;">예약인원 <span id="person-count">-</span></div>
</div>
<!-- 우측: 고객정보 입력 -->
<div class="box" id="box-info">
<div class="step-title" id="step03-title">STEP 03. 고객정보</div>
<form id="reserve-form" autocomplete="off">
<div class="form-group">
<label for="customer-name">고객명</label>
<input type="text" id="customer-name" name="customer-name" required>
</div>
<div class="form-group">
<label for="birthDate">생년월일 (8자리)</label>
<input type="text" id="birthDate" name="birthDate"
placeholder="예: 19900115 (8자리 숫자만)"
maxlength="8" required>
<!-- BirthDateValidator가 동적 생성하는 메시지 영역 -->
</div>
<div class="form-group">
<label for="customer-phone">연락처</label>
<input type="tel" id="customer-phone" name="customer-phone"
placeholder="고객님의 핸드폰 번호를 입력해 주세요."
required pattern="^01[0-9]{8,9}$" title="올바른 휴대폰 번호를 입력해주세요.">
<!-- 피드백 메시지는 common.js PhoneValidator가 동적 생성 -->
</div>
<div class="form-group">
<label for="customer-req">요청사항</label>
<textarea id="customer-req" name="customer-req"
placeholder="요청사항을 작성해 주세요."></textarea>
</div>
<div class="checkbox-group">
<input type="checkbox" id="agree" required>
<label for="agree">[필수] 서비스 이용 및 예약 신청을 위한 개인정보 제공에 동의</label>
</div>
<button type="submit" class="submit-btn" id="submit-btn" disabled>📅 예약 날짜를 선택해주세요</button>
</form>
</div>
</div>
</th:block>
<th:block layout:fragment="layoutContentScript">
<script>
// 생년월일 검증 클래스
class BirthDateValidator {
constructor(inputId, options = {}) {
this.inputElement = document.getElementById(inputId);
this.messageElement = null;
this.options = {
showMessage: true,
realTimeValidation: true,
minAge: 0,
maxAge: 150,
format: 'YYYYMMDD', // YYYYMMDD, YYYY-MM-DD
allowFuture: false,
onValidationChange: null,
...options
};
if (this.inputElement && this.options.showMessage) {
this.createMessageElement();
}
if (this.inputElement && this.options.realTimeValidation) {
this.bindEvents();
}
}
createMessageElement() {
this.messageElement = document.createElement('div');
this.messageElement.className = 'birth-date-message';
this.messageElement.style.display = 'none';
this.inputElement.parentNode.insertBefore(this.messageElement, this.inputElement.nextSibling);
}
bindEvents() {
this.inputElement.addEventListener('input', () => {
// 숫자만 입력되도록 제한
this.inputElement.value = this.inputElement.value.replace(/[^0-9]/g, '');
this.validateAndShowMessage();
});
this.inputElement.addEventListener('blur', () => {
this.validateAndShowMessage();
});
this.inputElement.addEventListener('keydown', (e) => {
// 백스페이스, 델리트, 탭, 화살표 키는 허용
if ([8, 9, 46, 37, 38, 39, 40].includes(e.keyCode)) {
return;
}
// 숫자가 아니면 입력 방지
if ((e.keyCode < 48 || e.keyCode > 57) && (e.keyCode < 96 || e.keyCode > 105)) {
e.preventDefault();
}
});
}
validateBirthDate(dateStr) {
if (!dateStr) return { valid: false, message: '생년월일을 입력해주세요.' };
let cleanDate = dateStr.replace(/[^0-9]/g, '');
// 길이 체크
if (cleanDate.length !== 8) {
return { valid: false, message: '생년월일은 8자리 숫자로 입력해주세요. (예: 19900115)' };
}
// 형식 체크
const datePattern = /^\d{8}$/;
if (!datePattern.test(cleanDate)) {
return { valid: false, message: '올바른 생년월일 형식이 아닙니다.' };
}
// 년월일 추출
const year = parseInt(cleanDate.slice(0, 4));
const month = parseInt(cleanDate.slice(4, 6));
const day = parseInt(cleanDate.slice(6, 8));
// 기본 범위 체크
const currentYear = new Date().getFullYear();
const minYear = currentYear - this.options.maxAge;
const maxYear = currentYear - this.options.minAge;
if (year < minYear || year > maxYear) {
return {
valid: false,
message: `출생연도는 ${minYear}년부터 ${maxYear}년 사이여야 합니다.`
};
}
if (month < 1 || month > 12) {
return { valid: false, message: '월은 01부터 12까지 입력 가능합니다.' };
}
if (day < 1 || day > 31) {
return { valid: false, message: '일은 01부터 31까지 입력 가능합니다.' };
}
// Date 객체로 실제 날짜 존재 여부 확인
const date = new Date(year, month - 1, day);
const isValidDate = (
date.getFullYear() === year &&
date.getMonth() === month - 1 &&
date.getDate() === day
);
if (!isValidDate) {
return { valid: false, message: '존재하지 않는 날짜입니다.' };
}
// 미래 날짜 체크
if (!this.options.allowFuture && date > new Date()) {
return { valid: false, message: '미래 날짜는 입력할 수 없습니다.' };
}
// 특별한 월별 날짜 체크
if (month === 4 || month === 6 || month === 9 || month === 11) {
if (day === 31) {
return { valid: false, message: '해당 월은 31일이 없습니다.' };
}
}
// 2월 윤년 체크
if (month === 2) {
const isLeapYear = (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0));
if (day > 29 || (day === 29 && !isLeapYear)) {
return { valid: false, message: isLeapYear ? '2월은 29일까지만 있습니다.' : '2월은 28일까지만 있습니다.' };
}
}
return { valid: true, message: '올바른 생년월일입니다.' };
}
validateAndShowMessage() {
const result = this.validateBirthDate(this.inputElement.value);
if (this.messageElement) {
this.showMessage(result.message, result.valid ? 'success' : 'error');
}
// 콜백 실행
if (typeof this.options.onValidationChange === 'function') {
this.options.onValidationChange(result, this.inputElement.value);
}
return result.valid;
}
showMessage(message, type) {
if (!this.messageElement) return;
this.messageElement.textContent = message;
this.messageElement.className = `birth-date-message ${type}`;
this.messageElement.style.display = message ? 'block' : 'none';
}
isValid() {
return this.validateBirthDate(this.inputElement.value).valid;
}
getAge() {
const result = this.validateBirthDate(this.inputElement.value);
if (!result.valid) return null;
const cleanDate = this.inputElement.value.replace(/[^0-9]/g, '');
const birthYear = parseInt(cleanDate.slice(0, 4));
const birthMonth = parseInt(cleanDate.slice(4, 6));
const birthDay = parseInt(cleanDate.slice(6, 8));
const today = new Date();
let age = today.getFullYear() - birthYear;
if (today.getMonth() + 1 < birthMonth ||
(today.getMonth() + 1 === birthMonth && today.getDate() < birthDay)) {
age--;
}
return age;
}
static isValidBirthDate(dateStr) {
const validator = new BirthDateValidator('temp', { showMessage: false });
return validator.validateBirthDate(dateStr).valid;
}
}
// 전역 변수
let birthDateValidator;
// 초기화
fn_SelectReservation(category_div_cd, category_no, post_no, procedure_id);
// 개선된 시술 삭제 함수 (1개일 때 삭제 방지)
function removeService(el) {
const serviceItems = document.querySelectorAll('.service-item');
const serviceCount = serviceItems.length;
// 시술이 1개만 남았을 때 삭제 방지
if (serviceCount <= 1) {
alert('최소 1개의 시술은 선택되어 있어야 합니다.');
return false;
}
// 삭제 확인
const serviceName = el.closest('.service-item').querySelector('span:first-child').textContent;
if (!confirm(`'${serviceName}' 시술을 삭제하시겠습니까?`)) {
return false;
}
// 시술 삭제
const removedItem = el.closest('.service-item');
const priceText = removedItem.querySelector('.price').textContent;
const price = parseInt(priceText.replace(/[^0-9]/g, '')) || 0;
removedItem.remove();
// 총 금액 재계산
updateTotalPrice();
// 시술 개수 업데이트
updateServiceCount();
// selectedTreatments 배열에서도 제거 (인덱스 기반)
const removedIndex = Array.from(serviceItems).indexOf(removedItem);
if (removedIndex !== -1 && selectedTreatments[removedIndex]) {
selectedTreatments.splice(removedIndex, 1);
}
return true;
}
// 총 금액 업데이트 함수
function updateTotalPrice() {
const serviceItems = document.querySelectorAll('.service-item');
let totalPrice = 0;
serviceItems.forEach(item => {
const priceText = item.querySelector('.price').textContent;
const price = parseInt(priceText.replace(/[^0-9]/g, '')) || 0;
totalPrice += price;
});
document.getElementById('total-price').textContent = totalPrice.toLocaleString() + '원';
}
// 시술 개수 및 삭제 버튼 상태 업데이트 함수
function updateServiceCount() {
const serviceItems = document.querySelectorAll('.service-item');
const serviceCount = serviceItems.length;
const serviceCountInfo = document.getElementById('service-count-info');
// 개수 표시 업데이트
serviceCountInfo.textContent = `선택된 시술: ${serviceCount}`;
// 1개일 때 스타일 변경
if (serviceCount === 1) {
serviceCountInfo.className = 'service-count-info single';
serviceCountInfo.textContent = `선택된 시술: ${serviceCount}개 (최소 필수)`;
} else {
serviceCountInfo.className = 'service-count-info';
}
// 삭제 버튼 상태 업데이트
serviceItems.forEach(item => {
const delBtn = item.querySelector('.del');
if (serviceCount <= 1) {
delBtn.className = 'del disabled';
delBtn.title = '최소 1개의 시술은 필요합니다';
} else {
delBtn.className = 'del';
delBtn.title = '삭제';
}
});
}
// 캘린더 데이터
const today = new Date();
let selectedYear = today.getFullYear();
let selectedMonth = today.getMonth();
let selectedDay = today.getDate();
let selectedDate = null;
let selectedTime = null;
const calendarTitle = document.getElementById('calendar-title');
const calendarTable = document.getElementById('calendar-table');
const timeSlots = document.getElementById('time-slots');
const personCount = document.getElementById('person-count');
// 폼 요소들
const form = document.getElementById('reserve-form');
const agree = document.getElementById('agree');
const submitBtn = document.getElementById('submit-btn');
const step02Title = document.getElementById('step02-title');
const step03Title = document.getElementById('step03-title');
// 이벤트 리스너 등록 (핸드폰 번호와 생년월일 제외한 필드들)
form.addEventListener('input', function(e) {
if (e.target.id !== 'customer-phone' && e.target.id !== 'birthDate') {
setTimeout(checkForm, 10); // Validator 처리 완료 대기
}
});
agree.addEventListener('change', checkForm);
// 개선된 checkForm 함수 (BirthDateValidator 연계)
function checkForm() {
const name = document.getElementById('customer-name').value.trim();
// common.js의 PhoneValidator 메서드 사용
let phoneValid = false;
try {
if (typeof PhoneValidator !== 'undefined' &&
PhoneValidator.isValid &&
typeof PhoneValidator.isValid === 'function') {
phoneValid = PhoneValidator.isValid('customer-phone');
} else {
// fallback: 기본 정규식 검증
const phone = document.getElementById('customer-phone').value.trim();
phoneValid = phone.match(/^01[0-9]{8,9}$/);
}
} catch (e) {
console.warn('PhoneValidator error:', e);
// fallback 검증
const phone = document.getElementById('customer-phone').value.trim();
phoneValid = phone.match(/^01[0-9]{8,9}$/);
}
// BirthDateValidator 사용
let birthDateValid = false;
try {
if (birthDateValidator && typeof birthDateValidator.isValid === 'function') {
birthDateValid = birthDateValidator.isValid();
} else {
// fallback: 기본 검증
const birthDate = document.getElementById('birthDate').value.trim();
birthDateValid = birthDate.match(/^\d{8}$/);
}
} catch (e) {
console.warn('BirthDateValidator error:', e);
// fallback 검증
const birthDate = document.getElementById('birthDate').value.trim();
birthDateValid = birthDate.match(/^\d{8}$/);
}
const conditions = {
date: !!selectedDate,
time: !!selectedTime,
name: !!name,
phone: phoneValid,
birthDate: birthDateValid,
agree: agree.checked
};
const valid = Object.values(conditions).every(Boolean);
submitBtn.disabled = !valid;
const completedSteps = Object.values(conditions).filter(Boolean).length;
const totalSteps = Object.keys(conditions).length;
updateStepStatus(conditions);
updateButtonText(conditions, completedSteps, totalSteps);
// CSS 클래스 업데이트
if (valid) {
submitBtn.className = 'submit-btn ready';
} else {
submitBtn.className = 'submit-btn step-progress';
}
}
function updateStepStatus(conditions) {
// STEP 02 상태 업데이트
if (conditions.date && conditions.time) {
step02Title.className = 'step-title completed';
step02Title.textContent = 'STEP 02. 예약 시간 선택 ✓';
} else {
step02Title.className = 'step-title';
step02Title.textContent = 'STEP 02. 예약 시간 선택';
}
// STEP 03 상태 업데이트
if (conditions.name && conditions.phone && conditions.birthDate && conditions.agree) {
step03Title.className = 'step-title completed';
step03Title.textContent = 'STEP 03. 고객정보 ✓';
} else {
step03Title.className = 'step-title';
step03Title.textContent = 'STEP 03. 고객정보';
}
}
function updateButtonText(conditions, completedSteps, totalSteps) {
if (!conditions.date) {
submitBtn.textContent = '📅 예약 날짜를 선택해주세요';
} else if (!conditions.time) {
submitBtn.textContent = '⏰ 예약 시간을 선택해주세요';
} else if (!conditions.name) {
submitBtn.textContent = '👤 고객명을 입력해주세요';
} else if (!conditions.birthDate) {
submitBtn.textContent = '📅 생년월일을 올바르게 입력해주세요';
} else if (!conditions.phone) {
submitBtn.textContent = '📱 연락처를 올바르게 입력해주세요';
} else if (!conditions.agree) {
submitBtn.textContent = '✅ 개인정보 동의를 체크해주세요';
} else {
submitBtn.textContent = '🎉 시술 예약하기';
}
// 진행률 표시 (선택사항)
if (completedSteps < totalSteps) {
const progressText = ` (${completedSteps}/${totalSteps})`;
submitBtn.textContent += progressText;
}
}
// 캘린더 렌더링
function renderCalendar(year, month) {
calendarTitle.textContent = `${year}.${(month+1).toString().padStart(2,'0')}`;
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month+1, 0);
const now = new Date();
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
let html = '<thead><tr>';
['일','월','화','수','목','금','토'].forEach(d => html += `<th>${d}</th>`);
html += '</tr></thead><tbody><tr>';
for(let i = 0; i < firstDay.getDay(); i++) {
html += '<td class="disabled"></td>';
}
for(let d = 1; d <= lastDay.getDate(); d++) {
const dateObj = new Date(year, month, d);
let classes = [];
// 과거 날짜 체크
const isPastDate = dateObj < todayDate;
if(dateObj.toDateString() === today.toDateString()) classes.push('today');
if(selectedDate && dateObj.toDateString() === selectedDate.toDateString()) classes.push('selected');
if(isPastDate || dateObj.getDay() === 0) classes.push('disabled');
const clickHandler = (isPastDate || dateObj.getDay() === 0) ? '' : `onclick="selectDate(${year},${month},${d})"`;
html += `<td class="${classes.join(' ')}" ${clickHandler}>${d}</td>`;
if((firstDay.getDay()+d) % 7 === 0 && d !== lastDay.getDate()) html += '</tr><tr>';
}
for(let i = (lastDay.getDay()+1) % 7; i && i < 7; i++) {
html += '<td class="disabled"></td>';
}
html += '</tr></tbody>';
calendarTable.innerHTML = html;
}
// 날짜 선택 (checkForm 호출 추가)
function selectDate(y, m, d) {
const tempDate = new Date(y, m, d);
const now = new Date();
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// 과거 날짜 체크
if (tempDate < todayDate) {
alert('과거 날짜는 선택할 수 없습니다.');
return;
}
// 일요일 체크
if (tempDate.getDay() === 0) {
alert('일요일은 선택할 수 없습니다.');
const nextMonday = new Date(now);
if (now.getDay() === 0) {
nextMonday.setDate(now.getDate() + 1);
} else {
nextMonday.setDate(now.getDate() + (8 - now.getDay()));
}
selectedDate = new Date(nextMonday.getFullYear(), nextMonday.getMonth(), nextMonday.getDate());
renderCalendar(selectedDate.getFullYear(), selectedDate.getMonth());
renderTimeSlots();
checkForm();
return;
}
selectedDate = tempDate;
renderCalendar(y, m);
renderTimeSlots();
checkForm();
}
// 월 이동 버튼 이벤트 (checkForm 호출 추가)
document.getElementById('prev-month').onclick = function() {
if(selectedMonth === 0) {
selectedYear--;
selectedMonth = 11;
} else {
selectedMonth--;
}
renderCalendar(selectedYear, selectedMonth);
renderTimeSlots();
checkForm(); // 월 변경 시 폼 검증
};
document.getElementById('next-month').onclick = function() {
if(selectedMonth === 11) {
selectedYear++;
selectedMonth = 0;
} else {
selectedMonth++;
}
renderCalendar(selectedYear, selectedMonth);
renderTimeSlots();
checkForm(); // 월 변경 시 폼 검증
};
// 시간대 정의
const diettimes_mon_fri = [
"10:00","10:30", "11:00","11:30", "12:00","12:30",
"13:00","13:30", "15:00","15:30", "16:00","16:30", "17:00","17:30",
"18:00","18:30"
];
const diettimes_tue_ths = [
"10:00","10:30", "11:00","11:30", "12:00","12:30",
"13:00","13:30", "15:00","15:30", "16:00","16:30", "17:00","17:30",
"18:00","18:30", "19:00","19:30"
];
const diettimes_wes_sat = [
"10:00","10:30", "11:00","11:30", "12:00","12:30",
"13:00","13:30", "14:00","14:30", "15:00","15:30"
];
// 시간 슬롯 렌더링
function renderTimeSlots() {
let html = '';
let dayOfWeek = selectedDate ? selectedDate.getDay() : null;
let slotArr = [];
if (dayOfWeek === 0) {
timeSlots.innerHTML = '';
personCount.textContent = selectedDate ? 1 : '-';
return;
}
if (dayOfWeek !== null) {
if (dayOfWeek === 1 || dayOfWeek === 5) { // 월(1), 금(5)
slotArr = diettimes_mon_fri;
} else if (dayOfWeek === 2 || dayOfWeek === 4) { // 화(2), 목(4)
slotArr = diettimes_tue_ths;
} else if (dayOfWeek === 3 || dayOfWeek === 6) { // 수(3), 토(6)
slotArr = diettimes_wes_sat;
}
}
const now = new Date();
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const isToday = selectedDate && selectedDate.toDateString() === todayDate.toDateString();
slotArr.forEach(t => {
let isDisabled = false;
let disabledClass = '';
let clickHandler = `onclick="selectTimeAndCall('${t}', this)"`;
// 오늘 날짜인 경우 현재 시간과 비교
if (isToday) {
const currentTime = now.getHours() * 100 + now.getMinutes();
const [hour, minute] = t.split(':').map(Number);
const slotTime = hour * 100 + minute;
if (slotTime <= currentTime) {
isDisabled = true;
disabledClass = ' disabled';
clickHandler = '';
}
}
const selectedClass = selectedTime === t && !isDisabled ? ' selected' : '';
html += `<button type="button" class="time-btn${selectedClass}${disabledClass}" ${clickHandler} ${isDisabled ? 'disabled' : ''}>${t}</button>`;
});
timeSlots.innerHTML = html;
personCount.textContent = selectedDate ? 1 : '-';
}
// 시간 선택 시 selectedTime 설정 및 onClickTime 호출 (checkForm 추가)
function selectTimeAndCall(t, el) {
const now = new Date();
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const isToday = selectedDate && selectedDate.toDateString() === todayDate.toDateString();
// 오늘 날짜인 경우 현재 시간과 비교
if (isToday) {
const currentTime = now.getHours() * 100 + now.getMinutes();
const [hour, minute] = t.split(':').map(Number);
const slotTime = hour * 100 + minute;
if (slotTime <= currentTime) {
alert('지난 시간은 선택할 수 없습니다.');
return;
}
}
selectedTime = t;
renderTimeSlots();
if (el) {
document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active'));
el.classList.add('active');
}
onClickTime(getSelectedDateStr(), t, el);
checkForm();
}
// 선택된 날짜를 yyyymmdd 문자열로 반환
function getSelectedDateStr() {
if (!selectedDate) return '';
const yyyy = selectedDate.getFullYear();
const mm = String(selectedDate.getMonth() + 1).padStart(2, '0');
const dd = String(selectedDate.getDate()).padStart(2, '0');
return `${yyyy}${mm}${dd}`;
}
window.selectTime = function(t) {
selectedTime = t;
renderTimeSlots();
}
// 날짜 선택 초기화
let initDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
if (initDate.getDay() === 0) {
initDate.setDate(initDate.getDate() + 1);
}
selectDate(initDate.getFullYear(), initDate.getMonth(), initDate.getDate());
form.onsubmit = function(e) {
e.preventDefault();
if(!selectedDate || !selectedTime) {
alert('예약 날짜와 시간을 선택해 주세요.');
return;
}
if (!birthDateValidator || !birthDateValidator.isValid()) {
alert('올바른 생년월일을 입력해 주세요.');
return;
}
fn_reservation();
form.reset();
submitBtn.disabled = true;
checkForm(); // 폼 리셋 후 재검증
};
let selectedTreatments = [];
function fn_reservation(){
let formData = new FormData();
if (selectedDate) {
const yyyy = selectedDate.getFullYear();
const mm = String(selectedDate.getMonth() + 1).padStart(2, '0');
const dd = String(selectedDate.getDate()).padStart(2, '0');
formData.append('SELECTED_DATE', `${yyyy}-${mm}-${dd}`);
}
if (selectedTime) {
formData.append('TIME', selectedTime);
}
formData.append('CATEGORY_DIV_CD', typeof category_div_cd !== 'undefined' ? category_div_cd : '');
formData.append('CATEGORY_NO', typeof category_no !== 'undefined' ? category_no : '');
formData.append('POST_NO', typeof post_no !== 'undefined' ? post_no : '');
formData.append('NAME', document.getElementById('customer-name').value);
formData.append('BIRTH_DATE', document.getElementById('birthDate').value);
formData.append('PHONE_NUMBER', document.getElementById('customer-phone').value);
formData.append('ETC', document.getElementById('customer-req').value);
formData.append('TREATMENT_INFOS', JSON.stringify(selectedTreatments));
$.ajax({
url: encodeURI('/webservice/insertReservation.do'),
data: formData,
dataType: 'json',
processData: false,
contentType: false,
type: 'POST',
async: true,
success: function(data){
if(data.msgCode=='0'){
alert('예약이 완료되었습니다.');
location.href="/webevent/selectListWebEventIntro.do";
}else{
modalEvent.danger("조회 오류", data.msgDesc);
}
},
error : function(xhr, status, error) {
modalEvent.danger("조회 오류", "조회 중 오류가 발생하였습니다. 잠시후 다시시도하십시오.");
},
beforeSend:function(){
$(".loading-image-layer").show();
},
complete:function(){
$(".loading-image-layer").hide();
}
});
}
function fn_SelectReservation(category_div_cd, category_no, post_no, procedure_id) {
let formData = new FormData();
formData.append('CATEGORY_DIV_CD', category_div_cd);
formData.append('CATEGORY_NO', category_no);
formData.append('POST_NO', post_no);
formData.append('PROCEDURE_ID', procedure_id);
$.ajax({
url: encodeURI('/webservice/selectReservation.do'),
data: formData,
dataType: 'json',
processData: false,
contentType: false,
type: 'POST',
async: true,
success: function(data) {
if(data.msgCode=='0') {
const serviceList = document.getElementById('service-list');
serviceList.innerHTML = '';
let totalprice = 0;
selectedTreatments = [];
if (data.reservation && data.reservation.length > 0) {
data.reservation.forEach(function(item, i) {
let price = item.DISCOUNT_PRICE != null ? item.DISCOUNT_PRICE : item.PRICE || 0;
totalprice += Number(price);
selectedTreatments.push({
MU_TREATMENT_ID: item.MU_TREATMENT_ID,
TREATMENT_NAME: item.TREATMENT_NAME,
TREATMENT_PROCEDURE_NAME: item.TREATMENT_PROCEDURE_NAME,
MU_TREATMENT_PROCEDURE_ID: item.MU_TREATMENT_PROCEDURE_ID
});
const li = document.createElement('div');
li.className = 'service-item';
li.innerHTML = `
<span>${item.TREATMENT_PROCEDURE_NAME}</span>
<span>
<span style="text-decoration:line-through; color:#bbb; font-size:0.95em; margin-right:6px;">${item.DISCOUNT_PRICE != null ? (item.PRICE || 0).toLocaleString() : ''}</span>
<span class="price">${Number(price).toLocaleString()}원</span>
<span class="del" title="삭제" onclick="removeService(this)">×</span>
</span>
`;
serviceList.appendChild(li);
});
}
document.getElementById('total-price').textContent = totalprice.toLocaleString() + '원';
// 시술 개수 및 삭제 버튼 상태 업데이트
updateServiceCount();
} else {
modalEvent.danger("조회 오류", data.msgDesc);
}
var date = new Date();
if(String(new Date()).split(" ") == "Sun") {
date = new Date();
date.setDate(date.getDate() + 1);
}
},
error : function(xhr, status, error) {
modalEvent.danger("조회 오류", "조회 중 오류가 발생하였습니다. 잠시후 다시시도하십시오.");
},
beforeSend:function() {
$(".loading-image-layer").show();
},
complete:function() {
$(".loading-image-layer").hide();
}
});
}
function onClickTime(selectedDate, time, el) {
document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active'));
if (el) el.classList.add('active');
let formData = new FormData();
formData.append('SELECTED_DATE', selectedDate.toString().substr(0, 4) + '-' + selectedDate.toString().substr(4, 2) + '-' + selectedDate.toString().substr(6, 2));
formData.append('TIME', time);
res_date = selectedDate.toString().substr(0, 4) + '-' + selectedDate.toString().substr(4, 2) + '-' + selectedDate.toString().substr(6, 2);
res_time = time;
$.ajax({
url: encodeURI('/webservice/selectReservationCnt.do'),
data: formData,
dataType: 'json',
processData: false,
contentType: false,
type: 'POST',
async: true,
success: function(data){
if(data.msgCode=='0'){
if (data.rows && data.rows.RES_CNT !== undefined) {
personCount.textContent = data.rows.RES_CNT;
} else {
personCount.textContent = '-';
}
}else{
modalEvent.danger("조회 오류", data.msgDesc);
}
},
error : function(xhr, status, error) {
modalEvent.danger("조회 오류", "조회 중 오류가 발생하였습니다. 잠시후 다시시도하십시오.");
},
beforeSend:function(){
$(".loading-image-layer").show();
},
complete:function(){
$(".loading-image-layer").hide();
}
});
}
$(document).ready(function() {
// common.js의 PhoneValidator 초기화
try {
if (typeof PhoneValidator !== 'undefined' && PhoneValidator.init) {
console.log('Initializing PhoneValidator from common.js');
PhoneValidator.init('customer-phone', {
showMessage: true,
realTimeValidation: true,
maxLength: 11,
allowedPrefixes: ['010', '011', '016', '017', '018', '019'],
onValidationChange: function(result, phoneNumber) {
// PhoneValidator 상태 변경 시 checkForm 호출
setTimeout(checkForm, 10);
}
});
} else {
console.warn('PhoneValidator not found in common.js');
}
} catch (e) {
console.error('PhoneValidator initialization error:', e);
}
// BirthDateValidator 초기화
try {
console.log('Initializing BirthDateValidator');
birthDateValidator = new BirthDateValidator('birthDate', {
showMessage: true,
realTimeValidation: true,
minAge: 0,
maxAge: 150,
format: 'YYYYMMDD',
allowFuture: false,
onValidationChange: function(result, birthDate) {
// BirthDateValidator 상태 변경 시 checkForm 호출
setTimeout(checkForm, 10);
}
});
console.log('BirthDateValidator initialized successfully');
} catch (e) {
console.error('BirthDateValidator initialization error:', e);
}
// PhoneValidator와 BirthDateValidator 이벤트 감지 (fallback 및 보완)
$('#customer-phone').on('input keyup blur paste', function() {
setTimeout(checkForm, 50);
});
$('#birthDate').on('input keyup blur paste', function() {
setTimeout(checkForm, 50);
});
// 초기 폼 상태 체크
setTimeout(() => {
checkForm();
updateServiceCount(); // 초기 시술 개수 상태 설정
}, 300); // Validator들 초기화 완료 대기
});
</script>
</th:block>
</html>