// 쿠팡 밀크런 캘린더 // // 화면이 열리면 구글 드라이브의 밀크런 출고리스트를 자동으로 읽어 // 시트 이름(YYYYMMDD)을 달력에 표시한다. // 달력에서 날짜를 고르면 그 시트의 제품코드/제품명/수량을 읽어 // 붙여넣기 입력을 채우고 곧바로 분석까지 실행한다. // (시트 주소는 서버에 설정되어 있어 화면에서 입력하지 않는다) (function () { 'use strict'; const DOW = ['일', '월', '화', '수', '목', '금', '토']; document.addEventListener('DOMContentLoaded', function () { const refreshBtn = document.getElementById('btn-gsheet-refresh'); const messageBox = document.getElementById('gsheet-message'); const fileInfo = document.getElementById('gsheet-file-info'); const calendarBox = document.getElementById('gsheet-calendar'); const otherBox = document.getElementById('gsheet-other-sheets'); if (!calendarBox) return; // 붙여넣기 입력 쪽 요소 (분석은 기존 로직을 그대로 재사용한다) const pasteInput = document.getElementById('milkrun-input'); const analyzeBtn = document.getElementById('btn-milkrun-analyze'); let sheetsByDate = {}; // 'YYYY-MM-DD' -> 시트 이름 let otherSheets = []; // 날짜로 못 읽은 시트 이름 let viewYear = null; let viewMonth = null; // 0-based let selectedDate = null; let busy = false; function escapeHtml(str) { return String(str == null ? '' : str) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } let messageTimer = null; function showMessage(text, kind, autoHideMs) { if (messageTimer) { clearTimeout(messageTimer); messageTimer = null; } if (!text) { messageBox.style.display = 'none'; messageBox.innerHTML = ''; return; } const palette = { error: { bg: '#fff5f5', border: '#feb2b2', color: '#c53030' }, info: { bg: '#ebf8ff', border: '#bee3f8', color: '#2b6cb0' }, success: { bg: '#f0fff4', border: '#9ae6b4', color: '#276749' }, // 눈에 잘 띄어야 하는 안내 (오늘 출고 없음 등) alert: { bg: '#fffbeb', border: '#f6ad55', color: '#c05621' } }; const c = palette[kind] || palette.info; messageBox.style.display = 'block'; messageBox.style.background = c.bg; messageBox.style.border = '2px solid ' + c.border; messageBox.style.color = c.color; messageBox.innerHTML = text; if (autoHideMs) { messageTimer = setTimeout(function () { messageBox.style.display = 'none'; messageBox.innerHTML = ''; messageTimer = null; }, autoHideMs); } } function dateKeyOf(date) { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0'); } // 달력은 [본문 + 로딩 덮개] 두 겹으로 만들어 둔다. // 본문만 다시 그리므로 덮개가 지워지지 않는다. function ensureCalendarShell() { if (calendarBox.dataset.ready === '1') return; calendarBox.style.position = 'relative'; calendarBox.innerHTML = '
' + ''; calendarBox.dataset.ready = '1'; } function setBusy(isBusy, label) { busy = isBusy; if (refreshBtn) { refreshBtn.disabled = isBusy; refreshBtn.style.opacity = isBusy ? '0.6' : '1'; refreshBtn.textContent = isBusy ? '읽는 중...' : '새로고침'; } ensureCalendarShell(); const overlay = document.getElementById('cal-overlay'); const overlayText = document.getElementById('cal-overlay-text'); const body = document.getElementById('cal-body'); if (overlay) overlay.style.display = isBusy ? 'flex' : 'none'; if (overlayText && label) overlayText.textContent = label; if (body) { // 읽는 동안에는 날짜를 누를 수 없게 한다 body.style.pointerEvents = isBusy ? 'none' : ''; body.style.opacity = isBusy ? '0.45' : ''; } } // ---- 시트 이름 → 날짜 ---- // '20260807(금)' 처럼 앞 8자리가 날짜인 이름을 달력에 올린다 function parseSheetDate(name) { const m = String(name).match(/(\d{4})(\d{2})(\d{2})/); if (!m) return null; const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]); if (mo < 1 || mo > 12 || d < 1 || d > 31) return null; const date = new Date(y, mo - 1, d); if (date.getFullYear() !== y || date.getMonth() !== mo - 1 || date.getDate() !== d) return null; return { key: m[1] + '-' + m[2] + '-' + m[3], year: y, month: mo - 1, day: d }; } function indexSheets(sheets) { sheetsByDate = {}; otherSheets = []; let latest = null; sheets.forEach(function (sheet) { const parsed = parseSheetDate(sheet.name); if (!parsed) { otherSheets.push(sheet.name); return; } sheetsByDate[parsed.key] = sheet.name; if (!latest || parsed.key > latest.key) latest = parsed; }); return latest; } // ---- 달력 ---- function renderCalendar(placeholderText) { calendarBox.style.display = 'block'; ensureCalendarShell(); const body = document.getElementById('cal-body'); if (viewYear === null) { const now = new Date(); viewYear = now.getFullYear(); viewMonth = now.getMonth(); } const first = new Date(viewYear, viewMonth, 1); const startDow = first.getDay(); const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); const monthPrefix = viewYear + '-' + String(viewMonth + 1).padStart(2, '0'); const monthCount = Object.keys(sheetsByDate).filter(function (key) { return key.indexOf(monthPrefix) === 0; }).length; let html = '' + '
' + '' + '
' + viewYear + '년 ' + (viewMonth + 1) + '월' + ' · 출고 ' + monthCount + '일
' + '' + '
' + '
'; DOW.forEach(function (label, index) { const color = index === 0 ? '#e53e3e' : (index === 6 ? '#3182ce' : '#718096'); html += '
' + label + '
'; }); for (let i = 0; i < startDow; i++) html += '
'; const todayKey = dateKeyOf(new Date()); for (let day = 1; day <= daysInMonth; day++) { const key = monthPrefix + '-' + String(day).padStart(2, '0'); const sheetName = sheetsByDate[key]; const isSelected = selectedDate === key; let style = 'text-align:center; padding:5px 0; border-radius:4px; font-size:0.78rem;'; // 오늘 날짜는 테두리로 표시해 기준점을 알 수 있게 한다 if (key === todayKey) style += ' outline:2px solid #ed8936; outline-offset:-2px;'; let attrs = ''; if (sheetName) { attrs = ' class="cal-day" data-date="' + key + '" title="' + escapeHtml(sheetName) + '"'; style += isSelected ? ' background:#2b6cb0; color:#fff; font-weight:700; cursor:pointer;' : ' background:#bee3f8; color:#2b6cb0; font-weight:600; cursor:pointer;'; } else { style += ' color:#cbd5e0;'; } html += '' + day + '
'; } html += ''; if (placeholderText) { html += '
' + escapeHtml(placeholderText) + '
'; } body.innerHTML = html; body.querySelectorAll('.cal-nav').forEach(function (btn) { btn.addEventListener('click', function () { const date = new Date(viewYear, viewMonth + Number(btn.dataset.move), 1); viewYear = date.getFullYear(); viewMonth = date.getMonth(); renderCalendar(); }); }); body.querySelectorAll('.cal-day').forEach(function (cell) { cell.addEventListener('click', function () { if (busy) return; selectDate(cell.dataset.date); }); }); } function renderOtherSheets() { if (!otherSheets.length) { otherBox.style.display = 'none'; otherBox.innerHTML = ''; return; } otherBox.style.display = 'block'; otherBox.innerHTML = '날짜로 읽지 못한 시트 ' + otherSheets.length + '개: ' + otherSheets.slice(0, 5).map(escapeHtml).join(', ') + (otherSheets.length > 5 ? ' 외' : ''); } // ---- 시트 목록 불러오기 ---- async function loadSheetTabs(forceRefresh) { selectedDate = null; showMessage('구글 시트에서 출고일을 읽는 중...', 'info'); setBusy(true, '출고일을 읽는 중...'); try { const res = await fetch('/api/coupang-milkrun/sheet-tabs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh: !!forceRefresh }) }); if (!res.ok) { let detail = '출고일을 불러오지 못했습니다. (서버 응답 ' + res.status + ')'; try { const data = await res.json(); if (data && data.detail) detail = data.detail; } catch (e) { /* 본문이 JSON이 아니면 기본 메시지 */ } showMessage(escapeHtml(detail), 'error'); renderCalendar('출고일을 불러오지 못했습니다'); return; } const data = await res.json(); const sheets = data.sheets || []; indexSheets(sheets); fileInfo.style.display = 'block'; fileInfo.innerHTML = '' + escapeHtml(data.file_name || '쿠팡 밀크런 출고리스트') + '' + ' · 출고일 ' + Object.keys(sheetsByDate).length + '일' + (data.cached ? ' · 최근 결과' : '') + ''; // 항상 오늘 날짜를 기준으로 본다 const today = new Date(); const todayKey = dateKeyOf(today); viewYear = today.getFullYear(); viewMonth = today.getMonth(); renderCalendar(); renderOtherSheets(); if (sheetsByDate[todayKey]) { // 오늘 출고가 있으면 바로 읽어서 분석까지 진행 (메시지는 selectDate가 남긴다) await selectDate(todayKey); } else { showMessage( '오늘 출고할 밀크런은 없습니다.', 'alert', 2000); } } catch (err) { console.error(err); showMessage('출고일 조회 중 오류가 발생했습니다.', 'error'); renderCalendar('출고일을 불러오지 못했습니다'); } finally { setBusy(false); } } // ---- 날짜 선택 → 값 읽기 → 분석 ---- async function selectDate(dateKey) { const sheetName = sheetsByDate[dateKey]; if (!sheetName) return; selectedDate = dateKey; renderCalendar(); showMessage('' + escapeHtml(sheetName) + ' 출고리스트를 읽는 중...', 'info'); setBusy(true, '출고리스트를 읽는 중...'); try { const res = await fetch('/api/coupang-milkrun/sheet-rows', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sheet: sheetName }) }); if (!res.ok) { let detail = '출고리스트를 읽지 못했습니다. (서버 응답 ' + res.status + ')'; try { const data = await res.json(); if (data && data.detail) detail = data.detail; } catch (e) { /* 무시 */ } showMessage(escapeHtml(detail), 'error'); return; } const data = await res.json(); const items = data.items || []; if (!items.length) { showMessage('' + escapeHtml(sheetName) + '에서 제품코드/수량을 찾지 못했습니다. ' + '시트 형식을 확인해주세요.', 'error'); return; } if (pasteInput) pasteInput.value = data.text || ''; if (analyzeBtn) analyzeBtn.click(); // 기존 분석 로직 재사용 const skipped = data.skipped ? ' (건너뛴 줄 ' + data.skipped + '개)' : ''; showMessage('' + escapeHtml(sheetName) + ' · ' + items.length + '줄을 불러와 분석했습니다.' + skipped, 'success'); } catch (err) { console.error(err); showMessage('출고리스트를 읽는 중 오류가 발생했습니다.', 'error'); } finally { setBusy(false); } } // ---- 시작 ---- if (refreshBtn) refreshBtn.addEventListener('click', function () { loadSheetTabs(true); }); // 밀크런 탭을 누를 때마다 구글 시트를 다시 읽는다 (시트가 추가/수정됐을 수 있으므로) const milkrunMenu = document.querySelector('[data-tab="milkrun-tab"]'); if (milkrunMenu) { milkrunMenu.addEventListener('click', function () { if (!busy) loadSheetTabs(true); }); } // 달력 틀을 먼저 보여주고(덮개가 '읽는 중'을 표시), 출고일은 뒤이어 채운다 renderCalendar(); loadSheetTabs(false); }); })();