pharmacy-pos-qr-system/backend/templates/admin_pos_live.html
thug0bin 9f10f8fdbb feat(pos-live): 고객 검색/매핑 + 비동기 마일리지 표시
- GET /api/customers/search: CD_PERSON 검색 (최근 활동순)
- PUT /api/pos-live/{order}/customer: SALE_MAIN 고객 업데이트
- GET /api/customers/{code}/mileage: 비동기 마일리지 조회
- UI: 고객 뱃지 클릭 → 검색 모달 → 선택 → 업데이트
- 마일리지: 이름+전화뒤4자리 매칭, 비동기 표시
2026-03-11 23:22:57 +09:00

1815 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>실시간 판매 조회 - 청춘약국</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc;
-webkit-font-smoothing: antialiased;
color: #1e293b;
}
/* ── 헤더 ── */
.header {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%);
padding: 28px 32px 24px;
color: #fff;
}
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-nav a {
color: rgba(255,255,255,0.8);
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.header-nav a:hover { color: #fff; }
.header h1 {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 6px;
}
.header p {
font-size: 14px;
opacity: 0.85;
}
/* ── 컨텐츠 ── */
.content {
max-width: 1400px;
margin: 0 auto;
padding: 24px 20px 60px;
}
/* ── 컨트롤 영역 ── */
.control-section {
background: #fff;
border-radius: 14px;
padding: 20px 24px;
margin-bottom: 20px;
border: 1px solid #e2e8f0;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
}
.control-left {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.control-right {
display: flex;
gap: 12px;
align-items: center;
}
.date-input {
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 15px;
font-family: 'JetBrains Mono', monospace;
width: 160px;
transition: all 0.2s;
}
.date-input:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #8b5cf6;
color: #fff;
}
.btn-primary:hover { background: #7c3aed; }
.btn-secondary {
background: #f1f5f9;
color: #475569;
}
.btn-secondary:hover { background: #e2e8f0; }
.btn-success {
background: #10b981;
color: #fff;
}
.btn-success:hover { background: #059669; }
.btn-qr {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #fff;
}
.btn-qr:hover { background: linear-gradient(135deg, #d97706, #b45309); }
.btn-qr:disabled {
background: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
.btn-kiosk {
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: #fff;
}
.btn-kiosk:hover { background: linear-gradient(135deg, #4f46e5, #4338ca); }
.btn-kiosk:disabled {
background: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
.auto-refresh {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #64748b;
}
.auto-refresh input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #8b5cf6;
}
/* ── 통계 카드 ── */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.stat-card {
background: #fff;
border-radius: 14px;
padding: 20px 24px;
border: 1px solid #e2e8f0;
}
.stat-label {
font-size: 13px;
color: #94a3b8;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
color: #1e293b;
}
.stat-value.highlight {
color: #8b5cf6;
}
/* ── 테이블 ── */
.table-section {
background: #fff;
border-radius: 14px;
border: 1px solid #e2e8f0;
overflow: hidden;
}
.table-header {
padding: 16px 24px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-title {
font-size: 16px;
font-weight: 600;
}
.table-count {
font-size: 14px;
color: #64748b;
}
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
thead {
background: #f8fafc;
}
th {
padding: 14px 16px;
text-align: left;
font-weight: 600;
color: #64748b;
white-space: nowrap;
border-bottom: 1px solid #e2e8f0;
}
th.center, td.center {
text-align: center;
}
th.right, td.right {
text-align: right;
}
td {
padding: 14px 16px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
tr:hover {
background: #faf5ff;
cursor: pointer;
}
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;
font-size: 13px;
color: #64748b;
}
.time {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}
.amount {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
color: #1e293b;
}
.customer-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.customer-badge:hover {
transform: scale(1.05);
}
.customer-badge.has-name {
background: #dbeafe;
color: #1e40af;
}
.customer-badge.has-name:hover {
background: #bfdbfe;
}
.customer-badge.no-name {
background: #f1f5f9;
color: #94a3b8;
border: 1px dashed #cbd5e1;
}
.customer-badge.no-name:hover {
background: #e2e8f0;
border-color: #3b82f6;
color: #2563eb;
}
.mileage-badge {
display: inline-block;
padding: 2px 6px;
margin-left: 6px;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: white;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
/* 고객 검색 모달 */
.customer-modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 2000;
align-items: center;
justify-content: center;
}
.customer-modal.show { display: flex; }
.customer-modal-content {
background: #fff;
border-radius: 16px;
padding: 24px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.customer-modal h3 {
margin: 0 0 16px 0;
color: #1e40af;
font-size: 18px;
}
.customer-search-box {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.customer-search-input {
flex: 1;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
}
.customer-search-input:focus {
outline: none;
border-color: #3b82f6;
}
.customer-search-btn {
padding: 12px 20px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
}
.customer-search-btn:hover {
background: #2563eb;
}
.customer-results {
flex: 1;
overflow-y: auto;
max-height: 400px;
}
.customer-result-item {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.customer-result-item:hover {
background: #f0f9ff;
border-color: #3b82f6;
}
.customer-result-name {
font-weight: 600;
color: #1e293b;
}
.customer-result-birth {
font-size: 12px;
color: #64748b;
margin-left: 8px;
}
.customer-result-activity {
font-size: 12px;
color: #94a3b8;
margin-top: 4px;
}
.customer-result-activity.recent {
color: #22c55e;
}
.customer-modal-close {
margin-top: 16px;
padding: 10px 20px;
background: #f1f5f9;
color: #64748b;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
}
.customer-modal-close:hover {
background: #e2e8f0;
}
.customer-no-results {
text-align: center;
color: #94a3b8;
padding: 40px 20px;
}
.customer-order-info {
background: #f0f9ff;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 13px;
color: #64748b;
}
/* 결제수단 뱃지 */
.pay-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.pay-card { background: #dbeafe; color: #1d4ed8; }
.pay-cash { background: #dcfce7; color: #15803d; }
.pay-receipt { background: #fef3c7; color: #b45309; }
.pay-mixed { background: #fae8ff; color: #a21caf; }
.pay-none { background: #f1f5f9; color: #94a3b8; }
/* 상태 아이콘 */
.status-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
font-size: 14px;
}
.status-icon.qr-yes { background: #dcfce7; }
.status-icon.qr-no { background: #fee2e2; }
.status-icon.claimed { background: #dbeafe; }
.claimed-info {
font-size: 12px;
color: #6366f1;
font-weight: 500;
}
.claimed-phone {
font-size: 11px;
color: #94a3b8;
font-family: 'JetBrains Mono', monospace;
}
/* ── 상세 패널 ── */
.detail-panel {
position: fixed;
top: 0;
right: -500px;
width: 480px;
height: 100vh;
background: #fff;
box-shadow: -4px 0 24px rgba(0,0,0,0.1);
transition: right 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.detail-panel.open {
right: 0;
}
.detail-header {
padding: 20px 24px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-title {
font-size: 18px;
font-weight: 700;
}
.detail-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #64748b;
padding: 4px;
}
.detail-close:hover { color: #1e293b; }
.detail-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.detail-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.detail-info-item {
background: #f8fafc;
padding: 14px 16px;
border-radius: 10px;
}
.detail-info-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
}
.detail-info-value {
font-size: 16px;
font-weight: 600;
}
.detail-items-title {
font-size: 14px;
font-weight: 600;
color: #64748b;
margin-bottom: 12px;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f1f5f9;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-item-name {
font-weight: 500;
flex: 1;
}
.detail-item-qty {
color: #64748b;
margin: 0 16px;
font-family: 'JetBrains Mono', monospace;
}
.detail-item-price {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}
.item-label-btn {
padding: 4px 8px;
background: linear-gradient(135deg, #f59e0b, #d97706);
border: none;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
margin-left: 8px;
transition: all 0.2s;
}
.item-label-btn:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(245,158,11,0.4);
}
.item-label-btn:disabled {
opacity: 0.6;
cursor: wait;
}
/* ── 오버레이 ── */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.3);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.overlay.visible {
opacity: 1;
visibility: visible;
}
/* ── 로딩 ── */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 60px;
color: #94a3b8;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #e2e8f0;
border-top-color: #8b5cf6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ── 빈 상태 ── */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #94a3b8;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
/* ── QR 모달 ── */
.qr-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.qr-modal.visible {
opacity: 1;
visibility: visible;
}
.qr-modal-content {
background: #fff;
border-radius: 16px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
transform: scale(0.9);
transition: transform 0.3s;
}
.qr-modal.visible .qr-modal-content {
transform: scale(1);
}
.qr-modal-header {
padding: 20px 24px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.qr-modal-title {
font-size: 18px;
font-weight: 700;
color: #1e293b;
}
.qr-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #64748b;
}
.qr-modal-body {
padding: 24px;
}
.qr-preview-container {
background: #f8fafc;
border-radius: 12px;
padding: 20px;
text-align: center;
margin-bottom: 20px;
}
.qr-preview-img {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.qr-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 24px;
}
.qr-info-item {
background: #f8fafc;
padding: 12px 16px;
border-radius: 10px;
}
.qr-info-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
}
.qr-info-value {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.qr-info-value.points {
color: #8b5cf6;
}
.qr-actions {
display: flex;
gap: 12px;
}
.qr-actions .btn {
flex: 1;
padding: 14px;
}
.printer-select {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.printer-option {
flex: 1;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #fff;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.printer-option:hover {
border-color: #8b5cf6;
}
.printer-option.selected {
border-color: #8b5cf6;
background: #faf5ff;
}
.printer-option-icon {
font-size: 24px;
margin-bottom: 4px;
}
.printer-option-name {
font-size: 13px;
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;
}
/* ── 반려동물 카드 ── */
.detail-pets-section {
margin: 16px 0;
}
.pets-grid {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.pet-card {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: linear-gradient(135deg, #fef3c7, #fde68a);
border-radius: 12px;
flex: 1;
min-width: 140px;
max-width: 200px;
}
.pet-photo {
width: 44px;
height: 44px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.pet-photo-placeholder {
width: 44px;
height: 44px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.pet-info {
flex: 1;
min-width: 0;
}
.pet-name {
font-weight: 700;
font-size: 14px;
color: #92400e;
}
.pet-breed {
font-size: 11px;
color: #a16207;
margin-top: 2px;
}
/* ── 반응형 ── */
@media (max-width: 768px) {
.control-section {
flex-direction: column;
align-items: stretch;
}
.control-left, .control-right {
justify-content: center;
}
.stats-row {
grid-template-columns: 1fr 1fr;
}
.detail-panel {
width: 100%;
right: -100%;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-nav">
<a href="/admin">← 관리자 홈</a>
<a href="/admin/pos">월간 매출</a>
</div>
<h1>📊 실시간 판매 조회</h1>
<p>POS 판매 내역을 실시간으로 확인합니다 (Qt GUI 웹 버전)</p>
</div>
<div class="content">
<!-- 컨트롤 영역 -->
<div class="control-section">
<div class="control-left">
<input type="date" id="dateInput" class="date-input">
<button class="btn btn-primary" onclick="loadSales()">조회</button>
<button class="btn btn-secondary" onclick="setToday()">오늘</button>
</div>
<div class="control-right">
<button class="btn btn-kiosk" id="kioskBtn" onclick="triggerKiosk()" disabled>📺 키오스크</button>
<button class="btn btn-qr" id="qrBtn" onclick="triggerQrPrint()" disabled>🏷️ 라벨출력</button>
<label class="auto-refresh">
<input type="checkbox" id="autoRefresh">
자동 새로고침 (30초)
</label>
<button class="btn btn-success" onclick="loadSales()">🔄 새로고침</button>
</div>
</div>
<!-- 통계 카드 -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">총 판매 건수</div>
<div class="stat-value" id="totalCount">-</div>
</div>
<div class="stat-card">
<div class="stat-label">총 매출액</div>
<div class="stat-value highlight" id="totalSales">-</div>
</div>
<div class="stat-card">
<div class="stat-label">QR 발행</div>
<div class="stat-value" id="qrCount">-</div>
</div>
<div class="stat-card">
<div class="stat-label">적립 완료</div>
<div class="stat-value" id="claimedCount">-</div>
</div>
</div>
<!-- 테이블 -->
<div class="table-section">
<div class="table-header">
<span class="table-title">판매 내역</span>
<span class="table-count" id="tableCount"></span>
</div>
<div class="table-wrapper">
<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>
<th class="center">결제</th>
<th class="center">품목</th>
<th class="center">QR</th>
<th>적립</th>
</tr>
</thead>
<tbody id="salesTable">
<tr>
<td colspan="7">
<div class="loading">
<div class="spinner"></div>
데이터 로딩 중...
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 상세 패널 -->
<div class="overlay" id="overlay" onclick="closeDetail()"></div>
<div class="detail-panel" id="detailPanel">
<div class="detail-header">
<span class="detail-title">판매 상세</span>
<button class="detail-close" onclick="closeDetail()">×</button>
</div>
<div class="detail-content" id="detailContent">
<!-- 동적 로드 -->
</div>
</div>
<script>
let salesData = [];
let refreshInterval = null;
// 초기화
document.addEventListener('DOMContentLoaded', () => {
setToday();
loadSales();
// 자동 새로고침 토글
document.getElementById('autoRefresh').addEventListener('change', (e) => {
if (e.target.checked) {
refreshInterval = setInterval(loadSales, 30000);
} else {
clearInterval(refreshInterval);
refreshInterval = null;
}
});
});
function setToday() {
const today = new Date();
const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, '0');
const dd = String(today.getDate()).padStart(2, '0');
document.getElementById('dateInput').value = `${yyyy}-${mm}-${dd}`;
}
async function loadSales() {
const dateInput = document.getElementById('dateInput').value;
const dateStr = dateInput.replace(/-/g, '');
document.getElementById('salesTable').innerHTML = `
<tr><td colspan="7">
<div class="loading"><div class="spinner"></div>데이터 로딩 중...</div>
</td></tr>
`;
try {
const res = await fetch(`/api/admin/pos-live?date=${dateStr}`);
const data = await res.json();
if (!data.success) {
throw new Error(data.error || '조회 실패');
}
salesData = data.sales;
renderTable(data);
updateStats(data);
} catch (err) {
document.getElementById('salesTable').innerHTML = `
<tr><td colspan="7">
<div class="empty-state">
<div class="empty-icon">⚠️</div>
<div>오류: ${err.message}</div>
</div>
</td></tr>
`;
}
}
function updateStats(data) {
document.getElementById('totalCount').textContent = data.count.toLocaleString();
document.getElementById('totalSales').textContent = '₩' + Math.floor(data.total_sales).toLocaleString();
const qrCount = data.sales.filter(s => s.qr_issued).length;
const claimedCount = data.sales.filter(s => s.claimed_name).length;
document.getElementById('qrCount').textContent = qrCount + ' / ' + data.count;
document.getElementById('claimedCount').textContent = claimedCount;
document.getElementById('tableCount').textContent = `${data.count}`;
}
function renderTable(data) {
// 전체 선택 체크박스 초기화
document.getElementById('selectAll').checked = false;
if (data.sales.length === 0) {
document.getElementById('salesTable').innerHTML = `
<tr><td colspan="8">
<div class="empty-state">
<div class="empty-icon">📭</div>
<div>판매 내역이 없습니다</div>
</div>
</td></tr>
`;
updateSelectedCount();
return;
}
const rows = data.sales.map((sale, idx) => {
const payBadge = getPayBadge(sale.pay_method);
const hasCustomer = sale.customer && sale.customer !== '[비고객]' && sale.customer_code !== '0000000000';
const mileageSpan = hasCustomer
? `<span class="mileage-badge" id="mileage-${sale.customer_code}" style="display:none;"></span>`
: '';
const customerBadge = hasCustomer
? `<span class="customer-badge has-name" onclick="event.stopPropagation();openCustomerModal('${sale.order_no}', '${escapeHtml(sale.customer)}', '${sale.customer_code}')">${escapeHtml(sale.customer)}</span>${mileageSpan}`
: `<span class="customer-badge no-name" onclick="event.stopPropagation();openCustomerModal('${sale.order_no}', '', '0000000000')">미입력</span>`;
const qrIcon = sale.qr_issued
? '<span class="status-icon qr-yes">✓</span>'
: '<span class="status-icon qr-no">✗</span>';
let claimedHtml = '-';
if (sale.claimed_name) {
// 반려동물 아이콘 생성
let petIcons = '';
if (sale.pets && sale.pets.length > 0) {
petIcons = sale.pets.map(p =>
p.species === 'dog' ? '🐕' : p.species === 'cat' ? '🐈' : '🐾'
).join('');
}
claimedHtml = `
<div class="claimed-info">${sale.claimed_name} ${petIcons}</div>
<div class="claimed-phone">${formatPhone(sale.claimed_phone)}</div>
`;
}
return `
<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>${customerBadge}</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();
// 비동기로 마일리지 조회
fetchMileagesAsync(data.sales);
}
async function fetchMileagesAsync(sales) {
// 고객코드 있는 건들만 수집 (중복 제거)
const cusCodes = [...new Set(
sales
.filter(s => s.customer_code && s.customer_code !== '0000000000')
.map(s => s.customer_code)
)];
// 각 고객코드에 대해 비동기 조회
for (const code of cusCodes) {
try {
const res = await fetch(`/api/customers/${code}/mileage`);
const data = await res.json();
if (data.success && data.mileage !== null) {
// 해당 코드의 모든 mileage-badge 업데이트
const badges = document.querySelectorAll(`#mileage-${code}`);
badges.forEach(badge => {
badge.textContent = data.mileage.toLocaleString() + 'P';
badge.style.display = 'inline-block';
});
}
} catch (err) {
console.warn(`마일리지 조회 실패 (${code}):`, err);
}
}
}
function getPayBadge(method) {
const badges = {
'카드': '<span class="pay-badge pay-card">카드</span>',
'현금': '<span class="pay-badge pay-cash">현금</span>',
'현영': '<span class="pay-badge pay-receipt">현영</span>',
'카드+현금': '<span class="pay-badge pay-mixed">복합</span>',
};
return badges[method] || '<span class="pay-badge pay-none">-</span>';
}
function formatPhone(phone) {
if (!phone) return '';
const p = phone.replace(/\D/g, '');
if (p.length === 11) {
return `${p.slice(0,3)}-${p.slice(3,7)}-${p.slice(7)}`;
}
return phone;
}
function closeDetail() {
document.getElementById('overlay').classList.remove('visible');
document.getElementById('detailPanel').classList.remove('open');
document.querySelectorAll('#salesTable tr').forEach(tr => tr.classList.remove('selected'));
}
// ESC 키로 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeDetail();
closeQrModal();
}
});
// ═══════════════════════════════════════════════════════════════
// 체크박스 선택 기능
// ═══════════════════════════════════════════════════════════════
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) {
// 선택 표시
document.querySelectorAll('#salesTable tr').forEach(tr => tr.classList.remove('selected'));
document.querySelector(`#salesTable tr[data-idx="${idx}"]`)?.classList.add('selected');
const sale = salesData[idx];
// 패널 열기
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>`
: '';
// 반려동물 섹션
let petsSection = '';
if (sale.pets && sale.pets.length > 0) {
const petsHtml = sale.pets.map(pet => {
const speciesIcon = pet.species === 'dog' ? '🐕' : pet.species === 'cat' ? '🐈' : '🐾';
const speciesName = pet.species === 'dog' ? '강아지' : pet.species === 'cat' ? '고양이' : '반려동물';
const photoHtml = pet.photo_url
? `<img src="${pet.photo_url}" class="pet-photo" alt="${pet.name}">`
: `<div class="pet-photo-placeholder">${speciesIcon}</div>`;
return `
<div class="pet-card">
${photoHtml}
<div class="pet-info">
<div class="pet-name">${speciesIcon} ${pet.name}</div>
<div class="pet-breed">${pet.breed || speciesName}</div>
</div>
</div>
`;
}).join('');
petsSection = `
<div class="detail-pets-section">
<div class="detail-items-title">🐾 반려동물</div>
<div class="pets-grid">${petsHtml}</div>
</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>
<div class="detail-info-value" style="font-size:13px; font-family:'JetBrains Mono'">${orderNo}</div>
</div>
<div class="detail-info-item">
<div class="detail-info-label">시간</div>
<div class="detail-info-value">${sale.time}</div>
</div>
<div class="detail-info-item">
<div class="detail-info-label">고객</div>
<div class="detail-info-value">${sale.customer}</div>
</div>
<div class="detail-info-item">
<div class="detail-info-label">결제수단</div>
<div class="detail-info-value">${sale.pay_method || '-'}</div>
</div>
<div class="detail-info-item">
<div class="detail-info-label">판매금액</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">QR 상태</div>
<div class="detail-info-value">${qrStatus}</div>
</div>
</div>
${claimedInfo}
${petsSection}
<div class="detail-items-title">📦 품목 목록</div>
<div id="itemsList">
<div class="loading"><div class="spinner"></div>품목 로딩 중...</div>
</div>
`;
// 품목 상세 로드
loadItemsDetail(orderNo);
}
async function loadItemsDetail(orderNo) {
try {
const res = await fetch(`/api/admin/pos-live/detail/${orderNo}`);
const data = await res.json();
if (data.success && data.items.length > 0) {
const itemsHtml = data.items.map(item => `
<div class="detail-item">
<span class="detail-item-name">${item.product_name}</span>
<span class="detail-item-qty">×${item.quantity}</span>
<span class="detail-item-price">₩${Math.floor(item.total_price).toLocaleString()}</span>
${item.barcode ? `<button class="item-label-btn" onclick="printOtcLabel('${item.barcode}', '${escapeHtml(item.product_name)}')">💊</button>` : ''}
</div>
`).join('');
document.getElementById('itemsList').innerHTML = itemsHtml;
} else {
document.getElementById('itemsList').innerHTML = '<div class="empty-state">품목 정보 없음</div>';
}
} catch (err) {
document.getElementById('itemsList').innerHTML = `<div class="empty-state">오류: ${err.message}</div>`;
}
}
// OTC 용법 라벨 인쇄
async function printOtcLabel(barcode, productName) {
const btn = event.target;
btn.disabled = true;
btn.textContent = '...';
try {
// 프리셋 확인
const checkRes = await fetch(`/api/admin/otc-labels/${barcode}`);
const checkData = await checkRes.json();
if (checkData.exists) {
// 프리셋 있음 → 바로 인쇄
const preset = checkData.label;
const printRes = await fetch('/api/admin/otc-labels/print', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
barcode: barcode,
drug_name: preset.display_name || productName,
effect: preset.effect || '',
dosage_instruction: preset.dosage_instruction || '',
usage_tip: preset.usage_tip || ''
})
});
const printData = await printRes.json();
if (printData.success) {
btn.textContent = '✓';
btn.style.background = '#10b981';
showToast(`용법 라벨 인쇄 완료!`, 'success');
} else {
btn.textContent = '💊';
btn.disabled = false;
showToast(printData.error || '인쇄 실패', 'error');
}
} else {
// 프리셋 없음 → 새 창으로 등록 페이지 열기
btn.textContent = '💊';
btn.disabled = false;
showToast('프리셋 미등록 → 등록 페이지로 이동', 'info');
window.open(`/admin/otc-labels?barcode=${barcode}&name=${encodeURIComponent(productName)}`, '_blank', 'width=1200,height=800');
}
} catch (err) {
btn.textContent = '💊';
btn.disabled = false;
showToast('오류: ' + err.message, 'error');
}
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);
}
// ═══════════════════════════════════════════════════════════════
// 상세 패널에서 단일 건 처리
// ═══════════════════════════════════════════════════════════════
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건씩)
// ═══════════════════════════════════════════════════════════════
async function triggerKiosk() {
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;
btn.textContent = '전송 중...';
try {
const res = await fetch('/api/kiosk/trigger', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
transaction_id: sale.order_no,
amount: sale.amount
})
});
const data = await res.json();
if (data.success) {
btn.textContent = `${data.points}P`;
btn.style.background = '#10b981';
showToast(`키오스크 전송 완료! (${data.points}P)`, 'success');
setTimeout(() => {
loadSales();
}, 1500);
} else {
showToast(data.message || '전송 실패', 'error');
btn.textContent = originalText;
btn.disabled = false;
}
} catch (err) {
showToast(`오류: ${err.message}`, 'error');
btn.textContent = originalText;
btn.disabled = false;
}
}
// ═══════════════════════════════════════════════════════════════
// 라벨 출력 (Brother QL-810W) - 여러 건 순차 출력
// ═══════════════════════════════════════════════════════════════
async function triggerQrPrint() {
if (selectedSales.length === 0) {
showToast('먼저 판매 건을 선택해주세요.', 'error');
return;
}
const btn = document.getElementById('qrBtn');
const originalText = btn.textContent;
btn.disabled = true;
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: sale.order_no,
printer: 'brother'
})
});
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++;
}
}
// 결과 표시
if (successCount > 0) {
btn.textContent = `${successCount}건 완료`;
btn.style.background = '#10b981';
showToast(`라벨 출력 완료! (${successCount}건)`, 'success');
}
if (errorCount > 0) {
showToast(`${errorCount}건 실패`, 'error');
}
setTimeout(() => {
loadSales();
}, 1500);
}
// ═══════════════════════════════════════════════════════════════
// 토스트 메시지
// ═══════════════════════════════════════════════════════════════
function showToast(message, type = 'info') {
// 기존 토스트 제거
const existing = document.querySelector('.toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'toast';
toast.style.cssText = `
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
padding: 14px 28px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
z-index: 9999;
animation: toastIn 0.3s ease;
${type === 'success'
? 'background: #10b981; color: white;'
: type === 'error'
? 'background: #ef4444; color: white;'
: 'background: #1e293b; color: white;'}
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// 토스트 애니메이션 추가
const toastStyle = document.createElement('style');
toastStyle.textContent = `
@keyframes toastIn {
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes toastOut {
from { opacity: 1; transform: translateX(-50%) translateY(0); }
to { opacity: 0; transform: translateX(-50%) translateY(20px); }
}
`;
document.head.appendChild(toastStyle);
// ═══════════════════════════════════════════════════════════════
// 고객 검색/매핑 모달
// ═══════════════════════════════════════════════════════════════
let currentOrderNo = null;
let currentCustomerName = '';
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function openCustomerModal(orderNo, customerName, customerCode) {
currentOrderNo = orderNo;
currentCustomerName = customerName;
document.getElementById('customerOrderInfo').textContent =
`주문번호: ${orderNo} | 현재: ${customerName || '미입력'}`;
document.getElementById('customerSearchInput').value = customerName || '';
document.getElementById('customerResults').innerHTML =
'<div class="customer-no-results">이름을 검색하세요</div>';
document.getElementById('customerModal').classList.add('show');
document.getElementById('customerSearchInput').focus();
}
function closeCustomerModal() {
document.getElementById('customerModal').classList.remove('show');
currentOrderNo = null;
}
async function searchCustomers() {
const name = document.getElementById('customerSearchInput').value.trim();
if (name.length < 2) {
document.getElementById('customerResults').innerHTML =
'<div class="customer-no-results">2자 이상 입력하세요</div>';
return;
}
try {
const res = await fetch(`/api/customers/search?name=${encodeURIComponent(name)}`);
const data = await res.json();
if (!data.success) {
document.getElementById('customerResults').innerHTML =
`<div class="customer-no-results">${data.error}</div>`;
return;
}
if (data.results.length === 0) {
document.getElementById('customerResults').innerHTML =
'<div class="customer-no-results">검색 결과가 없습니다</div>';
return;
}
const html = data.results.map(c => {
const birthDisplay = c.birth
? `(${c.birth.substring(0,2)}.${c.birth.substring(2,4)}.${c.birth.substring(4,6)})`
: '';
let activityHtml = '';
if (c.activity_type && c.days_ago !== null) {
const isRecent = c.days_ago <= 30;
activityHtml = `<div class="customer-result-activity ${isRecent ? 'recent' : ''}">
${c.activity_type === '조제' ? '📋' : '🛒'}
${c.activity_type} ${c.days_ago === 0 ? '오늘' : c.days_ago + '일 전'}
</div>`;
} else {
activityHtml = '<div class="customer-result-activity">활동 기록 없음</div>';
}
return `
<div class="customer-result-item" onclick="selectCustomer('${c.cus_code}', '${escapeHtml(c.name)}')">
<span class="customer-result-name">${escapeHtml(c.name)}</span>
<span class="customer-result-birth">${birthDisplay}</span>
${activityHtml}
</div>
`;
}).join('');
document.getElementById('customerResults').innerHTML = html;
} catch (err) {
document.getElementById('customerResults').innerHTML =
`<div class="customer-no-results">오류: ${err.message}</div>`;
}
}
async function selectCustomer(cusCode, cusName) {
if (!currentOrderNo) return;
try {
const res = await fetch(`/api/pos-live/${currentOrderNo}/customer`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cus_code: cusCode, cus_name: cusName })
});
const data = await res.json();
if (data.success) {
showToast(`${cusName}님으로 업데이트됨`, 'success');
closeCustomerModal();
loadSales(); // 테이블 새로고침
} else {
showToast('업데이트 실패: ' + data.error, 'error');
}
} catch (err) {
showToast('오류: ' + err.message, 'error');
}
}
// Enter 키로 검색
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('customerSearchInput')?.addEventListener('keypress', e => {
if (e.key === 'Enter') searchCustomers();
});
});
// 모달 외부 클릭 시 닫기
document.getElementById('customerModal')?.addEventListener('click', e => {
if (e.target.id === 'customerModal') closeCustomerModal();
});
</script>
<!-- 고객 검색 모달 -->
<div class="customer-modal" id="customerModal">
<div class="customer-modal-content">
<h3>👤 고객 매핑</h3>
<div class="customer-order-info" id="customerOrderInfo">주문번호: -</div>
<div class="customer-search-box">
<input type="text" class="customer-search-input" id="customerSearchInput"
placeholder="이름 검색..." autocomplete="off">
<button class="customer-search-btn" onclick="searchCustomers()">검색</button>
</div>
<div class="customer-results" id="customerResults">
<div class="customer-no-results">이름을 검색하세요</div>
</div>
<button class="customer-modal-close" onclick="closeCustomerModal()">닫기</button>
</div>
</div>
</body>
</html>