- Google Calendar 연동 예약 현황 페이지 (ko/en/ja/zh 4개 언어) - Apps Script 프록시로 예약 일정만 노출 (이벤트 상세 비공개) - localStorage 캐싱 + 인접 월 프리페치로 로딩 최적화 - 전체 36개 HTML 파일 인라인 헤더에 Schedule 네비게이션 링크 추가 - 스케줄 페이지 i18n.js 누락 수정 (언어 스위처 동작 복구) - i18n JSON에 schedule 관련 번역 키 추가 - sitemap.xml에 schedule URL 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
180 lines
6.3 KiB
JavaScript
180 lines
6.3 KiB
JavaScript
/**
|
|
* 밍글 스튜디오 - 예약 현황 캘린더
|
|
* Google Apps Script 프록시를 통해 캘린더 데이터를 가져와 표시
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
var APPS_SCRIPT_URL = 'https://script.google.com/macros/s/AKfycbwoaoSmEskbJod4gR5NjLnpWSkhIDUYaGBYk0y1Q865TzaJ4gctSrTOWXxRnVa7J9AASA/exec';
|
|
var CACHE_TTL = 30 * 60 * 1000; // 30분
|
|
|
|
var currentYear, currentMonth;
|
|
var calTitle = document.getElementById('calTitle');
|
|
var calBody = document.getElementById('calBody');
|
|
var prevBtn = document.getElementById('prevMonth');
|
|
var nextBtn = document.getElementById('nextMonth');
|
|
|
|
function init() {
|
|
var now = new Date();
|
|
currentYear = now.getFullYear();
|
|
currentMonth = now.getMonth() + 1;
|
|
|
|
prevBtn.addEventListener('click', function() { changeMonth(-1); });
|
|
nextBtn.addEventListener('click', function() { changeMonth(1); });
|
|
|
|
renderCalendar();
|
|
}
|
|
|
|
function changeMonth(delta) {
|
|
currentMonth += delta;
|
|
if (currentMonth > 12) { currentMonth = 1; currentYear++; }
|
|
else if (currentMonth < 1) { currentMonth = 12; currentYear--; }
|
|
renderCalendar();
|
|
}
|
|
|
|
function renderCalendar() {
|
|
updateTitle();
|
|
|
|
var cached = getCache(currentYear, currentMonth);
|
|
if (cached !== null) {
|
|
buildGrid(cached);
|
|
} else {
|
|
// 즉시 빈 그리드 렌더링 (체감 속도 향상)
|
|
buildGrid([]);
|
|
calBody.classList.add('loading');
|
|
fetchBookedDates(currentYear, currentMonth, function(dates) {
|
|
setCache(currentYear, currentMonth, dates);
|
|
buildGrid(dates);
|
|
calBody.classList.remove('loading');
|
|
prefetchAdjacent();
|
|
});
|
|
}
|
|
}
|
|
|
|
function prefetchAdjacent() {
|
|
var nextM = currentMonth + 1, nextY = currentYear;
|
|
if (nextM > 12) { nextM = 1; nextY++; }
|
|
if (getCache(nextY, nextM) === null) {
|
|
fetchBookedDates(nextY, nextM, function(dates) {
|
|
setCache(nextY, nextM, dates);
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- localStorage 캐싱 ---
|
|
function cacheKey(y, m) { return 'mingle_cal_' + y + '_' + m; }
|
|
|
|
function getCache(y, m) {
|
|
try {
|
|
var raw = localStorage.getItem(cacheKey(y, m));
|
|
if (!raw) return null;
|
|
var obj = JSON.parse(raw);
|
|
if (Date.now() - obj.ts > CACHE_TTL) {
|
|
localStorage.removeItem(cacheKey(y, m));
|
|
return null;
|
|
}
|
|
return obj.dates;
|
|
} catch (e) { return null; }
|
|
}
|
|
|
|
function setCache(y, m, dates) {
|
|
try {
|
|
localStorage.setItem(cacheKey(y, m), JSON.stringify({ dates: dates, ts: Date.now() }));
|
|
} catch (e) { /* quota exceeded 등 무시 */ }
|
|
}
|
|
|
|
// --- 제목 업데이트 ---
|
|
function updateTitle() {
|
|
var lang = (window.i18n && window.i18n.currentLang) || 'ko';
|
|
var monthNames = {
|
|
ko: ['1월','2월','3월','4월','5월','6월','7월','8월','9월','10월','11월','12월'],
|
|
en: ['January','February','March','April','May','June','July','August','September','October','November','December'],
|
|
ja: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
|
|
zh: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']
|
|
};
|
|
var names = monthNames[lang] || monthNames.ko;
|
|
if (lang === 'en') {
|
|
calTitle.textContent = names[currentMonth - 1] + ' ' + currentYear;
|
|
} else if (lang === 'ja' || lang === 'zh') {
|
|
calTitle.textContent = currentYear + '年 ' + names[currentMonth - 1];
|
|
} else {
|
|
calTitle.textContent = currentYear + '년 ' + names[currentMonth - 1];
|
|
}
|
|
}
|
|
|
|
// --- API 호출 ---
|
|
function fetchBookedDates(year, month, callback) {
|
|
if (!APPS_SCRIPT_URL) { callback([]); return; }
|
|
|
|
var url = APPS_SCRIPT_URL + '?year=' + year + '&month=' + month;
|
|
fetch(url)
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(data) { callback(data.bookedDates || []); })
|
|
.catch(function() { callback([]); });
|
|
}
|
|
|
|
// --- 그리드 렌더링 ---
|
|
function buildGrid(bookedDates) {
|
|
calBody.innerHTML = '';
|
|
|
|
var firstDay = new Date(currentYear, currentMonth - 1, 1).getDay();
|
|
var daysInMonth = new Date(currentYear, currentMonth, 0).getDate();
|
|
var today = new Date();
|
|
var todayStr = today.getFullYear() + '-' +
|
|
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
|
String(today.getDate()).padStart(2, '0');
|
|
|
|
var bookedSet = {};
|
|
for (var b = 0; b < bookedDates.length; b++) {
|
|
bookedSet[bookedDates[b]] = true;
|
|
}
|
|
|
|
var fragment = document.createDocumentFragment();
|
|
|
|
for (var e = 0; e < firstDay; e++) {
|
|
var emptyCell = document.createElement('div');
|
|
emptyCell.className = 'cal-cell empty';
|
|
fragment.appendChild(emptyCell);
|
|
}
|
|
|
|
for (var d = 1; d <= daysInMonth; d++) {
|
|
var dateStr = currentYear + '-' +
|
|
String(currentMonth).padStart(2, '0') + '-' +
|
|
String(d).padStart(2, '0');
|
|
|
|
var cell = document.createElement('div');
|
|
cell.className = 'cal-cell';
|
|
|
|
var dayNum = document.createElement('span');
|
|
dayNum.className = 'day-num';
|
|
dayNum.textContent = d;
|
|
|
|
var cellDate = new Date(currentYear, currentMonth - 1, d);
|
|
var isPast = cellDate < new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
|
|
if (dateStr === todayStr) {
|
|
cell.classList.add('today');
|
|
} else if (isPast) {
|
|
cell.classList.add('past');
|
|
} else if (bookedSet[dateStr]) {
|
|
cell.classList.add('booked');
|
|
} else {
|
|
cell.classList.add('available');
|
|
}
|
|
|
|
cell.appendChild(dayNum);
|
|
fragment.appendChild(cell);
|
|
}
|
|
|
|
calBody.appendChild(fragment);
|
|
}
|
|
|
|
document.addEventListener('langChanged', function() { updateTitle(); });
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|