// ============================================================ // 서브경로 호스팅 — fetch wrapper // 모든 /api/* 호출에 APP_BASE_PATH 를 prepend, 401 응답 시 dbx-main 로그인으로 이동. // ============================================================ (function () { const APP_BASE_PATH = (window.APP_BASE_PATH || '').replace(/\/$/, ''); const origFetch = window.fetch.bind(window); window.fetch = async function (input, init) { if (typeof input === 'string' && APP_BASE_PATH && input.startsWith('/api/')) { input = APP_BASE_PATH + input; } const response = await origFetch(input, init); if (response.status === 401) { // 세션 만료 — dbx-main 로그인으로 위임 (next 로 현재 위치 보존) const next = encodeURIComponent((APP_BASE_PATH || '') + window.location.pathname + window.location.search); window.location.href = (APP_BASE_PATH || '') + '/login?next=' + next; } return response; }; })(); function showToast(message) { const toast = document.createElement('div'); toast.textContent = message; toast.style.position = 'fixed'; toast.style.bottom = '20px'; toast.style.left = '50%'; toast.style.transform = 'translateX(-50%)'; toast.style.background = 'rgba(0, 0, 0, 0.7)'; toast.style.color = '#fff'; toast.style.padding = '8px 16px'; toast.style.borderRadius = '4px'; toast.style.zIndex = '9999'; toast.style.fontSize = '0.9rem'; toast.style.transition = 'opacity 0.3s ease'; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 1500); } async function copyTextToClipboard(text) { if (!text) return false; if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.warn('navigator.clipboard failed, trying fallback:', err); } } const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; textarea.style.top = '-9999px'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); try { return document.execCommand('copy'); } catch (err) { console.error('Clipboard fallback failed:', err); return false; } finally { textarea.remove(); } } function formatShortDate(dateStr) { if (!dateStr) return ""; const d = new Date(dateStr); if (isNaN(d)) return dateStr; const days = ["일", "월", "화", "수", "목", "금", "토"]; const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); const dow = days[d.getDay()]; return `${m}월 ${day}일(${dow})`; } document.addEventListener('DOMContentLoaded', () => { const ORDER_TYPE_LABELS = { noolak: '누락', pason: '파손', bullyang: '불량', misdelivery: '오배송', normal: '수동발주', ellen: '엘렌' }; const SPECIAL_ORDER_TYPES = new Set(['noolak', 'pason', 'bullyang', 'misdelivery']); // Tab switching logic const navLinks = document.querySelectorAll('.nav-links li'); const tabContents = document.querySelectorAll('.tab-content'); function setOrderView(view) { const orderTab = document.getElementById('order-tab'); const smsOptions = document.querySelector('.sms-options'); const targetSlot = document.getElementById(view === 'new' ? 'new-cs-options-slot' : 'classic-options-slot'); if (!orderTab || !smsOptions || !targetSlot) return; orderTab.classList.toggle('order-view-new', view === 'new'); orderTab.classList.toggle('order-view-classic', view !== 'new'); targetSlot.appendChild(smsOptions); } navLinks.forEach(link => { link.addEventListener('click', () => { navLinks.forEach(l => l.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); link.classList.add('active'); const target = link.getAttribute('data-tab'); document.getElementById(target).classList.add('active'); if (target === 'order-tab') { setOrderView(link.dataset.orderView || 'new'); const estExtra = document.getElementById('setting-ship-extra'); const lblObj = document.getElementById('lbl-ship-extra'); if (estExtra && lblObj) { let valStr = estExtra.value.replace(/,/g, ''); let val = valStr ? parseInt(valStr, 10) : 0; if (!isNaN(val)) { lblObj.textContent = '추가배송비 ' + val.toLocaleString() + '원'; } } } else if (target === 'code-tab') { if (typeof window.reRenderSetItemsForWrap === 'function') { window.reRenderSetItemsForWrap(); } } }); }); setOrderView(document.querySelector('.nav-links li.active[data-order-view]')?.dataset.orderView || 'new'); // ---- 카페24 자동발주 ---- (function initCafe24AutoOrder() { const authBtn = document.getElementById('cafe24AuthBtn'); const downloadBtn = document.getElementById('cafe24DownloadBtn'); const progressWrapper = document.getElementById('cafe24ProgressWrapper'); const progressText = document.getElementById('cafe24ProgressText'); const progressPercent = document.getElementById('cafe24ProgressPercent'); const progressBar = document.getElementById('cafe24ProgressBar'); const uploadForm = document.getElementById('cafe24UploadForm'); const uploadBtn = document.getElementById('cafe24UploadBtn'); const uploadSpinner = document.getElementById('cafe24UploadSpinner'); const fileDropArea = document.getElementById('cafe24FileDropArea'); const fileInput = document.getElementById('cafe24InvoiceFile'); const fileMessage = document.getElementById('cafe24FileMsg'); const resultBox = document.getElementById('cafe24ResultBox'); const resultTitle = document.getElementById('cafe24ResultTitle'); const resultText = document.getElementById('cafe24ResultText'); if (!authBtn || !downloadBtn) return; const sleep = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)); async function responseMessage(response, fallback) { try { const data = await response.json(); return data.message || data.detail || data.error || fallback; } catch (_) { return fallback; } } function showResult(success, title, message) { if (!resultBox || !resultTitle || !resultText) return; resultBox.classList.remove('hidden'); resultBox.style.backgroundColor = success ? 'rgba(47, 133, 90, 0.85)' : 'rgba(197, 48, 48, 0.85)'; resultBox.style.border = success ? '1px solid #68d391' : '1px solid #fc8181'; resultTitle.textContent = title; resultText.textContent = message; } function hideResult() { if (resultBox) resultBox.classList.add('hidden'); } function setAuthState(authenticated, message) { authBtn.textContent = authenticated ? '연동됨' : '재연동 필요'; authBtn.style.backgroundColor = authenticated ? '#2f855a' : '#c05621'; authBtn.title = message || ''; } async function refreshAuthState() { try { const response = await fetch('/api/cafe24/status', { cache: 'no-store' }); if (!response.ok) throw new Error('status request failed'); const data = await response.json(); setAuthState(Boolean(data.authenticated), data.message); } catch (_) { setAuthState(false, '카페24 인증 상태를 확인할 수 없습니다.'); } } function setDownloadBusy(isBusy) { downloadBtn.disabled = isBusy; downloadBtn.style.opacity = isBusy ? '0.65' : '1'; if (progressWrapper) progressWrapper.classList.toggle('hidden', !isBusy); if (!isBusy) return; if (progressText) progressText.textContent = '주문 조회 준비 중...'; if (progressPercent) progressPercent.textContent = '0%'; if (progressBar) progressBar.style.width = '0%'; } function updateProgress(current, total) { const hasTotal = Number(total) > 0; const percent = hasTotal ? Math.min(100, Math.round((Number(current) / Number(total)) * 100)) : 0; if (progressText) { progressText.textContent = hasTotal ? `주문 수집 중... ${Number(current).toLocaleString()} / ${Number(total).toLocaleString()}건` : '배송준비중 주문을 조회하고 있습니다...'; } if (progressPercent) progressPercent.textContent = hasTotal ? `${percent}%` : '조회 중'; if (progressBar) progressBar.style.width = hasTotal ? `${percent}%` : '12%'; } function downloadResponseBlob(response, blob) { const disposition = response.headers.get('Content-Disposition') || ''; const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i); const basicMatch = disposition.match(/filename="?([^";]+)"?/i); let filename = '카페24_발주.xlsx'; try { if (utf8Match) filename = decodeURIComponent(utf8Match[1]); else if (basicMatch) filename = basicMatch[1]; } catch (_) {} const url = window.URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); window.URL.revokeObjectURL(url); } authBtn.addEventListener('click', () => { window.location.href = (window.APP_BASE_PATH || '') + '/api/cafe24/login'; }); downloadBtn.addEventListener('click', async () => { hideResult(); setDownloadBusy(true); try { const startResponse = await fetch('/api/cafe24/start_download', { method: 'POST' }); if (!startResponse.ok) { throw new Error(await responseMessage(startResponse, '카페24 주문 조회를 시작하지 못했습니다.')); } const startData = await startResponse.json(); if (!startData.task_id) throw new Error('카페24 작업 번호를 받지 못했습니다.'); while (true) { await sleep(700); const progressResponse = await fetch(`/api/tasks/progress/${encodeURIComponent(startData.task_id)}`, { cache: 'no-store' }); if (!progressResponse.ok) { const error = new Error(await responseMessage(progressResponse, '카페24 주문 조회에 실패했습니다.')); error.isAuthError = progressResponse.status === 401; throw error; } const progressData = await progressResponse.json(); updateProgress(progressData.current, progressData.total); if (progressData.status !== 'completed') continue; if (progressText) progressText.textContent = '엑셀 파일 생성 완료'; if (progressPercent) progressPercent.textContent = '100%'; if (progressBar) progressBar.style.width = '100%'; const fileResponse = await fetch(`/api/tasks/download_file/${encodeURIComponent(startData.task_id)}`); if (!fileResponse.ok) { throw new Error(await responseMessage(fileResponse, '완성된 엑셀 파일을 내려받지 못했습니다.')); } downloadResponseBlob(fileResponse, await fileResponse.blob()); showResult(true, '다운로드 완료', '카페24 발주 엑셀 파일을 저장했습니다.'); break; } } catch (error) { if (error.isAuthError || /인증|연동|authenticate|expired/i.test(error.message || '')) { setAuthState(false, error.message); } showResult(false, '카페24 주문 다운로드 실패', error.message || '처리 중 오류가 발생했습니다.'); } finally { setDownloadBusy(false); } }); function updateSelectedFile() { const file = fileInput && fileInput.files ? fileInput.files[0] : null; if (fileMessage) { fileMessage.textContent = file ? `선택 파일: ${file.name}` : '엑셀 파일을 이곳에 드래그하거나 클릭하여 업로드하세요. (.xls, .xlsx)'; } if (uploadBtn) uploadBtn.disabled = !file; } if (fileDropArea && fileInput) { fileDropArea.addEventListener('click', event => { if (event.target !== fileInput) fileInput.click(); }); fileInput.addEventListener('change', updateSelectedFile); ['dragenter', 'dragover'].forEach(eventName => { fileDropArea.addEventListener(eventName, event => { event.preventDefault(); fileDropArea.style.borderColor = '#63b3ed'; fileDropArea.style.background = 'rgba(66, 153, 225, 0.15)'; }); }); ['dragleave', 'drop'].forEach(eventName => { fileDropArea.addEventListener(eventName, event => { event.preventDefault(); fileDropArea.style.borderColor = '#718096'; fileDropArea.style.background = 'rgba(255, 255, 255, 0.05)'; }); }); fileDropArea.addEventListener('drop', event => { if (!event.dataTransfer || !event.dataTransfer.files.length) return; fileInput.files = event.dataTransfer.files; updateSelectedFile(); }); } if (uploadForm && fileInput && uploadBtn) { uploadForm.addEventListener('submit', async event => { event.preventDefault(); if (!fileInput.files || !fileInput.files[0]) return; hideResult(); uploadBtn.disabled = true; uploadBtn.textContent = '송장 등록 중...'; if (uploadSpinner) uploadSpinner.classList.remove('hidden'); try { const formData = new FormData(uploadForm); const response = await fetch('/api/cafe24/upload_invoices', { method: 'POST', body: formData }); if (!response.ok) { const error = new Error(await responseMessage(response, '송장 등록에 실패했습니다.')); error.isAuthError = response.status === 401; throw error; } const data = await response.json(); const successCount = Number(data.success_count || 0); const failCount = Number(data.fail_count || 0); showResult( failCount === 0, failCount === 0 ? '송장 등록 완료' : '송장 등록 결과', `성공 ${successCount.toLocaleString()}건 / 실패 ${failCount.toLocaleString()}건` ); uploadForm.reset(); updateSelectedFile(); } catch (error) { if (error.isAuthError || /인증|연동|authenticate|expired/i.test(error.message || '')) { setAuthState(false, error.message); } showResult(false, '송장 등록 실패', error.message || '처리 중 오류가 발생했습니다.'); } finally { uploadBtn.textContent = '🚀 송장 일괄 발송처리'; if (uploadSpinner) uploadSpinner.classList.add('hidden'); updateSelectedFile(); } }); } updateSelectedFile(); refreshAuthState(); })(); // ---- 쿠팡 밀크런 낱개 분해 ---- (function initCoupangMilkrun() { const input = document.getElementById('milkrun-input'); const analyzeBtn = document.getElementById('btn-milkrun-analyze'); const clearBtn = document.getElementById('btn-milkrun-clear'); const downloadBtn = document.getElementById('btn-milkrun-download'); const linesTbody = document.getElementById('milkrun-lines-tbody'); const linesInfo = document.getElementById('milkrun-lines-info'); const totalsTbody = document.getElementById('milkrun-totals-tbody'); const unresolvedBox = document.getElementById('milkrun-unresolved-box'); const summaryInfo = document.getElementById('milkrun-summary-info'); const loadingOverlay = document.getElementById('milkrun-loading-overlay'); const loadingText = document.getElementById('milkrun-loading-text'); if (!input || !analyzeBtn || !totalsTbody) return; const LOADING_MESSAGES = [ { delay: 0, text: '계산 중...' }, { delay: 4000, text: '조금만 더 기다려줘' }, { delay: 8000, text: '이제 거의 다 되었어' }, { delay: 12000, text: '다 끝나가는 중' } ]; let loadingTimers = []; function setMilkrunLoading(isLoading) { if (loadingOverlay) loadingOverlay.style.display = isLoading ? 'flex' : 'none'; analyzeBtn.disabled = isLoading; loadingTimers.forEach(t => clearTimeout(t)); loadingTimers = []; if (isLoading && loadingText) { LOADING_MESSAGES.forEach(m => { if (m.delay === 0) { loadingText.textContent = m.text; } else { loadingTimers.push(setTimeout(() => { loadingText.textContent = m.text; }, m.delay)); } }); } } function escapeHtml(str) { return String(str == null ? '' : str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function resetMilkrunResult() { if (linesTbody) linesTbody.innerHTML = ''; if (linesInfo) linesInfo.textContent = ''; totalsTbody.innerHTML = ''; unresolvedBox.style.display = 'none'; unresolvedBox.innerHTML = ''; summaryInfo.textContent = ''; } function renderLineRow(line) { const hasMultiplier = line.multiplier && line.multiplier !== 1; const codeCell = hasMultiplier ? '
' + escapeHtml(line.code_raw) + '
→ ' + escapeHtml(line.base_code) + ' × ' + line.multiplier + '
' : escapeHtml(line.code_raw); const qtyCell = hasMultiplier ? line.qty.toLocaleString() + ' → ' + line.effective_qty.toLocaleString() + '' : line.qty.toLocaleString(); let breakdownCell; let rowStyle = ''; if (line.resolved && line.type === 'set') { const badge = '세트'; const breakdown = line.breakdown || []; const compositionParts = breakdown.map(function (b) { return escapeHtml(b.single_code) + ' × ' + (b.per_unit_qty != null ? b.per_unit_qty.toLocaleString() : '?'); }).join(', '); const resultParts = breakdown.map(function (b) { return escapeHtml(b.single_code) + ' × ' + b.qty.toLocaleString(); }).join(', '); breakdownCell = '
' + badge + '구성(1세트): ' + compositionParts + '
' + '
→ ' + line.effective_qty.toLocaleString() + '개 → ' + resultParts + '
'; } else if (line.resolved) { const badge = '단품'; const parts = (line.breakdown || []).map(function (b) { return escapeHtml(b.single_code) + ' × ' + b.qty.toLocaleString(); }).join(', '); breakdownCell = '' + badge + '' + parts + ''; } else { rowStyle = 'background:#000000; color:#f6e05e;'; breakdownCell = '⚠ 미등록 코드 — 낱개 합계 계산에서 제외됨'; } return '' + '' + codeCell + '' + '' + qtyCell + '' + '' + breakdownCell + '' + ''; } async function runMilkrunAnalysis() { const text = input.value; if (!text || !text.trim()) { resetMilkrunResult(); return; } setMilkrunLoading(true); try { const res = await fetch('/api/coupang-milkrun/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }); const data = await res.json(); if (data.status !== 'success') { alert('분석 중 오류가 발생했습니다.'); return; } if (linesTbody) { if (!data.lines.length) { linesTbody.innerHTML = '인식된 줄이 없습니다.'; } else { linesTbody.innerHTML = data.lines.map(renderLineRow).join(''); } } if (linesInfo) { const failCount = data.lines.filter(l => !l.resolved).length; linesInfo.textContent = failCount > 0 ? `총 ${data.lines.length}줄 · 미등록 ${failCount}줄` : `총 ${data.lines.length}줄 · 전부 인식됨`; } if (!data.totals.length) { totalsTbody.innerHTML = '인식된 항목이 없습니다. 붙여넣은 형식을 확인해주세요.'; } else { totalsTbody.innerHTML = data.totals.map(t => ` ${t.sabangnet_code || ''} ${t.single_code} ${t.name || ''} ${t.total_qty.toLocaleString()} `).join(''); } if (data.unresolved && data.unresolved.length > 0) { unresolvedBox.style.display = 'block'; unresolvedBox.innerHTML = '⚠ 코드표에 등록되지 않은 코드: ' + data.unresolved.map(u => `${u.code} (${u.qty.toLocaleString()}개)`).join(', '); } else { unresolvedBox.style.display = 'none'; unresolvedBox.innerHTML = ''; } const totalUnits = data.totals.reduce((sum, t) => sum + t.total_qty, 0); summaryInfo.innerHTML = `${data.lines.length}줄 입력
낱개 합계 ${totalUnits.toLocaleString()}개`; } catch (err) { console.error(err); alert('밀크런 분석 요청 중 오류가 발생했습니다.'); } finally { setMilkrunLoading(false); } } analyzeBtn.addEventListener('click', runMilkrunAnalysis); if (clearBtn) { clearBtn.addEventListener('click', () => { input.value = ''; resetMilkrunResult(); }); } input.addEventListener('paste', () => { setTimeout(runMilkrunAnalysis, 50); }); if (downloadBtn) { downloadBtn.addEventListener('click', async () => { const text = input.value; if (!text || !text.trim()) { alert('먼저 밀크런 내용을 붙여넣고 분석해주세요.'); return; } try { const res = await fetch('/api/coupang-milkrun/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }); if (!res.ok) { alert('엑셀 다운로드 중 오류가 발생했습니다.'); return; } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '쿠팡_밀크런_낱개코드.xlsx'; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); } catch (err) { console.error(err); alert('엑셀 다운로드 요청 중 오류가 발생했습니다.'); } }); } })(); // Auto format phone number document.getElementById('cust-phone').addEventListener('input', function (e) { let val = this.value.replace(/[^0-9]/g, ''); if (val.length > 0 && val[0] !== '0') { val = '010' + val; } let res = ''; if (val.startsWith('02')) { if (val.length < 3) res = val; else if (val.length < 6) res = val.replace(/(\d{2})(\d+)/, '$1-$2'); else if (val.length < 10) res = val.replace(/(\d{2})(\d{3})(\d+)/, '$1-$2-$3'); else res = val.replace(/(\d{2})(\d{4})(\d{4})/, '$1-$2-$3'); } else { if (val.length < 4) res = val; else if (val.length < 7) res = val.replace(/(\d{3})(\d+)/, '$1-$2'); else if (val.length < 11) res = val.replace(/(\d{3})(\d{3})(\d+)/, '$1-$2-$3'); else res = val.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); } this.value = res; }); function parseReshipmentInfo(text) { const lines = text .replace(/\r\n?/g, '\n') .split('\n') .map(line => line.trim()) .filter(Boolean); const headerIndex = lines.indexOf('[재발송 정보]'); if (headerIndex === -1) return null; const fields = {}; lines.slice(headerIndex + 1).forEach(line => { const match = line.match(/^(이름|주소|휴대폰번호)\s*[::]\s*(.+)$/); if (match) fields[match[1]] = match[2].trim(); }); if (!fields['이름'] || !fields['주소'] || !fields['휴대폰번호']) { return null; } return { name: fields['이름'], phone: fields['휴대폰번호'], address: fields['주소'] }; } async function handleClipboardPaste(targetField) { try { if (!navigator.clipboard || !navigator.clipboard.readText) { alert("현재 환경에서는 클립보드 연동 보안 정책으로 인해 자동 입력이 지원되지 않습니다.\\nHTTPS 상태일 때 동작합니다.\\n직접 붙여넣기(Ctrl+V)를 사용해주세요."); return; } const text = await navigator.clipboard.readText(); const trimmed = text.trim(); const digits = text.replace(/[^0-9]/g, ''); const hasPhone = digits.length >= 9 && digits.length <= 12; if (targetField === 'name') { const reshipmentInfo = parseReshipmentInfo(text); if (reshipmentInfo) { const nameInput = document.getElementById('cust-name'); const phoneInput = document.getElementById('cust-phone'); const addressInput = document.getElementById('cust-address'); nameInput.value = reshipmentInfo.name; nameInput.dispatchEvent(new Event('input', { bubbles: true })); phoneInput.value = reshipmentInfo.phone; phoneInput.dispatchEvent(new Event('input', { bubbles: true })); addressInput.value = reshipmentInfo.address; addressInput.dispatchEvent(new Event('input', { bubbles: true })); showToast('재발송 정보를 자동 입력했습니다.'); return; } } // 이름은 한글 2~5글자를 인식하도록 완화 const nameMatch = trimmed.match(/^[가-힣]{2,5}/); if (targetField === 'phone') { const phoneInput = document.getElementById('cust-phone'); if (hasPhone) { phoneInput.value = digits; phoneInput.dispatchEvent(new Event('input')); } } else if (targetField === 'name') { const nameInput = document.getElementById('cust-name'); if (nameMatch) { nameInput.value = nameMatch[0]; } } } catch (err) { console.error("Clipboard read error:", err); alert("클립보드 접근 권한이 없거나 읽어오지 못했습니다. 브라우저 주소창의 권한을 확인해주세요."); } } document.getElementById('cust-phone').addEventListener('click', function (e) { if (!this.value) handleClipboardPaste('phone'); }); document.getElementById('cust-name').addEventListener('click', function (e) { if (!this.value) handleClipboardPaste('name'); }); const pointInput = document.getElementById('sms-point-deduction'); if (pointInput) { pointInput.addEventListener('input', function(e) { let val = this.value.replace(/[^0-9]/g, ''); this.value = val ? parseInt(val, 10).toLocaleString() : ''; }); } // Global Data let configData = {}; // Fetch configuration fetch('/api/config') .then(res => res.json()) .then(data => { configData = data; if (data.APP_SETTINGS && data.APP_SETTINGS.font) { document.body.style.fontFamily = `"${data.APP_SETTINGS.font}", 'Inter', 'Malgun Gothic', sans-serif`; } else { document.body.style.fontFamily = ''; } buildOrderTab(data); buildGiftTab(data); if (typeof buildProductTab === 'function') { buildProductTab(data); } loadSmsHistory(); if (typeof loadManualHistory === 'function') { loadManualHistory(); } }) .catch(err => console.error("Error loading config:", err)); // Fetch local fonts using hybrid approach (Browser API first, Backend API fallback) async function initLocalFonts() { const fontSelect = document.getElementById('app-font-select'); if (!fontSelect) return; let fontNames = []; try { // Attempt to fetch via native browser API (works on HTTPS/localhost for all PC fonts) if ('queryLocalFonts' in window && window.isSecureContext) { const localFonts = await window.queryLocalFonts(); fontNames = [...new Set(localFonts.map(f => f.family))].sort(); } else { throw new Error("Local font API not available or context not secure"); } } catch (e) { console.warn("내 컴퓨터 폰트 직접 읽기 실패, 서버 폰트로 대체:", e); try { // Fallback to server registry (works locally without HTTPS but fails to get windows fonts on Linux cloud) const res = await fetch('/api/fonts'); fontNames = await res.json(); } catch (err) { console.error("서버 폰트 불러오기 실패:", err); } } fontSelect.innerHTML = ''; if (fontNames && fontNames.length > 0) { // 명시적으로 호스팅된 폰트 추가 const customFonts = ['NanumSquareNeo', 'Pretendard-Regular']; customFonts.forEach(f => { if (!fontNames.includes(f)) fontNames.unshift(f); }); // 중복 제거 및 정렬 fontNames = [...new Set(fontNames)].sort((a, b) => { const aIsCustom = customFonts.includes(a); const bIsCustom = customFonts.includes(b); if (aIsCustom && bIsCustom) return customFonts.indexOf(a) - customFonts.indexOf(b); if (aIsCustom) return -1; if (bIsCustom) return 1; return a.localeCompare(b); }); fontNames.forEach(f => { const opt = document.createElement('option'); opt.value = f; // Truncate display text to prevent native dropdowns from becoming excessively wide let displayText = f; if (displayText.length > 35) { displayText = displayText.substring(0, 35) + '...'; } opt.textContent = displayText; // Set the font family so users can preview the font in the dropdown opt.style.fontFamily = `"${f}", sans-serif`; fontSelect.appendChild(opt); }); if (configData.APP_SETTINGS && configData.APP_SETTINGS.font) { fontSelect.value = configData.APP_SETTINGS.font; } } else { fontSelect.innerHTML = ''; } } const btnFontDefault = document.getElementById('btn-font-default'); if (btnFontDefault) { btnFontDefault.addEventListener('click', () => { const fontSelect = document.getElementById('app-font-select'); if (fontSelect) fontSelect.value = ''; document.getElementById('btn-gift-save').style.backgroundColor = '#e53e3e'; }); } // Initialize fonts initLocalFonts(); // ============================================ // BUILD ORDER TAB // ============================================ const orderItemsContainer = document.getElementById('order-items-container'); const orderGroupButtons = document.getElementById('order-group-buttons'); const orderSelectedItems = document.getElementById('order-selected-items'); const orderSelectedCount = document.getElementById('order-selected-count'); const orderSummaryOrderAmount = document.getElementById('order-summary-order-amount'); const orderSummaryDepositAmount = document.getElementById('order-summary-deposit-amount'); const orderSummaryAdjustments = document.getElementById('order-summary-adjustments'); function showOrderItemGroup(card, button) { if (!orderItemsContainer || !card) return; orderItemsContainer.querySelectorAll('.order-item-group.is-active').forEach(item => { item.classList.remove('is-active'); }); orderGroupButtons.querySelectorAll('.order-group-button.is-active').forEach(item => { item.classList.remove('is-active'); }); const emptyGuide = orderItemsContainer.querySelector('.order-items-inline-empty'); if (emptyGuide) emptyGuide.style.display = 'none'; card.classList.add('is-active'); if (button) button.classList.add('is-active'); } function getOrderRowQuantity(row) { const qtyControl = row ? row.querySelector('.qty-order') : null; return Math.max(1, parseInt(qtyControl?.value, 10) || 1); } function updateOrderGroupBadges() { if (!orderGroupButtons || !orderItemsContainer) return; orderGroupButtons.querySelectorAll('.order-group-button').forEach(button => { const card = Array.from(orderItemsContainer.querySelectorAll('.order-item-group')) .find(item => item.dataset.groupKey === button.dataset.groupKey); const selectedCount = card ? card.querySelectorAll('.chk-order:checked').length : 0; button.classList.toggle('has-selection', selectedCount > 0); const badge = button.querySelector('.order-group-count'); if (badge) badge.textContent = selectedCount; }); } function getSelectedOrderItemTotal(checkbox, quantity) { const cost = parseInt(checkbox.dataset.cost, 10) || 0; const dcost = checkbox.dataset.dcost === 'null' ? null : (parseInt(checkbox.dataset.dcost, 10) || null); const orderType = document.querySelector('input[name="order-type"]:checked')?.value; const noLidChecked = document.getElementById('chk-no-lid')?.checked; if (SPECIAL_ORDER_TYPES.has(orderType) || noLidChecked) return 0; if (dcost !== null && quantity >= 3) { return Math.floor(quantity / 3) * dcost * 2 + (quantity % 3) * cost; } return cost * quantity; } function updateSelectedOrderItemsSummary() { if (!orderItemsContainer || !orderSelectedItems || !orderSelectedCount) return; const selected = new Map(); orderItemsContainer.querySelectorAll('.chk-order:checked').forEach(checkbox => { const row = checkbox.closest('.item-row'); const label = checkbox.dataset.label || checkbox.dataset.name || '상품'; const quantity = getOrderRowQuantity(row); selected.set(checkbox, { label, quantity, amount: getSelectedOrderItemTotal(checkbox, quantity), checkbox }); }); orderSelectedItems.innerHTML = ''; if (selected.size === 0) { const empty = document.createElement('p'); empty.className = 'order-selected-empty'; empty.textContent = '선택된 상품이 없습니다.'; orderSelectedItems.appendChild(empty); } else { selected.forEach(item => { const row = document.createElement('div'); row.className = 'order-selected-row'; const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'order-selected-remove'; remove.setAttribute('aria-label', `${item.label} 삭제`); remove.title = '선택 상품 삭제'; remove.textContent = '×'; const name = document.createElement('span'); name.className = 'order-selected-name'; name.title = item.label; name.textContent = item.label; const quantity = document.createElement('select'); quantity.className = 'order-selected-qty'; const qtyValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 30, 40, 50]; if (!qtyValues.includes(item.quantity)) qtyValues.push(item.quantity); qtyValues.sort((a, b) => a - b).forEach(value => { const option = document.createElement('option'); option.value = value; option.textContent = `${value}개`; option.selected = value === item.quantity; quantity.appendChild(option); }); const price = document.createElement('span'); price.className = 'order-selected-price'; price.textContent = `${item.amount.toLocaleString()}원`; remove.addEventListener('click', () => { item.checkbox.checked = false; const sourceRow = item.checkbox.closest('.item-row'); if (sourceRow) sourceRow.style.backgroundColor = ''; updateOrderNoLidState(); updateSelectedOrderItemsSummary(); }); quantity.addEventListener('change', () => { const sourceRow = item.checkbox.closest('.item-row'); const sourceQty = sourceRow?.querySelector('.qty-order'); if (!sourceQty) return; const nextValue = quantity.value; if (sourceQty.tagName === 'SELECT' && !Array.from(sourceQty.options).some(option => option.value === nextValue)) { const option = document.createElement('option'); option.value = nextValue; option.textContent = nextValue; sourceQty.appendChild(option); } sourceQty.value = nextValue; updateSelectedOrderItemsSummary(); }); row.append(remove, name, quantity, price); orderSelectedItems.appendChild(row); }); } orderSelectedCount.textContent = `${selected.size}종`; const payload = getOrderPayload(); const displayOrderAmount = selected.size > 0 ? payload.raw_total : 0; const displayDepositAmount = selected.size > 0 ? payload.total_amount : 0; if (orderSummaryOrderAmount) orderSummaryOrderAmount.textContent = `${displayOrderAmount.toLocaleString()}원`; if (orderSummaryDepositAmount) orderSummaryDepositAmount.textContent = `${displayDepositAmount.toLocaleString()}원`; if (orderSummaryAdjustments) { orderSummaryAdjustments.innerHTML = ''; const detailRows = []; if (selected.size > 0) { if (payload.discount_5_amount > 0) detailRows.push(['5% 할인 금액', -payload.discount_5_amount]); if (payload.coupon_amount > 0) detailRows.push(['쿠폰 할인 금액', -payload.coupon_amount]); if (payload.point_deduction > 0) detailRows.push(['적립금 차감', -payload.point_deduction]); if (payload.shipping_cost > 0) detailRows.push(['택배비', payload.shipping_cost]); if (document.getElementById('sms-ship-free')?.checked || payload.free_reason) { detailRows.push(['배송비', payload.shipping_cost > 0 ? payload.shipping_cost : '무료배송']); } if (payload.extra_shipping_cost > 0) detailRows.push(['추가배송비', payload.extra_shipping_cost]); if (document.getElementById('sms-june-promo')?.checked) detailRows.push(['금액별 추가상품', '적용']); } detailRows.forEach(([label, value]) => { const detail = document.createElement('div'); const valueText = typeof value === 'number' ? `${value < 0 ? '-' : ''}${Math.abs(value).toLocaleString()}원` : value; detail.innerHTML = `${label}${valueText}`; orderSummaryAdjustments.appendChild(detail); }); } updateOrderGroupBadges(); refreshLiveSmsPreview(); } orderItemsContainer.addEventListener('change', function(e) { if (e.target.classList.contains('qty-order') && e.target.tagName === 'SELECT' && e.target.value === 'manual') { const input = document.createElement('input'); input.type = 'number'; input.min = '1'; input.value = '1'; input.className = 'qty-order'; e.target.replaceWith(input); input.focus(); input.select(); } updateSelectedOrderItemsSummary(); }); orderItemsContainer.addEventListener('input', function(e) { if (e.target.classList.contains('qty-order')) updateSelectedOrderItemsSummary(); }); document.getElementById('order-items-container').addEventListener('blur', function(e) { if (e.target.classList.contains('qty-order') && e.target.tagName === 'INPUT') { if (!e.target.value) { const select = document.createElement('select'); select.className = 'qty-order'; const qtyOpts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 30, 40, 50]; const optHtml = qtyOpts.map(q => ``).join(''); const manualOpt = ``; select.innerHTML = manualOpt + optHtml; select.value = '1'; e.target.replaceWith(select); } else if (parseInt(e.target.value) < 1) { e.target.value = '1'; } updateSelectedOrderItemsSummary(); } }, true); function buildOrderTab(data) { const container = document.getElementById('order-items-container'); container.innerHTML = ''; if (orderGroupButtons) orderGroupButtons.innerHTML = ''; const emptyGuide = document.createElement('div'); emptyGuide.className = 'order-items-inline-empty'; emptyGuide.innerHTML = '📦위의 상품 분류를 선택하세요.'; container.appendChild(emptyGuide); let sections = []; if (data.GROUP_NAMES) { const order_gn = data.GROUP_NAMES.__order || Object.keys(data.GROUP_NAMES).filter(k => k !== '__order'); for (const key of order_gn) { if (key === '__order') continue; const title = data.GROUP_NAMES[key]; sections.push({ key: key, title: title }); } } else { // Fallback for older config files without GROUP_NAMES sections = [ { key: 'ORDER_SINGLE_1', title: '단품 아이템 (1)' }, { key: 'ORDER_SINGLE_2', title: '단품 아이템 (2)' }, { key: 'ORDER_SET_1', title: '세트 아이템 (1)' }, { key: 'ORDER_SET_2', title: '세트 아이템 (2)' }, { key: 'ORDER_EVENT_1', title: '이벤트 (1)' }, { key: 'ORDER_EVENT_2', title: '이벤트 (2)' }, { key: 'ORDER_GIFT', title: '사은품' } ]; } sections.forEach(sec => { if (!data[sec.key]) return; const card = document.createElement('div'); card.className = 'glass-card item-group-card order-item-group'; card.dataset.groupKey = sec.key; const popupHeader = document.createElement('div'); popupHeader.className = 'order-popup-header'; const title = document.createElement('h3'); title.textContent = sec.title; popupHeader.appendChild(title); card.appendChild(popupHeader); if (orderGroupButtons) { const groupButton = document.createElement('button'); groupButton.type = 'button'; groupButton.className = 'order-group-button'; groupButton.dataset.groupKey = sec.key; const buttonTitle = document.createElement('span'); buttonTitle.textContent = sec.title; const buttonCount = document.createElement('span'); buttonCount.className = 'order-group-count'; buttonCount.textContent = '0'; groupButton.append(buttonTitle, buttonCount); groupButton.addEventListener('click', () => showOrderItemGroup(card, groupButton)); orderGroupButtons.appendChild(groupButton); } const grid = document.createElement('div'); grid.className = 'item-list'; const order_1 = data[sec.key]?.__order || Object.keys(data[sec.key]).filter(k => k !== '__order'); order_1.forEach(rawLabel => { if (rawLabel === '__order') return; const value = data[sec.key][rawLabel]; let label = rawLabel.trim(); if (label.startsWith('---')) { const hr = document.createElement('hr'); hr.style.gridColumn = "1 / -1"; hr.style.border = "none"; hr.style.borderTop = "1.5px dashed black"; hr.style.margin = "8px 0"; grid.appendChild(hr); return; } let colorStyle = ''; const match = label.match(/_([ORBGP])$/i); if (match) { label = label.slice(0, -2).trim(); const colorMap = { 'O': '#ed8936', 'R': '#e53e3e', 'B': '#3182ce', 'G': '#1D6F42', 'P': '#d53f8c' }; colorStyle = `color: ${colorMap[match[1].toUpperCase()]}; font-weight: 700;`; } const safeValue = value || ""; const parts = safeValue.split('|').map(s => s.trim()); let code = parts[0] || "NONE"; let multiplier = 1; const matchCode = code.match(/_(\d+)$/); if (matchCode) { multiplier = parseInt(matchCode[1], 10) || 1; code = code.replace(/_\d+$/, ''); } const isFree = label.startsWith('*'); if (isFree) { label = label.replace(/^\*/, '').trim(); } let name = label; if (parts.length > 1 && parts[1] !== "") { name = parts[1]; } let cost = 0; let dcost = "null"; if (parts.length > 2 && parts[2] !== "") { cost = parseInt(parts[2].replace(/,/g, '')) || 0; dcost = parts.length > 3 && parts[3] !== "" ? parseInt(parts[3].replace(/,/g, '')) : "null"; } const row = document.createElement('div'); row.className = 'item-row'; let itemType = sec.key; const qtyOpts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 30, 40, 50]; const optHtml = qtyOpts.map(q => ``).join(''); const manualOpt = ``; row.innerHTML = ` `; grid.appendChild(row); row.querySelector('input[type="checkbox"]').addEventListener('change', updateOrderNoLidState); }); const subgroupColumns = []; let subgroupColumn = document.createElement('div'); subgroupColumn.className = 'order-item-subgroup'; Array.from(grid.children).forEach(child => { if (child.tagName === 'HR') { if (subgroupColumn.children.length > 0) subgroupColumns.push(subgroupColumn); subgroupColumn = document.createElement('div'); subgroupColumn.className = 'order-item-subgroup'; return; } subgroupColumn.appendChild(child); }); if (subgroupColumn.children.length > 0) subgroupColumns.push(subgroupColumn); subgroupColumns.forEach((column, index) => { const labels = Array.from(column.querySelectorAll('.chk-order')) .map(checkbox => (checkbox.dataset.label || '').trim()) .filter(Boolean); const families = labels.map(label => label .replace(/^\*/, '') .replace(/_\d+개$/, '') .replace(/\([^)]*\)/g, '') .replace(/\d[\d,.]*\s*(?:ml|mL|ML|ℓ|L|l|개|매|종)?/g, '') .replace(/[_+,/-]+/g, ' ') .replace(/\s+/g, ' ') .trim() ).filter(Boolean); const wordLists = families.map(family => family.split(' ')); const commonWords = []; if (wordLists.length > 0) { const shortestLength = Math.min(...wordLists.map(words => words.length)); for (let wordIndex = 0; wordIndex < shortestLength; wordIndex += 1) { const candidate = wordLists[0][wordIndex]; if (!wordLists.every(words => words[wordIndex] === candidate)) break; commonWords.push(candidate); } } const firstFamily = families[0] || `그룹 ${index + 1}`; const automaticTitle = commonWords.length > 0 ? commonWords.join(' ') : `${firstFamily}${labels.length > 1 ? ` 외 ${labels.length - 1}종` : ''}`; const subgroupSettingKey = `${sec.key}__${index}`; const savedTitle = (data.SUBGROUP_NAMES?.[subgroupSettingKey] || '').trim(); const subgroupTitle = document.createElement('div'); subgroupTitle.className = 'order-item-subgroup-title'; const renderSubgroupTitle = titleText => { subgroupTitle.replaceChildren(); subgroupTitle.title = titleText; const titleLabel = document.createElement('span'); titleLabel.className = 'order-item-subgroup-title-text'; titleLabel.textContent = titleText; const editButton = document.createElement('button'); editButton.type = 'button'; editButton.className = 'order-item-subgroup-edit'; editButton.textContent = '✏️'; editButton.title = '그룹 이름 수정'; editButton.setAttribute('aria-label', `${titleText} 그룹 이름 수정`); editButton.addEventListener('click', event => { event.stopPropagation(); const previousTitle = titleLabel.textContent; const input = document.createElement('input'); input.type = 'text'; input.className = 'order-item-subgroup-title-input'; input.value = previousTitle; input.maxLength = 40; input.setAttribute('aria-label', '그룹 이름 입력'); subgroupTitle.replaceChildren(input); input.focus(); input.select(); let finished = false; const finishEditing = async shouldSave => { if (finished) return; finished = true; if (!shouldSave) { renderSubgroupTitle(previousTitle); return; } const enteredName = input.value.trim(); const nextTitle = enteredName || automaticTitle; renderSubgroupTitle(nextTitle); if (nextTitle === previousTitle) return; try { const response = await fetch('/api/products/subgroup-name', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group_key: sec.key, subgroup_index: index, name: enteredName }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || '그룹 이름 저장에 실패했습니다.'); } if (!configData.SUBGROUP_NAMES) configData.SUBGROUP_NAMES = {}; if (enteredName) configData.SUBGROUP_NAMES[subgroupSettingKey] = enteredName; else delete configData.SUBGROUP_NAMES[subgroupSettingKey]; showToast('그룹 이름이 저장되었습니다.'); } catch (error) { renderSubgroupTitle(previousTitle); alert(error.message || '그룹 이름 저장에 실패했습니다.'); } }; input.addEventListener('keydown', keyEvent => { if (keyEvent.key === 'Enter') { keyEvent.preventDefault(); input.blur(); } else if (keyEvent.key === 'Escape') { keyEvent.preventDefault(); finishEditing(false); } }); input.addEventListener('blur', () => finishEditing(true)); }); subgroupTitle.append(titleLabel, editButton); }; renderSubgroupTitle(savedTitle || automaticTitle); column.prepend(subgroupTitle); }); grid.replaceChildren(...subgroupColumns); grid.style.setProperty('--subgroup-count', Math.max(1, subgroupColumns.length)); card.appendChild(grid); container.appendChild(card); }); const classicCards = Array.from(container.querySelectorAll(':scope > .order-item-group')); const classicColumnPlan = [[0], [1], [2], [3, 4], [5, 6], [7, 8]]; classicColumnPlan.forEach(cardIndexes => { const column = document.createElement('div'); column.className = 'classic-product-column'; cardIndexes.forEach(index => { if (classicCards[index]) column.appendChild(classicCards[index]); }); if (column.children.length > 0) container.appendChild(column); }); // Event listeners for config checks document.getElementById('sms-discount').addEventListener('change', e => { if (e.target.checked) document.getElementById('sms-june-promo').checked = false; if (e.target.checked) { document.getElementById('sms-coupon').disabled = true; } else { document.getElementById('sms-coupon').disabled = false; } }); document.getElementById('sms-coupon').addEventListener('change', e => { if (e.target.checked) { document.getElementById('sms-discount').disabled = true; } else { document.getElementById('sms-discount').disabled = false; } }); document.getElementById('sms-june-promo').addEventListener('change', e => { if (e.target.checked) document.getElementById('sms-discount').checked = false; }); document.getElementById('sms-ship-free').addEventListener('change', e => { // No need to handle old static shipping checkboxes }); updateSelectedOrderItemsSummary(); } function updateOrderNoLidState() { const checked = Array.from(document.querySelectorAll('.chk-order:checked')); const noLidChk = document.getElementById('chk-no-lid'); if (checked.length === 0) { noLidChk.disabled = true; noLidChk.checked = false; return; } const canEnable = checked.every(chk => chk.dataset.type === 'ORDER_SINGLE'); noLidChk.disabled = !canEnable; if (!canEnable) noLidChk.checked = false; } // ============================================ // ORDER ACTIONS // ============================================ function getCouponDiscount(amount) { if (amount >= 220000) return 25000; if (amount >= 150000) return 15000; if (amount >= 100000) return 10000; if (amount >= 70000) return 5000; if (amount >= 50000) return 3000; if (amount >= 30000) return 2000; return 0; } function getOrderPayload(excludedIndices = []) { const items = []; let totalCost = 0; let hasMarkedFreeShipping = false; document.querySelectorAll('.chk-order:checked').forEach((chk, idx) => { const row = chk.closest('.item-row'); const qty = parseInt(row.querySelector('.qty-order').value) || 1; const multiplier = parseInt(chk.dataset.multiplier) || 1; const orderLabel = chk.dataset.label; items.push({ code: chk.dataset.code, name: chk.dataset.name, label: orderLabel, item_type: chk.dataset.type, quantity: qty * multiplier }); const cost = parseInt(chk.dataset.cost) || 0; const dcost = chk.dataset.dcost === "null" ? null : (parseInt(chk.dataset.dcost) || null); let itemTotal = 0; if (dcost !== null && qty >= 3) { itemTotal = Math.floor(qty / 3) * dcost * 2 + (qty % 3) * cost; } else { itemTotal = cost * qty; } const orderType = document.querySelector('input[name="order-type"]:checked').value; const noLidChecked = document.getElementById('chk-no-lid').checked; if (SPECIAL_ORDER_TYPES.has(orderType) || noLidChecked) { itemTotal = 0; } if (!excludedIndices.includes(idx)) { totalCost += itemTotal; if (chk.dataset.isFree === "true") hasMarkedFreeShipping = true; } }); // 🟢 금액별 추가 상품 체킹 시 사은품 자동 탑재 if (document.getElementById('sms-june-promo').checked && configData['SMS_GIFT_RULES']) { let seen = new Set(); for (let i = 1; i <= 4; i++) { const sMin = configData['SMS_GIFT_RULES'][`condition${i}_min`]; const sMax = configData['SMS_GIFT_RULES'][`condition${i}_max`]; const sGiftStr = configData['SMS_GIFT_RULES'][`condition${i}_gift`]; if (!sGiftStr) continue; const minV = sMin ? parseInt(sMin) : 0; const maxV = sMax ? parseInt(sMax) : null; if (totalCost >= minV && (maxV === null || totalCost < maxV)) { if (!seen.has(sGiftStr)) { seen.add(sGiftStr); let foundCode = 'ZZ-0000'; let foundName = sGiftStr; let cleanLabel = sGiftStr.trim(); if (cleanLabel.match(/_([ORBGP])$/i)) cleanLabel = cleanLabel.slice(0, -2).trim(); let foundConfigValue = null; for (const secKey of Object.keys(configData)) { if (secKey === 'SMS_PRICING' || secKey === 'SMS_GIFT_RULES' || secKey === 'GROUP_NAMES' || secKey === 'SUBGROUP_NAMES') continue; const sectionData = configData[secKey] || {}; if (sectionData[sGiftStr]) { foundConfigValue = sectionData[sGiftStr]; break; } for (const [itemKey, itemValue] of Object.entries(sectionData)) { if (itemKey === '__order' || typeof itemValue !== 'string') continue; const parts = itemValue.split('|').map(s => s.trim()); const itemName = parts.length > 1 ? parts[1] : itemKey; if (itemName === sGiftStr || itemName === cleanLabel) { foundConfigValue = itemValue; break; } } if (foundConfigValue) break; } if (foundConfigValue) { const parts = foundConfigValue.split('|').map(s => s.trim()); foundCode = parts[0] || foundCode; foundName = parts.length > 1 ? parts[1] : cleanLabel; } else { console.warn('Gift item code not found, using fallback ZZ-0000:', sGiftStr); } items.push({ code: foundCode, name: foundName, label: cleanLabel, item_type: 'event', quantity: 1 }); } } } } let baseShippingVal = parseInt((configData.APP_SETTINGS && configData.APP_SETTINGS.shipping_fee) ? configData.APP_SETTINGS.shipping_fee : 3500); let extraShippingVal = parseInt((configData.APP_SETTINGS && configData.APP_SETTINGS.extra_shipping_fee) ? configData.APP_SETTINGS.extra_shipping_fee : 3000); let pointDeduction = parseInt(document.getElementById('sms-point-deduction').value.replace(/,/g, '')) || 0; let totalAfterDiscount = totalCost; let discount5Amount = 0; let couponAmount = 0; if (document.getElementById('sms-coupon').checked) { couponAmount = getCouponDiscount(totalCost); } if (document.getElementById('sms-discount').checked) { totalAfterDiscount = Math.floor((totalCost * 0.95) / 10) * 10; discount5Amount = totalCost - totalAfterDiscount; } if (couponAmount > 0) { totalAfterDiscount = Math.max(0, totalAfterDiscount - couponAmount); } if (pointDeduction > 0) { totalAfterDiscount = Math.max(0, totalAfterDiscount - pointDeduction); } let isAutoFree = totalAfterDiscount >= 50000; let isManualFree = document.getElementById('sms-ship-free').checked; let isFreeShip = hasMarkedFreeShipping || isAutoFree || isManualFree; let shippingCost = 0; let freeReason = ""; if (!isFreeShip) { shippingCost = baseShippingVal; } else { if (isAutoFree) freeReason = "5만원 이상"; else if (isManualFree) freeReason = "수동배송무료"; else if (hasMarkedFreeShipping) freeReason = "무료배송상품"; else freeReason = "무료"; } let extraShippingCost = 0; if (document.getElementById('sms-ship-extra').checked) { extraShippingCost = extraShippingVal; if (isFreeShip) freeReason += " / 도서산간 추가"; } const currentOrderType = document.querySelector('input[name="order-type"]:checked').value; if (SPECIAL_ORDER_TYPES.has(currentOrderType)) { shippingCost = 0; extraShippingCost = 0; } let finalPayable = totalAfterDiscount + shippingCost + extraShippingCost; return { customer_name: document.getElementById('cust-name').value.trim(), address: document.getElementById('cust-address').value.trim(), phone: document.getElementById('cust-phone').value.trim(), comment: "", order_type: document.querySelector('input[name="order-type"]:checked').value, no_lid: document.getElementById('chk-no-lid').checked, items: items, total_amount: finalPayable, raw_total: totalCost, discount_amount: discount5Amount + couponAmount, discount_5_amount: discount5Amount, coupon_amount: couponAmount, point_deduction: pointDeduction, shipping_cost: shippingCost, extra_shipping_cost: extraShippingCost, free_reason: freeReason }; } let currentSubmitContext = null; function submitOrder(url, isEllen = false) { const tempPayload = getOrderPayload(); tempPayload.is_ellen = isEllen; if (tempPayload.items.length === 0) { alert("발주 아이템을 선택해주세요."); return; } if (url.includes('submit')) { if (!tempPayload.customer_name) { alert("이름 항목이 누락되었습니다. 입력해주세요."); return; } if (!tempPayload.phone) { alert("연락처 항목이 누락되었습니다. 입력해주세요."); return; } if (!tempPayload.address) { alert("주소 항목이 누락되었습니다. 입력해주세요."); return; } } // Setup modal const listContainer = document.getElementById('exclude-items-list'); listContainer.innerHTML = ''; const updateTotalDisplay = () => { const excludedIndices = Array.from(document.querySelectorAll('.exclude-chk:checked')).map(c => parseInt(c.dataset.idx)); const livePayload = getOrderPayload(excludedIndices); let displayTotal = livePayload.total_amount; const chkSampleSend = document.getElementById('chk-sample-send'); if (chkSampleSend && chkSampleSend.checked) { displayTotal = 0; } document.getElementById('exclude-modal-totalAmount').innerText = displayTotal.toLocaleString(); let extraDiv = document.getElementById('exclude-extra-items'); if(!extraDiv) { extraDiv = document.createElement('div'); extraDiv.id = 'exclude-extra-items'; } let extraHTML = ''; if (livePayload.discount_5_amount > 0) { extraHTML += `
5% 할인 - ${livePayload.discount_5_amount.toLocaleString()}원
`; } if (livePayload.coupon_amount > 0) { extraHTML += `
쿠폰 할인 - ${livePayload.coupon_amount.toLocaleString()}원
`; } if (livePayload.point_deduction > 0) { extraHTML += `
적립금 차감 - ${livePayload.point_deduction.toLocaleString()}원
`; } if (livePayload.shipping_cost > 0) { extraHTML += `
배송비 + ${livePayload.shipping_cost.toLocaleString()}원
`; } else { if(!livePayload.free_reason) livePayload.free_reason = "기본"; extraHTML += `
배송비 (무료 - ${livePayload.free_reason}) 0원
`; } if (livePayload.extra_shipping_cost > 0) { extraHTML += `
추가 배송비 + ${livePayload.extra_shipping_cost.toLocaleString()}원
`; } extraDiv.innerHTML = extraHTML; listContainer.appendChild(extraDiv); document.querySelectorAll('.exclude-chk').forEach(c => { const priceSpan = c.closest('label').querySelector('.exclude-price-span'); if (priceSpan) { if (c.checked) { priceSpan.style.textDecoration = 'line-through'; priceSpan.style.textDecorationColor = 'red'; priceSpan.style.opacity = '0.5'; } else { priceSpan.style.textDecoration = 'none'; priceSpan.style.opacity = '1'; } } }); }; document.querySelectorAll('.chk-order:checked').forEach((chk, idx) => { const row = chk.closest('.item-row'); const qty = parseInt(row.querySelector('.qty-order').value) || 1; const multiplier = parseInt(chk.dataset.multiplier) || 1; const finalQty = qty * multiplier; const cost = parseInt(chk.dataset.cost) || 0; const dcost = chk.dataset.dcost === "null" ? null : (parseInt(chk.dataset.dcost) || null); let itemTotal = 0; if (dcost !== null && qty >= 3) { itemTotal = Math.floor(qty / 3) * dcost * 2 + (qty % 3) * cost; } else { itemTotal = cost * qty; } const orderType = document.querySelector('input[name="order-type"]:checked').value; const noLidChecked = document.getElementById('chk-no-lid').checked; if (SPECIAL_ORDER_TYPES.has(orderType) || noLidChecked) { itemTotal = 0; } let badges = []; if (SPECIAL_ORDER_TYPES.has(orderType)) badges.push(ORDER_TYPE_LABELS[orderType]); if (noLidChecked && chk.dataset.type === 'ORDER_SINGLE') badges.push('뚜껑(X)통(O)'); let reasonBadge = badges.length > 0 ? `${badges.join(', ')}` : ''; const div = document.createElement('div'); div.style.display = 'flex'; div.style.alignItems = 'center'; div.style.gap = '8px'; div.style.padding = '8px'; div.style.background = '#fff'; div.style.border = '1px solid #cbd5e0'; div.style.borderRadius = '4px'; div.innerHTML = ` `; const checkbox = div.querySelector('.exclude-chk'); checkbox.addEventListener('change', updateTotalDisplay); listContainer.appendChild(div); }); currentSubmitContext = { url, isEllen }; const chkSampleSend = document.getElementById('chk-sample-send'); if (chkSampleSend) { chkSampleSend.checked = false; chkSampleSend.onchange = (e) => { const isChecked = e.target.checked; document.querySelectorAll('.exclude-chk').forEach(c => c.checked = isChecked); updateTotalDisplay(); }; } updateTotalDisplay(); document.getElementById('exclude-modal').style.display = 'flex'; } document.getElementById('btn-exclude-cancel').addEventListener('click', () => { document.getElementById('exclude-modal').style.display = 'none'; currentSubmitContext = null; }); document.getElementById('btn-exclude-submit').addEventListener('click', () => { if (!currentSubmitContext) return; const excludedIndices = Array.from(document.querySelectorAll('.exclude-chk:checked')).map(c => parseInt(c.dataset.idx)); document.getElementById('exclude-modal').style.display = 'none'; doSubmitOrder(currentSubmitContext.url, currentSubmitContext.isEllen, excludedIndices); currentSubmitContext = null; }); function doSubmitOrder(url, isEllen = false, excludedIndices = []) { const payload = getOrderPayload(excludedIndices); payload.is_ellen = isEllen; const isSpecialType = SPECIAL_ORDER_TYPES.has(payload.order_type); const typeLabelMap = ORDER_TYPE_LABELS; let finalPayloadAmount = payload.total_amount; if (isSpecialType) finalPayloadAmount = 0; const chkSampleSend = document.getElementById('chk-sample-send'); if (chkSampleSend && chkSampleSend.checked) { finalPayloadAmount = 0; } const submitPayload = { ...payload, total_amount: finalPayloadAmount }; if (url.includes('submit')) { const manualTabLink = document.querySelector('li[data-tab="manual-tab"]'); if (manualTabLink) manualTabLink.click(); const loader = document.getElementById('sheet-loader'); if (loader) loader.style.display = 'flex'; } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(submitPayload) }).then(res => res.json()).then(data => { if (url.includes('submit')) { const loader = document.getElementById('sheet-loader'); if (loader) loader.style.display = 'none'; } if (data.status === 'success') { if (!url.includes('submit')) { alert(data.message); } else if (data.timestamp) { const histData = { timestamp: data.timestamp, customer_name: submitPayload.customer_name, total_amount: isSpecialType ? typeLabelMap[submitPayload.order_type] : submitPayload.total_amount }; fetch('/api/manual/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(histData) }).then(() => { if (typeof loadManualHistory === 'function') { loadManualHistory(); } }); } // Copy the same text that is written to the sheet remarks/comparison field. if (data.clipboard_text) { copyTextToClipboard(data.clipboard_text).then(copied => { if (!copied) { alert("클립보드 복사에 실패했습니다. 브라우저 권한을 확인해주세요."); } }); } document.getElementById('btn-reset').click(); if (url.includes('submit')) { const iframe = document.querySelector('#manual-tab iframe'); if (iframe) iframe.src = iframe.src; } } else { alert("Error: " + data.detail); } }).catch(err => { if (url.includes('submit')) { const loader = document.getElementById('sheet-loader'); if (loader) loader.style.display = 'none'; } alert("요청 중 오류 발생: " + err); }); } document.getElementById('btn-submit-normal').addEventListener('click', () => submitOrder('/api/order/submit', false)); document.getElementById('btn-submit-ellen').addEventListener('click', () => submitOrder('/api/order/submit', true)); document.getElementById('btn-download-sheet').addEventListener('click', () => { if (!confirm("수동발주로 입력한 자료는 엑셀 파일로 저장되며, 입력된 수동발주 내용은 사라집니다.\n진행하시겠습니까?")) return; const btn = document.getElementById('btn-download-sheet'); btn.innerText = '저장 중...'; btn.disabled = true; const progressContainer = document.getElementById('download-progress-container'); const progressBar = document.getElementById('download-progress-bar'); const progressText = document.getElementById('download-progress-text'); progressContainer.style.display = 'block'; progressBar.style.width = '0%'; progressText.innerText = '0%'; let progress = 0; const interval = setInterval(() => { if (progress < 90) { progress += Math.floor(Math.random() * 8) + 4; if (progress > 90) progress = 90; progressBar.style.width = progress + '%'; progressText.innerText = progress + '%'; } }, 500); fetch('/api/sheet/download_and_clear', { method: 'POST' }) .then(res => { if (!res.ok) throw new Error("서버에서 오류가 발생했습니다."); return res.blob(); }) .then(blob => { // Determine save filename client-side const now = new Date(); const weekdays = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"]; const dayStr = weekdays[now.getDay()]; const mm = String(now.getMonth() + 1).padStart(2, '0'); const dd = String(now.getDate()).padStart(2, '0'); const fileName = `${mm}월 ${dd}일 (${dayStr}) 5 수동발주.xlsx`; // Spawn a temporary web link to fetch the constructed Blob and trigger "Save as" const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = downloadUrl; a.download = fileName; document.body.appendChild(a); a.click(); // Cleanup Blob immediately window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(a); clearInterval(interval); progressBar.style.width = '100%'; progressText.innerText = '100%'; setTimeout(() => { alert("구글 엑셀 파일이 성공적으로 다운로드되고 시트가 초기화되었습니다."); progressContainer.style.display = 'none'; btn.innerText = '엑셀로 저장'; btn.disabled = false; const iframe = document.querySelector('#manual-tab iframe'); if (iframe) iframe.src = iframe.src; }, 400); }) .catch(err => { clearInterval(interval); alert("네트워크 오류/파일 저장 실패: " + err); progressContainer.style.display = 'none'; btn.innerText = '엑셀로 저장'; btn.disabled = false; }); }); document.getElementById('btn-reset').addEventListener('click', () => { document.querySelectorAll('.chk-order').forEach(c => { c.checked = false; const row = c.closest('.item-row'); if (row) row.style.backgroundColor = ''; }); document.querySelectorAll('.qty-order').forEach(i => i.value = "1"); document.getElementById('cust-name').value = ''; document.getElementById('cust-phone').value = ''; document.getElementById('cust-address').value = ''; document.getElementById('chk-no-lid').checked = false; document.getElementById('chk-no-lid').disabled = true; document.querySelector('input[name="order-type"][value="normal"]').checked = true; // sms reset as well document.getElementById('sms-discount').checked = false; document.getElementById('sms-discount').disabled = false; document.getElementById('sms-coupon').checked = false; document.getElementById('sms-coupon').disabled = false; document.getElementById('sms-june-promo').checked = false; document.getElementById('sms-ship-free').checked = false; document.getElementById('sms-ship-extra').checked = false; const ptInput = document.getElementById('sms-point-deduction'); if (ptInput) ptInput.value = ''; document.getElementById('sms-preview').innerHTML = ''; updateSelectedOrderItemsSummary(); }); // ============================================ // SMS CALCULATE WITH MATCHED PRICING // ============================================ function getAmountGiftText(orderAmount) { if (!configData['SMS_GIFT_RULES']) return ""; let matched = []; let rules = []; for (let i = 1; i <= 4; i++) { const sMin = configData['SMS_GIFT_RULES'][`condition${i}_min`]; const sMax = configData['SMS_GIFT_RULES'][`condition${i}_max`]; const sGift = configData['SMS_GIFT_RULES'][`condition${i}_gift`]; if (!sGift) continue; const minV = sMin ? parseInt(sMin) : 0; const maxV = sMax ? parseInt(sMax) : null; rules.push({ min: minV, max: maxV, gift: sGift }); } let seen = new Set(); rules.forEach(r => { if (orderAmount < r.min) return; if (r.max !== null && orderAmount >= r.max) return; if (!seen.has(r.gift)) { seen.add(r.gift); matched.push(r.gift); } }); if (matched.length === 0) return ""; return "\n(금액별 사은품 증정)\n" + matched.join("\n") + "\n"; } function buildSmsPreviewResult() { let totalCost = 0; let orderListStr = ""; let hasMarkedFreeShipping = false; const checkedItems = document.querySelectorAll('.chk-order:checked'); if (checkedItems.length === 0) return null; checkedItems.forEach(chk => { const row = chk.closest('.item-row'); const qty = parseInt(row.querySelector('.qty-order').value) || 1; const orderLabel = chk.dataset.label; const cost = parseInt(chk.dataset.cost) || 0; const dcost = chk.dataset.dcost === "null" ? null : (parseInt(chk.dataset.dcost) || null); const isFree = chk.dataset.isFree === "true"; if (isFree) hasMarkedFreeShipping = true; let itemTotal = 0; if (dcost !== null && qty >= 3) { itemTotal = Math.floor(qty / 3) * dcost * 2 + (qty % 3) * cost; } else { itemTotal = cost * qty; } const orderType = document.querySelector('input[name="order-type"]:checked').value; const noLidChecked = document.getElementById('chk-no-lid').checked; if (SPECIAL_ORDER_TYPES.has(orderType) || noLidChecked) { itemTotal = 0; } const cleanLabel = orderLabel.replace(/^\*/, '').trim(); let dispName = cleanLabel.replace("오리 ", ""); let smsQty = qty; const matchName = dispName.match(/_(\d+)개$/); if (matchName) { smsQty = qty * parseInt(matchName[1], 10); dispName = dispName.replace(/_\d+개$/, '').trim(); } if (cost === 0) { orderListStr += `${dispName} (가격미입력)\n`; } else { orderListStr += `${dispName} ${smsQty}개 ${itemTotal.toLocaleString()}원\n`; } totalCost += itemTotal; }); let baseShippingVal = parseInt((configData.APP_SETTINGS && configData.APP_SETTINGS.shipping_fee) ? configData.APP_SETTINGS.shipping_fee : 3500); let extraShippingVal = parseInt((configData.APP_SETTINGS && configData.APP_SETTINGS.extra_shipping_fee) ? configData.APP_SETTINGS.extra_shipping_fee : 3000); let pointDeduction = parseInt(document.getElementById('sms-point-deduction').value.replace(/,/g, '')) || 0; let totalAfterDiscount = totalCost; let discount5Amount = 0; let couponAmount = 0; if (document.getElementById('sms-coupon').checked) { couponAmount = getCouponDiscount(totalCost); } if (document.getElementById('sms-discount').checked) { totalAfterDiscount = Math.floor((totalCost * 0.95) / 10) * 10; discount5Amount = totalCost - totalAfterDiscount; } if (couponAmount > 0) { totalAfterDiscount = Math.max(0, totalAfterDiscount - couponAmount); } if (pointDeduction > 0) { totalAfterDiscount = Math.max(0, totalAfterDiscount - pointDeduction); } let junePromoText = ""; if (document.getElementById('sms-june-promo').checked) { junePromoText = getAmountGiftText(totalCost); } let isAutoFree = totalAfterDiscount >= 50000; let isManualFree = document.getElementById('sms-ship-free').checked; let isFreeShip = hasMarkedFreeShipping || isAutoFree || isManualFree; let shippingCost = 0; if (!isFreeShip) { shippingCost = baseShippingVal; } let extraShippingCost = 0; if (document.getElementById('sms-ship-extra').checked) { extraShippingCost = extraShippingVal; } const currentOrderType = document.querySelector('input[name="order-type"]:checked').value; const noLidChecked = document.getElementById('chk-no-lid').checked; if (SPECIAL_ORDER_TYPES.has(currentOrderType) || noLidChecked) { shippingCost = 0; extraShippingCost = 0; } let finalPayable = totalAfterDiscount + shippingCost + extraShippingCost; let depositVal = `${finalPayable.toLocaleString()}원`; if (hasMarkedFreeShipping) { if (extraShippingCost > 0) depositVal += "(무료배송, 추가배송비만 적용)"; else depositVal += "(무료배송 상품 포함)"; } else if (isFreeShip) depositVal += "(무료배송)"; let depositLine = `입금 금액: ${depositVal}`; const defaultHeader = "안녕하세요, 미라네 주방입니다! 😊 \n주문내용 확인 부탁드립니다."; const defaultFooter = "1. 아래 계좌로 입금해 주세요.\n\n▶입금 금액: {최종금액} \n▶입금 계좌 (농협): 317-0025-1542-51\n▶예금주: (주)더블엑스 코퍼레이션\n\n2. 입금 후 [성함/주소/연락처]를 남겨주세요.\n\n[배송 안내]\n■오후 1시 이전 입금 건은 당일 발송됩니다.\n★이벤트 상품 주문시 1~2일 정도 늦어질 수 있으니 너그러운 양해 부탁드립니다."; const rules = configData['SMS_GIFT_RULES'] || {}; let headerText = rules['header_text'] ? rules['header_text'].replace(/\\n/g, '\n') : defaultHeader; let footerText = rules['footer_text'] ? rules['footer_text'].replace(/\\n/g, '\n') : defaultFooter; footerText = footerText.replace(/{최종금액}/g, depositVal); let msg = `${headerText}\n\n*****주문 내용*****\n${orderListStr}${junePromoText}\n주문 금액: ${totalCost.toLocaleString()}원\n`; if (discount5Amount > 0) msg += `할인 금액: -${discount5Amount.toLocaleString()}원(5%)\n`; if (couponAmount > 0) msg += `쿠폰 할인: -${couponAmount.toLocaleString()}원\n`; if (pointDeduction > 0) msg += `적립금 차감: -${pointDeduction.toLocaleString()}원\n`; if (shippingCost > 0) msg += ` 택배비: ${shippingCost.toLocaleString()}원\n`; if (extraShippingCost > 0) msg += `추가배송비: ${extraShippingCost.toLocaleString()}원\n`; msg += `==========================\n${depositLine}\n\n${footerText}`; return { msg, finalPayable, pointDeduction, checkedItems }; } function refreshLiveSmsPreview() { const previewDiv = document.getElementById('sms-preview'); const result = buildSmsPreviewResult(); if (!result) { previewDiv.textContent = '선택된 상품이 없습니다.'; return null; } previewDiv.innerHTML = result.msg; return result; } const smsPreviewModal = document.getElementById('sms-preview-modal'); const btnSmsPreviewOpen = document.getElementById('btn-sms-preview-open'); const btnSmsPreviewClose = document.getElementById('btn-sms-preview-close'); function closeSmsPreviewModal() { smsPreviewModal.classList.remove('is-open'); smsPreviewModal.setAttribute('aria-hidden', 'true'); } if (btnSmsPreviewOpen) { btnSmsPreviewOpen.addEventListener('click', () => { refreshLiveSmsPreview(); smsPreviewModal.classList.add('is-open'); smsPreviewModal.setAttribute('aria-hidden', 'false'); btnSmsPreviewClose.focus(); }); } if (btnSmsPreviewClose) btnSmsPreviewClose.addEventListener('click', closeSmsPreviewModal); smsPreviewModal.addEventListener('click', event => { if (event.target === smsPreviewModal) closeSmsPreviewModal(); }); document.addEventListener('keydown', event => { if (event.key === 'Escape' && smsPreviewModal.classList.contains('is-open')) { closeSmsPreviewModal(); } }); function scheduleLiveOrderRefresh() { queueMicrotask(updateSelectedOrderItemsSummary); } const smsOptionsPanel = document.querySelector('.sms-options'); if (smsOptionsPanel) { smsOptionsPanel.addEventListener('change', scheduleLiveOrderRefresh); smsOptionsPanel.addEventListener('input', scheduleLiveOrderRefresh); } document.querySelectorAll('input[name="order-type"]').forEach(input => { input.addEventListener('change', scheduleLiveOrderRefresh); }); document.getElementById('chk-no-lid').addEventListener('change', scheduleLiveOrderRefresh); document.getElementById('btn-sms-calc').addEventListener('click', () => { const result = refreshLiveSmsPreview(); if (!result) { alert('선택된 상품이 없습니다.'); return; } const { msg, finalPayable, pointDeduction, checkedItems } = result; const previewDiv = document.getElementById('sms-preview'); copyTextToClipboard(previewDiv.innerText).then(copied => { if (copied) showToast('문자 내용이 복사되었습니다.'); }); // Save History const now = new Date(); const stamp = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`; const savedItems = []; checkedItems.forEach(chk => { const row = chk.closest('.item-row'); const qty = parseInt(row.querySelector('.qty-order').value) || 1; savedItems.push({ code: chk.dataset.code, label: chk.dataset.label, qty: qty }); }); const custName = document.getElementById('cust-name').value.trim(); const custPhone = document.getElementById('cust-phone').value.trim(); const custAddress = document.getElementById('cust-address').value.trim(); const histData = { timestamp: stamp, deposit_amount_str: finalPayable.toLocaleString() + "원", custName: custName, custPhone: custPhone, custAddress: custAddress, customer_name: custName, phone: custPhone, address: custAddress, orderType: document.querySelector('input[name="order-type"]:checked').value, noLid: document.getElementById('chk-no-lid').checked, items: savedItems, smsOpts: { discount: document.getElementById('sms-discount').checked, junePromo: document.getElementById('sms-june-promo').checked, shipExtra: document.getElementById('sms-ship-extra').checked, shipFree: document.getElementById('sms-ship-free').checked, pointDeduction: pointDeduction }, previewText: msg }; fetch('/api/sms/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(histData) }).then(() => { writeSmsCustomerCache(stamp, { custName: custName, custPhone: custPhone, custAddress: custAddress }); loadSmsHistory(); }); }); // ============================================ // SMS HISTORY // ============================================ function loadSmsHistory() { const u = document.getElementById('sms-history-list'); u.innerHTML = ''; fetch('/api/sms/history').then(r => r.json()).then(data => { data.slice().reverse().forEach((d, idx) => { const actualIdx = data.length - 1 - idx; const li = document.createElement('li'); li.style.cursor = 'pointer'; li.style.padding = '4px 0'; li.style.borderBottom = '1px solid #eee'; li.onclick = () => window.loadHistoryItem(actualIdx); const cachedCustomer = readSmsCustomerCache()[d.timestamp] || {}; const name = firstNonEmpty(d.custName, d.customer_name, cachedCustomer.custName, cachedCustomer.customer_name); const phone = firstNonEmpty(d.custPhone, d.phone, cachedCustomer.custPhone, cachedCustomer.phone); const address = firstNonEmpty(d.custAddress, d.address, cachedCustomer.custAddress, cachedCustomer.address); const hasCustInfo = (name && name.trim() && name.trim().toLowerCase() !== 'none') || (phone && phone.trim() && phone.trim().toLowerCase() !== 'none') || (address && address.trim() && address.trim().toLowerCase() !== 'none'); let amountStr = d.deposit_amount_str || ''; if (hasCustInfo) { amountStr = amountStr.replace('원', '원*'); } const spanDate = document.createElement('span'); spanDate.textContent = `${d.timestamp} | ${amountStr}`; li.appendChild(spanDate); const spanDel = document.createElement('span'); spanDel.className = 'action-text ms-2'; spanDel.style.marginLeft = '8px'; spanDel.title = '삭제'; spanDel.textContent = '❌'; spanDel.onclick = (e) => window.deleteHistory(actualIdx, e); li.appendChild(spanDel); u.appendChild(li); }); }); } const SMS_CUSTOMER_CACHE_KEY = 'smsCustomerHistoryByTimestamp'; function readSmsCustomerCache() { try { return JSON.parse(localStorage.getItem(SMS_CUSTOMER_CACHE_KEY) || '{}'); } catch (e) { return {}; } } function writeSmsCustomerCache(timestamp, customer) { if (!timestamp) return; const cache = readSmsCustomerCache(); cache[timestamp] = customer; localStorage.setItem(SMS_CUSTOMER_CACHE_KEY, JSON.stringify(cache)); } function firstNonEmpty(...values) { for (const value of values) { if (value !== undefined && value !== null && String(value).trim() !== '') { return value; } } return ''; } window.loadManualHistory = function () { const u = document.getElementById('manual-history-list'); if (!u) return; u.innerHTML = ''; fetch('/api/manual/history').then(r => r.json()).then(data => { let totalCount = data.length; let totalAmount = 0; data.forEach((d) => { if (typeof d.total_amount === 'number') totalAmount += d.total_amount; else if (typeof d.total_amount === 'string' && !isNaN(parseInt(d.total_amount))) totalAmount += parseInt(d.total_amount); }); let txtCount = document.getElementById('manual-total-count'); let txtAmount = document.getElementById('manual-total-amount'); if (txtCount) txtCount.textContent = totalCount; if (txtAmount) txtAmount.textContent = totalAmount.toLocaleString(); data.slice().reverse().forEach((d) => { const li = document.createElement('li'); li.style.padding = '4px 0'; li.style.borderBottom = '1px solid #eee'; li.style.display = 'flex'; li.style.justifyContent = 'space-between'; li.style.alignItems = 'center'; li.style.fontSize = '0.9rem'; const spanInfo = document.createElement('span'); const timeStr = d.timestamp.split(' ')[1] || d.timestamp; let amtDisplay = (typeof d.total_amount === 'number' || (typeof d.total_amount === 'string' && !isNaN(parseInt(d.total_amount)))) ? parseInt(d.total_amount).toLocaleString() + '원' : d.total_amount; spanInfo.textContent = `${timeStr} | ${d.customer_name} | ${amtDisplay}`; li.appendChild(spanInfo); const spanDel = document.createElement('span'); spanDel.className = 'action-text ms-2'; spanDel.style.marginLeft = '8px'; spanDel.style.cursor = 'pointer'; spanDel.title = '삭제'; spanDel.textContent = '❌'; spanDel.onclick = (e) => { e.stopPropagation(); if (confirm("🚨 [경고] 이 수동발주 기록을 삭제하시겠습니까?\n\n(구글 시트에서도 함께 삭제됩니다!)")) { const btn = e.target; btn.textContent = '⏳'; btn.style.pointerEvents = 'none'; fetch(`/api/manual/history/${encodeURIComponent(d.timestamp)}`, { method: 'DELETE' }) .then(() => { window.loadManualHistory(); const iframe = document.querySelector('#manual-tab iframe'); if (iframe) iframe.src = iframe.src; }) .catch(() => { alert("삭제 중 오류가 발생했습니다."); btn.textContent = '❌'; btn.style.pointerEvents = 'auto'; }); } }; li.appendChild(spanDel); u.appendChild(li); }); }).catch(err => console.error("Error loading manual history", err)); } window.loadHistoryItem = function (idx) { fetch('/api/sms/history').then(r => r.json()).then(data => { if (idx < 0 || idx >= data.length) return; const d = data[idx]; document.getElementById('btn-reset').click(); const cachedCustomer = readSmsCustomerCache()[d.timestamp] || {}; const savedCustName = firstNonEmpty(d.custName, d.customer_name, cachedCustomer.custName, cachedCustomer.customer_name); const savedCustPhone = firstNonEmpty(d.custPhone, d.phone, cachedCustomer.custPhone, cachedCustomer.phone); const savedCustAddress = firstNonEmpty(d.custAddress, d.address, cachedCustomer.custAddress, cachedCustomer.address); document.getElementById('cust-name').value = savedCustName; document.getElementById('cust-phone').value = savedCustPhone; document.getElementById('cust-address').value = savedCustAddress; if (d.orderType !== undefined) { const rb = document.querySelector(`input[name="order-type"][value="${d.orderType}"]`); if (rb) rb.checked = true; } // Fallback for Google Sheets history where items array is missing but items_str exists if ((!d.items || !Array.isArray(d.items)) && d.items_str) { d.items = []; const parts = d.items_str.split(','); parts.forEach(part => { const match = part.trim().match(/^(.*)[\[\(](.*?)[\]\)]\s*(\d+)개$/); if (match) { d.items.push({ label: match[1].trim(), code: match[2].trim(), qty: parseInt(match[3]) }); } }); } if (d.items && Array.isArray(d.items)) { d.items.forEach(item => { let chk = Array.from(document.querySelectorAll('.chk-order:not(:checked)')).find(c => c.dataset.code === item.code && c.dataset.label === item.label); if (!chk) { chk = Array.from(document.querySelectorAll('.chk-order:not(:checked)')).find(c => c.dataset.code === item.code || c.dataset.label === item.label); } if (chk) { chk.checked = true; const row = chk.closest('.item-row'); if (row) { const sel = row.querySelector('.qty-order'); if (sel) { if (sel.tagName === 'SELECT') { const optionExists = Array.from(sel.options).some(opt => opt.value == item.qty); if (!optionExists) { const input = document.createElement('input'); input.type = 'number'; input.min = '1'; input.value = item.qty; input.className = 'qty-order'; sel.replaceWith(input); } else { sel.value = item.qty; } } else { sel.value = item.qty; } } } } }); const anyChk = document.querySelector('.chk-order'); if (anyChk) { anyChk.dispatchEvent(new Event('change')); } updateSelectedOrderItemsSummary(); if (d.noLid !== undefined) document.getElementById('chk-no-lid').checked = d.noLid; } if (d.smsOpts) { document.getElementById('sms-discount').checked = !!d.smsOpts.discount; document.getElementById('sms-june-promo').checked = !!d.smsOpts.junePromo; document.getElementById('sms-ship-extra').checked = !!d.smsOpts.shipExtra; document.getElementById('sms-ship-free').checked = !!d.smsOpts.shipFree; if (d.smsOpts.pointDeduction) { const ptInput = document.getElementById('sms-point-deduction'); if(ptInput) ptInput.value = parseInt(d.smsOpts.pointDeduction).toLocaleString(); } else { const ptInput = document.getElementById('sms-point-deduction'); if(ptInput) ptInput.value = ''; } } if (d.previewText !== undefined) { document.getElementById('sms-preview').innerHTML = d.previewText; } updateSelectedOrderItemsSummary(); }); } window.deleteHistory = function (idx, e) { e.stopPropagation(); if (confirm("이 기록을 삭제하시겠습니까?")) { fetch(`/api/sms/history/${idx}`, { method: 'DELETE' }).then(() => loadSmsHistory()); } } document.getElementById('btn-sms-clear-all').addEventListener('click', () => { if (confirm("정말로 모든 기록을 삭제하시겠습니까?")) { fetch('/api/sms/history', { method: 'DELETE' }).then(() => loadSmsHistory()); } }); // ============================================ // GIFT TAB // ============================================ function buildGiftTab(data) { const container = document.getElementById('gift-rules-container'); container.innerHTML = ''; // 중복 방지를 위해 기존 내용 완전히 비우기 const giftsRaw = data['ORDER_GIFT'] ? (data['ORDER_GIFT'].__order || Object.keys(data['ORDER_GIFT'])) : []; const gifts = giftsRaw.filter(k => k !== '__order'); const rules = data['SMS_GIFT_RULES'] || {}; for (let i = 1; i <= 4; i++) { const card = document.createElement('div'); card.className = 'glass-card gift-rule-card'; const title = document.createElement('h3'); title.textContent = `조건 ${i}`; const minV = rules[`condition${i}_min`] || ''; const maxV = rules[`condition${i}_max`] || ''; const selGift = rules[`condition${i}_gift`] || ''; const fmt = (v) => { if (!v) return ''; let n = parseInt(String(v).replace(/,/g, ''), 10); return isNaN(n) ? '' : n.toLocaleString(); }; const inputs = document.createElement('div'); inputs.className = 'rule-inputs'; inputs.innerHTML = ` 이상 미만 `; const radioList = document.createElement('div'); radioList.className = 'gift-radio-list'; // None option radioList.innerHTML += ``; gifts.forEach(g => { radioList.innerHTML += ``; }); card.appendChild(title); card.appendChild(inputs); card.appendChild(radioList); container.appendChild(card); } document.querySelectorAll('.btn-clear-rule').forEach(btn => { btn.addEventListener('click', e => { const i = e.target.dataset.idx; document.getElementById(`rule${i}-min`).value = ''; document.getElementById(`rule${i}-max`).value = ''; document.querySelector(`input[name="rule${i}-gift"][value=""]`).checked = true; }); }); const defaultHeader = "안녕하세요, 미라네 주방입니다! 😊 \n주문내용 확인 부탁드립니다."; const defaultFooter = "1. 아래 계좌로 입금해 주세요.\n\n▶입금 금액: {최종금액} \n▶입금 계좌 (농협): 317-0025-1542-51\n▶예금주: (주)더블엑스 코퍼레이션\n\n2. 입금 후 [성함/주소/연락처]를 남겨주세요.\n\n[배송 안내]\n■오후 1시 이전 입금 건은 당일 발송됩니다.\n★이벤트 상품 주문시 1~2일 정도 늦어질 수 있으니 너그러운 양해 부탁드립니다."; let hText = rules['header_text'] ? rules['header_text'].replace(/\\n/g, '\n') : defaultHeader; let fText = rules['footer_text'] ? rules['footer_text'].replace(/\\n/g, '\n') : defaultFooter; document.getElementById('sms-header-text').value = hText; document.getElementById('sms-footer-text').value = fText; let appFont = data['APP_SETTINGS'] ? data['APP_SETTINGS']['font'] : ''; const fontSelect = document.getElementById('app-font-select'); if (fontSelect && appFont) { fontSelect.value = appFont; } let baseShippingVal = data['APP_SETTINGS'] && data['APP_SETTINGS']['shipping_fee'] ? data['APP_SETTINGS']['shipping_fee'] : '3500'; let extraShippingVal = data['APP_SETTINGS'] && data['APP_SETTINGS']['extra_shipping_fee'] ? data['APP_SETTINGS']['extra_shipping_fee'] : '3000'; document.getElementById('setting-ship-base').value = parseInt(baseShippingVal).toLocaleString(); document.getElementById('setting-ship-extra').value = parseInt(extraShippingVal).toLocaleString(); const lblShipExtra = document.getElementById('lbl-ship-extra'); if (lblShipExtra) { lblShipExtra.textContent = '추가배송비 ' + parseInt(extraShippingVal).toLocaleString() + '원'; } const btnGiftSave = document.getElementById('btn-gift-save'); // Reset color in case it was red btnGiftSave.style.backgroundColor = ''; const markUnsaved = () => { btnGiftSave.style.backgroundColor = '#e53e3e'; }; // Format money fields on input document.querySelectorAll('input[id^="rule"][id$="-min"], input[id^="rule"][id$="-max"], #setting-ship-base, #setting-ship-extra').forEach(el => { el.addEventListener('input', e => { let val = e.target.value.replace(/[^0-9]/g, ''); e.target.value = val ? parseInt(val, 10).toLocaleString() : ''; }); }); // Inputs within gift tab document.querySelectorAll('#gift-tab input, #gift-tab textarea, #gift-tab select').forEach(el => { el.removeEventListener('input', markUnsaved); el.removeEventListener('change', markUnsaved); el.addEventListener('input', markUnsaved); el.addEventListener('change', markUnsaved); }); document.querySelectorAll('.btn-clear-rule').forEach(btn => { btn.addEventListener('click', markUnsaved); }); } const btnGiftSave = document.getElementById('btn-gift-save'); if (btnGiftSave) { btnGiftSave.addEventListener('click', async () => { const payload = { rules: {}, app_settings: { shipping_fee: document.getElementById('setting-ship-base').value.replace(/[^0-9]/g, ''), extra_shipping_fee: document.getElementById('setting-ship-extra').value.replace(/[^0-9]/g, '') } }; for (let i = 1; i <= 4; i++) { const minEl = document.getElementById(`rule${i}-min`); const maxEl = document.getElementById(`rule${i}-max`); const giftEl = document.querySelector(`input[name="rule${i}-gift"]:checked`); if (minEl && maxEl && giftEl) { payload.rules[`condition${i}_min`] = minEl.value.replace(/[^0-9]/g, ''); payload.rules[`condition${i}_max`] = maxEl.value.replace(/[^0-9]/g, ''); payload.rules[`condition${i}_gift`] = giftEl.value; } } const hText = document.getElementById('sms-header-text'); if (hText) payload.rules['header_text'] = hText.value; const fText = document.getElementById('sms-footer-text'); if (fText) payload.rules['footer_text'] = fText.value; const fontSel = document.getElementById('app-font-select'); if (fontSel) payload.app_settings.font = fontSel.value; try { const res = await fetch('/api/gift/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { alert('저장되었습니다.'); btnGiftSave.style.backgroundColor = ''; fetch('/api/config').then(r => r.json()).then(d => { configData = d; const oc = document.getElementById('order-items-container'); if (oc) oc.innerHTML = ''; buildOrderTab(configData); buildGiftTab(configData); buildProductTab(configData); }); } else { alert('저장 실패'); } } catch (e) { alert('오류 발생'); } }); } const formRec = document.getElementById('form-return-receive'); if (formRec) { formRec.addEventListener('submit', async (e) => { e.preventDefault(); const payload = { receive_date: document.getElementById('rec-date').value, tracking_no: document.getElementById('rec-tracking-no').value.trim(), mall: document.getElementById('rec-mall').value, receiver_name: document.getElementById('rec-name').value, phone: document.getElementById('rec-phone').value, address: document.getElementById('rec-address').value, remarks: document.getElementById('rec-remarks').value, receive_status: document.getElementById('rec-status').value }; const recId = document.getElementById('rec-id'); if (recId && recId.value) { payload.id = parseInt(recId.value, 10); } try { const res = await fetch('/api/return/receive', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); let result = {}; try { result = await res.json(); } catch(err) {} if(res.ok && result.success !== false) { formRec.reset(); document.getElementById('rec-date').value = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; if(recId) recId.value = ''; if (typeof loadReturnReceivings === 'function') { loadReturnReceivings(); } if (typeof loadReturnRequests === 'function') { loadReturnRequests(); } } else if (result.error_type === 'duplicate') { const dup = result.data || {}; alert( `🚨 [안내] 이미 입고된 송장번호입니다.\n\n` + `• 입고날짜: ${dup.receive_date || ''}\n` + `• 이름: ${dup.receiver_name || ''}\n` + `• 핸드폰 번호: ${dup.phone || ''}\n` + `• 쇼핑몰: ${dup.mall || ''}\n` + `• 입고상태: ${dup.receive_status || ''}\n` + `• 비고: ${dup.remarks || ''}` ); } else { alert('등록 실패'); } } catch(e) { alert('오류 발생'); } }); } // Bulk receive logic const btnBulkReceive = document.getElementById('btn-bulk-receive'); const bulkReceiveModal = document.getElementById('bulk-receive-modal'); const btnBulkCancel = document.getElementById('btn-bulk-cancel'); const btnBulkSubmit = document.getElementById('btn-bulk-submit'); const bulkTrackingNumbers = document.getElementById('bulk-tracking-numbers'); const bulkTrackingLineNumbers = document.getElementById('bulk-tracking-line-numbers'); const bulkPrefixInput = document.getElementById('bulk-prefix-input'); const btnBulkPrefixSave = document.getElementById('btn-bulk-prefix-save'); const BULK_TRACKING_PREFIX_SETTING = 'bulk_tracking_prefix'; function getBulkTrackingPrefix() { if (!bulkPrefixInput) return ''; const prefix = bulkPrefixInput.value.replace(/\D/g, '').slice(0, 4); bulkPrefixInput.value = prefix; return prefix; } function loadBulkTrackingPrefix() { if (!bulkPrefixInput) return; bulkPrefixInput.value = (configData.APP_SETTINGS && configData.APP_SETTINGS[BULK_TRACKING_PREFIX_SETTING]) || ''; if (btnBulkPrefixSave) btnBulkPrefixSave.style.backgroundColor = '#4a5568'; } async function saveBulkTrackingPrefix({ silent = false } = {}) { const prefix = getBulkTrackingPrefix(); const res = await fetch('/api/app/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [BULK_TRACKING_PREFIX_SETTING]: prefix }) }); if (!res.ok) throw new Error('Failed to save bulk tracking prefix'); if (!configData.APP_SETTINGS) configData.APP_SETTINGS = {}; configData.APP_SETTINGS[BULK_TRACKING_PREFIX_SETTING] = prefix; if (btnBulkPrefixSave) btnBulkPrefixSave.style.backgroundColor = '#4a5568'; if (!silent) alert('저장되었습니다.'); return prefix; } loadBulkTrackingPrefix(); function updateBulkTrackingLineNumbers() { if (!bulkTrackingNumbers || !bulkTrackingLineNumbers) return; const lineCount = bulkTrackingNumbers.value.split(/\r\n|\r|\n/).length; bulkTrackingLineNumbers.textContent = Array.from( { length: lineCount }, (_, index) => index + 1 ).join('\n'); bulkTrackingLineNumbers.scrollTop = bulkTrackingNumbers.scrollTop; } if (btnBulkReceive && bulkReceiveModal) { btnBulkReceive.addEventListener('click', () => { bulkReceiveModal.style.display = 'flex'; loadBulkTrackingPrefix(); updateBulkTrackingLineNumbers(); }); if (btnBulkPrefixSave && bulkPrefixInput) { bulkPrefixInput.addEventListener('input', () => { getBulkTrackingPrefix(); btnBulkPrefixSave.style.backgroundColor = '#e53e3e'; }); btnBulkPrefixSave.addEventListener('click', async () => { try { await saveBulkTrackingPrefix(); } catch (err) { console.error('Bulk prefix save failed:', err); alert('송장 앞 숫자 저장에 실패했습니다.'); } }); } btnBulkCancel.addEventListener('click', () => { bulkReceiveModal.style.display = 'none'; bulkTrackingNumbers.value = ''; updateBulkTrackingLineNumbers(); }); if (bulkTrackingNumbers) { bulkTrackingNumbers.addEventListener('input', updateBulkTrackingLineNumbers); bulkTrackingNumbers.addEventListener('scroll', () => { if (bulkTrackingLineNumbers) { bulkTrackingLineNumbers.scrollTop = bulkTrackingNumbers.scrollTop; } }); bulkTrackingNumbers.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); if (btnBulkSubmit) btnBulkSubmit.click(); } }); } btnBulkSubmit.addEventListener('click', async () => { const lines = bulkTrackingNumbers.value.split('\n'); let savedPrefix = getBulkTrackingPrefix(); try { savedPrefix = await saveBulkTrackingPrefix({ silent: true }); } catch (err) { console.error('Bulk prefix save failed:', err); alert('송장 앞 숫자 저장에 실패했습니다.'); return; } const trackingNumbers = lines.map(l => { let val = l.trim(); if (val && savedPrefix && val.length === 8) { return savedPrefix + val; } return val; }).filter(l => l); if (trackingNumbers.length === 0) { alert('송장번호를 입력해주세요.'); return; } btnBulkSubmit.disabled = true; btnBulkSubmit.textContent = '처리 중...'; try { const res = await fetch('/api/return/receive/bulk', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ tracking_numbers: trackingNumbers }) }); if (res.ok) { const data = await res.json(); bulkReceiveModal.style.display = 'none'; bulkTrackingNumbers.value = ''; updateBulkTrackingLineNumbers(); if (typeof loadReturnReceivings === 'function') { loadReturnReceivings(); } if (typeof loadReturnRequests === 'function') { loadReturnRequests(); } } else { alert('일괄 등록 실패'); } } catch (e) { alert('오류 발생'); } finally { btnBulkSubmit.disabled = false; btnBulkSubmit.textContent = '일괄 등록'; } }); } // Load Lists async function loadReturnRequests() { const tbody = document.getElementById('tbody-return-requests'); if(!tbody) return; try { const res = await fetch('/api/return/request'); const recRes = await fetch('/api/return/receive'); if(res.ok && recRes.ok) { const data = await res.json(); const recData = await recRes.json(); const receivedTrackingNos = new Set((recData.data || []).map(r => r.tracking_no).filter(t => t)); tbody.innerHTML = ''; if(data.data) { const excludeReceived = document.getElementById('chk-exclude-received')?.checked; let displayData = data.data; if (excludeReceived) { displayData = displayData.filter(i => !(i.tracking_no && receivedTrackingNos.has(i.tracking_no))); } displayData.forEach(item => { const isReceived = item.tracking_no && receivedTrackingNos.has(item.tracking_no); const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; let displayRemarks = item.remarks || ''; if (isReceived) { tr.style.textDecoration = 'line-through'; tr.style.color = '#a0aec0'; displayRemarks += ' [입고완료]'; } tr.addEventListener('click', () => { document.getElementById('req-date').value = item.request_date || ''; document.getElementById('req-type').value = item.request_type || ''; document.getElementById('req-name').value = item.receiver_name || ''; document.getElementById('req-address').value = item.address || ''; document.getElementById('req-phone').value = item.phone || ''; document.getElementById('req-tracking-no').value = item.tracking_no || ''; document.getElementById('req-mall').value = item.mall || ''; document.getElementById('req-remarks').value = item.remarks || ''; const reqId = document.getElementById('req-id'); if(reqId) reqId.value = item.id; }); const todayStr = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; if (item.request_date && !item.request_date.startsWith(todayStr)) { tr.style.backgroundColor = '#cbd5e0'; } const rowDataJson = JSON.stringify(item).replace(/'/g, "'").replace(/"/g, """); tr.innerHTML = ` ${formatShortDate(item.request_date)} ${item.receiver_name} ${item.phone} ${item.tracking_no} ${item.request_type} ${displayRemarks} `; tbody.appendChild(tr); }); const chkAll = document.getElementById('chk-all-req'); if (chkAll) { chkAll.checked = false; chkAll.onchange = (e) => { document.querySelectorAll('.chk-req-row').forEach(cb => cb.checked = e.target.checked); }; } document.querySelectorAll('.btn-delete-req').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); if(confirm('정말로 삭제하시겠습니까?')) { try { const delRes = await fetch(`/api/return/request/${e.target.dataset.id}`, {method: 'DELETE'}); if(delRes.ok) { if (typeof loadReturnRequests === 'function') loadReturnRequests(); if (typeof loadReturnReceivings === 'function') loadReturnReceivings(); } } catch(err) { alert('삭제 실패'); } } }); }); } } } catch(e) {} } async function loadReturnReceivings() { const tbody = document.getElementById('tbody-return-receivings'); if(!tbody) return; try { const res = await fetch('/api/return/receive'); if(res.ok) { const data = await res.json(); tbody.innerHTML = ''; if(data.data) { const excludeCompleted = document.getElementById('chk-exclude-completed')?.checked; let displayData = data.data; if (excludeCompleted) { displayData = displayData.filter(i => i.receive_status !== '환불완료' && i.receive_status !== '교환완료'); } displayData.forEach(item => { const tr = document.createElement('tr'); tr.style.cursor = 'pointer'; tr.addEventListener('click', () => { document.getElementById('rec-date').value = item.receive_date || ''; document.getElementById('rec-tracking-no').value = item.tracking_no || ''; document.getElementById('rec-mall').value = item.mall || ''; document.getElementById('rec-name').value = item.receiver_name || ''; document.getElementById('rec-phone').value = item.phone || ''; document.getElementById('rec-address').value = item.address || ''; document.getElementById('rec-remarks').value = item.remarks || ''; const recStatus = document.getElementById('rec-status'); if (recStatus) recStatus.value = item.receive_status || '환불 대기'; const recId = document.getElementById('rec-id'); if (recId) recId.value = item.id; }); const todayStr = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; if (item.receive_date && !item.receive_date.startsWith(todayStr)) { tr.style.backgroundColor = '#cbd5e0'; } const isCompleted = (item.receive_status === '환불완료' || item.receive_status === '교환완료'); const statusStyle = isCompleted ? 'color: #e53e3e; font-weight: bold;' : ''; tr.innerHTML = ` ${formatShortDate(item.receive_date)} ${item.receiver_name} ${item.phone} ${item.tracking_no} ${item.mall || ''} ${item.receive_status} ${(item.remarks || '').replace(/맞교환/g, `맞교환`)} `; tbody.appendChild(tr); }); document.querySelectorAll('.btn-delete-rec').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); if(confirm('정말로 삭제하시겠습니까?')) { try { const delRes = await fetch(`/api/return/receive/${e.target.dataset.id}`, {method: 'DELETE'}); if(delRes.ok) { if (typeof loadReturnReceivings === 'function') loadReturnReceivings(); if (typeof loadReturnRequests === 'function') loadReturnRequests(); } } catch(err) { alert('삭제 실패'); } } }); }); } } } catch(e) {} } // Initial load const returnTabMenu = document.querySelector('[data-tab="return-tab"]'); const chkExcludeCompleted = document.getElementById('chk-exclude-completed'); if (chkExcludeCompleted) { chkExcludeCompleted.addEventListener('change', () => { loadReturnReceivings(); }); } const chkExcludeReceived = document.getElementById('chk-exclude-received'); if (chkExcludeReceived) { chkExcludeReceived.addEventListener('change', () => { loadReturnRequests(); }); } if(returnTabMenu) { returnTabMenu.addEventListener('click', () => { loadReturnRequests(); loadReturnReceivings(); }); // Load immediately on page load setTimeout(() => { loadReturnRequests(); loadReturnReceivings(); }, 500); } const reqTrackingNo = document.getElementById('req-tracking-no'); const recTrackingNo = document.getElementById('rec-tracking-no'); // Auto-paste 12-digit tracking number on click async function handleTrackingClick(e) { try { const text = await navigator.clipboard.readText(); if (!text) return; const cleanText = text.replace(/[-\s]/g, ''); // Check if exactly 12 digits as requested if (/^\d{12}$/.test(cleanText)) { e.target.value = cleanText; e.target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); } } catch(err) { console.log('Clipboard read failed: ', err); } } if (reqTrackingNo) reqTrackingNo.addEventListener('click', handleTrackingClick); if (recTrackingNo) recTrackingNo.addEventListener('click', handleTrackingClick); // Default dates to today const reqDate = document.getElementById('req-date'); if (reqDate) reqDate.value = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; const recDate = document.getElementById('rec-date'); if (recDate) recDate.value = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; // Search on Return Request tab if(reqTrackingNo) { reqTrackingNo.addEventListener('keydown', async (e) => { if (e.key === 'Enter') { e.preventDefault(); const no = reqTrackingNo.value.trim(); if(!no) return; try { const res = await fetch('/api/return/lookup?tracking_no=' + encodeURIComponent(no)); if(res.ok) { const data = await res.json(); if(data.success && data.data) { document.getElementById('req-mall').value = data.data.vendor || ''; document.getElementById('req-name').value = data.data.recipient_name || ''; document.getElementById('req-phone').value = data.data.recipient_phone || data.data.recipient_mobile || ''; document.getElementById('req-address').value = data.data.address || ''; } } } catch(e) { console.error('Lookup failed', e); } } }); } // Search on Return Receive tab if(recTrackingNo) { recTrackingNo.addEventListener('keydown', async (e) => { if (e.key === 'Enter') { e.preventDefault(); const no = recTrackingNo.value.trim(); if(!no) return; try { // First try to find in return requests const res1 = await fetch('/api/return/request?tracking_no=' + encodeURIComponent(no)); let foundInReq = false; if(res1.ok) { const data1 = await res1.json(); if(data1.success && data1.data && data1.data.length > 0) { const item = data1.data[0]; document.getElementById('rec-mall').value = item.mall || ''; document.getElementById('rec-name').value = item.receiver_name || ''; document.getElementById('rec-phone').value = item.phone || ''; const reqType = item.request_type || ''; const reqRemarks = item.remarks || ''; const autoRemark = reqRemarks ? `${reqType}/${reqRemarks}` : reqType; document.getElementById('rec-remarks').value = autoRemark; if (item.address) document.getElementById('rec-address').value = item.address; foundInReq = true; } } // If not found in requests, fallback to orderlist_app if(!foundInReq) { const res2 = await fetch('/api/return/lookup?tracking_no=' + encodeURIComponent(no)); if(res2.ok) { const data2 = await res2.json(); if(data2.success && data2.data) { document.getElementById('rec-mall').value = data2.data.vendor || ''; document.getElementById('rec-name').value = data2.data.recipient_name || ''; document.getElementById('rec-phone').value = data2.data.recipient_phone || data2.data.recipient_mobile || ''; document.getElementById('rec-address').value = data2.data.address || ''; } } } } catch(e) { console.error('Receive lookup failed', e); } } }); } // Submit Return Request const formReq = document.getElementById('form-return-request'); if(formReq) { formReq.addEventListener('submit', async (e) => { e.preventDefault(); const payload = { request_date: document.getElementById('req-date').value, request_type: document.getElementById('req-type').value, receiver_name: document.getElementById('req-name').value, address: document.getElementById('req-address').value, phone: document.getElementById('req-phone').value, tracking_no: document.getElementById('req-tracking-no').value, mall: document.getElementById('req-mall').value, remarks: document.getElementById('req-remarks').value }; const reqId = document.getElementById('req-id'); if (reqId && reqId.value) { payload.id = parseInt(reqId.value, 10); } try { const res = await fetch('/api/return/request', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); if(res.ok) { formReq.reset(); document.getElementById('req-date').value = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; if(reqId) reqId.value = ''; if (typeof loadReturnRequests === 'function') { loadReturnRequests(); } if (typeof loadReturnReceivings === 'function') { loadReturnReceivings(); } } else { alert('등록 실패'); } } catch(e) { alert('오류 발생'); } }); } // ============================================ // PRODUCT SETTINGS TAB // ============================================ window.productRulesGroupData = {}; window.productRulesListData = {}; function buildProductTab(data) { const container = document.getElementById('product-groups-container'); if (!container) return; container.innerHTML = ''; const groupNames = data['GROUP_NAMES'] || {}; window.productRulesGroupData = {}; window.productRulesListData = {}; let groupIdCounter = 0; const order_group = groupNames.__order || Object.keys(groupNames).filter(k => k !== '__order'); for (const gKey of order_group) { if (gKey === '__order') continue; const gName = groupNames[gKey]; groupIdCounter++; const gId = `pgroup-${groupIdCounter}`; window.productRulesGroupData[gId] = { old_code: gKey, new_code: gKey, name: gName }; const pList = data[gKey] || {}; window.productRulesListData[gId] = []; const order_3 = pList.__order || Object.keys(pList).filter(k => k !== '__order'); for (const pKey of order_3) { if (pKey === '__order') continue; const pVal = pList[pKey]; if (pKey.startsWith('---separator_')) { window.productRulesListData[gId].push({ is_separator: true }); } else { let is_free = pKey.startsWith('*'); let cleanKey = is_free ? pKey.substring(1) : pKey; let color = ''; if (cleanKey.endsWith('_O')) { color = '_O'; cleanKey = cleanKey.slice(0, -2); } else if (cleanKey.endsWith('_R')) { color = '_R'; cleanKey = cleanKey.slice(0, -2); } else if (cleanKey.endsWith('_B')) { color = '_B'; cleanKey = cleanKey.slice(0, -2); } else if (cleanKey.endsWith('_G')) { color = '_G'; cleanKey = cleanKey.slice(0, -2); } else if (cleanKey.endsWith('_P')) { color = '_P'; cleanKey = cleanKey.slice(0, -2); } let label = cleanKey; let parts = pVal.split('|').map(s => s.trim()); let code = parts[0] || ''; let name = parts[1] || ''; let price = parts.length > 2 ? parseInt(parts[2], 10) : 0; let dprice = parts.length > 3 ? parseInt(parts[3], 10) : 0; window.productRulesListData[gId].push({ is_separator: false, code, name, label, price, dprice, color, is_free }); } } renderProductGroup(gId, container); } const btnProductSave = document.getElementById('btn-product-save'); if (btnProductSave) btnProductSave.style.backgroundColor = ''; const markProductUnsaved = () => { if (btnProductSave) btnProductSave.style.backgroundColor = '#e53e3e'; }; container.addEventListener('input', markProductUnsaved); container.addEventListener('change', markProductUnsaved); container.addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON') markProductUnsaved(); }); } function renderProductGroup(gId, parentContainer) { let card = document.getElementById(gId); if (!card) { card = document.createElement('div'); card.id = gId; card.className = 'glass-card'; card.style.padding = '10px 15px'; card.style.marginBottom = '10px'; card.style.width = 'max-content'; // 박스 전체 폭을 콘텐츠에 맞게 축소 parentContainer.appendChild(card); } const gData = window.productRulesGroupData[gId]; const items = window.productRulesListData[gId]; const fmt = (v) => v ? parseInt(String(v).replace(/,/g, ''), 10).toLocaleString() : ''; // 엑셀 시트처럼 보이도록 +colgroup을 사용합니다. 헤더(thead)와 // 본문(tbody)이 같은 표(table)에 속하므로 table-layout:fixed 컬럼 폭이 // 브라우저에 의해 강제로 100% 동일하게 맞춰집니다. table-layout:fixed는 // width가 auto이면 컨테이너 크기에 따라 폭 계산이 애매해질 수 있어, // colgroup 합계와 정확히 같은 고정 width를 표에 명시적으로 지정합니다. const COL_WIDTHS = [34, 170, 90, 210, 80, 80, 350, 85, 55]; const TABLE_WIDTH = COL_WIDTHS.reduce((a, b) => a + b, 0); const CELL_BORDER = 'border-right: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;'; const ROW_BOTTOM = 'border-bottom: 1px solid #e2e8f0;'; const HEADER_TH = 'box-sizing: border-box; text-align: center; vertical-align: middle; padding: 8px 4px; font-size: 0.82rem; font-weight: 700;'; const TD_BASE = 'box-sizing: border-box; vertical-align: middle; height: 30px;'; const colgroup = COL_WIDTHS.map(w => ``).join(''); let html = `
${gData.name}
${colgroup} `; items.forEach((item, idx) => { if (item.is_separator) { html += ` `; } else { html += ` `; } }); html += `
표기명 코드 정식옵션명 가격 2+1가격 색상 옵션 무료배송 관리
--- 구분선 ---
`; card.innerHTML = html; const rows = card.querySelectorAll('.product-items-list > .pr-item-row'); rows.forEach((row, i) => { if (!items[i].is_separator) { row.querySelector('.p-label').addEventListener('input', e => items[i].label = e.target.value); row.querySelector('.p-code').addEventListener('input', e => items[i].code = e.target.value); row.querySelector('.p-name').addEventListener('input', e => items[i].name = e.target.value); ['.p-price', '.p-dprice'].forEach(sel => { row.querySelector(sel).addEventListener('input', e => { let val = e.target.value.replace(/[^0-9]/g, ''); if (sel === '.p-price') items[i].price = parseInt(val, 10) || 0; else items[i].dprice = parseInt(val, 10) || 0; e.target.value = val ? parseInt(val, 10).toLocaleString() : ''; }); }); row.querySelectorAll(`input[name="color-${gId}-${i}"]`).forEach(radio => { radio.addEventListener('change', e => { if (e.target.checked) items[i].color = e.target.value; }); }); row.querySelector('.p-free').addEventListener('change', e => items[i].is_free = e.target.checked); } }); // SortableJS for bulletproof drag & drop new Sortable(card.querySelector('.product-items-list'), { handle: '.drag-handle', animation: 150, onEnd: function (evt) { if (evt.oldIndex !== evt.newIndex) { const movedItem = items.splice(evt.oldIndex, 1)[0]; items.splice(evt.newIndex, 0, movedItem); const btnSave = document.getElementById('btn-product-save'); if (btnSave) btnSave.style.backgroundColor = '#e53e3e'; // Optional: renderProductGroup() if we need to sync data-idx, but items array is already updated in sync with DOM! // It's safer to re-render to attach listeners based on new indices though: renderProductGroup(gId, parentContainer); } } }); card.querySelectorAll('.p-del').forEach(b => { b.addEventListener('click', e => { const parent = document.getElementById('product-groups-container'); const y = parent ? parent.scrollTop : 0; const i = parseInt(e.target.dataset.idx, 10); items.splice(i, 1); renderProductGroup(gId, parentContainer); if (parent) requestAnimationFrame(() => parent.scrollTo(0, y)); }); }); card.querySelector('.btn-add-item').addEventListener('click', (e) => { e.preventDefault(); const parent = document.getElementById('product-groups-container'); const y = parent ? parent.scrollTop : 0; items.push({ is_separator: false, code: '', name: '', label: '', price: 0, dprice: 0, color: '', is_free: false }); renderProductGroup(gId, parentContainer); if (parent) requestAnimationFrame(() => parent.scrollTo(0, y)); }); card.querySelector('.btn-add-sep').addEventListener('click', (e) => { e.preventDefault(); const parent = document.getElementById('product-groups-container'); const y = parent ? parent.scrollTop : 0; items.push({ is_separator: true }); renderProductGroup(gId, parentContainer); if (parent) requestAnimationFrame(() => parent.scrollTo(0, y)); }); } const _btnProdSave = document.getElementById('btn-product-save'); if (_btnProdSave) { _btnProdSave.addEventListener('click', () => { const payload = { groups: [], products: {} }; // window.productRulesGroupData for (const gId in window.productRulesGroupData) { const gData = window.productRulesGroupData[gId]; if (gData.new_code && gData.name) { payload.groups.push(gData); payload.products[gData.new_code] = window.productRulesListData[gId]; } } document.getElementById('btn-product-save').innerText = '저장 중...'; fetch('/api/products/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(r => { if (!r.ok) throw new Error("Network response was not ok (status " + r.status + ")"); return r.json(); }).then(res => { alert("상품 설정이 저장되었습니다."); document.getElementById('btn-product-save').innerText = '상품 설정 저장'; fetch('/api/config').then(r => r.json()).then(d => { configData = d; document.getElementById('btn-product-save').style.backgroundColor = ''; // Clear all UI and rebuild const oc = document.getElementById('order-items-container'); if (oc) oc.innerHTML = ''; buildOrderTab(configData); buildGiftTab(configData); buildProductTab(configData); }); }).catch(err => { console.error(err); alert("저장 실패!"); document.getElementById('btn-product-save').innerText = '상품 설정 저장'; }); }); } const btnCopyManual = document.getElementById('btn-copy-manual-total'); if (btnCopyManual) { btnCopyManual.addEventListener('click', () => { const count = document.getElementById('manual-total-count').textContent; const amount = document.getElementById('manual-total-amount').textContent; const textToCopy = `수기 주문: ${count}건 / ${amount}원`; navigator.clipboard.writeText(textToCopy).then(() => { const toast = document.getElementById('toast-notification'); if (toast) { toast.style.opacity = '1'; setTimeout(() => { toast.style.opacity = '0'; }, 1500); } }).catch(err => { console.error('Failed to copy', err); }); }); } // ============================================ // CODE TAB (Master Data Mgmt) // ============================================ function initCodeTab() { const btnAddSin = document.getElementById('btn-add-single'); const btnAddSet = document.getElementById('btn-add-set'); if (!btnAddSin) return; let editingSingleCode = null; let editingSetCode = null; let singleItemsList = []; let setItemsList = []; let singleSort = { key: 'item_code', asc: true }; let setSort = { key: 'item_code', asc: true }; let singleSearch = ''; let setSearch = ''; // 검색어(공백 무시, 대소문자 무시)로 이름을 필터링 const normalizeKeyword = (str) => (str || '').toString().toLowerCase().replace(/\s+/g, ''); const matchKeyword = (name, keyword) => !keyword || normalizeKeyword(name).includes(keyword); const emptyRowHtml = (colspan) => `검색 결과가 없습니다.`; // Render Single Items const renderSingleItems = () => { const tbody = document.getElementById('single-items-tbody'); tbody.innerHTML = ''; singleItemsList.sort((a,b) => { let valA = a[singleSort.key] || ''; let valB = b[singleSort.key] || ''; if(valA < valB) return singleSort.asc ? -1 : 1; if(valA > valB) return singleSort.asc ? 1 : -1; return 0; }); const visibleSingleItems = singleItemsList.filter(item => matchKeyword(item.name, singleSearch)); if (visibleSingleItems.length === 0) tbody.innerHTML = emptyRowHtml(4); visibleSingleItems.forEach(item => { const tr = document.createElement('tr'); tr.className = 'hover-row'; tr.style.height = '27px'; tr.style.borderBottom = '1px solid #e2e8f0'; tr.innerHTML = ` ${item.item_code} ${item.sabangnet_code} ${item.name}
`; tbody.appendChild(tr); }); document.querySelectorAll('.th-sortable[data-list="single"]').forEach(th => { const icon = th.querySelector('.sort-icon'); if(!icon) return; if(th.dataset.key === singleSort.key) { icon.innerText = singleSort.asc ? '▲' : '▼'; icon.style.color = '#3182ce'; } else { icon.innerText = '↕'; icon.style.color = '#e2e8f0'; } }); }; // Load data const loadSingleItems = () => { return fetch('/api/codes/single').then(r=>r.json()).then(res => { singleItemsList = res.data; renderSingleItems(); }); }; const renderSetItems = () => { const tbody = document.getElementById('set-items-tbody'); tbody.innerHTML = ''; let tooltip = document.getElementById('set-comp-tooltip'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.id = 'set-comp-tooltip'; tooltip.style.position = 'fixed'; tooltip.style.background = '#fff'; tooltip.style.border = '1px solid #cbd5e0'; tooltip.style.padding = '8px'; tooltip.style.borderRadius = '5px'; tooltip.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; tooltip.style.display = 'none'; tooltip.style.zIndex = '9999'; tooltip.style.pointerEvents = 'none'; tooltip.style.flexDirection = 'column'; tooltip.style.gap = '2px'; document.body.appendChild(tooltip); } const mapCompInfo = c => { const sInfo = singleItemsList.find(s => s.item_code === c.single_code); let displayName = sInfo ? sInfo.name : c.single_code; displayName = displayName.replace(/미라클 통 /g, '').trim(); const isHwangto = displayName.includes('황토'); const bg = isHwangto ? 'background:#feebc8;' : 'background:#edf2f7;'; const border = isHwangto ? 'border:1px solid #fbd38d;' : 'border:1px solid #cbd5e0;'; const qtyColor = isHwangto ? '#dd6b20' : '#3182ce'; const textColor = isHwangto ? 'color:#c05621;' : ''; return `${displayName} ${c.quantity}`; }; setItemsList.sort((a,b) => { let valA = a[setSort.key] || ''; let valB = b[setSort.key] || ''; if(valA < valB) return setSort.asc ? -1 : 1; if(valA > valB) return setSort.asc ? 1 : -1; return 0; }); const visibleSetItems = setItemsList.filter(item => matchKeyword(item.name, setSearch)); if (visibleSetItems.length === 0) tbody.innerHTML = emptyRowHtml(4); visibleSetItems.forEach(item => { const tr = document.createElement('tr'); tr.className = 'hover-row'; tr.style.height = '27px'; tr.style.borderBottom = '1px solid #e2e8f0'; const compsHtml = item.components.map(mapCompInfo).join(''); tr.innerHTML = ` ${item.item_code} ${item.name || ''}
${compsHtml}
`; tr._itemComps = item.components; tbody.appendChild(tr); }); // 모든 행이 그려진 후 브라우저가 그리드를 계산할 여유를 주기 위해 지연실행 setTimeout(() => { const rows = tbody.querySelectorAll('tr'); rows.forEach(tr => { const wrapper = tr.querySelector('.comps-wrapper'); if (!wrapper) return; const spans = wrapper.querySelectorAll('.comp-span'); const indicator = wrapper.querySelector('.more-indicator'); if (spans.length > 0) { let hiddenCount = 0; let lastVisibleIndex = spans.length - 1; // X좌표를 기준으로 컨테이너를 벗어나는 아이템 탐색 const maxLimit = wrapper.offsetWidth - 48; // 오른쪽 48px는 '외 N개' 표기를 위해 예약 for (let i = 0; i < spans.length; i++) { if (spans[i].offsetLeft + spans[i].offsetWidth > maxLimit) { lastVisibleIndex = i - 1; break; } } if (lastVisibleIndex < spans.length - 1 && lastVisibleIndex >= 0) { hiddenCount = spans.length - 1 - lastVisibleIndex; for (let i = lastVisibleIndex + 1; i < spans.length; i++) { spans[i].style.display = 'none'; } indicator.innerText = `외 ${hiddenCount}개`; indicator.style.display = 'inline-block'; } else if (lastVisibleIndex < 0 && spans.length > 1) { // 매우 좁은 공간일 경우 적어도 1개는 숨기고 처리 hiddenCount = spans.length - 1; for (let i = 1; i < spans.length; i++) { spans[i].style.display = 'none'; } indicator.innerText = `외 ${hiddenCount}개`; indicator.style.display = 'inline-block'; } if (hiddenCount > 0 && tr._itemComps) { const compsCell = tr.querySelector('.comps-cell'); compsCell.addEventListener('mouseenter', (e) => { compsCell._hoverTimeout = setTimeout(() => { tooltip.innerHTML = tr._itemComps.map(mapCompInfo).map(html => `
${html}
`).join(''); tooltip.style.display = 'flex'; tooltip.style.left = (e.clientX + 40) + 'px'; tooltip.style.top = (e.clientY - 10) + 'px'; }, 1000); }); compsCell.addEventListener('mousemove', (e) => { if (tooltip.style.display === 'flex') { tooltip.style.left = (e.clientX + 40) + 'px'; tooltip.style.top = (e.clientY - 10) + 'px'; } }); compsCell.addEventListener('mouseleave', () => { clearTimeout(compsCell._hoverTimeout); tooltip.style.display = 'none'; }); } } }); }, 50); document.querySelectorAll('.th-sortable[data-list="set"]').forEach(th => { const icon = th.querySelector('.sort-icon'); if(!icon) return; if(th.dataset.key === setSort.key) { icon.innerText = setSort.asc ? '▲' : '▼'; icon.style.color = '#3182ce'; } else { icon.innerText = '↕'; icon.style.color = '#e2e8f0'; } }); }; const loadSetItems = () => { return fetch('/api/codes/set').then(r=>r.json()).then(res => { setItemsList = res.data; renderSetItems(); }); }; const loadSingleItemsAndSetItems = () => { loadSingleItems().then(() => loadSetItems()); }; window.reRenderSetItemsForWrap = () => { if (setItemsList && setItemsList.length > 0) renderSetItems(); }; window.addEventListener('resize', () => { if(document.getElementById('code-tab').classList.contains('active')) { clearTimeout(window._setItemsResizeTimer); window._setItemsResizeTimer = setTimeout(() => { if (setItemsList && setItemsList.length > 0) renderSetItems(); }, 100); } }); loadSingleItemsAndSetItems(); // 실시간 이름 검색 (단품 / 세트) const bindSearchBox = (inputId, clearBtnId, onChange) => { const input = document.getElementById(inputId); const clearBtn = document.getElementById(clearBtnId); if (!input) return; input.addEventListener('input', () => onChange(normalizeKeyword(input.value))); input.addEventListener('keydown', (e) => { if (e.key === 'Escape') { input.value = ''; onChange(''); } }); if (clearBtn) { clearBtn.addEventListener('click', () => { input.value = ''; onChange(''); input.focus(); }); } }; bindSearchBox('single-search-input', 'btn-single-search-clear', (kw) => { singleSearch = kw; renderSingleItems(); }); bindSearchBox('set-search-input', 'btn-set-search-clear', (kw) => { setSearch = kw; renderSetItems(); }); // Sorting events document.querySelectorAll('.th-sortable').forEach(th => { th.addEventListener('click', (e) => { const listType = th.dataset.list; const key = th.dataset.key; if(listType === 'single') { if(singleSort.key === key) singleSort.asc = !singleSort.asc; else { singleSort.key = key; singleSort.asc = true; } renderSingleItems(); } else if(listType === 'set') { if(setSort.key === key) setSort.asc = !setSort.asc; else { setSort.key = key; setSort.asc = true; } renderSetItems(); } }); }); document.getElementById('code-tab').addEventListener('click', (e) => { if(e.target.parentElement && e.target.parentElement.classList.contains('btn-del-sin')) return; // ignore if click is inside not button const btnDelSin = e.target.closest('.btn-del-sin'); if(btnDelSin) { if(confirm('단품 항목을 삭제하시겠습니까? (연결된 세트 구성품도 삭제될 수 있습니다)')) { fetch('/api/codes/single/' + btnDelSin.dataset.code, {method: 'DELETE'}).then(()=> loadSingleItemsAndSetItems()); } return; } const btnDelSet = e.target.closest('.btn-del-set'); if(btnDelSet) { if(confirm('세트 항목을 삭제하시겠습니까?')) { fetch('/api/codes/set/' + btnDelSet.dataset.code, {method: 'DELETE'}).then(()=> { setItemsList = []; loadSetItems(); }); } return; } const btnEditSin = e.target.closest('.btn-edit-sin'); if(btnEditSin) { const code = btnEditSin.dataset.code; fetch('/api/codes/single').then(r=>r.json()).then(res => { const item = res.data.find(d => d.item_code === code); if(item) { editingSingleCode = code; document.getElementById('modal-single-title').innerText = '📦 단품 수정'; document.getElementById('sin-item-code').value = item.item_code; document.getElementById('sin-item-code').disabled = true; document.getElementById('sin-sabangnet-code').value = item.sabangnet_code; document.getElementById('sin-name').value = item.name; document.getElementById('single-item-modal').style.display = 'flex'; } }); } const btnEditSet = e.target.closest('.btn-edit-set'); if(btnEditSet) { const code = btnEditSet.dataset.code; fetch('/api/codes/set').then(r=>r.json()).then(res => { const item = res.data.find(d => d.item_code === code); if(item) { editingSetCode = code; document.getElementById('modal-set-title').innerText = '🍱 세트 수정'; document.getElementById('set-item-code').value = item.item_code; document.getElementById('set-item-code').disabled = true; document.getElementById('set-name').value = item.name; const wrapper = document.getElementById('set-components-wrapper'); wrapper.innerHTML = ''; item.components.forEach(c => addSetComponentRow(c.single_code, c.quantity)); document.getElementById('set-item-modal').style.display = 'flex'; } }); } }); // Single Modal const sinItemCodeInput = document.getElementById('sin-item-code'); const sinItemWarn = document.getElementById('sin-item-warn'); sinItemCodeInput.addEventListener('input', (e) => { let val = e.target.value.toUpperCase(); e.target.value = val; if (val.length > 0 && !/^[A-Z]/.test(val)) { sinItemWarn.textContent = '⚠️ 아이템 코드 첫 글자는 영문이어야 합니다.'; sinItemWarn.style.display = 'block'; } else if(!editingSingleCode && val && singleItemsList.some(s => s.item_code === val)) { sinItemWarn.textContent = '⚠️ 이미 존재하는 단품 코드입니다!'; sinItemWarn.style.display = 'block'; } else { sinItemWarn.style.display = 'none'; } }); btnAddSin.addEventListener('click', () => { editingSingleCode = null; document.getElementById('modal-single-title').innerText = '📦 새 단품 추가'; sinItemCodeInput.value = ''; sinItemCodeInput.disabled = false; if(sinItemWarn) sinItemWarn.style.display = 'none'; document.getElementById('sin-sabangnet-code').value = ''; document.getElementById('sin-name').value = ''; document.getElementById('single-item-modal').style.display = 'flex'; }); document.getElementById('btn-sin-cancel').addEventListener('click', () => { document.getElementById('single-item-modal').style.display = 'none'; }); document.getElementById('btn-sin-save').addEventListener('click', () => { const itemCode = document.getElementById('sin-item-code').value.trim(); let sabangnet = document.getElementById('sin-sabangnet-code').value.trim(); const name = document.getElementById('sin-name').value.trim(); if(!itemCode || !sabangnet || !name) { alert('모든 항목을 입력해주세요.'); return; } if (itemCode.length > 0 && !/^[A-Z]/.test(itemCode)) { alert('아이템 코드의 첫 글자는 반드시 영문이어야 합니다!'); return; } if(!editingSingleCode && singleItemsList.some(s => s.item_code === itemCode)) { alert('이미 존재하는 단품 아이템 코드입니다!'); return; } if(!sabangnet.endsWith('-0001')) sabangnet += '-0001'; const payload = {item_code: itemCode, sabangnet_code: sabangnet, name: name}; const method = editingSingleCode ? 'PUT' : 'POST'; const url = editingSingleCode ? '/api/codes/single/' + encodeURIComponent(editingSingleCode) : '/api/codes/single'; fetch(url, { method: method, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }).then(r => r.json()).then(res => { if(res.detail) { alert(res.detail); } else { document.getElementById('single-item-modal').style.display = 'none'; loadSingleItems(); } }); }); // Single sabangnet auto append -0001 on blur document.getElementById('sin-sabangnet-code').addEventListener('blur', (e) => { let val = e.target.value.trim(); if(val && !val.endsWith('-0001') && !val.includes('-')) { e.target.value = val + '-0001'; } }); // Set Modal const setItemCodeInput = document.getElementById('set-item-code'); const setItemWarn = document.getElementById('set-item-warn'); setItemCodeInput.addEventListener('input', (e) => { let val = e.target.value.toUpperCase(); e.target.value = val; if (val.length > 0 && !/^[A-Z]/.test(val)) { setItemWarn.textContent = '⚠️ 아이템 코드 첫 글자는 영문이어야 합니다.'; setItemWarn.style.display = 'block'; } else if(!editingSetCode && val && setItemsList.some(s => s.item_code === val)) { setItemWarn.textContent = '⚠️ 이미 존재하는 세트 코드입니다!'; setItemWarn.style.display = 'block'; } else { setItemWarn.style.display = 'none'; } }); const addSetComponentRow = (sc = '', qt = 1) => { const div = document.createElement('div'); div.style.display = 'flex'; div.style.gap = '5px'; div.style.alignItems = 'center'; let optionsHtml = ''; singleItemsList.forEach(s => { const selected = (s.item_code === sc) ? 'selected' : ''; optionsHtml += ``; }); div.innerHTML = `
`; div.querySelector('.comp-del').addEventListener('click', () => div.remove()); document.getElementById('set-components-wrapper').appendChild(div); }; btnAddSet.addEventListener('click', () => { editingSetCode = null; document.getElementById('modal-set-title').innerText = '🍱 새 세트 추가'; setItemCodeInput.value = ''; setItemCodeInput.disabled = false; if(setItemWarn) setItemWarn.style.display = 'none'; document.getElementById('set-name').value = ''; document.getElementById('set-components-wrapper').innerHTML = ''; addSetComponentRow(); document.getElementById('set-item-modal').style.display = 'flex'; }); document.getElementById('btn-add-set-component').addEventListener('click', () => { addSetComponentRow(); }); document.getElementById('btn-set-cancel').addEventListener('click', () => { document.getElementById('set-item-modal').style.display = 'none'; }); document.getElementById('btn-set-save').addEventListener('click', () => { const itemCode = document.getElementById('set-item-code').value.trim(); const name = document.getElementById('set-name').value.trim(); if(!itemCode) { alert('세트 아이템 코드를 입력해주세요.'); return; } if (itemCode.length > 0 && !/^[A-Z]/.test(itemCode)) { alert('아이템 코드의 첫 글자는 반드시 영문이어야 합니다!'); return; } if(!editingSetCode && setItemsList.some(s => s.item_code === itemCode)) { alert('이미 존재하는 세트 아이템 코드입니다!'); return; } const components = []; const rows = document.getElementById('set-components-wrapper').querySelectorAll(':scope > div'); rows.forEach(r => { const compCodeEl = r.querySelector('.comp-code'); const compQtyEl = r.querySelector('.comp-qty'); if (compCodeEl && compQtyEl) { const sc = compCodeEl.value.trim(); const qt = parseInt(compQtyEl.value, 10); if(sc && !isNaN(qt) && qt > 0) { components.push({single_code: sc, quantity: qt}); } } }); if(components.length === 0) { alert('유효한 단품 구성품을 최소 1개 이상 선택해주세요.'); return; } const payload = {item_code: itemCode, components: components, sabangnet_code: "", name: name}; const method = editingSetCode ? 'PUT' : 'POST'; const url = editingSetCode ? '/api/codes/set/' + encodeURIComponent(editingSetCode) : '/api/codes/set'; fetch(url, { method: method, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }).then(r => r.json()).then(res => { if(res.detail) { alert(res.detail); } else { document.getElementById('set-item-modal').style.display = 'none'; loadSetItems(); } }); }); // Initialize SortableJS on set components wrapper const setCompWrapper = document.getElementById('set-components-wrapper'); if (setCompWrapper && typeof Sortable !== 'undefined') { new Sortable(setCompWrapper, { handle: '.drag-handle', animation: 150, ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen' }); } } initCodeTab(); }); const btnEmailReq = document.getElementById('btn-email-req'); if (btnEmailReq) { btnEmailReq.addEventListener('click', async () => { const checkedBoxes = document.querySelectorAll('.chk-req-row:checked'); if (checkedBoxes.length === 0) { alert('이메일로 보낼 반품 신청 항목을 선택해주세요.'); return; } const d = new Date(); const yyyy = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); const days = ['일', '월', '화', '수', '목', '금', '토']; const dow = days[d.getDay()]; const subject = `★ ${yyyy}년 ${m}월 ${dd}일(${dow}) 미라네주방 반품 신청`; let plainTextGreeting = "안녕하세요.\r\n오늘 반품 신청합니다.\r\n내용은 아래와 같습니다.\r\n감사합니다.\r\n\r\n"; let ptIndex = 1; checkedBoxes.forEach(cb => { try { const item = JSON.parse(cb.dataset.item); let ptStatus = item.request_type || ''; if (ptStatus === '맞교환') { ptStatus = '★맞교환★'; } plainTextGreeting += `${ptIndex}. 이름: ${item.receiver_name || ''} / 연락처: ${item.phone || ''} / 송장번호: ${item.tracking_no || ''} / 상태: ${ptStatus} `; ptIndex++; } catch(e) {} }); let htmlTable = ` `; let index = 1; checkedBoxes.forEach(cb => { try { const item = JSON.parse(cb.dataset.item); let statusText = item.request_type || ''; let trStyle = ''; if (statusText === '맞교환') { trStyle = ' style="font-weight: bold; color: red;"'; } htmlTable += ` `; } catch(e) {} }); htmlTable += `
번호 이름 전화번호 송장번호 상태
${index++} ${item.receiver_name || ''} ${item.phone || ''} ${item.tracking_no || ''} ${statusText}
`; try { const blobHtml = new Blob([htmlTable], { type: 'text/html' }); const blobText = new Blob(["표가 클립보드에 복사되었습니다. 메일 본문에 붙여넣기 하세요."], { type: 'text/plain' }); const data = new ClipboardItem({ 'text/html': blobHtml, 'text/plain': blobText }); navigator.clipboard.write([data]).then(() => { showToast("표가 클립보드에 복사되었습니다! 아웃룩 본문에 '붙여넣기(Ctrl+V)' 하세요."); setTimeout(() => { const mailtoLink = `mailto:skylove6969@hanmail.net?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(plainTextGreeting)}`; window.location.href = mailtoLink; }, 1000); }).catch(err => { alert('클립보드 복사에 실패했습니다.'); const mailtoLink = `mailto:skylove6969@hanmail.net?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(plainTextGreeting)}`; window.location.href = mailtoLink; }); } catch(e) { const mailtoLink = `mailto:skylove6969@hanmail.net?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(plainTextGreeting)}`; window.location.href = mailtoLink; } }); } // Clear form when clicking empty space around inputs function handleFormEmptyClick(e, formId) { // If the click is on an input, select, button, label, or textarea, do nothing if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'LABEL' || e.target.tagName === 'TEXTAREA') return; if (e.target.closest('button')) return; // Otherwise it's an empty space click const form = document.getElementById(formId); if (form) { form.reset(); // Reset hidden IDs and dates if (formId === 'form-return-request') { const reqId = document.getElementById('req-id'); if (reqId) reqId.value = ''; const reqDate = document.getElementById('req-date'); if (reqDate) reqDate.value = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; } else if (formId === 'form-return-receive') { const recId = document.getElementById('rec-id'); if (recId) recId.value = ''; const recDate = document.getElementById('rec-date'); if (recDate) recDate.value = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; } } } const reqFormContainer = document.getElementById('form-return-request'); if (reqFormContainer) { reqFormContainer.addEventListener('click', (e) => handleFormEmptyClick(e, 'form-return-request')); } const recFormContainer = document.getElementById('form-return-receive'); if (recFormContainer) { recFormContainer.addEventListener('click', (e) => handleFormEmptyClick(e, 'form-return-receive')); } // ============================================ // Version Update Notice // ============================================ document.addEventListener('DOMContentLoaded', () => { let CURRENT_APP_VERSION = "9.0"; const versionEl = document.getElementById('app-version-text'); if (versionEl) { const text = versionEl.textContent.trim(); const match = text.match(/Ver\s+([0-9.]+)/i); if (match) { CURRENT_APP_VERSION = match[1]; } } const updateModal = document.getElementById('update-notice-modal'); if (updateModal) { document.getElementById('update-notice-version').textContent = CURRENT_APP_VERSION; const storageKey = `hide_update_popup_${CURRENT_APP_VERSION}`; if (localStorage.getItem(storageKey) !== 'true') { updateModal.style.display = 'flex'; } document.getElementById('btn-close-update-notice').addEventListener('click', () => { if (document.getElementById('chk-hide-update-notice').checked) { localStorage.setItem(storageKey, 'true'); } updateModal.style.display = 'none'; // Switch to Version History tab const historyTab = document.querySelector('[data-tab="version-history-tab"]'); if (historyTab) { historyTab.click(); } }); } });