pharmacy-pos-qr-system/backend/templates/admin_rx_usage.html
thug0bin be95f8b3d1 feat(order): 수인약품 선별 주문 및 rx-usage 주문 전송 개선
- order_api.py: 수인 주문 시 submit_order_selective() 사용 (기존 품목 보존)
- admin_rx_usage.html: cart_only=false로 변경 (장바구니+주문확정)
- 버튼 텍스트 변경: 장바구니 담기 → 주문 전송
2026-03-06 21:52:40 +09:00

2862 lines
104 KiB
HTML

<!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;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-card: #1e293b;
--bg-card-hover: #334155;
--border: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-teal: #14b8a6;
--accent-blue: #3b82f6;
--accent-purple: #a855f7;
--accent-amber: #f59e0b;
--accent-emerald: #10b981;
--accent-rose: #f43f5e;
--accent-orange: #f97316;
--accent-cyan: #06b6d4;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
/* ══════════════════ 헤더 ══════════════════ */
.header {
background: linear-gradient(135deg, #0891b2 0%, #06b6d4 50%, #22d3ee 100%);
padding: 20px 24px;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.header-inner {
max-width: 1600px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 10px;
}
.header-left p {
font-size: 13px;
opacity: 0.85;
margin-top: 4px;
}
.header-nav {
display: flex;
gap: 8px;
}
.header-nav a {
color: rgba(255,255,255,0.85);
text-decoration: none;
font-size: 13px;
font-weight: 500;
padding: 8px 14px;
border-radius: 8px;
background: rgba(255,255,255,0.1);
transition: all 0.2s;
}
.header-nav a:hover {
background: rgba(255,255,255,0.2);
color: #fff;
}
.header-nav a.active {
background: rgba(255,255,255,0.25);
}
/* ══════════════════ 컨텐츠 ══════════════════ */
.content {
max-width: 1600px;
margin: 0 auto;
padding: 24px;
}
/* ══════════════════ 검색 영역 ══════════════════ */
.search-bar {
background: var(--bg-card);
border-radius: 16px;
padding: 20px 24px;
margin-bottom: 20px;
border: 1px solid var(--border);
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: flex-end;
}
.search-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.search-group label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.search-group input, .search-group select {
padding: 10px 14px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
color: var(--text-primary);
min-width: 140px;
transition: all 0.2s;
}
.search-group input[type="date"] {
min-width: 160px;
}
.search-group input:focus, .search-group select:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.2);
}
.search-group input::placeholder { color: var(--text-muted); }
.search-btn {
background: linear-gradient(135deg, var(--accent-cyan), #0891b2);
color: #fff;
border: none;
padding: 10px 28px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.search-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4);
}
/* ══════════════════ 통계 카드 ══════════════════ */
.stats-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-card);
border-radius: 14px;
padding: 20px;
border: 1px solid var(--border);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
.stat-card.cyan::before { background: var(--accent-cyan); }
.stat-card.blue::before { background: var(--accent-blue); }
.stat-card.purple::before { background: var(--accent-purple); }
.stat-card.amber::before { background: var(--accent-amber); }
.stat-card.emerald::before { background: var(--accent-emerald); }
.stat-card.orange::before { background: var(--accent-orange); }
.stat-icon {
font-size: 24px;
margin-bottom: 12px;
}
.stat-value {
font-size: 26px;
font-weight: 700;
letter-spacing: -1px;
margin-bottom: 4px;
}
.stat-card.cyan .stat-value { color: var(--accent-cyan); }
.stat-card.blue .stat-value { color: var(--accent-blue); }
.stat-card.purple .stat-value { color: var(--accent-purple); }
.stat-card.amber .stat-value { color: var(--accent-amber); }
.stat-card.emerald .stat-value { color: var(--accent-emerald); }
.stat-card.orange .stat-value { color: var(--accent-orange); }
.stat-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ══════════════════ 뷰 컨트롤 ══════════════════ */
.view-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.view-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.rx-badge {
background: linear-gradient(135deg, var(--accent-cyan), #0891b2);
color: #fff;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
}
.view-actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.action-btn.primary {
background: linear-gradient(135deg, var(--accent-emerald), var(--accent-teal));
color: #fff;
}
.action-btn.primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.action-btn.secondary {
background: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-secondary);
}
.action-btn.secondary:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
/* ══════════════════ 테이블 ══════════════════ */
.table-wrap {
background: var(--bg-card);
border-radius: 16px;
border: 1px solid var(--border);
overflow: hidden;
}
.usage-table {
width: 100%;
border-collapse: collapse;
}
.usage-table th {
padding: 14px 16px;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: left;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
}
.usage-table th.center { text-align: center; }
.usage-table th.right { text-align: right; }
.usage-table td {
padding: 14px 16px;
font-size: 13px;
border-bottom: 1px solid rgba(255,255,255,0.05);
vertical-align: middle;
}
.usage-table tr:hover {
background: rgba(255,255,255,0.02);
}
.usage-table tr.selected {
background: rgba(6, 182, 212, 0.1);
}
/* 체크박스 */
.check-col {
width: 40px;
text-align: center;
}
.custom-check {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-radius: 4px;
cursor: pointer;
appearance: none;
background: var(--bg-primary);
transition: all 0.2s;
}
.custom-check:checked {
background: var(--accent-cyan);
border-color: var(--accent-cyan);
}
.custom-check:checked::after {
content: '✓';
display: block;
text-align: center;
color: #fff;
font-size: 12px;
font-weight: bold;
line-height: 16px;
}
/* 제품 셀 */
.product-cell {
display: flex;
align-items: center;
gap: 12px;
}
.product-thumb {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 8px;
background: var(--bg-secondary);
flex-shrink: 0;
}
.product-thumb-placeholder {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0c4a6e 0%, #164e63 100%);
border-radius: 8px;
flex-shrink: 0;
border: 1px solid rgba(6, 182, 212, 0.2);
font-size: 18px;
}
.product-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.product-name {
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-code {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-muted);
}
.location-badge {
display: inline-block;
background: rgba(99, 102, 241, 0.15);
color: var(--accent-indigo);
font-size: 10px;
font-weight: 500;
padding: 1px 5px;
border-radius: 4px;
margin-left: 4px;
font-family: inherit;
}
/* 수량 관련 */
.qty-cell {
text-align: center;
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}
.qty-high { color: var(--accent-rose); }
.qty-mid { color: var(--accent-amber); }
.qty-low { color: var(--accent-emerald); }
/* 주문 수량 입력 */
.order-input {
width: 70px;
padding: 8px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
font-family: 'JetBrains Mono', monospace;
color: var(--text-primary);
text-align: center;
}
.order-input:focus {
outline: none;
border-color: var(--accent-cyan);
}
/* 공급업체 뱃지 */
.supplier-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
background: rgba(6, 182, 212, 0.2);
color: #22d3ee;
max-width: 120px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ══════════════════ 로딩/빈 상태 ══════════════════ */
.loading-state, .empty-state {
text-align: center;
padding: 80px 20px;
color: var(--text-muted);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--accent-cyan);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
/* ══════════════════ 주문 장바구니 ══════════════════ */
.cart-drawer {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 420px;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 200;
display: flex;
flex-direction: column;
}
.cart-drawer.open {
transform: translateX(0);
}
.cart-header {
padding: 20px 24px;
background: linear-gradient(135deg, #0891b2, var(--accent-cyan));
display: flex;
justify-content: space-between;
align-items: center;
}
.cart-header h2 {
font-size: 18px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.cart-close {
background: rgba(255,255,255,0.2);
border: none;
width: 32px;
height: 32px;
border-radius: 8px;
color: #fff;
font-size: 18px;
cursor: pointer;
}
.cart-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.cart-item {
background: var(--bg-card);
border-radius: 12px;
padding: 14px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.cart-item-info {
flex: 1;
min-width: 0;
}
.cart-item-name {
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cart-item-qty {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
.cart-item-actions {
display: flex;
gap: 6px;
align-items: center;
}
.cart-item-order {
background: rgba(16, 185, 129, 0.2);
border: none;
color: var(--accent-emerald);
padding: 4px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.cart-item-order:hover {
background: rgba(16, 185, 129, 0.4);
}
.cart-item-remove {
background: rgba(244, 63, 94, 0.2);
border: none;
color: var(--accent-rose);
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.cart-footer {
padding: 20px 24px;
background: var(--bg-card);
border-top: 1px solid var(--border);
}
.cart-summary {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.cart-total-label {
font-size: 14px;
color: var(--text-secondary);
}
.cart-total-value {
font-size: 24px;
font-weight: 700;
color: var(--accent-cyan);
}
.cart-submit {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #0891b2, var(--accent-cyan));
border: none;
border-radius: 10px;
color: #fff;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.cart-submit:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(6, 182, 212, 0.4);
}
.cart-submit:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* ══════════════════ 장바구니 FAB ══════════════════ */
.cart-fab {
position: fixed;
bottom: 32px;
right: 32px;
width: 64px;
height: 64px;
background: linear-gradient(135deg, #0891b2, var(--accent-cyan));
border: none;
border-radius: 20px;
color: #fff;
font-size: 28px;
cursor: pointer;
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.4);
transition: all 0.2s;
z-index: 150;
}
.cart-fab:hover {
transform: scale(1.05);
}
.cart-badge {
position: absolute;
top: -6px;
right: -6px;
width: 24px;
height: 24px;
background: var(--accent-rose);
border-radius: 50%;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
/* ══════════════════ 토스트 ══════════════════ */
.toast {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%) translateY(20px);
padding: 14px 28px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
font-size: 14px;
font-weight: 500;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
opacity: 0;
transition: all 0.3s;
z-index: 300;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast.success { border-color: var(--accent-emerald); }
.toast.error { border-color: var(--accent-rose); }
/* ══════════════════ 반응형 ══════════════════ */
@media (max-width: 1400px) {
.stats-grid { grid-template-columns: repeat(3, 1fr); }
}
/* 세로 모니터 (좁은 화면) 전용 */
@media (max-width: 1100px) {
.usage-table th,
.usage-table td {
padding: 10px 6px;
font-size: 11px;
}
.usage-table th {
font-size: 10px;
letter-spacing: 0;
}
.usage-table th:nth-child(2) { width: auto !important; } /* 약품 */
.product-cell { gap: 6px; }
.product-thumb,
.product-thumb-placeholder {
width: 28px;
height: 28px;
font-size: 12px;
}
.product-name { font-size: 11px; }
.product-code { font-size: 9px; }
.qty-cell { font-size: 11px; }
.order-input {
width: 55px;
padding: 5px 2px;
font-size: 12px;
}
}
@media (max-width: 768px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.header-nav { display: none; }
.search-bar { flex-direction: column; }
.search-group { width: 100%; }
.search-group input, .search-group select { width: 100%; }
.cart-drawer { width: 100%; }
.view-controls { flex-direction: column; gap: 12px; }
}
</style>
</head>
<body>
<div class="header">
<div class="header-inner">
<div class="header-left">
<h1>💊 전문의약품 사용량 · 주문</h1>
<p>처방전 조제 데이터 기반 발주</p>
</div>
<nav class="header-nav">
<a href="/admin">📊 대시보드</a>
<a href="/admin/usage">🏪 OTC 사용량</a>
<a href="/admin/rx-usage" class="active">💊 Rx 사용량</a>
<a href="/pmr">📋 처방전</a>
</nav>
</div>
</div>
<div class="content">
<!-- 검색 -->
<div class="search-bar">
<div class="search-group">
<label>시작일</label>
<input type="date" id="startDate">
</div>
<div class="search-group">
<label>종료일</label>
<input type="date" id="endDate">
</div>
<div class="search-group">
<label>검색어</label>
<input type="text" id="searchInput" placeholder="약품명, 코드...">
</div>
<div class="search-group">
<label>정렬</label>
<select id="sortSelect">
<option value="qty_desc">투약량 많은순</option>
<option value="qty_asc">투약량 적은순</option>
<option value="rx_desc">처방건수 많은순</option>
<option value="name_asc">이름순</option>
<option value="amount_desc">금액 높은순</option>
</select>
</div>
<button class="search-btn" onclick="loadUsageData()">🔍 조회</button>
<button class="search-btn" style="background: linear-gradient(135deg, #a855f7, #7c3aed);" onclick="openBalanceModal()">💰 도매상 잔고</button>
</div>
<!-- 통계 -->
<div class="stats-grid">
<div class="stat-card cyan">
<div class="stat-icon">📅</div>
<div class="stat-value" id="statPeriod">-</div>
<div class="stat-label">조회 기간</div>
</div>
<div class="stat-card blue">
<div class="stat-icon">💊</div>
<div class="stat-value" id="statProducts">-</div>
<div class="stat-label">처방 품목</div>
</div>
<div class="stat-card purple">
<div class="stat-icon">🔢</div>
<div class="stat-value" id="statTotalQty">-</div>
<div class="stat-label">총 처방수량</div>
</div>
<div class="stat-card amber">
<div class="stat-icon">📊</div>
<div class="stat-value" id="statTotalDose">-</div>
<div class="stat-label">총 투약량</div>
</div>
<div class="stat-card emerald">
<div class="stat-icon">💰</div>
<div class="stat-value" id="statTotalAmount">-</div>
<div class="stat-label">총 약가</div>
</div>
<div class="stat-card orange">
<div class="stat-icon">🛒</div>
<div class="stat-value" id="statCartCount">0</div>
<div class="stat-label">주문 품목</div>
</div>
</div>
<!-- 컨트롤 -->
<div class="view-controls">
<div class="view-title">
<span class="rx-badge">Rx</span>
<span>품목별 사용량</span>
<span id="resultCount" style="font-size:13px;color:var(--text-muted);font-weight:400;"></span>
</div>
<div class="view-actions">
<button class="action-btn secondary" onclick="selectAll()">☑️ 전체 선택</button>
<button class="action-btn secondary" onclick="selectNone()">⬜ 선택 해제</button>
<button class="action-btn primary" onclick="addSelectedToCart()">🛒 선택 항목 장바구니 추가</button>
</div>
</div>
<!-- 테이블 -->
<div class="table-wrap">
<table class="usage-table">
<thead>
<tr>
<th class="check-col"><input type="checkbox" class="custom-check" id="checkAll" onchange="toggleCheckAll()"></th>
<th style="width:32%">약품</th>
<th class="center">현재고</th>
<th class="center">처방횟수</th>
<th class="center">투약량</th>
<th class="right">약가</th>
<th class="center" style="width:90px">주문수량</th>
</tr>
</thead>
<tbody id="usageTableBody">
<tr>
<td colspan="7">
<div class="loading-state">
<div class="loading-spinner"></div>
<div>데이터 로딩 중...</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 장바구니 FAB -->
<button class="cart-fab" onclick="openCart()">
🛒
<span class="cart-badge" id="cartBadge" style="display:none;">0</span>
</button>
<!-- 장바구니 Drawer -->
<div class="cart-drawer" id="cartDrawer">
<div class="cart-header">
<h2>🛒 전문약 주문</h2>
<button class="cart-close" onclick="closeCart()"></button>
</div>
<div class="cart-body" id="cartBody">
<div class="empty-state">
<div class="empty-icon">🛒</div>
<div>장바구니가 비었습니다</div>
</div>
</div>
<div class="cart-footer">
<div class="cart-summary">
<span class="cart-total-label">총 주문 품목</span>
<span class="cart-total-value" id="cartTotalItems">0개</span>
</div>
<button class="cart-submit" id="submitOrderBtn" onclick="submitOrder()" disabled>
📤 주문서 생성하기
</button>
</div>
</div>
<!-- 토스트 -->
<div class="toast" id="toast"></div>
<script>
let usageData = [];
let cart = [];
// 초기화
document.addEventListener('DOMContentLoaded', function() {
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
document.getElementById('startDate').value = todayStr;
document.getElementById('endDate').value = todayStr;
loadUsageData();
});
// ──────────────── 데이터 로드 ────────────────
function loadUsageData() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const search = document.getElementById('searchInput').value;
const sort = document.getElementById('sortSelect').value;
document.getElementById('usageTableBody').innerHTML = `
<tr><td colspan="7">
<div class="loading-state">
<div class="loading-spinner"></div>
<div>데이터 로딩 중...</div>
</div>
</td></tr>`;
let url = `/api/rx-usage?start_date=${startDate}&end_date=${endDate}&sort=${sort}`;
if (search) url += `&search=${encodeURIComponent(search)}`;
fetch(url)
.then(res => res.json())
.then(data => {
if (data.success) {
usageData = data.items;
// 통계 업데이트
document.getElementById('statPeriod').textContent = data.stats.period_days + '일';
document.getElementById('statProducts').textContent = data.stats.product_count.toLocaleString();
document.getElementById('statTotalQty').textContent = data.stats.total_qty.toLocaleString();
document.getElementById('statTotalDose').textContent = data.stats.total_dose.toLocaleString();
document.getElementById('statTotalAmount').textContent = formatPrice(data.stats.total_amount);
document.getElementById('resultCount').textContent = `(${data.items.length}개)`;
renderTable();
} else {
document.getElementById('usageTableBody').innerHTML = `
<tr><td colspan="7">
<div class="empty-state">
<div class="empty-icon">⚠️</div>
<div>오류: ${data.error}</div>
</div>
</td></tr>`;
}
})
.catch(err => {
document.getElementById('usageTableBody').innerHTML = `
<tr><td colspan="7">
<div class="empty-state">
<div class="empty-icon">❌</div>
<div>데이터 로드 실패</div>
</div>
</td></tr>`;
});
}
// ──────────────── 테이블 렌더링 ────────────────
function renderTable() {
const tbody = document.getElementById('usageTableBody');
if (usageData.length === 0) {
tbody.innerHTML = `
<tr><td colspan="7">
<div class="empty-state">
<div class="empty-icon">💊</div>
<div>해당 기간 처방 내역이 없습니다</div>
</div>
</td></tr>`;
return;
}
tbody.innerHTML = usageData.map((item, idx) => {
const qtyClass = item.total_dose >= 100 ? 'qty-high' : item.total_dose >= 30 ? 'qty-mid' : 'qty-low';
const inCart = cart.find(c => c.drug_code === item.drug_code);
return `
<tr data-idx="${idx}" class="${inCart ? 'selected' : ''}">
<td class="check-col">
<input type="checkbox" class="custom-check item-check" data-idx="${idx}" ${inCart ? 'checked' : ''}>
</td>
<td>
<div class="product-cell">
${item.thumbnail
? `<img src="data:image/jpeg;base64,${item.thumbnail}" class="product-thumb" alt="">`
: `<div class="product-thumb-placeholder">💊</div>`
}
<div class="product-info">
<span class="product-name">${escapeHtml(item.product_name)}</span>
<span class="product-code">${item.drug_code}${item.supplier ? ` · ${escapeHtml(item.supplier)}` : ''}${item.location ? ` <span class="location-badge">📍${escapeHtml(item.location)}</span>` : ''}</span>
</div>
</div>
</td>
<td class="qty-cell" style="color:${item.current_stock <= 0 ? 'var(--accent-rose)' : item.current_stock < item.total_dose ? 'var(--accent-amber)' : 'var(--accent-emerald)'};">
${item.current_stock.toLocaleString()}
</td>
<td class="qty-cell" style="color:var(--text-secondary);">${item.prescription_count}건</td>
<td class="qty-cell ${qtyClass}">${item.total_dose}</td>
<td style="text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;">
${formatPrice(item.total_amount)}
</td>
<td style="text-align:center;">
<input type="number" class="order-input"
data-idx="${idx}"
min="0"
value="${inCart ? inCart.qty : Math.ceil(item.total_dose / 10) * 10}"
placeholder="0">
</td>
</tr>
`;
}).join('');
}
// ──────────────── 선택 ────────────────
function toggleCheckAll() {
const checked = document.getElementById('checkAll').checked;
document.querySelectorAll('.item-check').forEach(cb => {
cb.checked = checked;
const row = cb.closest('tr');
row.classList.toggle('selected', checked);
});
}
function selectAll() {
document.getElementById('checkAll').checked = true;
toggleCheckAll();
}
function selectNone() {
document.getElementById('checkAll').checked = false;
toggleCheckAll();
}
// ──────────────── 장바구니 ────────────────
function addSelectedToCart() {
const checked = document.querySelectorAll('.item-check:checked');
if (checked.length === 0) {
showToast('선택된 품목이 없습니다', 'error');
return;
}
let added = 0;
checked.forEach(cb => {
const idx = parseInt(cb.dataset.idx);
const item = usageData[idx];
const qtyInput = document.querySelector(`.order-input[data-idx="${idx}"]`);
const qty = parseInt(qtyInput.value) || Math.ceil(item.total_dose / 10) * 10;
if (qty > 0) {
const existing = cart.find(c => c.drug_code === item.drug_code);
if (existing) {
existing.qty = qty;
} else {
cart.push({
drug_code: item.drug_code,
product_name: item.product_name,
supplier: item.supplier,
qty: qty
});
added++;
}
}
});
updateCartUI();
renderTable();
showToast(`${added}개 품목이 장바구니에 추가되었습니다`, 'success');
}
function removeFromCart(drugCode) {
cart = cart.filter(c => c.drug_code !== drugCode);
updateCartUI();
renderTable();
}
function updateCartUI() {
const badge = document.getElementById('cartBadge');
const count = cart.length;
badge.textContent = count;
badge.style.display = count > 0 ? 'flex' : 'none';
document.getElementById('statCartCount').textContent = count;
document.getElementById('cartTotalItems').textContent = count + '개';
document.getElementById('submitOrderBtn').disabled = count === 0;
const body = document.getElementById('cartBody');
if (count === 0) {
body.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🛒</div>
<div>장바구니가 비었습니다</div>
</div>`;
} else {
body.innerHTML = cart.map(item => `
<div class="cart-item">
<div class="cart-item-info">
<div class="cart-item-name">${escapeHtml(item.product_name)}</div>
<div class="cart-item-qty">${item.supplier || '-'} · ${item.qty}개</div>
</div>
<div class="cart-item-actions">
<button class="cart-item-order" onclick="orderSingleItem('${item.drug_code}')">📤주문</button>
<button class="cart-item-remove" onclick="removeFromCart('${item.drug_code}')">✕</button>
</div>
</div>
`).join('');
}
}
function openCart() {
document.getElementById('cartDrawer').classList.add('open');
}
function closeCart() {
document.getElementById('cartDrawer').classList.remove('open');
}
// ──────────────── 도매상 설정 (확장 가능) ────────────────
const WHOLESALERS = {
geoyoung: {
id: 'geoyoung',
name: '지오영',
icon: '🏭',
logo: '/static/img/logo_geoyoung.ico',
color: '#06b6d4',
gradient: 'linear-gradient(135deg, #0891b2, #06b6d4)',
filterFn: (item) => item.supplier === '지오영' || item.wholesaler === 'geoyoung',
getCode: (item) => item.geoyoung_code || item.drug_code
},
sooin: {
id: 'sooin',
name: '수인약품',
icon: '💊',
logo: '/static/img/logo_sooin.svg',
color: '#a855f7',
gradient: 'linear-gradient(135deg, #7c3aed, #a855f7)',
filterFn: (item) => item.supplier === '수인약품' || item.wholesaler === 'sooin',
getCode: (item) => item.sooin_code || item.drug_code
},
baekje: {
id: 'baekje',
name: '백제약품',
icon: '💉',
logo: '/static/img/logo_baekje.png',
color: '#f59e0b',
gradient: 'linear-gradient(135deg, #d97706, #f59e0b)',
filterFn: (item) => item.supplier === '백제약품' || item.wholesaler === 'baekje',
getCode: (item) => item.baekje_code || item.drug_code
}
};
// ──────────────── 주문 제출 ────────────────
let currentOrderWholesaler = null;
function submitOrder() {
if (cart.length === 0) return;
// 도매상별 분류
const itemsByWholesaler = {};
let otherItems = [...cart];
for (const [wsId, ws] of Object.entries(WHOLESALERS)) {
const wsItems = cart.filter(ws.filterFn);
if (wsItems.length > 0) {
itemsByWholesaler[wsId] = wsItems;
otherItems = otherItems.filter(item => !wsItems.includes(item));
}
}
const wsIds = Object.keys(itemsByWholesaler);
if (wsIds.length === 0) {
// API 지원 안 되는 품목만 있으면 클립보드
submitOrderClipboard();
return;
}
// 여러 도매상 품목이 있을 때 선택 모달
if (wsIds.length > 1) {
openWholesalerSelectModal(itemsByWholesaler, otherItems);
} else {
openOrderConfirmModal(wsIds[0], itemsByWholesaler[wsIds[0]]);
}
}
// 다중 도매상 선택을 위한 전역 변수
let pendingWholesalerItems = {};
let pendingOtherItems = [];
function openWholesalerSelectModal(itemsByWholesaler, otherItems) {
pendingWholesalerItems = itemsByWholesaler;
pendingOtherItems = otherItems;
const modal = document.getElementById('multiWholesalerModal');
const body = document.getElementById('multiWholesalerBody');
const wsIds = Object.keys(itemsByWholesaler);
let html = `
<div class="multi-ws-summary">
<p style="margin-bottom:16px;color:var(--text-secondary);">
장바구니에 <b>${wsIds.length}개 도매상</b>의 품목이 있습니다.
</p>
`;
// 각 도매상별 품목 표시
wsIds.forEach(wsId => {
const ws = WHOLESALERS[wsId];
const items = itemsByWholesaler[wsId];
html += `
<div class="multi-ws-card ${wsId}">
<div class="multi-ws-header">
<span class="multi-ws-icon">${ws.icon}</span>
<span class="multi-ws-name">${ws.name}</span>
<span class="multi-ws-count">${items.length}개 품목</span>
<label class="multi-ws-checkbox">
<input type="checkbox" id="ws_check_${wsId}" checked>
<span>포함</span>
</label>
</div>
<div class="multi-ws-items">
${items.slice(0, 3).map(item => `
<div class="multi-ws-item">· ${item.product_name} (${item.qty}개)</div>
`).join('')}
${items.length > 3 ? `<div class="multi-ws-item more">... 외 ${items.length - 3}개</div>` : ''}
</div>
</div>
`;
});
if (otherItems.length > 0) {
html += `
<div class="multi-ws-card other">
<div class="multi-ws-header">
<span class="multi-ws-icon">📋</span>
<span class="multi-ws-name">기타 (API 미지원)</span>
<span class="multi-ws-count">${otherItems.length}개 품목</span>
</div>
<div class="multi-ws-items">
<div class="multi-ws-item" style="color:var(--text-muted);">클립보드 복사로 처리됩니다</div>
</div>
</div>
`;
}
html += `</div>`;
body.innerHTML = html;
modal.classList.add('show');
}
function closeMultiWholesalerModal() {
document.getElementById('multiWholesalerModal').classList.remove('show');
}
// 선택된 도매상 전체 일괄 처리
async function executeAllWholesalers(dryRun = false) {
const wsIds = Object.keys(pendingWholesalerItems);
// 체크된 도매상만 필터
const selectedWsIds = wsIds.filter(wsId => {
const checkbox = document.getElementById(`ws_check_${wsId}`);
return checkbox && checkbox.checked;
});
if (selectedWsIds.length === 0) {
showToast('선택된 도매상이 없습니다', 'error');
return;
}
// 버튼 비활성화
const btnTest = document.getElementById('btnMultiTest');
const btnReal = document.getElementById('btnMultiReal');
btnTest.disabled = true;
btnReal.disabled = true;
btnReal.textContent = '처리 중...';
const allResults = [];
let totalSuccess = 0;
let totalFailed = 0;
// 각 도매상 순차 처리
for (const wsId of selectedWsIds) {
const ws = WHOLESALERS[wsId];
const items = pendingWholesalerItems[wsId];
showToast(`${ws.icon} ${ws.name} 처리 중... (${selectedWsIds.indexOf(wsId) + 1}/${selectedWsIds.length})`, 'info');
try {
const payload = {
wholesaler_id: wsId,
items: items.map(item => ({
drug_code: item.drug_code,
kd_code: item.geoyoung_code || item.sooin_code || item.drug_code,
internal_code: item.internal_code,
product_name: item.product_name,
manufacturer: item.supplier,
specification: item.specification || '',
order_qty: item.qty,
usage_qty: item.usage_qty || 0,
current_stock: item.current_stock || 0
})),
reference_period: `${document.getElementById('startDate').value}~${document.getElementById('endDate').value}`,
dry_run: dryRun,
cart_only: false // 장바구니 + 주문 확정까지
};
const response = await fetch('/api/order/quick-submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(180000) // 주문 확정까지 3분
});
const result = await response.json();
result.wholesaler_id = wsId;
allResults.push(result);
if (result.success) {
totalSuccess += result.success_count || 0;
totalFailed += result.failed_count || 0;
} else {
totalFailed += items.length;
}
} catch (err) {
allResults.push({
wholesaler_id: wsId,
success: false,
error: err.message,
success_count: 0,
failed_count: items.length
});
totalFailed += items.length;
}
}
// 기타 품목 클립보드 처리
if (pendingOtherItems.length > 0) {
submitOrderClipboardItems(pendingOtherItems);
}
closeMultiWholesalerModal();
showMultiOrderResultModal(allResults, totalSuccess, totalFailed, dryRun);
// 버튼 복원
btnTest.disabled = false;
btnReal.disabled = false;
btnReal.textContent = '📤 전체 주문 전송';
}
// 특정 품목만 클립보드 복사
function submitOrderClipboardItems(items) {
let orderText = `📋 기타 품목 (API 미지원)\n━━━━━━━━━━━━━━━━━━\n`;
items.forEach((item, i) => {
orderText += `${i+1}. ${item.product_name} - ${item.qty}\n`;
});
navigator.clipboard.writeText(orderText);
}
// 다중 도매상 결과 모달
function showMultiOrderResultModal(results, totalSuccess, totalFailed, isDryRun) {
const modal = document.getElementById('orderResultModal');
const content = document.getElementById('orderResultContent');
const header = modal.querySelector('.order-modal-header h3');
const headerDiv = modal.querySelector('.order-modal-header');
headerDiv.style.background = 'linear-gradient(135deg, #059669, #10b981)';
header.innerHTML = '📋 전체 주문 결과';
const statusEmoji = totalFailed === 0 ? '✅' : totalSuccess === 0 ? '❌' : '⚠️';
const modeText = isDryRun ? '[테스트]' : '';
let html = `
<div class="result-header ${totalFailed === 0 ? 'success' : 'partial'}">
<span class="result-emoji">${statusEmoji}</span>
<span class="result-title">${modeText} ${results.length}개 도매상 처리 완료</span>
</div>
<div class="result-summary">
<div class="result-stat">
<span class="stat-label">도매상</span>
<span class="stat-value">${results.length}개</span>
</div>
<div class="result-stat">
<span class="stat-label">총 성공</span>
<span class="stat-value success">${totalSuccess}개</span>
</div>
<div class="result-stat">
<span class="stat-label">총 실패</span>
<span class="stat-value ${totalFailed > 0 ? 'failed' : ''}">${totalFailed}개</span>
</div>
</div>
`;
// 각 도매상별 결과
results.forEach(result => {
const wsId = result.wholesaler_id || result.wholesaler;
const ws = WHOLESALERS[wsId] || {icon: '📦', name: wsId, gradient: 'var(--bg-card)'};
const isSuccess = result.success && result.failed_count === 0;
html += `
<div class="multi-result-card" style="margin-bottom:12px;">
<div class="ws-header ${wsId}" style="margin-bottom:8px;">
<span>${ws.icon}</span>
<span class="ws-name">${ws.name}</span>
<span style="margin-left:auto;font-size:12px;color:${isSuccess ? 'var(--accent-emerald)' : 'var(--accent-rose)'};">
${isSuccess ? '✓ 완료' : '⚠ 일부실패'}
</span>
</div>
<div style="padding:0 12px 12px;">
${result.success ? `
<div style="font-size:12px;color:var(--text-secondary);">
성공: ${result.success_count}개 / 실패: ${result.failed_count}
${result.order_no ? ` · 주문번호: ${result.order_no}` : ''}
</div>
` : `
<div style="font-size:12px;color:var(--accent-rose);">
오류: ${result.error || '처리 실패'}
</div>
`}
</div>
</div>
`;
});
if (isDryRun) {
html += `<div class="result-note">💡 테스트 모드입니다. "전체 장바구니 담기" 버튼으로 실제 진행하세요.</div>`;
} else if (totalSuccess > 0) {
html += `<div class="result-note" style="background:rgba(16,185,129,0.1);color:#10b981;">
🛒 각 도매상 장바구니에 담겼습니다. <b>각 사이트에서 최종 확정</b>이 필요합니다.
</div>`;
}
content.innerHTML = html;
modal.classList.add('show');
}
function openOrderConfirmModal(wholesalerId, items) {
currentOrderWholesaler = wholesalerId;
const ws = WHOLESALERS[wholesalerId];
const modal = document.getElementById('orderConfirmModal');
const tbody = document.getElementById('orderConfirmBody');
const headerDiv = modal.querySelector('.order-modal-header');
// 도매상별 헤더 및 본문 텍스트 변경
document.getElementById('orderConfirmTitle').innerHTML = `${ws.icon} ${ws.name} 주문 확인`;
document.getElementById('orderConfirmWholesaler').textContent = ws.name;
headerDiv.style.background = ws.gradient;
let html = '';
items.forEach((item, idx) => {
html += `
<tr>
<td>${escapeHtml(item.product_name)}</td>
<td class="mono">${item.specification || '-'}</td>
<td class="mono">${item.qty}</td>
</tr>`;
});
tbody.innerHTML = html;
document.getElementById('orderConfirmCount').textContent = items.length;
modal.classList.add('show');
}
function closeOrderConfirmModal() {
document.getElementById('orderConfirmModal').classList.remove('show');
currentOrderWholesaler = null;
}
// ──────────────── 개별 품목 실제 주문 ────────────────
async function orderSingleItem(drugCode) {
const item = cart.find(i => i.drug_code === drugCode);
if (!item) {
showToast('품목을 찾을 수 없습니다', 'error');
return;
}
// 도매상 결정
let wholesaler = 'geoyoung';
for (const [wsId, ws] of Object.entries(WHOLESALERS)) {
if (ws.filterFn(item)) {
wholesaler = wsId;
break;
}
}
const ws = WHOLESALERS[wholesaler];
// 확인 다이얼로그
if (!confirm(`${ws.icon} ${ws.name}에 실제 주문하시겠습니까?\n\n품목: ${item.product_name}\n수량: ${item.qty}\n\n⚠️ 이 작업은 실제 주문을 진행합니다!`)) {
return;
}
showToast(`📤 ${item.product_name} 주문 중...`, 'info');
try {
const payload = {
wholesaler_id: wholesaler,
items: [{
drug_code: item.drug_code,
kd_code: item.geoyoung_code || item.sooin_code || item.drug_code,
internal_code: item.internal_code,
product_name: item.product_name,
manufacturer: item.supplier,
specification: item.specification || '',
order_qty: item.qty,
usage_qty: item.usage_qty || 0,
current_stock: item.current_stock || 0
}],
reference_period: `${document.getElementById('startDate').value}~${document.getElementById('endDate').value}`,
dry_run: false,
cart_only: false // 실제 주문까지!
};
const response = await fetch('/api/order/quick-submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success && result.success_count > 0) {
showToast(`${item.product_name} 주문 완료!`, 'success');
// 장바구니에서 제거
removeFromCart(drugCode);
} else {
const errorMsg = result.results?.[0]?.result_message || result.error || '주문 실패';
showToast(`${errorMsg}`, 'error');
}
} catch (err) {
showToast(`❌ 오류: ${err.message}`, 'error');
}
}
async function executeOrder(dryRun = true) {
const wholesaler = currentOrderWholesaler || 'geoyoung';
// 해당 도매상 품목 필터
const ws = WHOLESALERS[wholesaler];
const items = cart.filter(ws.filterFn);
if (items.length === 0) {
showToast(`${ws.name} 품목이 없습니다`, 'error');
return;
}
// 버튼 비활성화
const btnTest = document.getElementById('btnOrderTest');
const btnReal = document.getElementById('btnOrderReal');
btnTest.disabled = true;
btnReal.disabled = true;
btnTest.textContent = dryRun ? '처리 중...' : '🧪 테스트';
btnReal.textContent = !dryRun ? '처리 중...' : '🛒 장바구니 담기';
try {
const payload = {
wholesaler_id: wholesaler,
items: items.map(item => ({
drug_code: item.drug_code,
kd_code: item.geoyoung_code || item.sooin_code || item.drug_code,
internal_code: item.internal_code,
product_name: item.product_name,
manufacturer: item.supplier,
specification: item.specification || '',
order_qty: item.qty,
usage_qty: item.usage_qty || 0,
current_stock: item.current_stock || 0
})),
reference_period: `${document.getElementById('startDate').value}~${document.getElementById('endDate').value}`,
dry_run: dryRun,
cart_only: false // 장바구니 + 주문 확정까지
};
// 타임아웃 설정
const timeoutMs = dryRun ? 120000 : 180000; // 테스트 2분, 실제 3분
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch('/api/order/quick-submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
signal: controller.signal
});
clearTimeout(timeoutId);
const result = await response.json();
closeOrderConfirmModal();
if (result.success) {
showOrderResultModal(result);
} else {
showToast(`❌ 주문 실패: ${result.error}`, 'error');
}
} catch (err) {
showToast(`❌ 오류: ${err.message}`, 'error');
} finally {
btnTest.disabled = false;
btnReal.disabled = false;
btnTest.textContent = '🧪 테스트';
btnReal.textContent = '📤 주문 전송';
}
}
function showOrderResultModal(result) {
const modal = document.getElementById('orderResultModal');
const content = document.getElementById('orderResultContent');
const header = modal.querySelector('.order-modal-header h3');
const headerDiv = modal.querySelector('.order-modal-header');
// 도매상 정보 가져오기
const wholesalerId = result.wholesaler || currentOrderWholesaler || 'geoyoung';
const ws = WHOLESALERS[wholesalerId] || WHOLESALERS.geoyoung;
// 결과 모달 헤더 스타일 적용
header.innerHTML = `${ws.icon} ${ws.name} 주문 결과`;
headerDiv.style.background = ws.gradient;
const isDryRun = result.dry_run;
const isCartOnly = result.cart_only;
const statusEmoji = result.failed_count === 0 ? '✅' : result.success_count === 0 ? '❌' : '⚠️';
// 결과 타이틀
let resultTitle = '';
if (isDryRun) {
resultTitle = `[테스트] ${ws.name} 시뮬레이션 완료`;
} else if (isCartOnly) {
resultTitle = `${ws.name} 장바구니 담기 ${result.failed_count === 0 ? '완료' : '처리됨'}`;
} else {
resultTitle = `${ws.name} 주문 ${result.failed_count === 0 ? '완료' : '처리됨'}`;
}
let html = `
<div class="result-header ${result.failed_count === 0 ? 'success' : 'partial'}">
<span class="result-emoji">${statusEmoji}</span>
<span class="result-title">${resultTitle}</span>
</div>
<div class="result-summary">
<div class="result-stat">
<span class="stat-label">도매상</span>
<span class="stat-value">${ws.icon} ${ws.name}</span>
</div>
<div class="result-stat">
<span class="stat-label">주문번호</span>
<span class="stat-value mono">${result.order_no}</span>
</div>
<div class="result-stat">
<span class="stat-label">성공</span>
<span class="stat-value success">${result.success_count}개</span>
</div>
<div class="result-stat">
<span class="stat-label">실패</span>
<span class="stat-value ${result.failed_count > 0 ? 'failed' : ''}">${result.failed_count}개</span>
</div>
</div>
<table class="result-table">
<thead><tr><th>품목</th><th>수량</th><th>결과</th></tr></thead>
<tbody>`;
(result.results || []).forEach(item => {
const isSuccess = item.status === 'success';
html += `
<tr class="${isSuccess ? '' : 'failed-row'}">
<td>${escapeHtml(item.product_name)}</td>
<td class="mono">${item.order_qty}</td>
<td class="${isSuccess ? 'result-ok' : 'result-fail'}">
${isSuccess ? '✓' : '✗'} ${item.result_code}
${item.result_message ? `<br><small>${escapeHtml(item.result_message)}</small>` : ''}
${item.price ? `<br><small style="color:var(--accent-amber)">단가: ${item.price.toLocaleString()}원</small>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
if (isDryRun && result.success_count > 0) {
html += `<div class="result-note">💡 테스트 모드입니다. "장바구니 담기" 버튼으로 실제 진행하세요.</div>`;
}
// 장바구니만 담은 경우 안내
if (!isDryRun && isCartOnly && result.success_count > 0) {
html += `<div class="result-note" style="background:rgba(6,182,212,0.1);color:#06b6d4;">
🛒 ${ws.name} 장바구니에 담겼습니다. <b>사이트에서 최종 확정</b>이 필요합니다.
${wholesalerId === 'geoyoung' ? '<br>개별 품목은 📤주문 버튼으로 바로 주문할 수 있습니다.' : ''}
</div>`;
}
// 도매상별 안내
if (!isDryRun && result.note) {
const noteColors = {
geoyoung: 'rgba(6,182,212,0.1);color:#06b6d4',
sooin: 'rgba(168,85,247,0.1);color:#a855f7',
baekje: 'rgba(245,158,11,0.1);color:#f59e0b'
};
const noteStyle = noteColors[wholesalerId] || 'rgba(100,100,100,0.1);color:#888';
html += `<div class="result-note" style="background:${noteStyle};">📌 ${result.note}</div>`;
}
content.innerHTML = html;
modal.classList.add('show');
}
function closeOrderResultModal() {
document.getElementById('orderResultModal').classList.remove('show');
}
// 기존 클립보드 방식 (지오영 아닌 품목용)
function submitOrderClipboard() {
if (cart.length === 0) return;
const bySupplier = {};
cart.forEach(item => {
const sup = item.supplier || '미지정';
if (!bySupplier[sup]) bySupplier[sup] = [];
bySupplier[sup].push(item);
});
let orderText = `💊 청춘약국 전문의약품 발주서\n`;
orderText += `━━━━━━━━━━━━━━━━━━━━━━━━\n`;
orderText += `📅 작성일: ${new Date().toLocaleDateString('ko-KR')}\n`;
orderText += `📋 조회기간: ${document.getElementById('startDate').value} ~ ${document.getElementById('endDate').value}\n\n`;
Object.entries(bySupplier).forEach(([supplier, items]) => {
orderText += `\n${supplier}\n`;
orderText += `─────────────────────────\n`;
items.forEach((item, i) => {
orderText += `${i+1}. ${item.product_name}\n`;
orderText += ` 주문수량: ${item.qty}\n`;
});
});
orderText += `\n━━━━━━━━━━━━━━━━━━━━━━━━\n`;
orderText += `${cart.length}개 품목\n`;
navigator.clipboard.writeText(orderText).then(() => {
showToast('📋 주문서가 클립보드에 복사되었습니다!', 'success');
}).catch(() => {
const ta = document.createElement('textarea');
ta.value = orderText;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast('📋 주문서가 클립보드에 복사되었습니다!', 'success');
});
}
// ──────────────── 유틸 ────────────────
function formatPrice(num) {
return new Intl.NumberFormat('ko-KR').format(num);
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
}
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast ' + type + ' show';
setTimeout(() => toast.classList.remove('show'), 3000);
}
// 엔터키 검색
document.getElementById('searchInput').addEventListener('keypress', e => {
if (e.key === 'Enter') loadUsageData();
});
// ──────────────── 도매상 재고 조회 (지오영 + 수인 + 백제) ────────────────
let currentWholesaleItem = null;
window.wholesaleItems = { geoyoung: [], sooin: [], baekje: [] };
function openWholesaleModal(idx) {
const item = usageData[idx];
if (!item) return;
currentWholesaleItem = item;
// 모달 열기
document.getElementById('geoModalProductName').textContent = item.product_name;
document.getElementById('geoModalDrugCode').textContent = item.drug_code;
document.getElementById('geoModalUsage').textContent = item.total_dose.toLocaleString() + '개';
document.getElementById('geoModalStock').textContent = item.current_stock.toLocaleString() + '개';
document.getElementById('geoyoungModal').classList.add('show');
// 로딩 표시
document.getElementById('geoResultBody').innerHTML = `
<div class="geo-loading">
<div class="loading-spinner"></div>
<div>도매상 재고 조회 중... (지오영 + 수인 + 백제)</div>
</div>`;
document.getElementById('geoSearchKeyword').style.display = 'none';
// 세 도매상 동시 호출
searchAllWholesalers(item.drug_code, item.product_name);
}
function closeGeoyoungModal() {
document.getElementById('geoyoungModal').classList.remove('show');
currentWholesaleItem = null;
}
async function searchAllWholesalers(kdCode, productName) {
const resultBody = document.getElementById('geoResultBody');
// 세 도매상 동시 호출
const [geoResult, sooinResult, baekjeResult] = await Promise.all([
searchGeoyoungAPI(kdCode, productName),
searchSooinAPI(kdCode),
searchBaekjeAPI(kdCode)
]);
// 결과 저장
window.wholesaleItems = {
geoyoung: geoResult.items || [],
sooin: sooinResult.items || [],
baekje: baekjeResult.items || []
};
// 통합 렌더링
renderWholesaleResults(geoResult, sooinResult, baekjeResult);
}
async function searchGeoyoungAPI(kdCode, productName) {
try {
// 1차: 보험코드로 검색
let response = await fetch(`/api/geoyoung/stock?kd_code=${encodeURIComponent(kdCode)}`);
let data = await response.json();
// 결과 없으면 성분명으로 재검색
if (data.success && data.count === 0) {
response = await fetch(`/api/geoyoung/stock-by-name?product_name=${encodeURIComponent(productName)}`);
data = await response.json();
}
return data;
} catch (err) {
return { success: false, error: err.message, items: [] };
}
}
async function searchSooinAPI(kdCode) {
try {
const response = await fetch(`/api/sooin/stock?keyword=${encodeURIComponent(kdCode)}`);
const data = await response.json();
return data;
} catch (err) {
return { success: false, error: err.message, items: [] };
}
}
async function searchBaekjeAPI(kdCode) {
try {
const response = await fetch(`/api/baekje/stock?keyword=${encodeURIComponent(kdCode)}`);
const data = await response.json();
return data;
} catch (err) {
return { success: false, error: err.message, items: [] };
}
}
function renderWholesaleResults(geoResult, sooinResult, baekjeResult) {
const resultBody = document.getElementById('geoResultBody');
const geoItems = geoResult.items || [];
const sooinItems = sooinResult.items || [];
const baekjeItems = (baekjeResult && baekjeResult.items) || [];
// 재고 있는 것 먼저 정렬
geoItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
sooinItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
baekjeItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
let html = '';
// ═══════ 지오영 섹션 ═══════
html += `<div class="ws-section">
<div class="ws-header geoyoung">
<img class="ws-logo" src="/static/img/logo_geoyoung.ico" alt="지오영">
<span class="ws-name">지오영</span>
<span class="ws-count">${geoItems.length}건</span>
</div>`;
if (geoItems.length > 0) {
html += `<table class="geo-table">
<thead><tr><th>제품명</th><th>규격</th><th>단가</th><th>재고</th><th></th></tr></thead>
<tbody>`;
geoItems.forEach((item, idx) => {
const hasStock = item.stock > 0;
const priceText = item.price ? item.price.toLocaleString() + '원' : '-';
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.product_name)}</span>
<span class="geo-code">${item.insurance_code}</span>
</div>
</td>
<td class="geo-spec">${item.specification}</td>
<td class="geo-price">${priceText}</td>
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
<td>${hasStock ? `<button class="geo-add-btn" onclick="addToCartFromWholesale('geoyoung', ${idx})">담기</button>` : ''}</td>
</tr>`;
});
html += '</tbody></table>';
} else {
html += `<div class="ws-empty">📭 검색 결과 없음</div>`;
}
html += '</div>';
// ═══════ 수인약품 섹션 ═══════
html += `<div class="ws-section">
<div class="ws-header sooin">
<img class="ws-logo" src="/static/img/logo_sooin.svg" alt="수인">
<span class="ws-name">수인약품</span>
<span class="ws-count">${sooinItems.length}건</span>
</div>`;
if (sooinItems.length > 0) {
html += `<table class="geo-table">
<thead><tr><th>제품명</th><th>규격</th><th>단가</th><th>재고</th><th></th></tr></thead>
<tbody>`;
sooinItems.forEach((item, idx) => {
const hasStock = item.stock > 0;
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.name)}</span>
<span class="geo-code">${item.code} · ${item.manufacturer || ''}</span>
</div>
</td>
<td class="geo-spec">${item.spec || '-'}</td>
<td class="geo-price">${item.price ? item.price.toLocaleString() + '원' : '-'}</td>
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
<td>${hasStock ? `<button class="geo-add-btn sooin" onclick="addToCartFromWholesale('sooin', ${idx})">담기</button>` : ''}</td>
</tr>`;
});
html += '</tbody></table>';
} else {
html += `<div class="ws-empty">📭 검색 결과 없음</div>`;
}
html += '</div>';
// ═══════ 백제약품 섹션 ═══════
html += `<div class="ws-section">
<div class="ws-header baekje">
<span class="ws-logo">💉</span>
<span class="ws-name">백제약품</span>
<span class="ws-count">${baekjeItems.length}건</span>
</div>`;
if (baekjeItems.length > 0) {
html += `<table class="geo-table">
<thead><tr><th>제품명</th><th>규격</th><th>단가</th><th>재고</th><th></th></tr></thead>
<tbody>`;
baekjeItems.forEach((item, idx) => {
const hasStock = item.stock > 0;
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.name)}</span>
<span class="geo-code">${item.insurance_code || ''} · ${item.manufacturer || ''}</span>
</div>
</td>
<td class="geo-spec">${item.spec || '-'}</td>
<td class="geo-price">${item.price ? item.price.toLocaleString() + '원' : '-'}</td>
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
<td>${hasStock ? `<button class="geo-add-btn baekje" onclick="addToCartFromWholesale('baekje', ${idx})">담기</button>` : ''}</td>
</tr>`;
});
html += '</tbody></table>';
} else {
html += `<div class="ws-empty">📭 검색 결과 없음</div>`;
}
html += '</div>';
resultBody.innerHTML = html;
}
function addToCartFromWholesale(wholesaler, idx) {
const items = window.wholesaleItems[wholesaler];
const item = items[idx];
if (!item || !currentWholesaleItem) return;
// 규격에서 수량 추출
const spec = wholesaler === 'geoyoung' ? item.specification : (item.spec || '');
const specMatch = spec.match(/(\d+)/);
const specQty = specMatch ? parseInt(specMatch[1]) : 1;
// 필요 수량 계산
const needed = currentWholesaleItem.total_dose;
const suggestedQty = Math.ceil(needed / specQty);
const supplierNames = { geoyoung: '지오영', sooin: '수인약품', baekje: '백제약품' };
const supplierName = supplierNames[wholesaler] || wholesaler;
const productName = wholesaler === 'geoyoung' ? item.product_name : item.name;
const qty = prompt(`[${supplierName}] 주문 수량 (${spec} 기준)\n\n필요량: ${needed}\n규격: ${specQty}개/단위\n추천: ${suggestedQty}단위 (${suggestedQty * specQty}개)`, suggestedQty);
if (!qty || isNaN(qty)) return;
// 장바구니에 추가
const cartItem = {
drug_code: currentWholesaleItem.drug_code,
product_name: productName,
supplier: supplierName,
qty: parseInt(qty),
specification: spec,
wholesaler: wholesaler,
internal_code: item.internal_code,
geoyoung_code: wholesaler === 'geoyoung' ? item.insurance_code : null,
sooin_code: wholesaler === 'sooin' ? item.code : null,
baekje_code: wholesaler === 'baekje' ? item.internal_code : null
};
// 기존 항목 체크 (같은 도매상 + 같은 규격)
const existing = cart.find(c =>
c.drug_code === currentWholesaleItem.drug_code &&
c.specification === spec &&
c.wholesaler === wholesaler
);
if (existing) {
existing.qty = parseInt(qty);
} else {
cart.push(cartItem);
}
updateCartUI();
closeGeoyoungModal();
showToast(`✅ [${supplierName}] ${productName} (${spec}) ${qty}개 추가`, 'success');
}
// 하위 호환 (기존 함수명 유지)
function openGeoyoungModal(idx) { openWholesaleModal(idx); }
function addGeoyoungToCart(idx) { addToCartFromWholesale('geoyoung', idx); }
// 테이블 행 더블클릭으로 도매상 모달 열기
document.addEventListener('dblclick', function(e) {
const row = e.target.closest('tr[data-idx]');
if (row) {
const idx = parseInt(row.dataset.idx);
openWholesaleModal(idx);
}
});
</script>
<!-- 도매상 재고 조회 모달 (지오영 + 수인) -->
<div class="geo-modal" id="geoyoungModal">
<div class="geo-modal-content">
<div class="geo-modal-header">
<h3>📦 도매상 재고 조회</h3>
<button class="geo-close" onclick="closeGeoyoungModal()"></button>
</div>
<div class="geo-modal-info">
<div class="geo-info-row">
<span class="geo-label">약품명</span>
<span class="geo-value" id="geoModalProductName">-</span>
</div>
<div class="geo-info-row">
<span class="geo-label">보험코드</span>
<span class="geo-value mono" id="geoModalDrugCode">-</span>
</div>
<div class="geo-info-row">
<span class="geo-label">사용량</span>
<span class="geo-value highlight" id="geoModalUsage">-</span>
</div>
<div class="geo-info-row">
<span class="geo-label">현재고</span>
<span class="geo-value" id="geoModalStock">-</span>
</div>
</div>
<div class="geo-search-info" id="geoSearchKeyword" style="display:none;"></div>
<div class="geo-result" id="geoResultBody">
<div class="geo-loading">
<div class="loading-spinner"></div>
<div>지오영 재고 조회 중...</div>
</div>
</div>
</div>
</div>
<style>
/* 지오영 모달 스타일 */
.geo-modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 500;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.geo-modal.show { display: flex; }
.geo-modal-content {
background: var(--bg-secondary);
border-radius: 16px;
width: 90%;
max-width: 700px;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid var(--accent-cyan);
box-shadow: 0 8px 32px rgba(6, 182, 212, 0.3);
}
.geo-modal-header {
padding: 16px 20px;
background: linear-gradient(135deg, #0891b2, var(--accent-cyan));
display: flex;
justify-content: space-between;
align-items: center;
}
.geo-modal-header h3 {
font-size: 16px;
font-weight: 700;
}
.geo-close {
background: rgba(255,255,255,0.2);
border: none;
width: 28px;
height: 28px;
border-radius: 6px;
color: #fff;
cursor: pointer;
}
.geo-modal-info {
padding: 16px 20px;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
}
.geo-info-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
}
.geo-label {
color: var(--text-muted);
font-size: 12px;
}
.geo-value {
font-weight: 600;
font-size: 13px;
}
.geo-value.mono {
font-family: 'JetBrains Mono', monospace;
}
.geo-value.highlight {
color: var(--accent-cyan);
}
.geo-search-info {
padding: 8px 20px;
background: rgba(6, 182, 212, 0.1);
font-size: 12px;
color: var(--accent-cyan);
}
.geo-result {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.geo-loading, .geo-error, .geo-empty {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.geo-error { color: var(--accent-rose); }
.geo-table {
width: 100%;
border-collapse: collapse;
}
.geo-table th {
padding: 10px 12px;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-align: left;
border-bottom: 1px solid var(--border);
}
.geo-table td {
padding: 12px;
font-size: 12px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.geo-table tr:hover {
background: rgba(255,255,255,0.02);
}
.geo-table tr.no-stock {
opacity: 0.5;
}
.geo-product {
display: flex;
flex-direction: column;
gap: 2px;
}
.geo-name {
font-weight: 500;
}
.geo-code {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--text-muted);
}
.geo-spec {
font-weight: 600;
color: var(--accent-amber);
}
.geo-stock {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
text-align: center;
}
.geo-stock.in-stock { color: var(--accent-emerald); }
.geo-stock.out-stock { color: var(--text-muted); }
.geo-add-btn {
padding: 6px 12px;
background: var(--accent-cyan);
border: none;
border-radius: 6px;
color: #fff;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.geo-add-btn:hover {
background: #0891b2;
transform: scale(1.05);
}
.geo-add-btn.sooin {
background: var(--accent-purple);
}
.geo-add-btn.sooin:hover {
background: #7c3aed;
}
.geo-add-btn.baekje {
background: var(--accent-amber);
}
.geo-add-btn.baekje:hover {
background: #d97706;
}
.geo-price {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
}
/* 도매상 섹션 구분 */
.ws-section {
margin-bottom: 20px;
}
.ws-section:last-child {
margin-bottom: 0;
}
.ws-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
margin-bottom: 8px;
font-weight: 600;
}
.ws-header.geoyoung {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(8, 145, 178, 0.1));
border-left: 3px solid var(--accent-cyan);
}
.ws-header.sooin {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(124, 58, 237, 0.1));
border-left: 3px solid var(--accent-purple);
}
.ws-header.baekje {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.2), rgba(217, 119, 6, 0.1));
border-left: 3px solid var(--accent-amber);
}
.ws-logo {
width: 24px;
height: 24px;
border-radius: 4px;
object-fit: contain;
}
.ws-name {
flex: 1;
font-size: 14px;
}
.ws-count {
font-size: 12px;
color: var(--text-muted);
background: rgba(255,255,255,0.1);
padding: 2px 8px;
border-radius: 10px;
}
.ws-empty {
text-align: center;
padding: 20px;
color: var(--text-muted);
font-size: 13px;
}
/* 주문 확인 모달 */
.order-modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8);
z-index: 600;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.order-modal.show { display: flex; }
.order-modal-content {
background: var(--bg-secondary);
border-radius: 16px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid var(--accent-violet);
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3);
}
.order-modal-header {
padding: 16px 20px;
background: linear-gradient(135deg, #7c3aed, var(--accent-violet));
display: flex;
justify-content: space-between;
align-items: center;
}
.order-modal-header h3 { font-size: 16px; font-weight: 700; }
.order-close {
background: rgba(255,255,255,0.2);
border: none;
width: 28px; height: 28px;
border-radius: 6px;
color: #fff;
cursor: pointer;
}
.order-modal-body {
padding: 20px;
overflow-y: auto;
}
.order-confirm-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.order-confirm-table th {
padding: 8px;
font-size: 11px;
color: var(--text-muted);
text-align: left;
border-bottom: 1px solid var(--border);
}
.order-confirm-table td {
padding: 10px 8px;
font-size: 12px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.order-confirm-table .mono {
font-family: 'JetBrains Mono', monospace;
}
.order-modal-footer {
padding: 16px 20px;
background: var(--bg-card);
display: flex;
gap: 12px;
justify-content: flex-end;
}
.btn-order-test {
padding: 10px 20px;
background: var(--accent-amber);
border: none;
border-radius: 8px;
color: #000;
font-weight: 600;
cursor: pointer;
}
.btn-order-real {
padding: 10px 20px;
background: var(--accent-emerald);
border: none;
border-radius: 8px;
color: #fff;
font-weight: 600;
cursor: pointer;
}
.btn-order-test:disabled, .btn-order-real:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 주문 결과 모달 */
.result-header {
text-align: center;
padding: 20px;
border-radius: 12px;
margin-bottom: 16px;
}
.result-header.success { background: rgba(16, 185, 129, 0.1); }
.result-header.partial { background: rgba(245, 158, 11, 0.1); }
.result-emoji { font-size: 32px; display: block; margin-bottom: 8px; }
.result-title { font-size: 16px; font-weight: 600; }
.result-summary {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.result-stat {
flex: 1;
text-align: center;
padding: 12px;
background: var(--bg-card);
border-radius: 8px;
}
.stat-label { display: block; font-size: 11px; color: var(--text-muted); }
.stat-value { display: block; font-size: 16px; font-weight: 700; margin-top: 4px; }
.stat-value.success { color: var(--accent-emerald); }
.stat-value.failed { color: var(--accent-rose); }
.result-table {
width: 100%;
border-collapse: collapse;
}
.result-table th {
padding: 8px;
font-size: 11px;
color: var(--text-muted);
text-align: left;
border-bottom: 1px solid var(--border);
}
.result-table td {
padding: 10px 8px;
font-size: 12px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.result-table .failed-row { background: rgba(244, 63, 94, 0.1); }
.result-ok { color: var(--accent-emerald); }
.result-fail { color: var(--accent-rose); }
.result-note {
margin-top: 16px;
padding: 12px;
background: rgba(6, 182, 212, 0.1);
border-radius: 8px;
font-size: 12px;
color: var(--accent-cyan);
}
/* 다중 도매상 선택 모달 스타일 */
.multi-ws-summary {
padding: 0 4px;
}
.multi-ws-card {
background: var(--bg-card);
border-radius: 12px;
margin-bottom: 12px;
overflow: hidden;
border: 1px solid var(--border);
}
.multi-ws-card.geoyoung {
border-left: 3px solid var(--accent-cyan);
}
.multi-ws-card.sooin {
border-left: 3px solid var(--accent-purple);
}
.multi-ws-card.baekje {
border-left: 3px solid var(--accent-amber);
}
.multi-ws-card.other {
border-left: 3px solid var(--text-muted);
opacity: 0.7;
}
.multi-ws-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
background: rgba(255,255,255,0.02);
}
.multi-ws-icon {
font-size: 20px;
}
.multi-ws-name {
font-weight: 600;
font-size: 14px;
flex: 1;
}
.multi-ws-count {
font-size: 12px;
color: var(--text-muted);
background: rgba(255,255,255,0.1);
padding: 3px 10px;
border-radius: 12px;
}
.multi-ws-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--accent-emerald);
cursor: pointer;
}
.multi-ws-checkbox input {
width: 18px;
height: 18px;
accent-color: var(--accent-emerald);
}
.multi-ws-items {
padding: 10px 16px 14px;
border-top: 1px solid rgba(255,255,255,0.05);
}
.multi-ws-item {
font-size: 12px;
color: var(--text-secondary);
padding: 3px 0;
}
.multi-ws-item.more {
color: var(--text-muted);
font-style: italic;
}
</style>
<!-- 주문 확인 모달 -->
<div class="order-modal" id="orderConfirmModal">
<div class="order-modal-content">
<div class="order-modal-header">
<h3 id="orderConfirmTitle">🏭 지오영 주문 확인</h3>
<button class="order-close" onclick="closeOrderConfirmModal()"></button>
</div>
<div class="order-modal-body">
<p style="margin-bottom:12px;color:var(--text-secondary);">
<span id="orderConfirmCount">0</span>개 품목을 <span id="orderConfirmWholesaler">지오영</span> 장바구니에 담습니다.
</p>
<p style="margin-bottom:12px;font-size:12px;color:var(--accent-amber);">
⚠️ 장바구니 담기만 진행됩니다. 도매상 사이트에서 최종 확정이 필요합니다.
</p>
<table class="order-confirm-table">
<thead><tr><th>품목명</th><th>규격</th><th>수량</th></tr></thead>
<tbody id="orderConfirmBody"></tbody>
</table>
</div>
<div class="order-modal-footer">
<button class="btn-order-test" id="btnOrderTest" onclick="executeOrder(true)">🧪 테스트</button>
<button class="btn-order-real" id="btnOrderReal" onclick="executeOrder(false)">📤 주문 전송</button>
</div>
</div>
</div>
<!-- 주문 결과 모달 -->
<div class="order-modal" id="orderResultModal">
<div class="order-modal-content" style="max-width:600px;">
<div class="order-modal-header" style="background:linear-gradient(135deg, #059669, var(--accent-emerald));">
<h3>📋 주문 결과</h3>
<button class="order-close" onclick="closeOrderResultModal()"></button>
</div>
<div class="order-modal-body" id="orderResultContent">
</div>
<div class="order-modal-footer">
<button class="btn-order-test" onclick="closeOrderResultModal()">닫기</button>
</div>
</div>
</div>
<!-- 다중 도매상 선택 모달 -->
<div class="order-modal" id="multiWholesalerModal">
<div class="order-modal-content" style="max-width:550px;">
<div class="order-modal-header" style="background:linear-gradient(135deg, #059669, #10b981);">
<h3>🛒 전체 도매상 주문</h3>
<button class="order-close" onclick="closeMultiWholesalerModal()"></button>
</div>
<div class="order-modal-body" id="multiWholesalerBody">
</div>
<div class="order-modal-footer">
<button class="btn-order-test" id="btnMultiTest" onclick="executeAllWholesalers(true)">🧪 테스트</button>
<button class="btn-order-real" id="btnMultiReal" onclick="executeAllWholesalers(false)">📤 전체 주문 전송</button>
</div>
</div>
</div>
<!-- 잔고 조회 모달 -->
<div class="order-modal" id="balanceModal">
<div class="order-modal-content" style="max-width:500px;">
<div class="order-modal-header" style="background:linear-gradient(135deg, #7c3aed, #a855f7);">
<h3>💰 도매상 잔고 현황</h3>
<button class="order-close" onclick="closeBalanceModal()"></button>
</div>
<div class="order-modal-body" id="balanceContent">
<div class="loading-state">
<div class="loading-spinner"></div>
<div>잔고 조회 중...</div>
</div>
</div>
<div class="order-modal-footer">
<button class="btn-order-test" onclick="loadBalances()">🔄 새로고침</button>
<button class="btn-order-real" onclick="closeBalanceModal()">닫기</button>
</div>
</div>
</div>
<style>
/* 잔고 모달 스타일 */
.balance-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.balance-card {
background: var(--bg-secondary);
border-radius: 12px;
padding: 20px;
border: 1px solid var(--border);
display: flex;
align-items: center;
gap: 16px;
}
.balance-card.loading {
opacity: 0.6;
}
.balance-card.error {
border-color: var(--accent-rose);
background: rgba(244, 63, 94, 0.1);
}
.balance-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.balance-icon.baekje { background: linear-gradient(135deg, #d97706, #f59e0b); }
.balance-icon.geoyoung { background: linear-gradient(135deg, #0891b2, #06b6d4); }
.balance-icon.sooin { background: linear-gradient(135deg, #7c3aed, #a855f7); }
.balance-logo-wrap {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.balance-logo {
width: 40px;
height: 40px;
object-fit: contain;
border-radius: 8px;
}
.balance-info {
flex: 1;
min-width: 0;
}
.balance-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.balance-detail {
font-size: 11px;
color: var(--text-muted);
}
.balance-amount {
text-align: right;
}
.balance-value {
font-size: 22px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
}
.balance-value.baekje { color: #f59e0b; }
.balance-value.geoyoung { color: #06b6d4; }
.balance-value.sooin { color: #a855f7; }
.balance-label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.balance-summary {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(6, 182, 212, 0.1));
border-radius: 12px;
padding: 20px;
display: flex;
justify-content: space-around;
align-items: center;
margin-top: 12px;
gap: 16px;
}
.summary-item {
text-align: center;
flex: 1;
}
.summary-label {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 8px;
}
.summary-value {
font-size: 24px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
}
.summary-value.sales {
color: #10b981;
}
.summary-value.balance {
background: linear-gradient(135deg, #a855f7, #06b6d4);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.summary-divider {
width: 1px;
height: 50px;
background: var(--border);
}
.balance-updated {
text-align: center;
font-size: 11px;
color: var(--text-muted);
margin-top: 12px;
}
</style>
<script>
// ──────────────── 잔고 조회 ────────────────
function openBalanceModal() {
document.getElementById('balanceModal').classList.add('show');
loadBalances();
}
function closeBalanceModal() {
document.getElementById('balanceModal').classList.remove('show');
}
// 도매상 설정 (중앙 관리)
const WHOLESALER_CONFIG = {
baekje: {
id: 'baekje', name: '백제약품', icon: '💉',
logo: '/static/img/logo_baekje.svg',
color: '#f59e0b',
balanceApi: '/api/baekje/balance',
salesApi: '/api/baekje/monthly-sales'
},
geoyoung: {
id: 'geoyoung', name: '지오영', icon: '🏭',
logo: '/static/img/logo_geoyoung.ico',
color: '#06b6d4',
balanceApi: '/api/geoyoung/balance',
salesApi: '/api/geoyoung/monthly-sales'
},
sooin: {
id: 'sooin', name: '수인약품', icon: '💊',
logo: '/static/img/logo_sooin.svg',
color: '#a855f7',
balanceApi: '/api/sooin/balance',
salesApi: '/api/sooin/monthly-sales'
}
};
const WHOLESALER_ORDER = ['baekje', 'geoyoung', 'sooin'];
async function loadBalances() {
const content = document.getElementById('balanceContent');
content.innerHTML = `
<div class="loading-state">
<div class="loading-spinner"></div>
<div>잔고 및 매출 조회 중...</div>
</div>`;
const wholesalers = WHOLESALER_ORDER.map(id => WHOLESALER_CONFIG[id]);
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const balanceResults = {};
const salesResults = {};
let totalBalance = 0;
let totalSales = 0;
// 병렬로 잔고 + 월간매출 조회
await Promise.all(wholesalers.flatMap(ws => [
// 잔고 조회
fetch(ws.balanceApi).then(r => r.json()).then(data => {
balanceResults[ws.id] = data;
if (data.success && data.balance) totalBalance += data.balance;
}).catch(err => {
balanceResults[ws.id] = { success: false, error: err.message };
}),
// 월간 매출 조회
fetch(`${ws.salesApi}?year=${year}&month=${month}`).then(r => r.json()).then(data => {
salesResults[ws.id] = data;
if (data.success && data.total_amount) totalSales += data.total_amount;
}).catch(err => {
salesResults[ws.id] = { success: false, error: err.message };
})
]));
// 결과 렌더링
let html = '<div class="balance-grid">';
wholesalers.forEach(ws => {
const balData = balanceResults[ws.id] || {};
const salesData = salesResults[ws.id] || {};
const isError = !balData.success;
const balance = balData.balance || 0;
const monthlySales = salesData.total_amount || 0;
const monthlyPaid = salesData.total_paid || 0;
html += `
<div class="balance-card ${isError ? 'error' : ''}">
<div class="balance-logo-wrap">
<img src="${ws.logo}" alt="${ws.name}" class="balance-logo"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="balance-icon ${ws.id}" style="display:none;">${ws.icon}</div>
</div>
<div class="balance-info">
<div class="balance-name">${ws.name}</div>
<div class="balance-detail">
${isError
? `${balData.error || '조회 실패'}`
: `${month}월 매출: <span style="color:${ws.color};font-weight:600;">${monthlySales.toLocaleString()}원</span>
${monthlyPaid > 0 ? ` · 입금: ${monthlyPaid.toLocaleString()}` : ''}`}
</div>
</div>
<div class="balance-amount">
<div class="balance-value ${ws.id}">
${isError ? '-' : balance.toLocaleString()}
</div>
<div class="balance-label">현재 잔고</div>
</div>
</div>`;
});
html += `
<div class="balance-summary">
<div class="summary-item">
<div class="summary-label">📊 ${month}월 총 주문</div>
<div class="summary-value sales">${totalSales.toLocaleString()}원</div>
</div>
<div class="summary-divider"></div>
<div class="summary-item">
<div class="summary-label">💰 총 미수금</div>
<div class="summary-value balance">${totalBalance.toLocaleString()}원</div>
</div>
</div>
<div class="balance-updated">🕐 ${new Date().toLocaleString('ko-KR')}</div>
</div>`;
content.innerHTML = html;
}
</script>
</body>
</html>