feat: 체크박스 선택 방식으로 UX 개선
- 테이블 헤더에 전체 선택 체크박스 추가
- 각 행에 개별 체크박스 추가
- 체크박스 클릭 = 선택만 (상세 패널 안 열림)
- 행 클릭 = 상세 패널 열기 (기존 동작 유지)
- 여러 건 선택 → 일괄 라벨 출력 가능
- 버튼에 선택 건수 표시: '📺 키오스크 (3건)'
This commit is contained in:
parent
e10b50e0c3
commit
f1e609ba9f
@ -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);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Loading…
Reference in New Issue
Block a user