feat(반품관리): 약품 위치 컬럼 추가

- API: return-candidates에서 CD_item_position 조인하여 위치 정보 반환
- 테이블에 '위치' 컬럼 추가 (노란색 뱃지 스타일)
- 위치 미지정 약품은 '-' 표시
This commit is contained in:
thug0bin 2026-03-08 10:46:43 +09:00
parent c71c9ad678
commit 71d1916efb
2 changed files with 183 additions and 156 deletions

View File

@ -5360,8 +5360,9 @@ def api_return_candidates():
# 약품코드 목록 추출
drug_codes = [row['drug_code'] for row in rows]
# MSSQL에서 가격 정보 조회 (한 번에)
# MSSQL에서 가격 + 위치 정보 조회 (한 번에)
price_map = {}
location_map = {}
if drug_codes:
try:
mssql_conn_str = (
@ -5378,20 +5379,24 @@ def api_return_candidates():
# IN 절 생성 (SQL Injection 방지를 위해 파라미터화)
# Price가 없으면 Saleprice, Topprice 순으로 fallback
# CD_item_position JOIN으로 위치 정보도 함께 조회
placeholders = ','.join(['?' for _ in drug_codes])
mssql_cursor.execute(f"""
SELECT DrugCode,
COALESCE(NULLIF(Price, 0), NULLIF(Saleprice, 0), NULLIF(Topprice, 0), 0) as BestPrice
FROM CD_GOODS
WHERE DrugCode IN ({placeholders})
SELECT G.DrugCode,
COALESCE(NULLIF(G.Price, 0), NULLIF(G.Saleprice, 0), NULLIF(G.Topprice, 0), 0) as BestPrice,
ISNULL(POS.CD_NM_sale, '') as Location
FROM CD_GOODS G
LEFT JOIN CD_item_position POS ON G.DrugCode = POS.DrugCode
WHERE G.DrugCode IN ({placeholders})
""", drug_codes)
for row in mssql_cursor.fetchall():
price_map[row[0]] = float(row[1]) if row[1] else 0
location_map[row[0]] = row[2] or ''
mssql_conn.close()
except Exception as e:
logging.warning(f"MSSQL 가격 조회 실패: {e}")
logging.warning(f"MSSQL 가격/위치 조회 실패: {e}")
# 전체 데이터 조회 (통계용)
cursor.execute("SELECT drug_code, current_stock, months_since_use, months_since_purchase FROM return_candidates")
@ -5432,6 +5437,7 @@ def api_return_candidates():
'current_stock': stock,
'unit_price': price,
'recoverable_amount': recoverable,
'location': location_map.get(code, ''),
'last_prescription_date': row['last_prescription_date'],
'months_since_use': row['months_since_use'],
'last_purchase_date': row['last_purchase_date'],

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>반품 관리 - 청춘약국</title>
<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">
@ -38,7 +38,7 @@
min-height: 100vh;
}
/* ══════════════════ 헤더 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ?¤ë<C2A4>” ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.header {
background: linear-gradient(135deg, #dc2626 0%, #f97316 50%, #f59e0b 100%);
padding: 20px 24px;
@ -89,14 +89,14 @@
background: rgba(255,255,255,0.25);
}
/* ══════════════════ 컨텐츠 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> 컨í…<C3AD>ì¸??<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.content {
max-width: 1800px;
margin: 0 auto;
padding: 24px;
}
/* ══════════════════ 통계 카드 (2줄) ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ?µê³„ 카드 (2ì¤? ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.stats-row {
display: flex;
gap: 16px;
@ -162,7 +162,7 @@
margin-top: 2px;
}
/* ══════════════════ 필터 바 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ?„í„° ë°??<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.filter-bar {
background: var(--bg-card);
border-radius: 16px;
@ -218,7 +218,7 @@
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4);
}
/* ══════════════════ 긴급도 탭 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> 긴급?????<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.urgency-tabs {
display: flex;
gap: 8px;
@ -256,7 +256,7 @@
background: rgba(255,255,255,0.3);
}
/* ══════════════════ 테이블 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ?Œì<C592>´ë¸??<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.table-wrap {
background: var(--bg-card);
border-radius: 16px;
@ -299,7 +299,7 @@
background: rgba(249, 115, 22, 0.05);
}
/* 약품 셀 */
/* ?½í’ˆ ?€ */
.drug-cell {
display: flex;
flex-direction: column;
@ -316,7 +316,26 @@
color: var(--text-muted);
}
/* 긴급도 배지 */
/* ?„치 ?€ */
.location-cell {
text-align: center;
}
.location-badge {
display: inline-block;
background: rgba(251, 191, 36, 0.2);
color: var(--accent-amber);
font-size: 11px;
font-weight: 600;
padding: 4px 8px;
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
}
.location-empty {
color: var(--text-muted);
font-size: 12px;
}
/* 긴급??ë°°ì? */
.urgency-badge {
display: inline-flex;
align-items: center;
@ -339,7 +358,7 @@
color: var(--text-muted);
}
/* 상태 배지 */
/* ?<3F>태 ë°°ì? */
.status-badge {
display: inline-block;
padding: 4px 10px;
@ -354,7 +373,7 @@
.status-badge.disposed { background: rgba(244, 63, 94, 0.2); color: var(--accent-rose); }
.status-badge.resolved { background: rgba(100, 116, 139, 0.2); color: var(--text-muted); }
/* 수량/날짜/금액 셀 */
/* ?˜ëŸ‰/? ì§œ/금액 ?€ */
.qty-cell {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
@ -385,7 +404,7 @@
color: var(--text-muted);
}
/* 액션 버튼 */
/* ?¡ì…˜ 버튼 */
.action-btn {
padding: 6px 12px;
border: none;
@ -404,7 +423,7 @@
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4);
}
/* ══════════════════ 페이지네이션 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ?˜ì<CB9C>´ì§€?¤ì<C2A4>´???<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.pagination {
display: flex;
justify-content: center;
@ -428,7 +447,7 @@
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.page-info { color: var(--text-muted); font-size: 13px; }
/* ══════════════════ 모달 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> 모달 ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.modal-overlay {
position: fixed;
inset: 0;
@ -496,7 +515,7 @@
.drug-detail-label { color: var(--text-muted); }
.drug-detail-value { font-weight: 600; font-family: 'JetBrains Mono', monospace; }
/* ══════════════════ 로딩/빈 상태 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> 로딩/ë¹??<3F>태 ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.loading-state, .empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.loading-spinner {
width: 40px; height: 40px;
@ -509,7 +528,7 @@
@keyframes spin { to { transform: rotate(360deg); } }
.empty-icon { font-size: 48px; margin-bottom: 16px; }
/* ══════════════════ 토스트 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ? ìФ???<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.toast {
position: fixed;
bottom: 32px;
@ -531,7 +550,7 @@
.toast.success { border-color: var(--accent-emerald); }
.toast.error { border-color: var(--accent-rose); }
/* ══════════════════ 반응형 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ë°˜ì<CB9C>???<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
@media (max-width: 1200px) {
.stats-row { flex-wrap: wrap; }
.stat-card { min-width: calc(33% - 12px); }
@ -545,7 +564,7 @@
.urgency-tabs { flex-wrap: wrap; }
}
/* ══════════════════ 입고이력 모달 ══════════════════ */
/* ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ?…ê³ ?´ë ¥ 모달 ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> */
.purchase-modal {
position: fixed;
inset: 0;
@ -669,7 +688,7 @@
background: var(--bg-card-hover);
color: var(--text-primary);
}
/* 테이블 행 더블클릭 가능 표시 */
/* ?Œì<C592>´ë¸????”블?´ë¦­ ê°€???œì‹œ */
#dataTableBody tr {
cursor: pointer;
}
@ -682,154 +701,155 @@
<div class="header">
<div class="header-inner">
<div class="header-left">
<h1>📦 반품 후보 관리</h1>
<p>장기 미사용 의약품 반품/폐기 관리</p>
<h1>?“¦ 반품 ?„ë³´ ê´€ë¦?/h1>
<p>?¥ê¸° 미사???˜ì•½??반품/?<3F>기 ê´€ë¦?/p>
</div>
<nav class="header-nav">
<a href="/admin">📊 대시보드</a>
<a href="/admin/rx-usage">💊 Rx 사용량</a>
<a href="/admin/return-management" class="active">📦 반품관리</a>
<a href="/admin">?“Š ?€?œë³´??/a>
<a href="/admin/rx-usage">?’Š Rx ?¬ìš©??/a>
<a href="/admin/return-management" class="active">?“¦ 반품관ë¦?/a>
</nav>
</div>
</div>
<div class="content">
<!-- 통계 카드 1줄: 건수 -->
<!-- ?µê³„ 카드 1ì¤? 건수 -->
<div class="stats-row">
<div class="stat-card rose">
<div class="stat-icon">🔴</div>
<div class="stat-icon">?”´</div>
<div class="stat-value" id="statCritical">-</div>
<div class="stat-label">3년+ 미사용</div>
<div class="stat-sub">긴급 처리 필요</div>
<div class="stat-label">3?? 미사??/div>
<div class="stat-sub">긴급 처리 ?„ìš”</div>
</div>
<div class="stat-card amber">
<div class="stat-icon">🟠</div>
<div class="stat-icon">?Ÿ </div>
<div class="stat-value" id="statWarning">-</div>
<div class="stat-label">2년+ 미사용</div>
<div class="stat-sub">검토 권장</div>
<div class="stat-label">2?? 미사??/div>
<div class="stat-sub">검??권장</div>
</div>
<div class="stat-card cyan">
<div class="stat-icon">📋</div>
<div class="stat-icon">?“‹</div>
<div class="stat-value" id="statPending">-</div>
<div class="stat-label">미결정</div>
<div class="stat-sub">pending 상태</div>
<div class="stat-label">미결??/div>
<div class="stat-sub">pending ?<3F>태</div>
</div>
<div class="stat-card emerald">
<div class="stat-icon"></div>
<div class="stat-icon">??/div>
<div class="stat-value" id="statProcessed">-</div>
<div class="stat-label">처리완료</div>
<div class="stat-label">처리?„료</div>
</div>
<div class="stat-card purple">
<div class="stat-icon">📊</div>
<div class="stat-icon">?“Š</div>
<div class="stat-value" id="statTotal">-</div>
<div class="stat-label">전체 후보</div>
<div class="stat-label">?„ì²´ ?„ë³´</div>
</div>
</div>
<!-- 통계 카드 2줄: 회수가능 금액 -->
<!-- ?µê³„ 카드 2ì¤? ?Œìˆ˜ê°€??금액 -->
<div class="stats-row amount-row">
<div class="stat-card gold">
<div class="stat-icon">💰</div>
<div class="stat-icon">?’°</div>
<div class="stat-value" id="statTotalAmount">-</div>
<div class="stat-label">총 회수가능 금액</div>
<div class="stat-sub">전체 반품 대상</div>
<div class="stat-label">ì´??Œìˆ˜ê°€??금액</div>
<div class="stat-sub">?„ì²´ 반품 ?€??/div>
</div>
<div class="stat-card rose">
<div class="stat-icon">🔴💵</div>
<div class="stat-icon">?”´?’µ</div>
<div class="stat-value small" id="statCriticalAmount">-</div>
<div class="stat-label">3년+ 금액</div>
<div class="stat-sub">긴급 회수 대상</div>
<div class="stat-label">3?? 금액</div>
<div class="stat-sub">긴급 ?Œìˆ˜ ?€??/div>
</div>
<div class="stat-card amber">
<div class="stat-icon">🟠💵</div>
<div class="stat-icon">?Ÿ ?’µ</div>
<div class="stat-value small" id="statWarningAmount">-</div>
<div class="stat-label">2년+ 금액</div>
<div class="stat-sub">주의 대상</div>
<div class="stat-label">2?? 금액</div>
<div class="stat-sub">주ì<EFBFBD>˜ ?€??/div>
</div>
</div>
<!-- 필터 바 -->
<!-- ?„í„° ë°?-->
<div class="filter-bar">
<div class="filter-group">
<label>상태</label>
<label>?<3F>태</label>
<select id="filterStatus">
<option value="">전체</option>
<option value="pending" selected>미결정 (pending)</option>
<option value="reviewed">검토됨</option>
<option value="returned">반품완료</option>
<option value="">?„ì²´</option>
<option value="pending" selected>미결??(pending)</option>
<option value="reviewed">ê²€? ë<C2A0>¨</option>
<option value="returned">반품?„료</option>
<option value="keep">보류</option>
<option value="disposed">폐기</option>
<option value="resolved">재고소진</option>
<option value="disposed">?<3F>기</option>
<option value="resolved">?¬ê³ ?Œì§„</option>
</select>
</div>
<div class="filter-group">
<label>긴급도</label>
<label>긴급??/label>
<select id="filterUrgency">
<option value="">전체</option>
<option value="critical">🔴 3년+ (긴급)</option>
<option value="warning">🟠 2년+ (주의)</option>
<option value="normal">일반</option>
<option value="">?„ì²´</option>
<option value="critical">?”´ 3?? (긴급)</option>
<option value="warning">?Ÿ  2?? (주ì<C2BC>˜)</option>
<option value="normal">?¼ë°˜</option>
</select>
</div>
<div class="filter-group">
<label>검색어</label>
<input type="text" id="searchInput" placeholder="약품명, 코드...">
<label>ê²€?‰ì–´</label>
<input type="text" id="searchInput" placeholder="?½í’ˆëª? 코드...">
</div>
<div class="filter-group">
<label>정렬</label>
<label>?•ë ¬</label>
<select id="sortSelect">
<option value="months_desc">미사용기간 긴 순</option>
<option value="months_asc">미사용기간 짧은 순</option>
<option value="stock_desc">재고량 많은 순</option>
<option value="name_asc">약품명순</option>
<option value="detected_desc">감지일 최신순</option>
<option value="months_desc">미사?©ê¸°ê°?ê¸???/option>
<option value="months_asc">미사?©ê¸°ê°?ì§§ì? ??/option>
<option value="stock_desc">?¬ê³ ??ë§Žì? ??/option>
<option value="name_asc">?½í’ˆëª…순</option>
<option value="detected_desc">ê°<EFBFBD>ì???최신??/option>
</select>
</div>
<button class="filter-btn" onclick="loadData()">🔍 조회</button>
<button class="filter-btn" onclick="loadData()">?”<> 조회</button>
</div>
<!-- 긴급도 탭 -->
<!-- 긴급????-->
<div class="urgency-tabs">
<button class="urgency-tab active" data-urgency="" onclick="setUrgencyTab('')">
전체 <span class="count" id="tabAll">0</span>
?„ì²´ <span class="count" id="tabAll">0</span>
</button>
<button class="urgency-tab" data-urgency="critical" onclick="setUrgencyTab('critical')">
🔴 3년+ <span class="count" id="tabCritical">0</span>
?”´ 3?? <span class="count" id="tabCritical">0</span>
</button>
<button class="urgency-tab" data-urgency="warning" onclick="setUrgencyTab('warning')">
🟠 2년+ <span class="count" id="tabWarning">0</span>
?Ÿ  2?? <span class="count" id="tabWarning">0</span>
</button>
<button class="urgency-tab" data-urgency="normal" onclick="setUrgencyTab('normal')">
일반 <span class="count" id="tabNormal">0</span>
?¼ë°˜ <span class="count" id="tabNormal">0</span>
</button>
</div>
<!-- 데이터 테이블 -->
<!-- ?°ì<C2B0>´???Œì<C592>´ë¸?-->
<div style="margin-bottom: 8px; font-size: 12px; color: var(--text-muted);">
💡 행 더블클릭 → 입고이력 확인 (도매상 연락처)
?’¡ ???”블?´ë¦­ ???…ê³ ?´ë ¥ ?•ì<E280A2>¸ (?„매???°ë<C2B0>½ì²?
</div>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th style="width:4%">긴급</th>
<th style="width:24%">약품</th>
<th class="center" style="width:6%">현재고</th>
<th class="right" style="width:8%">단가</th>
<th class="right" style="width:10%">회수가능금액</th>
<th class="center" style="width:10%">마지막 처방</th>
<th class="center" style="width:8%">미사용</th>
<th class="center" style="width:10%">마지막 입고</th>
<th class="center" style="width:7%">상태</th>
<th style="width:10%">액션</th>
<th style="width:20%">?½í’ˆ</th>
<th class="center" style="width:6%">?„치</th>
<th class="center" style="width:5%">?„재ê³?/th>
<th class="right" style="width:7%">?¨ê?</th>
<th class="right" style="width:9%">?Œìˆ˜ê°€?¥ê¸ˆ??/th>
<th class="center" style="width:9%">마ì?ë§?처방</th>
<th class="center" style="width:7%">미사??/th>
<th class="center" style="width:9%">마ì?ë§??…ê³ </th>
<th class="center" style="width:6%">?<3F>태</th>
<th style="width:9%">?¡ì…˜</th>
</tr>
</thead>
<tbody id="dataTableBody">
<tr>
<td colspan="10">
<td colspan="11">
<div class="loading-state">
<div class="loading-spinner"></div>
<div>데이터 로딩 중...</div>
<div>?°ì<C2B0>´??로딩 ì¤?..</div>
</div>
</td>
</tr>
@ -837,70 +857,70 @@
</table>
</div>
<!-- 페이지네이션 -->
<!-- ?˜ì<CB9C>´ì§€?¤ì<C2A4>´??-->
<div class="pagination" id="pagination"></div>
</div>
<!-- 상태 변경 모달 -->
<!-- ?<3F>태 변�모달 -->
<div class="modal-overlay" id="statusModal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">📋 상태 변경</h3>
<h3 class="modal-title">?“‹ ?<3F>태 ë³€ê²?/h3>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<div class="drug-detail" id="modalDrugDetail"></div>
<div class="form-group">
<label>변경할 상태</label>
<label>변경할 ?<3F>태</label>
<select id="modalStatus">
<option value="reviewed">검토됨</option>
<option value="returned">반품완료</option>
<option value="keep">보류 (유지)</option>
<option value="disposed">폐기</option>
<option value="resolved">재고소진</option>
<option value="reviewed">ê²€? ë<C2A0>¨</option>
<option value="returned">반품?„료</option>
<option value="keep">보류 (? ì?)</option>
<option value="disposed">?<3F>기</option>
<option value="resolved">?¬ê³ ?Œì§„</option>
</select>
</div>
<div class="form-group">
<label>결정 사유</label>
<textarea id="modalReason" placeholder="상태 변경 사유를 입력하세요... (보류 선택 시 필수)"></textarea>
<label>ê²°ì • ?¬ìœ </label>
<textarea id="modalReason" placeholder="?<3F>태 ë³€ê²??¬ìœ ë¥??…ë ¥?˜ì„¸??.. (보류 ? íƒ<C3AD> ???„수)"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn cancel" onclick="closeModal()">취소</button>
<button class="modal-btn submit" onclick="submitStatusChange()">변경 저장</button>
<button class="modal-btn submit" onclick="submitStatusChange()">ë³€ê²??€??/button>
</div>
</div>
</div>
<!-- 토스트 -->
<!-- ? ìФ??-->
<div class="toast" id="toast"></div>
<!-- 입고이력 모달 -->
<!-- ?…ê³ ?´ë ¥ 모달 -->
<div class="purchase-modal" id="purchaseModal">
<div class="purchase-modal-content">
<div class="purchase-modal-header">
<h3>📦 입고 이력</h3>
<h3>?“¦ ?…ê³  ?´ë ¥</h3>
<div class="drug-name" id="purchaseDrugName">-</div>
</div>
<div class="purchase-modal-body">
<table class="purchase-history-table">
<thead>
<tr>
<th>도매상</th>
<th>입고일</th>
<th>수량</th>
<th>단가</th>
<th>?„매??/th>
<th>?…ê³ ??/th>
<th>?˜ëŸ‰</th>
<th>?¨ê?</th>
</tr>
</thead>
<tbody id="purchaseHistoryBody">
<tr><td colspan="4" class="purchase-empty"><div class="icon">📭</div><p>로딩 중...</p></td></tr>
<tr><td colspan="4" class="purchase-empty"><div class="icon">?“­</div><p>로딩 ì¤?..</p></td></tr>
</tbody>
</table>
</div>
<div class="purchase-modal-footer">
<button class="purchase-modal-btn" onclick="closePurchaseModal()">닫기</button>
<button class="purchase-modal-btn" onclick="closePurchaseModal()">?«ê¸°</button>
</div>
</div>
</div>
@ -920,7 +940,7 @@
async function loadData() {
const tbody = document.getElementById('dataTableBody');
tbody.innerHTML = `<tr><td colspan="10"><div class="loading-state"><div class="loading-spinner"></div><div>데이터 로딩 중...</div></div></td></tr>`;
tbody.innerHTML = `<tr><td colspan="11"><div class="loading-state"><div class="loading-spinner"></div><div>?°ì<C2B0>´??로딩 ì¤?..</div></div></td></tr>`;
const status = document.getElementById('filterStatus').value;
const urgency = document.getElementById('filterUrgency').value;
@ -944,10 +964,10 @@
currentPage = 1;
renderTable();
} else {
tbody.innerHTML = `<tr><td colspan="10"><div class="empty-state"><div class="empty-icon">⚠️</div><div>오류: ${data.error}</div></div></td></tr>`;
tbody.innerHTML = `<tr><td colspan="11"><div class="empty-state"><div class="empty-icon">? ï¸<C3AF></div><div>?¤ë¥˜: ${data.error}</div></div></td></tr>`;
}
} catch (err) {
tbody.innerHTML = `<tr><td colspan="10"><div class="empty-state"><div class="empty-icon"></div><div>데이터 로드 실패</div></div></td></tr>`;
tbody.innerHTML = `<tr><td colspan="11"><div class="empty-state"><div class="empty-icon">??/div><div>?°ì<C2B0>´??로드 ?¤íŒ¨</div></div></td></tr>`;
}
}
@ -958,23 +978,23 @@
document.getElementById('statProcessed').textContent = stats.processed || 0;
document.getElementById('statTotal').textContent = stats.total || 0;
// 금액 표시
// 금액 ?œì‹œ
document.getElementById('statTotalAmount').textContent = formatAmount(stats.total_amount || 0);
document.getElementById('statCriticalAmount').textContent = formatAmount(stats.critical_amount || 0);
document.getElementById('statWarningAmount').textContent = formatAmount(stats.warning_amount || 0);
}
function formatAmount(amount) {
if (!amount || amount === 0) return '₩0';
if (!amount || amount === 0) return '??';
if (amount >= 1000000) {
return '₩' + (amount / 10000).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '만';
return '?? + (amount / 10000).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + 'ë§?;
}
return '₩' + Math.round(amount).toLocaleString();
return '?? + Math.round(amount).toLocaleString();
}
function formatPrice(price) {
if (!price || price === 0) return '-';
return '₩' + Math.round(price).toLocaleString();
return '?? + Math.round(price).toLocaleString();
}
function updateTabs() {
@ -1016,7 +1036,7 @@
}
if (filteredData.length === 0) {
tbody.innerHTML = `<tr><td colspan="10"><div class="empty-state"><div class="empty-icon">📦</div><div>해당 조건의 반품 후보가 없습니다</div></div></td></tr>`;
tbody.innerHTML = `<tr><td colspan="11"><div class="empty-state"><div class="empty-icon">?“¦</div><div>?´ë‹¹ ì¡°ê±´??반품 ?„ë³´ê°€ ?†ìе?ˆë‹¤</div></div></td></tr>`;
document.getElementById('pagination').innerHTML = '';
return;
}
@ -1035,7 +1055,7 @@
<tr class="${urgencyClass}" ondblclick="openPurchaseModal('${item.drug_code}', '${escapeHtml(item.drug_name).replace(/'/g, "\\'")}')">
<td>
<span class="urgency-badge ${urgency}">
${urgency === 'critical' ? '🔴' : urgency === 'warning' ? '🟠' : '⚪'}
${urgency === 'critical' ? '?”´' : urgency === 'warning' ? '?Ÿ ' : '??}
</span>
</td>
<td>
@ -1044,9 +1064,10 @@
<span class="drug-code">${item.drug_code}</span>
</div>
</td>
<td class="location-cell">${item.location ? `<span class="location-badge">${escapeHtml(item.location)}</span>` : '<span class="location-empty">-</span>'}</td>
<td class="qty-cell">${item.current_stock || 0}</td>
<td class="amount-cell ${item.unit_price ? '' : 'zero'}">${formatPrice(item.unit_price)}</td>
<td class="amount-cell ${hasAmount ? '' : 'zero'}">${hasAmount ? '₩' + Math.round(item.recoverable_amount).toLocaleString() : '-'}</td>
<td class="amount-cell ${hasAmount ? '' : 'zero'}">${hasAmount ? '?? + Math.round(item.recoverable_amount).toLocaleString() : '-'}</td>
<td class="date-cell">${formatDate(item.last_prescription_date) || '-'}</td>
<td class="months-cell ${urgency}">
${item.months_since_use ? item.months_since_use + '개월' : '-'}
@ -1054,7 +1075,7 @@
<td class="date-cell">${formatDate(item.last_purchase_date) || '-'}</td>
<td><span class="status-badge ${item.status}">${getStatusLabel(item.status)}</span></td>
<td>
<button class="action-btn primary" onclick="event.stopPropagation();openStatusModal(${item.id})">상태 변경</button>
<button class="action-btn primary" onclick="event.stopPropagation();openStatusModal(${item.id})">?<3F>태 변�/button>
</td>
</tr>
`;
@ -1067,12 +1088,12 @@
const pagination = document.getElementById('pagination');
if (totalPages <= 1) {
pagination.innerHTML = `<span class="page-info">총 ${totalItems}건</span>`;
pagination.innerHTML = `<span class="page-info">ì´?${totalItems}ê±?/span>`;
return;
}
let html = `<button class="page-btn" onclick="goToPage(1)" ${currentPage === 1 ? 'disabled' : ''}>«</button>`;
html += `<button class="page-btn" onclick="goToPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}></button>`;
html += `<button class="page-btn" onclick="goToPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>??/button>`;
const maxVisible = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
@ -1083,9 +1104,9 @@
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="goToPage(${i})">${i}</button>`;
}
html += `<button class="page-btn" onclick="goToPage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}></button>`;
html += `<button class="page-btn" onclick="goToPage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>??/button>`;
html += `<button class="page-btn" onclick="goToPage(${totalPages})" ${currentPage === totalPages ? 'disabled' : ''}>»</button>`;
html += `<span class="page-info" style="margin-left:12px;">총 ${totalItems}건</span>`;
html += `<span class="page-info" style="margin-left:12px;">ì´?${totalItems}ê±?/span>`;
pagination.innerHTML = html;
}
@ -1106,27 +1127,27 @@
<div class="drug-detail-name">${escapeHtml(item.drug_name)}</div>
<div class="drug-detail-info">
<div class="drug-detail-item">
<span class="drug-detail-label">약품코드</span>
<span class="drug-detail-label">?½í’ˆì½”드</span>
<span class="drug-detail-value">${item.drug_code}</span>
</div>
<div class="drug-detail-item">
<span class="drug-detail-label">현재고</span>
<span class="drug-detail-label">?„재ê³?/span>
<span class="drug-detail-value">${item.current_stock || 0}</span>
</div>
<div class="drug-detail-item">
<span class="drug-detail-label">단가</span>
<span class="drug-detail-label">?¨ê?</span>
<span class="drug-detail-value">${formatPrice(item.unit_price)}</span>
</div>
<div class="drug-detail-item">
<span class="drug-detail-label">회수가능금액</span>
<span class="drug-detail-value" style="color:var(--accent-gold)">${hasAmount ? '₩' + Math.round(item.recoverable_amount).toLocaleString() : '-'}</span>
<span class="drug-detail-label">?Œìˆ˜ê°€?¥ê¸ˆ??/span>
<span class="drug-detail-value" style="color:var(--accent-gold)">${hasAmount ? '?? + Math.round(item.recoverable_amount).toLocaleString() : '-'}</span>
</div>
<div class="drug-detail-item">
<span class="drug-detail-label">마지막 처방</span>
<span class="drug-detail-label">마ì?ë§?처방</span>
<span class="drug-detail-value">${formatDate(item.last_prescription_date) || '-'}</span>
</div>
<div class="drug-detail-item">
<span class="drug-detail-label">미사용 기간</span>
<span class="drug-detail-label">미사??기간</span>
<span class="drug-detail-value">${item.months_since_use ? item.months_since_use + '개월' : '-'}</span>
</div>
</div>
@ -1149,7 +1170,7 @@
const reason = document.getElementById('modalReason').value;
if (status === 'keep' && !reason.trim()) {
showToast('보류 상태에서는 사유 입력이 필수입니다', 'error');
showToast('보류 ?<3F>태?<3F>서???¬ìœ  ?…ë ¥???„수?…니??, 'error');
return;
}
@ -1163,14 +1184,14 @@
const data = await res.json();
if (data.success) {
showToast('상태가 변경되었습니다', 'success');
showToast('?<3F>태가 변경ë<C2BD>˜?ˆìе?ˆë‹¤', 'success');
closeModal();
loadData();
} else {
showToast('변경 실패: ' + data.error, 'error');
showToast('ë³€ê²??¤íŒ¨: ' + data.error, 'error');
}
} catch (err) {
showToast('서버 오류', 'error');
showToast('?œë²„ ?¤ë¥˜', 'error');
}
}
@ -1191,12 +1212,12 @@
function getStatusLabel(status) {
const labels = {
'pending': '미결정',
'reviewed': '검토됨',
'returned': '반품완료',
'pending': '미결??,
'reviewed': 'ê²€? ë<C2A0>¨',
'returned': '반품?„료',
'keep': '보류',
'disposed': '폐기',
'resolved': '재고소진'
'disposed': '?<3F>기',
'resolved': '?¬ê³ ?Œì§„'
};
return labels[status] || status;
}
@ -1208,14 +1229,14 @@
setTimeout(() => { toast.classList.remove('show'); }, 3000);
}
// ══════════════════ 입고이력 모달 ══════════════════
// ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2> ?…ê³ ?´ë ¥ 모달 ?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>?<3F>â•<C3A2>
async function openPurchaseModal(drugCode, drugName) {
const modal = document.getElementById('purchaseModal');
const nameEl = document.getElementById('purchaseDrugName');
const tbody = document.getElementById('purchaseHistoryBody');
nameEl.textContent = drugName || drugCode;
tbody.innerHTML = '<tr><td colspan="4" class="purchase-empty"><div class="icon"></div><p>입고이력 조회 중...</p></td></tr>';
tbody.innerHTML = '<tr><td colspan="4" class="purchase-empty"><div class="icon">??/div><p>?…ê³ ?´ë ¥ 조회 ì¤?..</p></td></tr>';
modal.classList.add('open');
try {
@ -1224,13 +1245,13 @@
if (data.success) {
if (data.history.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="purchase-empty"><div class="icon">📭</div><p>입고 이력이 없습니다</p></td></tr>';
tbody.innerHTML = '<tr><td colspan="4" class="purchase-empty"><div class="icon">?“­</div><p>?…ê³  ?´ë ¥???†ìе?ˆë‹¤</p></td></tr>';
} else {
tbody.innerHTML = data.history.map(h => `
<tr>
<td>
<div class="supplier-name">${escapeHtml(h.supplier)}</div>
${h.supplier_tel ? `<div class="supplier-tel" onclick="event.stopPropagation();copyToClipboard('${h.supplier_tel}')" title="클릭하여 복사">📞 ${h.supplier_tel}</div>` : ''}
${h.supplier_tel ? `<div class="supplier-tel" onclick="event.stopPropagation();copyToClipboard('${h.supplier_tel}')" title="?´ë¦­?˜ì—¬ 복사">?“ž ${h.supplier_tel}</div>` : ''}
</td>
<td class="purchase-date">${h.date}</td>
<td class="purchase-qty">${h.quantity.toLocaleString()}</td>
@ -1239,10 +1260,10 @@
`).join('');
}
} else {
tbody.innerHTML = `<tr><td colspan="4" class="purchase-empty"><div class="icon">⚠️</div><p>조회 실패: ${data.error}</p></td></tr>`;
tbody.innerHTML = `<tr><td colspan="4" class="purchase-empty"><div class="icon">? ï¸<C3AF></div><p>조회 ?¤íŒ¨: ${data.error}</p></td></tr>`;
}
} catch (err) {
tbody.innerHTML = `<tr><td colspan="4" class="purchase-empty"><div class="icon"></div><p>오류: ${err.message}</p></td></tr>`;
tbody.innerHTML = `<tr><td colspan="4" class="purchase-empty"><div class="icon">??/div><p>?¤ë¥˜: ${err.message}</p></td></tr>`;
}
}
@ -1252,7 +1273,7 @@
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showToast(`📋 ${text} 복사됨`, 'success');
showToast(`?“‹ ${text} 복사??, 'success');
}).catch(() => {
const input = document.createElement('input');
input.value = text;
@ -1260,11 +1281,11 @@
input.select();
document.execCommand('copy');
document.body.removeChild(input);
showToast(`📋 ${text} 복사됨`, 'success');
showToast(`?“‹ ${text} 복사??, 'success');
});
}
// 모달 외부 클릭 시 닫기
// 모달 ?¸ë? ?´ë¦­ ???«ê¸°
document.getElementById('purchaseModal').addEventListener('click', function(e) {
if (e.target === this) closePurchaseModal();
});