feat: 체크박스 선택 방식으로 UX 개선

- 테이블 헤더에 전체 선택 체크박스 추가
- 각 행에 개별 체크박스 추가
- 체크박스 클릭 = 선택만 (상세 패널 안 열림)
- 행 클릭 = 상세 패널 열기 (기존 동작 유지)
- 여러 건 선택 → 일괄 라벨 출력 가능
- 버튼에 선택 건수 표시: '📺 키오스크 (3건)'
This commit is contained in:
thug0bin 2026-03-02 15:50:21 +09:00
parent e10b50e0c3
commit f1e609ba9f

View File

@ -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 @@
<table>
<thead>
<tr>
<th class="center" style="width:40px"><input type="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
<th>시간</th>
<th class="right">금액</th>
<th>고객</th>
@ -759,15 +772,19 @@
}
function renderTable(data) {
// 전체 선택 체크박스 초기화
document.getElementById('selectAll').checked = false;
if (data.sales.length === 0) {
document.getElementById('salesTable').innerHTML = `
<tr><td colspan="7">
<tr><td colspan="8">
<div class="empty-state">
<div class="empty-icon">📭</div>
<div>판매 내역이 없습니다</div>
</div>
</td></tr>
`;
updateSelectedCount();
return;
}
@ -787,19 +804,23 @@
}
return `
<tr onclick="showDetail('${sale.order_no}', ${idx})" data-idx="${idx}">
<td><span class="time">${sale.time}</span></td>
<td class="right"><span class="amount">₩${Math.floor(sale.amount).toLocaleString()}</span></td>
<td><span class="${customerClass}">${sale.customer}</span></td>
<td class="center">${payBadge}</td>
<td class="center">${sale.item_count}</td>
<td class="center">${qrIcon}</td>
<td>${claimedHtml}</td>
<tr data-idx="${idx}">
<td class="center" onclick="event.stopPropagation()">
<input type="checkbox" class="row-checkbox" data-idx="${idx}" onchange="onRowSelect(${idx})">
</td>
<td onclick="showDetail('${sale.order_no}', ${idx})"><span class="time">${sale.time}</span></td>
<td class="right" onclick="showDetail('${sale.order_no}', ${idx})"><span class="amount">₩${Math.floor(sale.amount).toLocaleString()}</span></td>
<td onclick="showDetail('${sale.order_no}', ${idx})"><span class="${customerClass}">${sale.customer}</span></td>
<td class="center" onclick="showDetail('${sale.order_no}', ${idx})">${payBadge}</td>
<td class="center" onclick="showDetail('${sale.order_no}', ${idx})">${sale.item_count}</td>
<td class="center" onclick="showDetail('${sale.order_no}', ${idx})">${qrIcon}</td>
<td onclick="showDetail('${sale.order_no}', ${idx})">${claimedHtml}</td>
</tr>
`;
}).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);
}
// ═══════════════════════════════════════════════════════════════