From f1e609ba9fb984e9f59e0bfc89c7d16639606b0a Mon Sep 17 00:00:00 2001 From: thug0bin Date: Mon, 2 Mar 2026 15:50:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20UX?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테이블 헤더에 전체 선택 체크박스 추가 - 각 행에 개별 체크박스 추가 - 체크박스 클릭 = 선택만 (상세 패널 안 열림) - 행 클릭 = 상세 패널 열기 (기존 동작 유지) - 여러 건 선택 → 일괄 라벨 출력 가능 - 버튼에 선택 건수 표시: '📺 키오스크 (3건)' --- backend/templates/admin_pos_live.html | 235 +++++++++++++++++--------- 1 file changed, 153 insertions(+), 82 deletions(-) diff --git a/backend/templates/admin_pos_live.html b/backend/templates/admin_pos_live.html index ddc27db..08df6aa 100644 --- a/backend/templates/admin_pos_live.html +++ b/backend/templates/admin_pos_live.html @@ -236,6 +236,18 @@ tr.selected { background: #ede9fe; } + .row-checkbox { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: #8b5cf6; + } + #selectAll { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: #8b5cf6; + } .order-no { font-family: 'JetBrains Mono', monospace; @@ -648,6 +660,7 @@ + @@ -759,15 +772,19 @@ } function renderTable(data) { + // 전체 선택 체크박스 초기화 + document.getElementById('selectAll').checked = false; + if (data.sales.length === 0) { document.getElementById('salesTable').innerHTML = ` - `; + updateSelectedCount(); return; } @@ -787,19 +804,23 @@ } return ` - - - - - - - - + + + + + + + + + `; }).join(''); document.getElementById('salesTable').innerHTML = rows; + updateSelectedCount(); } function getPayBadge(method) { @@ -836,29 +857,65 @@ }); // ═══════════════════════════════════════════════════════════════ - // 키오스크 & 라벨 출력 기능 + // 체크박스 선택 기능 // ═══════════════════════════════════════════════════════════════ - let selectedSale = null; - let selectedIdx = -1; + let selectedSales = []; // 선택된 판매 건들 - // 테이블 행 선택 시 버튼 활성화 + function toggleSelectAll() { + const selectAll = document.getElementById('selectAll').checked; + document.querySelectorAll('.row-checkbox').forEach(cb => { + cb.checked = selectAll; + }); + updateSelectedCount(); + } + + function onRowSelect(idx) { + updateSelectedCount(); + } + + function getSelectedSales() { + const selected = []; + document.querySelectorAll('.row-checkbox:checked').forEach(cb => { + const idx = parseInt(cb.dataset.idx); + if (salesData[idx]) { + selected.push(salesData[idx]); + } + }); + return selected; + } + + function updateSelectedCount() { + selectedSales = getSelectedSales(); + const count = selectedSales.length; + + const kioskBtn = document.getElementById('kioskBtn'); + const qrBtn = document.getElementById('qrBtn'); + + if (count > 0) { + kioskBtn.disabled = false; + qrBtn.disabled = false; + kioskBtn.textContent = `📺 키오스크 (${count}건)`; + qrBtn.textContent = `🏷️ 라벨출력 (${count}건)`; + } else { + kioskBtn.disabled = true; + qrBtn.disabled = true; + kioskBtn.textContent = '📺 키오스크'; + qrBtn.textContent = '🏷️ 라벨출력'; + } + } + + // ═══════════════════════════════════════════════════════════════ + // 상세 패널 (별도 기능) + // ═══════════════════════════════════════════════════════════════ + + // 행 클릭 시 상세 패널 표시 (체크박스와 별개) function showDetail(orderNo, idx) { - selectedIdx = idx; // 선택 표시 document.querySelectorAll('#salesTable tr').forEach(tr => tr.classList.remove('selected')); document.querySelector(`#salesTable tr[data-idx="${idx}"]`)?.classList.add('selected'); const sale = salesData[idx]; - selectedSale = sale; - - // 버튼 활성화 - document.getElementById('kioskBtn').disabled = false; - document.getElementById('qrBtn').disabled = false; - - // QR 발행 여부에 따라 버튼 텍스트 변경 - const qrBtn = document.getElementById('qrBtn'); - qrBtn.textContent = sale.qr_issued ? '🔄 재출력' : '🏷️ 라벨출력'; // 패널 열기 document.getElementById('overlay').classList.add('visible'); @@ -925,14 +982,20 @@ } // ═══════════════════════════════════════════════════════════════ - // 키오스크 전송 + // 키오스크 전송 (선택된 건 중 첫 번째만 - 키오스크는 1건씩) // ═══════════════════════════════════════════════════════════════ async function triggerKiosk() { - if (!selectedSale) { - alert('먼저 판매 건을 선택해주세요.'); + if (selectedSales.length === 0) { + showToast('먼저 판매 건을 선택해주세요.', 'error'); return; } + // 키오스크는 1건만 전송 (여러 건 선택 시 첫 번째) + const sale = selectedSales[0]; + if (selectedSales.length > 1) { + showToast('키오스크는 1건씩 전송됩니다. 첫 번째 건을 전송합니다.', 'info'); + } + const btn = document.getElementById('kioskBtn'); const originalText = btn.textContent; btn.disabled = true; @@ -943,8 +1006,8 @@ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - transaction_id: selectedSale.order_no, - amount: selectedSale.amount + transaction_id: sale.order_no, + amount: sale.amount }) }); @@ -953,17 +1016,11 @@ if (data.success) { btn.textContent = `✓ ${data.points}P`; btn.style.background = '#10b981'; - - // 토스트 메시지 showToast(`키오스크 전송 완료! (${data.points}P)`, 'success'); - // 테이블 새로고침 setTimeout(() => { loadSales(); - btn.textContent = originalText; - btn.style.background = ''; - btn.disabled = false; - }, 2000); + }, 1500); } else { showToast(data.message || '전송 실패', 'error'); btn.textContent = originalText; @@ -977,70 +1034,84 @@ } // ═══════════════════════════════════════════════════════════════ - // 라벨 출력 (Brother QL-810W) + // 라벨 출력 (Brother QL-810W) - 여러 건 순차 출력 // ═══════════════════════════════════════════════════════════════ async function triggerQrPrint() { - if (!selectedSale) { - alert('먼저 판매 건을 선택해주세요.'); + if (selectedSales.length === 0) { + showToast('먼저 판매 건을 선택해주세요.', 'error'); return; } const btn = document.getElementById('qrBtn'); const originalText = btn.textContent; btn.disabled = true; - btn.textContent = '출력 중...'; - try { - // 1. QR 미발행이면 먼저 생성 - if (!selectedSale.qr_issued) { - const genRes = await fetch('/api/admin/qr/generate', { + let successCount = 0; + let errorCount = 0; + + for (let i = 0; i < selectedSales.length; i++) { + const sale = selectedSales[i]; + btn.textContent = `출력 중... (${i + 1}/${selectedSales.length})`; + + try { + // 1. QR 미발행이면 먼저 생성 + if (!sale.qr_issued) { + const genRes = await fetch('/api/admin/qr/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + order_no: sale.order_no, + amount: sale.amount, + preview: false + }) + }); + const genData = await genRes.json(); + if (!genData.success) { + errorCount++; + continue; + } + } + + // 2. 프린터 출력 + const res = await fetch('/api/admin/qr/print', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - order_no: selectedSale.order_no, - amount: selectedSale.amount, - preview: false + order_no: sale.order_no, + printer: 'brother' }) }); - const genData = await genRes.json(); - if (!genData.success) { - throw new Error(genData.error); - } - } - - // 2. 프린터 출력 - const res = await fetch('/api/admin/qr/print', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - order_no: selectedSale.order_no, - printer: 'brother' - }) - }); - - const data = await res.json(); - - if (data.success) { - btn.textContent = '✓ 출력완료'; - btn.style.background = '#10b981'; - showToast(data.message, 'success'); - setTimeout(() => { - loadSales(); - btn.textContent = originalText; - btn.style.background = ''; - btn.disabled = false; - }, 2000); - } else { - showToast(data.error || '출력 실패', 'error'); - btn.textContent = originalText; - btn.disabled = false; + const data = await res.json(); + if (data.success) { + successCount++; + } else { + errorCount++; + } + + // 프린터 간격 (연속 출력 시 0.5초 대기) + if (i < selectedSales.length - 1) { + await new Promise(r => setTimeout(r, 500)); + } + + } catch (err) { + errorCount++; } - } catch (err) { - showToast(`오류: ${err.message}`, 'error'); - btn.textContent = originalText; - btn.disabled = false; } + + // 결과 표시 + if (successCount > 0) { + btn.textContent = `✓ ${successCount}건 완료`; + btn.style.background = '#10b981'; + showToast(`라벨 출력 완료! (${successCount}건)`, 'success'); + } + if (errorCount > 0) { + showToast(`${errorCount}건 실패`, 'error'); + } + + setTimeout(() => { + loadSales(); + }, 1500); } // ═══════════════════════════════════════════════════════════════
시간 금액 고객
+
📭
판매 내역이 없습니다
${sale.time}₩${Math.floor(sale.amount).toLocaleString()}${sale.customer}${payBadge}${sale.item_count}${qrIcon}${claimedHtml}
+ + ${sale.time}₩${Math.floor(sale.amount).toLocaleString()}${sale.customer}${payBadge}${sale.item_count}${qrIcon}${claimedHtml}