// 자사몰 행사 탭 - 카페24 주문/발주 파일 가공 // // 업로드 → 조건 선택 → 미리보기 → 가공된 엑셀 다운로드 흐름을 담당한다. // 기능이 늘어나면 initFirstComeGift() 처럼 기능별 초기화 함수를 추가하고 // 왼쪽 '기능' 목록 버튼(.me-feature-btn)과 패널(#me-panel-)을 연결한다. (function () { 'use strict'; document.addEventListener('DOMContentLoaded', function () { const tab = document.getElementById('mall-event-tab'); if (!tab) return; initFeatureSwitcher(); initFirstComeGift(); initEventExtract(); }); // ------------------------------------------------------------------ // 기능 공통 헬퍼 // ------------------------------------------------------------------ function escapeHtml(str) { return String(str == null ? '' : str) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function normalize(str) { return String(str || '').toLowerCase().replace(/\s+/g, ''); } async function readError(res, fallback) { // 413은 앱이 아니라 앞단 웹서버(nginx 등)가 용량 제한으로 막은 것이라 // 응답에 이유가 없다. 원인을 바로 알 수 있게 따로 안내한다. if (res.status === 413) { return '파일이 너무 커서 서버가 업로드를 거부했습니다 (413). ' + '서버 웹서버(nginx 등)의 업로드 용량 제한(client_max_body_size)을 늘려야 합니다.'; } try { const data = await res.json(); if (data && data.detail) { // FastAPI 검증 오류는 detail이 배열로 온다 if (Array.isArray(data.detail)) { return data.detail.map(function (d) { return d.msg || JSON.stringify(d); }).join(', '); } return data.detail; } } catch (e) { /* 본문이 JSON이 아니면 아래 기본 메시지 */ } // 서버 오류(500 등)는 본문이 없으므로 상태 코드라도 남겨 원인을 좁힌다 return fallback + ' (서버 응답 ' + res.status + ')'; } // 서버가 Content-Disposition으로 내려준 파일명을 꺼낸다 function filenameFromResponse(res, fallback) { const disposition = res.headers.get('Content-Disposition') || ''; const match = disposition.match(/filename\*=UTF-8''([^;]+)/i); if (match) { try { return decodeURIComponent(match[1]); } catch (e) { /* 무시 */ } } return fallback; } // 탭 전체를 덮는 로딩 오버레이 (기능들이 공유) function setOverlay(isLoading, text) { const overlay = document.getElementById('me-loading-overlay'); const overlayText = document.getElementById('me-loading-text'); if (overlay) overlay.style.display = isLoading ? 'flex' : 'none'; if (overlayText && text) overlayText.textContent = text; } // 파일 선택 버튼 + 드래그 앤 드롭을 한 번에 붙인다 function bindDropzone(dropzone, fileInput, pickBtn, onFile) { if (!dropzone || !fileInput) return; if (pickBtn) { pickBtn.addEventListener('click', function (e) { e.stopPropagation(); fileInput.click(); }); } dropzone.addEventListener('click', function () { fileInput.click(); }); fileInput.addEventListener('change', function () { if (fileInput.files && fileInput.files[0]) onFile(fileInput.files[0]); }); ['dragenter', 'dragover'].forEach(function (type) { dropzone.addEventListener(type, function (e) { e.preventDefault(); e.stopPropagation(); dropzone.style.background = '#ebf8ff'; dropzone.style.borderColor = '#3182ce'; }); }); ['dragleave', 'drop'].forEach(function (type) { dropzone.addEventListener(type, function (e) { e.preventDefault(); e.stopPropagation(); dropzone.style.background = '#f7fafc'; dropzone.style.borderColor = '#cbd5e0'; }); }); dropzone.addEventListener('drop', function (e) { const files = e.dataTransfer && e.dataTransfer.files; if (files && files.length > 0) onFile(files[0]); }); } // 탭 밖에 파일을 떨어뜨렸을 때 브라우저가 파일을 열어버리는 것 방지 ['dragover', 'drop'].forEach(function (type) { window.addEventListener(type, function (e) { const inDropzone = e.target.closest && e.target.closest('#me-dropzone, #ee-dropzone'); if (!inDropzone) e.preventDefault(); }); }); // blob 응답을 파일로 저장 async function saveBlob(res, fallbackName) { const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filenameFromResponse(res, fallbackName); document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); } // ------------------------------------------------------------------ // 기능 전환 // ------------------------------------------------------------------ function initFeatureSwitcher() { const buttons = document.querySelectorAll('.me-feature-btn'); if (buttons.length < 2) return; // 기능이 하나면 전환할 것이 없다 buttons.forEach(function (btn) { btn.addEventListener('click', function () { buttons.forEach(function (b) { b.classList.remove('active'); b.style.background = '#fff'; b.style.color = '#4a5568'; b.style.borderColor = '#e2e8f0'; }); btn.classList.add('active'); btn.style.background = '#ebf8ff'; btn.style.color = '#2b6cb0'; btn.style.borderColor = '#bee3f8'; document.querySelectorAll('.me-feature-panel').forEach(function (panel) { panel.style.display = 'none'; }); const target = document.getElementById('me-panel-' + btn.dataset.feature); if (target) target.style.display = 'flex'; }); }); } // ------------------------------------------------------------------ // 기능 1) 선착순 사은품 추가 // ------------------------------------------------------------------ function initFirstComeGift() { const dropzone = document.getElementById('me-dropzone'); const fileInput = document.getElementById('me-file-input'); const pickBtn = document.getElementById('btn-me-pick-file'); const resetBtn = document.getElementById('btn-me-reset'); const fileInfo = document.getElementById('me-file-info'); const fileError = document.getElementById('me-file-error'); const orderListBox = document.getElementById('me-order-list'); const orderSearch = document.getElementById('me-order-search'); const selectAllBtn = document.getElementById('btn-me-select-all'); const selectNoneBtn = document.getElementById('btn-me-select-none'); const selectedInfo = document.getElementById('me-order-selected-info'); const limitInput = document.getElementById('me-limit'); const giftSelect = document.getElementById('me-gift-select'); const giftQtyInput = document.getElementById('me-gift-qty'); const applyBtn = document.getElementById('btn-me-apply'); const warningBox = document.getElementById('me-warning-box'); const resultInfo = document.getElementById('me-result-info'); const resultTbody = document.getElementById('me-result-tbody'); const downloadFullBtn = document.getElementById('btn-me-download-full'); const downloadGiftBtn = document.getElementById('btn-me-download-gift'); const overlay = document.getElementById('me-loading-overlay'); const overlayText = document.getElementById('me-loading-text'); if (!dropzone || !fileInput) return; let currentFile = null; let orderItems = []; // [{name, order_count}] const selectedNames = new Set(); let searchKeyword = ''; let previewReady = false; function setLoading(isLoading, text) { setOverlay(isLoading, text); applyBtn.disabled = isLoading; } function showFileError(message) { if (!fileError) return; if (!message) { fileError.style.display = 'none'; fileError.textContent = ''; return; } fileError.style.display = 'block'; fileError.textContent = message; } // ---- 다운로드 버튼 활성/비활성 ---- function setDownloadEnabled(enabled) { previewReady = enabled; [downloadFullBtn, downloadGiftBtn].forEach(function (btn) { if (!btn) return; btn.disabled = !enabled; btn.style.opacity = enabled ? '1' : '0.5'; btn.style.cursor = enabled ? 'pointer' : 'default'; }); } // 조건이 바뀌면 이전 미리보기 결과는 더 이상 유효하지 않다 function invalidatePreview() { if (!previewReady) return; setDownloadEnabled(false); resultInfo.innerHTML = '조건이 바뀌었습니다. "사은품 적용 결과 보기"를 다시 눌러주세요.'; } // ---- 파일 업로드 ---- function resetAll() { currentFile = null; orderItems = []; selectedNames.clear(); searchKeyword = ''; fileInput.value = ''; if (orderSearch) orderSearch.value = ''; fileInfo.style.display = 'none'; fileInfo.innerHTML = ''; showFileError(''); orderListBox.innerHTML = '
발주서를 업로드하면 주문 목록이 표시됩니다.
'; selectedInfo.textContent = ''; resultTbody.innerHTML = ''; resultInfo.textContent = ''; warningBox.style.display = 'none'; warningBox.innerHTML = ''; setDownloadEnabled(false); } async function handleFile(file) { if (!file) return; const lower = file.name.toLowerCase(); if (lower.endsWith('.xls')) { showFileError('구형 .xls 파일은 처리할 수 없습니다. 엑셀에서 .xlsx로 저장한 뒤 올려주세요.'); return; } if (!lower.endsWith('.xlsx') && !lower.endsWith('.xlsm')) { showFileError('엑셀 파일(.xlsx)만 업로드할 수 있습니다.'); return; } showFileError(''); currentFile = file; selectedNames.clear(); searchKeyword = ''; if (orderSearch) orderSearch.value = ''; setDownloadEnabled(false); resultTbody.innerHTML = ''; resultInfo.textContent = ''; warningBox.style.display = 'none'; setLoading(true, '발주서를 분석하는 중...'); try { const form = new FormData(); form.append('file', file); const res = await fetch('/api/mall-event/first-come/analyze', { method: 'POST', body: form }); if (!res.ok) { showFileError(await readError(res, '발주서를 분석하지 못했습니다.')); currentFile = null; return; } const data = await res.json(); orderItems = data.order_list_items || []; fileInfo.style.display = 'block'; fileInfo.innerHTML = '
📄 ' + escapeHtml(file.name) + '
' + '
시트: ' + escapeHtml(data.sheet_name) + ' · 헤더 ' + data.header_row + '행
' + '
데이터 ' + Number(data.total_rows).toLocaleString() + '행 · 주문 ' + Number(data.total_orders).toLocaleString() + '건 · 상품 ' + orderItems.length + '종
'; renderOrderList(); } catch (err) { console.error(err); showFileError('업로드 중 오류가 발생했습니다.'); currentFile = null; } finally { setLoading(false); } } bindDropzone(dropzone, fileInput, pickBtn, handleFile); if (resetBtn) resetBtn.addEventListener('click', resetAll); // ---- 주문목록 리스트 ---- function renderOrderList() { if (!orderItems.length) { orderListBox.innerHTML = '
Q열(주문목록)에서 상품을 찾지 못했습니다.
'; updateSelectedInfo(); return; } const visible = orderItems.filter(function (item) { return !searchKeyword || normalize(item.name).includes(searchKeyword); }); if (!visible.length) { orderListBox.innerHTML = '
검색 결과가 없습니다.
'; updateSelectedInfo(); return; } orderListBox.innerHTML = visible.map(function (item) { const checked = selectedNames.has(item.name) ? ' checked' : ''; return '' + ''; }).join(''); updateSelectedInfo(); } function updateSelectedInfo() { if (!selectedNames.size) { selectedInfo.textContent = orderItems.length ? '선택된 상품이 없습니다.' : ''; return; } const names = Array.from(selectedNames); const preview = names.slice(0, 2).join(', '); const more = names.length > 2 ? ' 외 ' + (names.length - 2) + '개' : ''; selectedInfo.innerHTML = '선택 ' + names.length + '개 — ' + escapeHtml(preview + more); } orderListBox.addEventListener('change', function (e) { const check = e.target.closest('.me-order-check'); if (!check) return; if (check.checked) selectedNames.add(check.value); else selectedNames.delete(check.value); updateSelectedInfo(); invalidatePreview(); }); if (orderSearch) { orderSearch.addEventListener('input', function () { searchKeyword = normalize(orderSearch.value); renderOrderList(); }); } if (selectAllBtn) { selectAllBtn.addEventListener('click', function () { orderItems.forEach(function (item) { if (!searchKeyword || normalize(item.name).includes(searchKeyword)) selectedNames.add(item.name); }); renderOrderList(); invalidatePreview(); }); } if (selectNoneBtn) { selectNoneBtn.addEventListener('click', function () { selectedNames.clear(); renderOrderList(); invalidatePreview(); }); } // ---- 사은품 목록 (코드표 세트 코드 중 이름에 '사은품'이 들어간 것만) ---- const GIFT_NAME_KEYWORD = '사은품'; async function loadGiftOptions() { try { const res = await fetch('/api/codes/set'); if (!res.ok) throw new Error('status ' + res.status); const data = await res.json(); const allSets = (data && data.data) || []; const sets = allSets.filter(function (s) { return String(s.name || '').includes(GIFT_NAME_KEYWORD); }); if (!sets.length) { giftSelect.innerHTML = ''; return; } sets.sort(function (a, b) { return String(a.name || '').localeCompare(String(b.name || ''), 'ko'); }); giftSelect.innerHTML = '' + sets.map(function (s) { return ''; }).join(''); } catch (err) { console.error(err); giftSelect.innerHTML = ''; } } loadGiftOptions(); // 코드표 탭에서 세트를 수정하고 돌아왔을 때 목록을 새로 받는다 const mallEventMenu = document.querySelector('[data-tab="mall-event-tab"]'); if (mallEventMenu) { mallEventMenu.addEventListener('click', function () { if (!giftSelect.value) loadGiftOptions(); }); } // ---- 조건 변경 감지 ---- document.querySelectorAll('.me-limit-preset').forEach(function (btn) { btn.addEventListener('click', function () { limitInput.value = btn.dataset.value; invalidatePreview(); }); }); [limitInput, giftSelect, giftQtyInput].forEach(function (el) { if (el) el.addEventListener('change', invalidatePreview); }); if (limitInput) limitInput.addEventListener('input', invalidatePreview); if (giftQtyInput) giftQtyInput.addEventListener('input', invalidatePreview); // ---- 조건 수집/검증 ---- function buildFormData() { if (!currentFile) { alert('먼저 발주서 엑셀 파일을 업로드해주세요.'); return null; } if (!selectedNames.size) { alert('사은품이 지급되는 주문을 최소 1개 선택해주세요.'); return null; } const limit = parseInt(limitInput.value, 10); if (!limit || limit < 1) { alert('선착순 인원을 1명 이상으로 입력해주세요.'); return null; } const giftCode = giftSelect.value; if (!giftCode) { alert('사은품(세트 코드)을 선택해주세요.'); return null; } const giftQty = parseInt(giftQtyInput.value, 10); if (!giftQty || giftQty < 1) { alert('사은품 수량을 1개 이상으로 입력해주세요.'); return null; } const giftName = giftSelect.options[giftSelect.selectedIndex].dataset.name || ''; const form = new FormData(); form.append('file', currentFile); form.append('selected_json', JSON.stringify(Array.from(selectedNames))); form.append('limit', String(limit)); form.append('gift_item_code', giftCode); form.append('gift_name', giftName); form.append('gift_qty', String(giftQty)); return form; } // ---- 미리보기 ---- applyBtn.addEventListener('click', async function () { const form = buildFormData(); if (!form) return; setLoading(true, '사은품 대상을 계산하는 중...'); try { const res = await fetch('/api/mall-event/first-come/preview', { method: 'POST', body: form }); if (!res.ok) { alert(await readError(res, '사은품 적용 중 오류가 발생했습니다.')); return; } const data = await res.json(); if (data.warnings && data.warnings.length) { warningBox.style.display = 'block'; warningBox.innerHTML = data.warnings.map(function (w) { return '⚠ ' + escapeHtml(w); }).join('
'); } else { warningBox.style.display = 'none'; warningBox.innerHTML = ''; } const added = data.added || []; if (!added.length) { resultTbody.innerHTML = '사은품이 적용된 주문이 없습니다.'; resultInfo.innerHTML = '적용 대상 0건'; setDownloadEnabled(false); return; } resultTbody.innerHTML = added.map(function (row, idx) { return '' + '' + (idx + 1) + '' + '' + escapeHtml(row.order_no) + '' + '' + escapeHtml(row.order_date) + '' + '' + escapeHtml(row.seq) + '' + ''; }).join(''); resultInfo.innerHTML = '조건 충족 주문 ' + Number(data.matched_order_count).toLocaleString() + '건 중 ' + '' + Number(data.applied_count).toLocaleString() + '건에 사은품 행 추가'; setDownloadEnabled(true); } catch (err) { console.error(err); alert('사은품 적용 요청 중 오류가 발생했습니다.'); } finally { setLoading(false); } }); // ---- 다운로드 ---- async function download(mode, fallbackName, label) { const form = buildFormData(); if (!form) return; form.append('mode', mode); setLoading(true, label + ' 파일을 만드는 중...'); try { const res = await fetch('/api/mall-event/first-come/download', { method: 'POST', body: form }); if (!res.ok) { alert(await readError(res, '엑셀 다운로드 중 오류가 발생했습니다.')); return; } await saveBlob(res, fallbackName); } catch (err) { console.error(err); alert('엑셀 다운로드 요청 중 오류가 발생했습니다.'); } finally { setLoading(false); } } // 서버가 Content-Disposition으로 이름을 내려주지만, 못 읽었을 때 쓸 대비책 function baseName() { const name = (currentFile && currentFile.name) || '발주서.xlsx'; return name.replace(/\.(xlsx|xlsm|xls)$/i, ''); } if (downloadFullBtn) { downloadFullBtn.addEventListener('click', function () { if (downloadFullBtn.disabled) return; // 전체 발주 파일은 업로드한 파일과 같은 이름으로 받는다 download('full', baseName() + '.xlsx', '전체 발주'); }); } if (downloadGiftBtn) { downloadGiftBtn.addEventListener('click', function () { if (downloadGiftBtn.disabled) return; download('gift', baseName() + '_선착순사은품_대상주문.xlsx', '사은품 대상 주문'); }); } setDownloadEnabled(false); } // ------------------------------------------------------------------ // 기능 2) 행사 주문 추출 // 주문 CSV → K열(주문상품명) 첫 " -" 앞부분으로 상품 선택 → // 해당상품만/해당주문 조건으로 행을 걸러 엑셀로 내려받는다. // ------------------------------------------------------------------ function initEventExtract() { const dropzone = document.getElementById('ee-dropzone'); const fileInput = document.getElementById('ee-file-input'); const pickBtn = document.getElementById('btn-ee-pick-file'); const resetBtn = document.getElementById('btn-ee-reset'); const fileInfo = document.getElementById('ee-file-info'); const fileError = document.getElementById('ee-file-error'); const productListBox = document.getElementById('ee-product-list'); const productSearch = document.getElementById('ee-product-search'); const selectAllBtn = document.getElementById('btn-ee-select-all'); const selectNoneBtn = document.getElementById('btn-ee-select-none'); const selectedInfo = document.getElementById('ee-selected-info'); const warningBox = document.getElementById('ee-warning-box'); const resultInfo = document.getElementById('ee-result-info'); const downloadBtn = document.getElementById('btn-ee-download'); const modeRadios = document.querySelectorAll('input[name="ee-mode"]'); if (!dropzone || !fileInput) return; let currentFile = null; let productItems = []; // [{name, row_count, order_count}] const selectedNames = new Set(); let searchKeyword = ''; function setLoading(isLoading, text) { setOverlay(isLoading, text); downloadBtn.disabled = isLoading; downloadBtn.style.opacity = isLoading ? '0.6' : '1'; } // 조건이 바뀌면 직전 다운로드 결과 안내는 지운다 function clearResult() { resultInfo.innerHTML = ''; } function currentMode() { const checked = document.querySelector('input[name="ee-mode"]:checked'); return checked ? checked.value : 'product'; } function showFileError(message) { if (!fileError) return; fileError.style.display = message ? 'block' : 'none'; fileError.textContent = message || ''; } function resetAll() { currentFile = null; productItems = []; selectedNames.clear(); searchKeyword = ''; fileInput.value = ''; if (productSearch) productSearch.value = ''; fileInfo.style.display = 'none'; fileInfo.innerHTML = ''; showFileError(''); productListBox.innerHTML = '
CSV를 업로드하면 상품 목록이 표시됩니다.
'; selectedInfo.textContent = ''; resultInfo.innerHTML = ''; warningBox.style.display = 'none'; warningBox.innerHTML = ''; clearResult(); } // ---- 업로드 & 분석 ---- async function handleFile(file) { if (!file) return; if (!file.name.toLowerCase().endsWith('.csv')) { showFileError('CSV 파일(.csv)만 업로드할 수 있습니다.'); return; } showFileError(''); currentFile = file; selectedNames.clear(); searchKeyword = ''; if (productSearch) productSearch.value = ''; clearResult(); resultInfo.innerHTML = ''; warningBox.style.display = 'none'; setLoading(true, 'CSV를 분석하는 중...'); try { const form = new FormData(); form.append('file', file); const res = await fetch('/api/mall-event/event-extract/analyze', { method: 'POST', body: form }); if (!res.ok) { showFileError(await readError(res, 'CSV를 분석하지 못했습니다.')); currentFile = null; return; } const data = await res.json(); productItems = data.product_items || []; fileInfo.style.display = 'block'; fileInfo.innerHTML = '
📄 ' + escapeHtml(file.name) + '
' + '
인코딩: ' + escapeHtml(data.encoding) + ' · 헤더 ' + data.header_row + '행 · ' + data.column_count + '개 열
' + '
데이터 ' + Number(data.total_rows).toLocaleString() + '행 · 주문 ' + Number(data.total_orders).toLocaleString() + '건 · 상품 ' + productItems.length + '종
'; if (data.warnings && data.warnings.length) { warningBox.style.display = 'block'; warningBox.innerHTML = data.warnings.map(function (w) { return '⚠ ' + escapeHtml(w); }).join('
'); } renderProductList(); } catch (err) { console.error(err); showFileError('업로드 중 오류가 발생했습니다.'); currentFile = null; } finally { setLoading(false); } } bindDropzone(dropzone, fileInput, pickBtn, handleFile); if (resetBtn) resetBtn.addEventListener('click', resetAll); // ---- 상품 목록 ---- function renderProductList() { if (!productItems.length) { productListBox.innerHTML = '
K열(주문상품명)에서 상품을 찾지 못했습니다.
'; updateSelectedInfo(); return; } const visible = productItems.filter(function (item) { return !searchKeyword || normalize(item.name).includes(searchKeyword); }); if (!visible.length) { productListBox.innerHTML = '
검색 결과가 없습니다.
'; updateSelectedInfo(); return; } productListBox.innerHTML = visible.map(function (item) { const checked = selectedNames.has(item.name) ? ' checked' : ''; return '' + ''; }).join(''); updateSelectedInfo(); } function updateSelectedInfo() { if (!selectedNames.size) { selectedInfo.textContent = productItems.length ? '선택된 상품이 없습니다.' : ''; return; } const names = Array.from(selectedNames); const preview = names.slice(0, 2).join(', '); const more = names.length > 2 ? ' 외 ' + (names.length - 2) + '개' : ''; selectedInfo.innerHTML = '선택 ' + names.length + '개 — ' + escapeHtml(preview + more); } productListBox.addEventListener('change', function (e) { const check = e.target.closest('.ee-product-check'); if (!check) return; if (check.checked) selectedNames.add(check.value); else selectedNames.delete(check.value); updateSelectedInfo(); clearResult(); }); if (productSearch) { productSearch.addEventListener('input', function () { searchKeyword = normalize(productSearch.value); renderProductList(); }); } if (selectAllBtn) { selectAllBtn.addEventListener('click', function () { productItems.forEach(function (item) { if (!searchKeyword || normalize(item.name).includes(searchKeyword)) selectedNames.add(item.name); }); renderProductList(); clearResult(); }); } if (selectNoneBtn) { selectNoneBtn.addEventListener('click', function () { selectedNames.clear(); renderProductList(); clearResult(); }); } // ---- 추출 조건 라디오 ---- modeRadios.forEach(function (radio) { radio.addEventListener('change', function () { modeRadios.forEach(function (r) { const box = r.closest('label'); if (!box) return; const on = r.checked; box.style.borderColor = on ? '#bee3f8' : '#e2e8f0'; box.style.background = on ? '#ebf8ff' : '#fff'; const title = box.querySelector('span > span'); if (title) title.style.color = on ? '#2b6cb0' : '#2d3748'; }); clearResult(); }); }); // ---- 조건 수집 ---- function buildFormData() { if (!currentFile) { alert('먼저 주문 CSV 파일을 업로드해주세요.'); return null; } if (!selectedNames.size) { alert('추출할 상품을 최소 1개 선택해주세요.'); return null; } const form = new FormData(); form.append('file', currentFile); form.append('selected_json', JSON.stringify(Array.from(selectedNames))); form.append('mode', currentMode()); return form; } // ---- 다운로드 (중간 확인 단계 없이 바로 파일 생성) ---- downloadBtn.addEventListener('click', async function () { if (downloadBtn.disabled) return; const form = buildFormData(); if (!form) return; const modeLabel = currentMode() === 'product' ? '해당상품만 추출' : '해당 주문 추출'; const suffix = currentMode() === 'product' ? '_행사추출_해당상품만' : '_행사추출_해당주문'; const fallback = ((currentFile && currentFile.name) || '주문').replace(/\.csv$/i, '') + suffix + '.xlsx'; warningBox.style.display = 'none'; warningBox.innerHTML = ''; clearResult(); setLoading(true, '추출 파일을 만드는 중...'); try { const res = await fetch('/api/mall-event/event-extract/download', { method: 'POST', body: form }); if (!res.ok) { alert(await readError(res, '엑셀 다운로드 중 오류가 발생했습니다.')); return; } // 서버가 헤더로 실어 보낸 추출 결과를 화면에 남긴다 const rows = Number(res.headers.get('X-Extract-Rows') || 0); const orders = Number(res.headers.get('X-Extract-Orders') || 0); const matched = Number(res.headers.get('X-Extract-Matched-Rows') || 0); const total = Number(res.headers.get('X-Extract-Total-Rows') || 0); await saveBlob(res, fallback); resultInfo.innerHTML = '
✓ 다운로드 완료
' + '
조건: ' + modeLabel + '
' + '
상품이 들어있는 행 ' + matched.toLocaleString() + '행
' + '
추출 결과 ' + rows.toLocaleString() + '행' + ' · 주문 ' + orders.toLocaleString() + '건
' + '
전체 ' + total.toLocaleString() + '행 중
'; } catch (err) { console.error(err); alert('엑셀 다운로드 요청 중 오류가 발생했습니다.'); } finally { setLoading(false); } }); clearResult(); } })();