휴일 진료 설정
This commit is contained in:
470
src/main/resources/static/css/web/makeReservation.css
Normal file
470
src/main/resources/static/css/web/makeReservation.css
Normal file
@@ -0,0 +1,470 @@
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Disabled calendar cell - 강화된 스타일 */
|
||||
.calendar-table td.disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.calendar-table td.disabled:hover {
|
||||
background: none !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
598
src/main/resources/static/js/makeReservation.js
Normal file
598
src/main/resources/static/js/makeReservation.js
Normal file
@@ -0,0 +1,598 @@
|
||||
// 휴일(완전 휴무) 설정
|
||||
const disabledSpecificDates = [
|
||||
'2025-12-25', // 크리스마스
|
||||
'2026-01-01' // 신정
|
||||
];
|
||||
|
||||
// 단축 진료일 (15:30까지, 점심시간 없음)
|
||||
const shortWorkingDates = [
|
||||
'2025-12-24', // 크리스마스 이브
|
||||
'2025-12-31' // 연말
|
||||
];
|
||||
|
||||
// 점심시간 (14:00 ~ 15:00)
|
||||
const lunchTimeStart = 1400; // 14:00
|
||||
const lunchTimeEnd = 1500; // 15:00
|
||||
|
||||
// 날짜가 휴무일인지 확인
|
||||
function isDateDisabled(date) {
|
||||
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
return disabledSpecificDates.includes(dateStr);
|
||||
}
|
||||
|
||||
// 단축 근무일인지 확인
|
||||
function isShortWorkingDate(date) {
|
||||
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
return shortWorkingDates.includes(dateStr);
|
||||
}
|
||||
|
||||
// 점심시간인지 확인 (월화수목금만 해당)
|
||||
function isLunchTime(timeStr) {
|
||||
const timeNum = parseInt(timeStr.replace(':', '')); // "14:30" -> 1430
|
||||
return timeNum >= lunchTimeStart && timeNum < lunchTimeEnd;
|
||||
}
|
||||
|
||||
// 생년월일 검증 클래스
|
||||
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',
|
||||
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 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까지 입력 가능합니다.' };
|
||||
|
||||
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: '미래 날짜는 입력할 수 없습니다.' };
|
||||
|
||||
return { valid: true, message: '올바른 생년월일입니다.' };
|
||||
}
|
||||
|
||||
validateAndShowMessage() {
|
||||
const result = this.validateBirthDate(this.inputElement.value);
|
||||
if (this.messageElement) {
|
||||
this.messageElement.textContent = result.message;
|
||||
this.messageElement.className = `birth-date-message ${result.valid ? 'success' : 'error'}`;
|
||||
this.messageElement.style.display = 'block';
|
||||
}
|
||||
if (typeof this.options.onValidationChange === 'function') {
|
||||
this.options.onValidationChange(result, this.inputElement.value);
|
||||
}
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.validateBirthDate(this.inputElement.value).valid;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 변수
|
||||
let birthDateValidator;
|
||||
let selectedTreatments = [];
|
||||
let selectedDate = null;
|
||||
let selectedTime = null;
|
||||
let selectedYear, selectedMonth;
|
||||
|
||||
// DOM 요소
|
||||
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');
|
||||
|
||||
// 진료시간 설정 (점심시간 제외)
|
||||
const times_mon_wed_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 times_tue_thu = [
|
||||
"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 times_sat_short = [
|
||||
"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 removeService(el) {
|
||||
const serviceItems = document.querySelectorAll('.service-item');
|
||||
if (serviceItems.length <= 1) {
|
||||
alert('최소 1개의 시술은 선택되어 있어야 합니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const serviceName = el.closest('.service-item').querySelector('span:first-child').textContent;
|
||||
if (!confirm(`'${serviceName}' 시술을 삭제하시겠습니까?`)) return false;
|
||||
|
||||
el.closest('.service-item').remove();
|
||||
updateTotalPrice();
|
||||
updateServiceCount();
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateTotalPrice() {
|
||||
const serviceItems = document.querySelectorAll('.service-item');
|
||||
let totalPrice = 0;
|
||||
serviceItems.forEach(item => {
|
||||
const priceText = item.querySelector('.price').textContent;
|
||||
totalPrice += parseInt(priceText.replace(/[^0-9]/g, '')) || 0;
|
||||
});
|
||||
document.getElementById('total-price').textContent = totalPrice.toLocaleString() + '원';
|
||||
}
|
||||
|
||||
function updateServiceCount() {
|
||||
const serviceItems = document.querySelectorAll('.service-item');
|
||||
const count = serviceItems.length;
|
||||
const info = document.getElementById('service-count-info');
|
||||
info.textContent = `선택된 시술: ${count}개${count === 1 ? ' (최소 필수)' : ''}`;
|
||||
info.className = count === 1 ? 'service-count-info single' : 'service-count-info';
|
||||
|
||||
serviceItems.forEach(item => {
|
||||
const delBtn = item.querySelector('.del');
|
||||
if (count <= 1) {
|
||||
delBtn.className = 'del disabled';
|
||||
delBtn.title = '최소 1개의 시술은 필요합니다';
|
||||
} else {
|
||||
delBtn.className = 'del';
|
||||
delBtn.title = '삭제';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 캘린더 렌더링 (일요일 + 공휴일만 휴무)
|
||||
function renderCalendar(year, month) {
|
||||
selectedYear = year;
|
||||
selectedMonth = month;
|
||||
calendarTitle.textContent = `${year}.${String(month + 1).padStart(2, '0')}`;
|
||||
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const todayDate = new Date();
|
||||
todayDate.setHours(0, 0, 0, 0);
|
||||
|
||||
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;
|
||||
const isSunday = dateObj.getDay() === 0;
|
||||
const isHoliday = isDateDisabled(dateObj);
|
||||
|
||||
if(dateObj.toDateString() === new Date().toDateString()) classes.push('today');
|
||||
if(selectedDate && dateObj.toDateString() === selectedDate.toDateString()) classes.push('selected');
|
||||
|
||||
if(isPastDate || isSunday || isHoliday) classes.push('disabled');
|
||||
|
||||
const clickHandler = (isPastDate || isSunday || isHoliday) ?
|
||||
'' : `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(); i < 6; 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 todayDate = new Date();
|
||||
todayDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (tempDate < todayDate) {
|
||||
alert('과거 날짜는 선택할 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tempDate.getDay() === 0) {
|
||||
alert('일요일은 휴무일입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDateDisabled(tempDate)) {
|
||||
alert('해당 날짜는 휴무일입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedDate = tempDate;
|
||||
renderCalendar(y, m);
|
||||
renderTimeSlots();
|
||||
checkForm();
|
||||
}
|
||||
|
||||
// 월 이동
|
||||
document.getElementById('prev-month').onclick = function() {
|
||||
selectedMonth === 0 ? (selectedYear--, selectedMonth = 11) : selectedMonth--;
|
||||
renderCalendar(selectedYear, selectedMonth);
|
||||
};
|
||||
|
||||
document.getElementById('next-month').onclick = function() {
|
||||
selectedMonth === 11 ? (selectedYear++, selectedMonth = 0) : selectedMonth++;
|
||||
renderCalendar(selectedYear, selectedMonth);
|
||||
};
|
||||
|
||||
// 시간 슬롯 렌더링 (점심시간 제외 + 특수일 처리)
|
||||
function renderTimeSlots() {
|
||||
if (!selectedDate) {
|
||||
timeSlots.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const dayOfWeek = selectedDate.getDay();
|
||||
if (dayOfWeek === 0 || isDateDisabled(selectedDate)) {
|
||||
timeSlots.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let slotArr;
|
||||
if (isShortWorkingDate(selectedDate)) {
|
||||
slotArr = times_sat_short; // 12/24, 12/31: 토요일과 동일
|
||||
} else if ([1, 3, 5].includes(dayOfWeek)) { // 월수금
|
||||
slotArr = times_mon_wed_fri;
|
||||
} else if ([2, 4].includes(dayOfWeek)) { // 화목
|
||||
slotArr = times_tue_thu;
|
||||
} else if (dayOfWeek === 6) { // 토
|
||||
slotArr = times_sat_short;
|
||||
} else {
|
||||
timeSlots.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const isToday = selectedDate.toDateString() === todayDate.toDateString();
|
||||
|
||||
let html = '';
|
||||
slotArr.forEach(t => {
|
||||
let isDisabled = false;
|
||||
if (isToday) {
|
||||
const currentTime = now.getHours() * 100 + now.getMinutes();
|
||||
const [hour, minute] = t.split(':').map(Number);
|
||||
if (hour * 100 + minute <= currentTime) isDisabled = true;
|
||||
}
|
||||
|
||||
const selectedClass = selectedTime === t && !isDisabled ? ' selected' : '';
|
||||
const disabledAttr = isDisabled ? 'disabled' : '';
|
||||
html += `<button type="button" class="time-btn${selectedClass}${isDisabled ? ' disabled' : ''}"
|
||||
${isDisabled ? '' : `onclick="selectTimeAndCall('${t}', this)"`} ${disabledAttr}>${t}</button>`;
|
||||
});
|
||||
|
||||
timeSlots.innerHTML = html;
|
||||
}
|
||||
|
||||
function selectTimeAndCall(t, el) {
|
||||
const now = new Date();
|
||||
const todayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const isToday = selectedDate.toDateString() === todayDate.toDateString();
|
||||
|
||||
if (isToday) {
|
||||
const currentTime = now.getHours() * 100 + now.getMinutes();
|
||||
const [hour, minute] = t.split(':').map(Number);
|
||||
if (hour * 100 + minute <= 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);
|
||||
checkForm();
|
||||
}
|
||||
|
||||
function getSelectedDateStr() {
|
||||
if (!selectedDate) return '';
|
||||
return selectedDate.getFullYear() +
|
||||
String(selectedDate.getMonth() + 1).padStart(2, '0') +
|
||||
String(selectedDate.getDate()).padStart(2, '0');
|
||||
}
|
||||
|
||||
// 폼 검증
|
||||
function checkForm() {
|
||||
const name = document.getElementById('customer-name').value.trim();
|
||||
const phone = document.getElementById('customer-phone').value.trim();
|
||||
const birthDate = document.getElementById('birthDate').value.trim();
|
||||
|
||||
const phoneValid = phone.match(/^01[0-9]{8,9}$/) ||
|
||||
(typeof PhoneValidator !== 'undefined' && PhoneValidator.isValid?.('customer-phone'));
|
||||
const birthDateValid = birthDate.match(/^\d{8}$/) ||
|
||||
(birthDateValidator?.isValid?.());
|
||||
|
||||
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;
|
||||
|
||||
updateStepStatus(conditions);
|
||||
updateButtonText(conditions);
|
||||
|
||||
submitBtn.className = valid ? 'submit-btn ready' :
|
||||
selectedDate ? 'submit-btn step-progress' : 'submit-btn';
|
||||
}
|
||||
|
||||
function updateStepStatus(conditions) {
|
||||
step02Title.textContent = (conditions.date && conditions.time) ? 'STEP 02. 예약 시간 선택 ✓' : 'STEP 02. 예약 시간 선택';
|
||||
step02Title.className = (conditions.date && conditions.time) ? 'step-title completed' : 'step-title';
|
||||
|
||||
step03Title.textContent = (conditions.name && conditions.phone && conditions.birthDate && conditions.agree) ?
|
||||
'STEP 03. 고객정보 ✓' : 'STEP 03. 고객정보';
|
||||
step03Title.className = (conditions.name && conditions.phone && conditions.birthDate && conditions.agree) ?
|
||||
'step-title completed' : 'step-title';
|
||||
}
|
||||
|
||||
function updateButtonText(conditions) {
|
||||
if (!conditions.date) return submitBtn.textContent = '📅 예약 날짜를 선택해주세요';
|
||||
if (!conditions.time) return submitBtn.textContent = '⏰ 예약 시간을 선택해주세요';
|
||||
if (!conditions.name) return submitBtn.textContent = '👤 고객명을 입력해주세요';
|
||||
if (!conditions.birthDate) return submitBtn.textContent = '📅 생년월일을 올바르게 입력해주세요';
|
||||
if (!conditions.phone) return submitBtn.textContent = '📱 연락처를 올바르게 입력해주세요';
|
||||
if (!conditions.agree) return submitBtn.textContent = '✅ 개인정보 동의를 체크해주세요';
|
||||
submitBtn.textContent = '🎉 시술 예약하기';
|
||||
}
|
||||
|
||||
// 초기화
|
||||
const today = new Date();
|
||||
selectedYear = today.getFullYear();
|
||||
selectedMonth = today.getMonth();
|
||||
|
||||
form.addEventListener('input', (e) => {
|
||||
if (e.target.id !== 'customer-phone' && e.target.id !== 'birthDate') setTimeout(checkForm, 10);
|
||||
});
|
||||
agree.addEventListener('change', checkForm);
|
||||
|
||||
form.onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
if (!selectedDate || !selectedTime) {
|
||||
alert('예약 날짜와 시간을 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (!birthDateValidator?.isValid()) {
|
||||
alert('올바른 생년월일을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
fn_reservation();
|
||||
};
|
||||
|
||||
// AJAX 함수들 (기존 그대로)
|
||||
function fn_reservation() {
|
||||
let formData = new FormData();
|
||||
if (selectedDate) {
|
||||
formData.append('SELECTED_DATE', `${selectedDate.getFullYear()}-${String(selectedDate.getMonth() + 1).padStart(2, '0')}-${String(selectedDate.getDate()).padStart(2, '0')}`);
|
||||
}
|
||||
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() {
|
||||
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?.length > 0) {
|
||||
data.reservation.forEach(item => {
|
||||
let price = 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 div = document.createElement('div');
|
||||
div.className = 'service-item';
|
||||
div.innerHTML = `
|
||||
<span>${item.TREATMENT_PROCEDURE_NAME}</span>
|
||||
<span>
|
||||
${item.DISCOUNT_PRICE != null ? `<span style="text-decoration:line-through; color:#bbb; font-size:0.95em; margin-right:6px;">${(item.PRICE || 0).toLocaleString()}원</span>` : ''}
|
||||
<span class="price">${Number(price).toLocaleString()}원</span>
|
||||
<span class="del" title="삭제" onclick="removeService(this)">×</span>
|
||||
</span>
|
||||
`;
|
||||
serviceList.appendChild(div);
|
||||
});
|
||||
}
|
||||
document.getElementById('total-price').textContent = totalprice.toLocaleString() + '원';
|
||||
updateServiceCount();
|
||||
} else {
|
||||
modalEvent.danger("조회 오류", data.msgDesc);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
modalEvent.danger("조회 오류", "조회 중 오류가 발생하였습니다. 잠시후 다시시도하십시오.");
|
||||
},
|
||||
beforeSend: function() { $(".loading-image-layer").show(); },
|
||||
complete: function() { $(".loading-image-layer").hide(); }
|
||||
});
|
||||
}
|
||||
|
||||
function onClickTime(selectedDateStr, time) {
|
||||
let formData = new FormData();
|
||||
formData.append('SELECTED_DATE', selectedDateStr);
|
||||
formData.append('TIME', time);
|
||||
|
||||
$.ajax({
|
||||
url: encodeURI('/webservice/selectReservationCnt.do'),
|
||||
data: formData,
|
||||
dataType: 'json',
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: 'POST',
|
||||
async: true,
|
||||
success: function(data) {
|
||||
personCount.textContent = data.msgCode == '0' && data.rows?.RES_CNT !== undefined ?
|
||||
data.rows.RES_CNT : '-';
|
||||
},
|
||||
error: function() {
|
||||
personCount.textContent = '-';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 문서 준비 완료
|
||||
$(document).ready(function() {
|
||||
// Validator 초기화
|
||||
try {
|
||||
birthDateValidator = new BirthDateValidator('birthDate', {
|
||||
showMessage: true,
|
||||
realTimeValidation: true,
|
||||
onValidationChange: () => setTimeout(checkForm, 10)
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('BirthDateValidator init error:', e);
|
||||
}
|
||||
|
||||
// 초기 캘린더 및 시술 로드
|
||||
const today = new Date();
|
||||
let initDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
while (initDate.getDay() === 0 || isDateDisabled(initDate)) {
|
||||
initDate.setDate(initDate.getDate() + 1);
|
||||
}
|
||||
|
||||
renderCalendar(today.getFullYear(), today.getMonth());
|
||||
fn_SelectReservation(category_div_cd, category_no, post_no, procedure_id);
|
||||
|
||||
setTimeout(checkForm, 500);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user