pharmacy-pos-qr-system/backend/templates/admin_rx_usage.html
thug0bin 83ecf88bd4 feat(animal-chat): APC 코드 2024년 체계 지원 및 피부약 2단계 추천
## APC 코드 체계 확장
- 기존: 023%만 검색 (~2023년 제품만)
- 변경: 02% OR 92% + 13자리 검증
  - 02%: 2023년 이전 item_seq (9자리) 기반 APC
  - 92%: 2024년 이후 item_seq (10자리) 기반 APC
- 999% 등 청구프로그램 임의코드는 제외

## 동물약 챗봇 피부약 추천 개선
- 피부약 2단계 추천 구조 추가
  - 1차(치료): 의약품 (개시딘겔, 테르비덤 등)
  - 2차(보조케어): 의약외품 (스킨카솔 - 회복기 피부보호)
- 스킨카솔은 의약외품임을 명시하여 치료제로 오인 방지

## 기타
- RAG 테스트 스크립트 추가
- 수인약품 API 문서화
2026-03-11 14:20:44 +09:00

3384 lines
129 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;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;
}
/* ══════════════════ 주문량 로딩 ══════════════════ */
.order-loading {
display: inline-block;
color: var(--accent-cyan);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
/* ══════════════════ 주문량 툴팁 ══════════════════ */
.order-qty-cell {
position: relative;
cursor: pointer;
}
.order-qty-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
min-width: 140px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
pointer-events: none;
}
.order-qty-cell:hover .order-qty-tooltip {
opacity: 1;
visibility: visible;
bottom: calc(100% + 8px);
}
.order-qty-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--border);
}
.order-qty-tooltip-title {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.order-qty-tooltip-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
font-size: 12px;
}
.order-qty-tooltip-row:not(:last-child) {
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.order-qty-vendor {
display: flex;
align-items: center;
gap: 6px;
}
.order-qty-vendor-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.order-qty-vendor-dot.geoyoung { background: #06b6d4; }
.order-qty-vendor-dot.sooin { background: #a855f7; }
.order-qty-vendor-dot.baekje { background: #f59e0b; }
.order-qty-vendor-dot.dongwon { background: #22c55e; }
.order-qty-value {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
color: var(--text-primary);
}
.order-qty-total {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid var(--border);
font-weight: 700;
color: var(--accent-cyan);
}
/* ══════════════════ 헤더 ══════════════════ */
.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;
}
.patient-badge {
display: inline-block;
background: rgba(156, 163, 175, 0.15);
color: #9ca3af;
font-size: 10px;
font-weight: 500;
padding: 2px 6px;
border-radius: 4px;
margin-left: 4px;
font-family: inherit;
}
.patient-badge.has-today {
background: rgba(236, 72, 153, 0.2);
color: #ec4899;
}
.today-patient {
color: #ec4899;
font-weight: 700;
}
/* 수량 관련 */
.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(); loadOrderData();">🔍 조회</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:28%">약품</th>
<th class="center">현재고</th>
<th class="center">처방횟수</th>
<th class="center">투약량</th>
<th class="center" style="color:var(--accent-cyan);">주문량</th>
<th class="center" style="color:var(--accent-purple);font-size:11px;">선호도매상</th>
<th class="right">매출액</th>
<th class="center" style="width:90px">주문수량</th>
</tr>
</thead>
<tbody id="usageTableBody">
<tr>
<td colspan="9">
<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 = [];
let orderDataByKd = {};
let preferredVendors = {}; // 약품별 선호 도매상
// 선호 도매상 표시
function getPreferredVendor(drugCode) {
const v = preferredVendors[drugCode];
if (!v || !v.success) return '-';
const recent = v.recent_vendor;
const most = v.most_frequent_vendor;
if (!recent && !most) return '-';
const shorten = (n) => n ? n.replace('강원','').replace('(주)','').replace('지점','').replace('약품','').substring(0,3) : '';
const rn = recent ? shorten(recent.vendor_name) : '';
const mn = most ? shorten(most.vendor_name) : '';
if (rn === mn && rn) return `<span style="color:#10b981" title="${most?.vendor_name}">${rn}</span>`;
let h = '';
if (mn) h += `<span style="color:#a855f7" title="최다(${most.order_count}회)">★${mn}</span>`;
if (rn && rn !== mn) h += `<br><span style="color:#888" title="최근">▸${rn}</span>`;
return h || '-';
}
// 선호 도매상 로드
async function loadPreferredVendors(codes) {
if (!codes || !codes.length) return;
try {
const r = await fetch('/api/order/drugs/preferred-vendors', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({drug_codes: codes, period: 365})
});
const d = await r.json();
if (d.success) { preferredVendors = d.results; renderTable(); }
} catch(e) { console.error('선호도매상 로드 실패:', e); }
} // 도매상 주문량 합산 (KD코드별) - 지오영 + 수인
// 초기화
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();
loadOrderData(); // 수인약품 주문량 로드
});
// ──────────────── 도매상 주문량 조회 (지오영 + 수인 + 백제 + 동원 합산) ────────────────
async function loadOrderData() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
orderDataLoading = true;
orderDataByKd = {};
try {
// 지오영 + 수인 + 백제 + 동원 병렬 조회
const [geoRes, sooinRes, baekjeRes, dongwonRes] = await Promise.all([
fetch(`/api/geoyoung/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })),
fetch(`/api/sooin/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })),
fetch(`/api/baekje/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false })),
fetch(`/api/dongwon/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`).then(r => r.json()).catch(() => ({ success: false }))
]);
let totalOrders = 0;
// 도매상 정보 (확장 가능)
const vendorConfig = {
geoyoung: { name: '지오영', icon: '🏭', res: geoRes },
sooin: { name: '수인', icon: '💜', res: sooinRes },
baekje: { name: '백제', icon: '💉', res: baekjeRes },
dongwon: { name: '동원', icon: '🟠', res: dongwonRes }
};
// 각 도매상 데이터 합산 (상세 정보 포함)
for (const [vendorId, config] of Object.entries(vendorConfig)) {
const res = config.res;
if (res.success && res.by_kd_code) {
for (const [kd, data] of Object.entries(res.by_kd_code)) {
if (!orderDataByKd[kd]) {
orderDataByKd[kd] = {
product_name: data.product_name,
spec: data.spec,
boxes: 0,
units: 0,
details: [] // 도매상별 상세 배열
};
}
const boxes = data.boxes || 0;
const units = data.units || 0;
orderDataByKd[kd].boxes += boxes;
orderDataByKd[kd].units += units;
// 상세 정보 추가 (수량이 있는 경우만)
if (units > 0 || boxes > 0) {
orderDataByKd[kd].details.push({
vendor: vendorId,
name: config.name,
boxes: boxes,
units: units
});
}
}
totalOrders += res.order_count || 0;
console.log(`${config.icon} ${config.name} 주문량:`, Object.keys(res.by_kd_code).length, '품목,', res.order_count, '건');
}
}
console.log('📦 4사 합산 주문량:', Object.keys(orderDataByKd).length, '품목,', totalOrders, '건 주문');
} catch(err) {
console.warn('주문량 조회 실패:', err);
orderDataByKd = {};
} finally {
orderDataLoading = false;
renderTable(); // 로딩 완료 후 테이블 갱신
}
}
// KD코드로 주문량 조회 (툴팁 포함)
let orderDataLoading = true; // 로딩 상태
function getOrderedQty(kdCode) {
if (orderDataLoading) return '<span class="order-loading">···</span>';
const order = orderDataByKd[kdCode];
if (!order || order.units === 0) return '-';
// 상세 정보가 없거나 1개만 있으면 단순 표시
if (!order.details || order.details.length <= 1) {
const vendorName = order.details && order.details[0] ? order.details[0].name : '';
return `<span title="${vendorName}">${order.units.toLocaleString()}</span>`;
}
// 2개 이상 도매상이면 툴팁 표시
let tooltipHtml = `<div class="order-qty-tooltip">
<div class="order-qty-tooltip-title">도매상별 주문</div>`;
for (const detail of order.details) {
tooltipHtml += `
<div class="order-qty-tooltip-row">
<span class="order-qty-vendor">
<span class="order-qty-vendor-dot ${detail.vendor}"></span>
${detail.name}
</span>
<span class="order-qty-value">${detail.units.toLocaleString()}</span>
</div>`;
}
tooltipHtml += `
<div class="order-qty-tooltip-row order-qty-total">
<span>합계</span>
<span>${order.units.toLocaleString()}</span>
</div>
</div>`;
return `<div class="order-qty-cell">
${order.units.toLocaleString()}
${tooltipHtml}
</div>`;
}
// ──────────────── 데이터 로드 ────────────────
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="9">
<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();
// 선호 도매상 로드 (백그라운드)
loadPreferredVendors(data.items.map(i => i.drug_code));
} else {
document.getElementById('usageTableBody').innerHTML = `
<tr><td colspan="9">
<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="9">
<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="9">
<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>` : ''}${item.patient_names ? ` <span class="patient-badge ${item.today_patients ? 'has-today' : ''}" title="${item.patient_count}명 사용${item.today_patients ? ' (오늘: ' + item.today_patients + ')' : ''}">👤${formatPatientNames(item.patient_names, item.today_patients)}</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 class="qty-cell" style="color:var(--accent-cyan);">${getOrderedQty(item.drug_code)}</td>
<td class="qty-cell" style="font-size:10px;">${getPreferredVendor(item.drug_code)}</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
},
dongwon: {
id: 'dongwon',
name: '동원약품',
icon: '🏥',
logo: '/static/img/logo_dongwon.png',
color: '#22c55e',
gradient: 'linear-gradient(135deg, #16a34a, #22c55e)',
filterFn: (item) => item.supplier === '동원약품' || item.wholesaler === 'dongwon',
getCode: (item) => item.dongwon_code || item.internal_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 = [];
let wholesalerLimits = {}; // 도매상 한도 캐시
async function openWholesalerSelectModal(itemsByWholesaler, otherItems) {
pendingWholesalerItems = itemsByWholesaler;
pendingOtherItems = otherItems;
const modal = document.getElementById('multiWholesalerModal');
const body = document.getElementById('multiWholesalerBody');
// 로딩 표시
body.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-muted);">📊 한도 및 월 매출 조회 중...</div>';
modal.classList.add('show');
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
// 1. 도매상 한도 정보 가져오기
try {
const res = await fetch('/api/order/wholesaler/limits');
const data = await res.json();
if (data.success) {
data.limits.forEach(l => {
wholesalerLimits[l.wholesaler_id] = l;
});
}
} catch (e) {
console.warn('한도 조회 실패:', e);
}
// 2. 실제 월 매출 가져오기 (도매상 API 호출)
const wholesalerConfigs = WHOLESALER_ORDER.map(id => WHOLESALER_CONFIG[id]);
await Promise.all(wholesalerConfigs.map(async ws => {
try {
const salesRes = await fetch(`${ws.salesApi}?year=${year}&month=${month}`);
const salesData = await salesRes.json();
if (salesData.success && wholesalerLimits[ws.id]) {
// 실제 월 매출로 current_usage 업데이트
wholesalerLimits[ws.id].current_usage = salesData.total_amount || 0;
wholesalerLimits[ws.id].usage_percent = wholesalerLimits[ws.id].monthly_limit > 0
? Math.round((salesData.total_amount || 0) / wholesalerLimits[ws.id].monthly_limit * 1000) / 10
: 0;
wholesalerLimits[ws.id].remaining = wholesalerLimits[ws.id].monthly_limit - (salesData.total_amount || 0);
}
} catch (e) {
console.warn(`${ws.id} 월매출 조회 실패:`, e);
}
}));
const wsIds = Object.keys(itemsByWholesaler);
// 전체 총액 계산
let grandTotal = 0;
wsIds.forEach(wsId => {
itemsByWholesaler[wsId].forEach(item => {
grandTotal += (item.unit_price || 0) * item.qty;
});
});
let html = `
<div class="multi-ws-summary">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<p style="color:var(--text-secondary);margin:0;">
장바구니에 <b>${wsIds.length}개 도매상</b>의 품목이 있습니다.
</p>
<div style="font-size:18px;font-weight:700;color:var(--accent-emerald);font-family:'JetBrains Mono',monospace;">
${grandTotal > 0 ? '₩' + grandTotal.toLocaleString() : ''}
</div>
</div>
`;
// 각 도매상별 품목 표시
wsIds.forEach(wsId => {
const ws = WHOLESALERS[wsId];
const items = itemsByWholesaler[wsId];
const limit = wholesalerLimits[wsId];
// 도매상별 소계
const wsTotal = items.reduce((sum, item) => sum + (item.unit_price || 0) * item.qty, 0);
// 한도 정보 계산
let limitHtml = '';
if (limit) {
const afterOrder = limit.current_usage + wsTotal;
const afterPercent = (afterOrder / limit.monthly_limit * 100).toFixed(1);
const isOver = afterOrder > limit.monthly_limit;
const isWarning = afterPercent >= (limit.warning_threshold * 100);
limitHtml = `
<div style="margin-top:8px;padding:8px 12px;background:var(--bg-tertiary);border-radius:6px;font-size:12px;">
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
<span>월 한도</span>
<span style="font-family:'JetBrains Mono',monospace;">${(limit.monthly_limit/10000).toLocaleString()}만원</span>
</div>
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
<span>이번달 사용</span>
<span style="font-family:'JetBrains Mono',monospace;">${(limit.current_usage/10000).toLocaleString()}만원 (${limit.usage_percent}%)</span>
</div>
<div style="display:flex;justify-content:space-between;color:${isOver ? 'var(--accent-red)' : isWarning ? 'var(--accent-amber)' : 'var(--accent-emerald)'};">
<span>주문 후</span>
<span style="font-family:'JetBrains Mono',monospace;font-weight:600;">
${(afterOrder/10000).toLocaleString()}만원 (${afterPercent}%)
${isOver ? ' ⚠️ 초과!' : isWarning ? ' ⚠️' : ' ✓'}
</span>
</div>
</div>
`;
}
html += `
<div class="multi-ws-card ${wsId}">
<div class="multi-ws-header">
<img src="${ws.logo}" alt="${ws.name}" style="width:24px;height:24px;object-fit:contain;margin-right:8px;">
<span class="multi-ws-name">${ws.name}</span>
<span class="multi-ws-count">${items.length}개 품목</span>
${wsTotal > 0 ? `<span style="margin-left:auto;margin-right:12px;font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--accent-cyan);">₩${wsTotal.toLocaleString()}</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 => {
const itemAmt = (item.unit_price || 0) * item.qty;
return `<div class="multi-ws-item">· ${item.product_name} (${item.qty}개)${itemAmt > 0 ? ` <span style="color:var(--text-muted);">${itemAmt.toLocaleString()}원</span>` : ''}</div>`;
}).join('')}
${items.length > 3 ? `<div class="multi-ws-item more">... 외 ${items.length - 3}개</div>` : ''}
</div>
${limitHtml}
</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, cartOnly = 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 btnCart = document.getElementById('btnMultiCart');
const btnReal = document.getElementById('btnMultiReal');
btnTest.disabled = true;
btnCart.disabled = true;
btnReal.disabled = true;
// 진행 상태 표시
if (dryRun) {
btnTest.textContent = '처리 중...';
} else if (cartOnly) {
btnCart.textContent = '처리 중...';
} else {
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: cartOnly // true=장바구니만, 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, cartOnly);
// 버튼 복원
btnTest.disabled = false;
btnCart.disabled = false;
btnReal.disabled = false;
btnTest.textContent = '🧪 테스트';
btnCart.textContent = '🛒 장바구니만';
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, isCartOnly = false) {
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 ? '[테스트]' : isCartOnly ? '[장바구니]' : '';
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 = `<img src="${ws.logo}" alt="${ws.name}" style="width:24px;height:24px;object-fit:contain;vertical-align:middle;margin-right:8px;">${ws.name} 주문 확인`;
document.getElementById('orderConfirmWholesaler').textContent = ws.name;
headerDiv.style.background = ws.gradient;
let html = '';
let totalAmount = 0;
items.forEach((item, idx) => {
// 예상 금액 계산 (단가 × 수량)
const unitPrice = item.unit_price || item.price || 0;
const itemAmount = unitPrice * item.qty;
totalAmount += itemAmount;
html += `
<tr>
<td>${escapeHtml(item.product_name)}</td>
<td class="mono">${item.specification || '-'}</td>
<td class="mono" style="text-align:center;">${item.qty}</td>
<td class="mono" style="text-align:right;">${itemAmount > 0 ? itemAmount.toLocaleString() + '원' : '-'}</td>
</tr>`;
});
tbody.innerHTML = html;
document.getElementById('orderConfirmCount').textContent = items.length;
document.getElementById('orderConfirmTotal').innerHTML = totalAmount > 0
? `${totalAmount.toLocaleString()}`
: '<span style="font-size:12px;color:var(--text-muted);">금액 미정</span>';
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');
// 🔍 디버그: 장바구니 아이템 확인
console.log('[DEBUG] orderSingleItem - cart item:', JSON.stringify(item, null, 2));
console.log('[DEBUG] internal_code:', item.internal_code);
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, cartOnly = false) {
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 btnCart = document.getElementById('btnOrderCart');
const btnReal = document.getElementById('btnOrderReal');
btnTest.disabled = true;
btnCart.disabled = true;
btnReal.disabled = true;
// 진행 상태 표시
if (dryRun) {
btnTest.textContent = '처리 중...';
} else if (cartOnly) {
btnCart.textContent = '처리 중...';
} else {
btnReal.textContent = '처리 중...';
}
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: cartOnly // true=장바구니만, 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;
btnCart.disabled = false;
btnReal.disabled = false;
btnTest.textContent = '🧪 테스트';
btnCart.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 formatPatientNames(allNames, todayNames) {
if (!allNames) return '';
if (!todayNames) return escapeHtml(allNames);
const todaySet = new Set(todayNames.split(', ').map(n => n.trim()));
const names = allNames.split(', ');
return names.map(name => {
const trimmed = name.trim();
if (todaySet.has(trimmed)) {
return `<strong class="today-patient">${escapeHtml(trimmed)}</strong>`;
}
return escapeHtml(trimmed);
}).join(', ');
}
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: [], dongwon: [] };
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, dongwonResult] = await Promise.all([
searchGeoyoungAPI(kdCode, productName),
searchSooinAPI(kdCode),
searchBaekjeAPI(kdCode),
searchDongwonAPI(kdCode, productName)
]);
// 결과 저장
window.wholesaleItems = {
geoyoung: geoResult.items || [],
sooin: sooinResult.items || [],
baekje: baekjeResult.items || [],
dongwon: dongwonResult.items || []
};
// 통합 렌더링
renderWholesaleResults(geoResult, sooinResult, baekjeResult, dongwonResult);
}
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: [] };
}
}
async function searchDongwonAPI(kdCode, productName) {
try {
// 1차: KD코드(보험코드)로 검색 (searchType=0)
let response = await fetch(`/api/dongwon/stock?keyword=${encodeURIComponent(kdCode)}`);
let data = await response.json();
// 결과 없으면 제품명으로 재검색
if (data.success && data.count === 0 && productName) {
// 제품명 정제: "휴니즈레바미피드정_(0.1g/1정)" → "휴니즈레바미피드정"
let cleanName = productName.split('_')[0].split('(')[0].trim();
if (cleanName) {
response = await fetch(`/api/dongwon/stock?keyword=${encodeURIComponent(cleanName)}`);
data = await response.json();
}
}
return data;
} catch (err) {
return { success: false, error: err.message, items: [] };
}
}
function renderWholesaleResults(geoResult, sooinResult, baekjeResult, dongwonResult) {
const resultBody = document.getElementById('geoResultBody');
const geoItems = geoResult.items || [];
const sooinItems = sooinResult.items || [];
const baekjeItems = (baekjeResult && baekjeResult.items) || [];
const dongwonItems = (dongwonResult && dongwonResult.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);
dongwonItems.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>';
// ═══════ 동원약품 섹션 ═══════
html += `<div class="ws-section">
<div class="ws-header dongwon">
<span class="ws-logo">🏥</span>
<span class="ws-name">동원약품</span>
<span class="ws-count">${dongwonItems.length}건</span>
</div>`;
if (dongwonItems.length > 0) {
html += `<table class="geo-table">
<thead><tr><th>제품명</th><th>규격</th><th>단가</th><th>재고</th><th></th></tr></thead>
<tbody>`;
dongwonItems.forEach((item, idx) => {
const hasStock = item.stock > 0;
// 동원: code=KD코드(보험코드), internal_code=내부코드(주문용)
const displayCode = item.code || item.internal_code || '';
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.name)}</span>
<span class="geo-code">${displayCode} · ${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 dongwon" onclick="addToCartFromWholesale('dongwon', ${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: '백제약품', dongwon: '동원약품' };
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 unitPrice = item.price || item.unit_price || 0;
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,
dongwon_code: wholesaler === 'dongwon' ? item.internal_code : null, // 동원: 내부코드로 주문
unit_price: unitPrice // 💰 단가 추가
};
// 🔍 디버그: 장바구니 추가 시 internal_code 확인
console.log('[DEBUG] addToCartFromWholesale');
console.log('[DEBUG] wholesaler item:', JSON.stringify(item, null, 2));
console.log('[DEBUG] cartItem internal_code:', cartItem.internal_code);
// 기존 항목 체크 (같은 도매상 + 같은 규격)
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-add-btn.dongwon {
background: #22c55e;
}
.geo-add-btn.dongwon:hover {
background: #16a34a;
}
.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-header.dongwon {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(22, 163, 74, 0.1));
border-left: 3px solid #22c55e;
}
.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.dongwon {
border-left: 3px solid #22c55e;
}
.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" style="max-width:600px;">
<div class="order-modal-header">
<h3 id="orderConfirmTitle">🏭 지오영 주문 확인</h3>
<button class="order-close" onclick="closeOrderConfirmModal()"></button>
</div>
<div class="order-modal-body">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<p style="color:var(--text-secondary);margin:0;">
<span id="orderConfirmCount">0</span>개 품목을 <span id="orderConfirmWholesaler">지오영</span> 장바구니에 담습니다.
</p>
<div id="orderConfirmTotal" style="font-size:18px;font-weight:700;color:var(--accent-cyan);font-family:'JetBrains Mono',monospace;">
₩0
</div>
</div>
<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><th style="text-align:right;">예상금액</th></tr></thead>
<tbody id="orderConfirmBody"></tbody>
</table>
</div>
<div class="order-modal-footer" style="gap:8px;">
<button class="btn-order-test" id="btnOrderTest" onclick="executeOrder(true, false)">🧪 테스트</button>
<button class="btn-order-cart" id="btnOrderCart" onclick="executeOrder(false, true)" style="background:linear-gradient(135deg, #0891b2, #06b6d4);">🛒 장바구니만</button>
<button class="btn-order-real" id="btnOrderReal" onclick="executeOrder(false, 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:650px;">
<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" style="gap:8px;">
<button class="btn-order-test" id="btnMultiTest" onclick="executeAllWholesalers(true, false)">🧪 테스트</button>
<button class="btn-order-cart" id="btnMultiCart" onclick="executeAllWholesalers(false, true)" style="background:linear-gradient(135deg, #0891b2, #06b6d4);">🛒 장바구니만</button>
<button class="btn-order-real" id="btnMultiReal" onclick="executeAllWholesalers(false, 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'
},
dongwon: {
id: 'dongwon', name: '동원약품', icon: '🏥',
logo: '/static/img/logo_dongwon.png',
color: '#22c55e',
balanceApi: '/api/dongwon/balance',
salesApi: '/api/dongwon/monthly-orders'
}
};
const WHOLESALER_ORDER = ['baekje', 'geoyoung', 'sooin', 'dongwon'];
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>