feat: 상세 패널에 키오스크/라벨출력 액션 버튼 추가

- 상세 패널 상단에 2열 액션 버튼 배치
- 📺 키오스크: 해당 건 즉시 전송
- 🏷️ 라벨출력: QR 생성 + Brother QL 출력
- 버튼에 예상 적립 포인트 표시
- 호버 효과 + 로딩 상태 표시
- QR 발행 여부, 적립 완료 정보 표시
This commit is contained in:
thug0bin 2026-03-02 15:59:47 +09:00
parent f1e609ba9f
commit 695c1f707f

View File

@ -582,6 +582,61 @@
font-weight: 600;
}
/* ── 상세 패널 액션 버튼 ── */
.detail-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.detail-action-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 12px;
border: none;
border-radius: 14px;
cursor: pointer;
transition: all 0.2s;
}
.detail-action-btn.kiosk {
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white;
}
.detail-action-btn.kiosk:hover {
background: linear-gradient(135deg, #4f46e5, #4338ca);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
}
.detail-action-btn.qr {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
}
.detail-action-btn.qr:hover {
background: linear-gradient(135deg, #d97706, #b45309);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.4);
}
.detail-action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
.action-icon {
font-size: 28px;
margin-bottom: 6px;
}
.action-text {
font-size: 14px;
font-weight: 700;
}
.action-sub {
font-size: 12px;
opacity: 0.85;
margin-top: 2px;
}
/* ── 반응형 ── */
@media (max-width: 768px) {
.control-section {
@ -921,8 +976,32 @@
document.getElementById('overlay').classList.add('visible');
document.getElementById('detailPanel').classList.add('open');
const expectedPoints = Math.floor(sale.amount * 0.03);
const qrStatus = sale.qr_issued
? `<span style="color:#10b981">✓ QR 발행됨</span>`
: `<span style="color:#94a3b8">미발행</span>`;
const claimedInfo = sale.claimed_name
? `<div style="margin-top:8px; padding:10px; background:#dcfce7; border-radius:8px; font-size:13px;">
<strong>${sale.claimed_name}</strong>님 적립 완료
</div>`
: '';
// 기본 정보 표시
document.getElementById('detailContent').innerHTML = `
<!-- 액션 버튼 -->
<div class="detail-actions">
<button class="detail-action-btn kiosk" onclick="triggerKioskSingle('${orderNo}', ${sale.amount})">
<span class="action-icon">📺</span>
<span class="action-text">키오스크</span>
<span class="action-sub">${expectedPoints}P</span>
</button>
<button class="detail-action-btn qr" onclick="triggerQrPrintSingle('${orderNo}', ${sale.amount}, ${sale.qr_issued})">
<span class="action-icon">🏷️</span>
<span class="action-text">${sale.qr_issued ? '재출력' : '라벨출력'}</span>
<span class="action-sub">${expectedPoints}P</span>
</button>
</div>
<div class="detail-info">
<div class="detail-info-item">
<div class="detail-info-label">거래번호</div>
@ -942,13 +1021,14 @@
</div>
<div class="detail-info-item">
<div class="detail-info-label">판매금액</div>
<div class="detail-info-value">₩${Math.floor(sale.amount).toLocaleString()}</div>
<div class="detail-info-value" style="font-weight:700; color:#8b5cf6">₩${Math.floor(sale.amount).toLocaleString()}</div>
</div>
<div class="detail-info-item">
<div class="detail-info-label">할인</div>
<div class="detail-info-value">₩${Math.floor(sale.discount).toLocaleString()}</div>
<div class="detail-info-label">QR 상태</div>
<div class="detail-info-value">${qrStatus}</div>
</div>
</div>
${claimedInfo}
<div class="detail-items-title">📦 품목 목록</div>
<div id="itemsList">
<div class="loading"><div class="spinner"></div>품목 로딩 중...</div>
@ -981,6 +1061,81 @@
}
}
// ═══════════════════════════════════════════════════════════════
// 상세 패널에서 단일 건 처리
// ═══════════════════════════════════════════════════════════════
async function triggerKioskSingle(orderNo, amount) {
const btn = event.target.closest('.detail-action-btn');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="action-icon"></span><span class="action-text">전송 중...</span>';
try {
const res = await fetch('/api/kiosk/trigger', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transaction_id: orderNo, amount: amount })
});
const data = await res.json();
if (data.success) {
btn.innerHTML = `<span class="action-icon"></span><span class="action-text">전송 완료!</span><span class="action-sub">${data.points}P</span>`;
showToast(`키오스크 전송 완료! (${data.points}P)`, 'success');
setTimeout(() => loadSales(), 1500);
} else {
btn.innerHTML = originalHtml;
btn.disabled = false;
showToast(data.message || '전송 실패', 'error');
}
} catch (err) {
btn.innerHTML = originalHtml;
btn.disabled = false;
showToast(`오류: ${err.message}`, 'error');
}
}
async function triggerQrPrintSingle(orderNo, amount, isReprint) {
const btn = event.target.closest('.detail-action-btn');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="action-icon"></span><span class="action-text">출력 중...</span>';
try {
// QR 미발행이면 먼저 생성
if (!isReprint) {
const genRes = await fetch('/api/admin/qr/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_no: orderNo, amount: amount, preview: false })
});
const genData = await genRes.json();
if (!genData.success) throw new Error(genData.error);
}
// 프린터 출력
const res = await fetch('/api/admin/qr/print', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_no: orderNo, printer: 'brother' })
});
const data = await res.json();
if (data.success) {
btn.innerHTML = '<span class="action-icon"></span><span class="action-text">출력 완료!</span>';
showToast(data.message, 'success');
setTimeout(() => loadSales(), 1500);
} else {
btn.innerHTML = originalHtml;
btn.disabled = false;
showToast(data.error || '출력 실패', 'error');
}
} catch (err) {
btn.innerHTML = originalHtml;
btn.disabled = false;
showToast(`오류: ${err.message}`, 'error');
}
}
// ═══════════════════════════════════════════════════════════════
// 키오스크 전송 (선택된 건 중 첫 번째만 - 키오스크는 1건씩)
// ═══════════════════════════════════════════════════════════════