pharmacy-pos-qr-system/backend/templates/pmr.html
thug0bin 1b33f82fd4 feat: PAAI (Pharmacist Assistant AI) 기능 구현
- PAAI 로그 테이블 스키마 (paai_logs_schema.sql)
- PAAI 로거 모듈 (db/paai_logger.py)
- /pmr/api/paai/analyze API 엔드포인트
- KIMS API 연동 (KD코드 기반 상호작용 조회)
- Clawdbot AI 연동 (HTTP API)
- PMR 화면 PAAI 버튼 및 모달
- Admin 페이지 (/admin/paai)
- 피드백 수집 기능
2026-03-05 00:36:51 +09:00

1711 lines
69 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>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'Malgun Gothic', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
/* 헤더 */
.header {
background: rgba(255,255,255,0.95);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 1.5rem;
color: #4c1d95;
}
.header h1 span { font-size: 0.9rem; color: #6b7280; margin-left: 10px; }
/* 날짜 선택 & 통계 */
.controls {
display: flex;
align-items: center;
gap: 15px;
}
.date-picker {
padding: 8px 15px;
border: 2px solid #8b5cf6;
border-radius: 8px;
font-size: 1rem;
color: #4c1d95;
cursor: pointer;
}
.stats-box {
display: flex;
gap: 15px;
}
.stat-item {
background: #f3e8ff;
padding: 8px 15px;
border-radius: 8px;
text-align: center;
}
.stat-item .num { font-size: 1.3rem; font-weight: bold; color: #7c3aed; }
.stat-item .label { font-size: 0.75rem; color: #6b7280; }
/* 메인 컨테이너 */
.main-container {
display: flex;
height: calc(100vh - 80px);
padding: 20px;
gap: 20px;
}
/* 왼쪽: 환자 목록 */
.patient-list {
width: 380px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
overflow: hidden;
}
.patient-list-header {
background: #4c1d95;
color: #fff;
padding: 15px 20px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.patient-list-header .count {
background: rgba(255,255,255,0.2);
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
}
.patient-items {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.patient-card {
background: #f8fafc;
border: 2px solid transparent;
border-radius: 10px;
padding: 12px 15px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.patient-card:hover { background: #ede9fe; border-color: #c4b5fd; }
.patient-card.active { background: #ddd6fe; border-color: #8b5cf6; }
.patient-card .top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.patient-card .name { font-size: 1.1rem; font-weight: 600; color: #1e1b4b; }
.patient-card .order {
background: #8b5cf6;
color: #fff;
padding: 2px 10px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.patient-card .info { font-size: 0.85rem; color: #64748b; }
.patient-card .hospital { font-size: 0.8rem; color: #8b5cf6; margin-top: 4px; }
/* 오른쪽: 처방 상세 */
.prescription-detail {
flex: 1;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
overflow: hidden;
}
.detail-header {
background: linear-gradient(135deg, #7c3aed, #a855f7);
color: #fff;
padding: 20px 25px;
}
.detail-header .patient-name { font-size: 1.5rem; font-weight: 700; }
.detail-header .patient-info { font-size: 0.9rem; opacity: 0.9; margin-top: 5px; }
.detail-header .rx-info {
display: flex;
gap: 20px;
margin-top: 12px;
font-size: 0.85rem;
flex-wrap: wrap;
}
.detail-header .rx-info span {
background: rgba(255,255,255,0.2);
padding: 4px 12px;
border-radius: 15px;
}
.detail-header .rx-info .disease-badge {
background: #fef3c7 !important;
color: #92400e !important;
margin-left: 5px;
}
.detail-header .rx-info .otc-badge {
background: #dbeafe !important;
color: #1e40af !important;
cursor: pointer;
transition: all 0.2s;
}
.detail-header .rx-info .otc-badge:hover {
background: #bfdbfe !important;
transform: scale(1.05);
}
/* PAAI 버튼 */
.detail-header .rx-info .paai-badge {
background: linear-gradient(135deg, #10b981, #059669) !important;
color: #fff !important;
cursor: pointer;
transition: all 0.2s;
font-weight: 600;
}
.detail-header .rx-info .paai-badge:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
/* PAAI 모달 */
.paai-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 1100;
justify-content: center;
align-items: center;
padding: 20px;
}
.paai-modal.show { display: flex; }
.paai-modal-content {
background: #fff;
border-radius: 16px;
width: 100%;
max-width: 700px;
max-height: 85vh;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
}
.paai-modal-header {
background: linear-gradient(135deg, #10b981, #059669);
color: #fff;
padding: 20px 25px;
display: flex;
justify-content: space-between;
align-items: center;
}
.paai-modal-header h3 { font-size: 1.3rem; }
.paai-modal-close {
background: rgba(255,255,255,0.2);
border: none;
color: #fff;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
transition: background 0.2s;
}
.paai-modal-close:hover { background: rgba(255,255,255,0.3); }
.paai-modal-body {
padding: 25px;
overflow-y: auto;
flex: 1;
}
.paai-loading {
text-align: center;
padding: 60px 20px;
}
.paai-loading .spinner {
width: 50px;
height: 50px;
border: 4px solid #e5e7eb;
border-top-color: #10b981;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.paai-section {
margin-bottom: 24px;
}
.paai-section-title {
font-size: 1rem;
font-weight: 700;
color: #374151;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.paai-section-content {
background: #f9fafb;
padding: 15px;
border-radius: 10px;
font-size: 0.95rem;
line-height: 1.6;
color: #4b5563;
}
.paai-list {
list-style: none;
padding: 0;
margin: 0;
}
.paai-list li {
padding: 8px 0;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: flex-start;
gap: 10px;
}
.paai-list li:last-child { border-bottom: none; }
.paai-list li::before {
content: '•';
color: #10b981;
font-weight: bold;
}
.paai-caution {
background: #fef3c7 !important;
border-left: 4px solid #f59e0b;
}
.paai-otc-rec {
background: #dbeafe !important;
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 10px;
}
.paai-otc-rec .product { font-weight: 600; color: #1e40af; }
.paai-otc-rec .reason { font-size: 0.9rem; color: #64748b; margin-top: 4px; }
.paai-kims-severe {
background: #fee2e2 !important;
border-left: 4px solid #ef4444;
}
.paai-modal-footer {
padding: 15px 25px;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.paai-feedback {
display: flex;
gap: 10px;
align-items: center;
}
.paai-feedback span { font-size: 0.9rem; color: #6b7280; }
.paai-feedback button {
padding: 8px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.paai-feedback button:hover { border-color: #10b981; }
.paai-feedback button.selected { background: #d1fae5; border-color: #10b981; }
.paai-timing {
font-size: 0.8rem;
color: #9ca3af;
}
/* OTC 모달 */
.otc-modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 1000;
overflow-y: auto;
}
.otc-modal-content {
max-width: 600px;
margin: 40px auto;
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.otc-modal-header {
background: linear-gradient(135deg, #3b82f6, #60a5fa);
color: #fff;
padding: 20px 25px;
display: flex;
justify-content: space-between;
align-items: center;
}
.otc-modal-header h3 { margin: 0; font-size: 1.2rem; }
.otc-modal-close {
background: none;
border: none;
color: #fff;
font-size: 1.5rem;
cursor: pointer;
opacity: 0.8;
}
.otc-modal-close:hover { opacity: 1; }
.otc-summary {
display: flex;
gap: 20px;
padding: 15px 25px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.otc-summary-item {
text-align: center;
}
.otc-summary-item .num {
font-size: 1.5rem;
font-weight: 700;
color: #1e40af;
}
.otc-summary-item .label {
font-size: 0.75rem;
color: #64748b;
}
.otc-frequent {
padding: 15px 25px;
border-bottom: 1px solid #e2e8f0;
}
.otc-frequent h4 {
margin: 0 0 10px 0;
font-size: 0.9rem;
color: #475569;
}
.otc-frequent-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.otc-frequent-item {
background: #eff6ff;
color: #1e40af;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
}
.otc-purchases {
max-height: 300px;
overflow-y: auto;
padding: 15px 25px;
}
.otc-purchase {
border: 1px solid #e2e8f0;
border-radius: 10px;
margin-bottom: 12px;
overflow: hidden;
}
.otc-purchase-header {
background: #f1f5f9;
padding: 10px 15px;
display: flex;
justify-content: space-between;
font-size: 0.85rem;
}
.otc-purchase-header .date { color: #475569; font-weight: 600; }
.otc-purchase-header .amount { color: #1e40af; font-weight: 600; }
.otc-purchase-items {
padding: 10px 15px;
}
.otc-purchase-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 0.85rem;
border-bottom: 1px solid #f1f5f9;
}
.otc-purchase-item:last-child { border-bottom: none; }
.otc-purchase-item .thumb {
width: 40px;
height: 40px;
border-radius: 6px;
object-fit: cover;
background: #f1f5f9;
flex-shrink: 0;
}
.otc-purchase-item .thumb-placeholder {
width: 40px;
height: 40px;
border-radius: 6px;
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
border: 2px dashed #cbd5e1;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.otc-purchase-item .thumb-placeholder svg {
width: 20px;
height: 20px;
fill: #94a3b8;
}
.otc-purchase-item .info {
flex: 1;
min-width: 0;
}
.otc-purchase-item .name { color: #1e293b; font-weight: 500; }
.otc-purchase-item .category { font-size: 0.75rem; color: #64748b; }
.otc-purchase-item .qty { color: #64748b; white-space: nowrap; }
/* 약품 목록 */
.medication-list {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.med-table {
width: 100%;
border-collapse: collapse;
}
.med-table th {
background: #f1f5f9;
padding: 12px 15px;
text-align: left;
font-weight: 600;
color: #475569;
border-bottom: 2px solid #e2e8f0;
position: sticky;
top: 0;
}
.med-table td {
padding: 12px 15px;
border-bottom: 1px solid #e2e8f0;
vertical-align: middle;
}
.med-table tr:hover { background: #f8fafc; }
.med-name { font-weight: 600; color: #1e293b; }
.med-code { font-size: 0.75rem; color: #94a3b8; }
.med-dosage {
background: #dbeafe;
color: #1e40af;
padding: 4px 10px;
border-radius: 6px;
font-weight: 600;
font-size: 0.9rem;
display: inline-block;
}
.med-form {
background: #fef3c7;
color: #92400e;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
}
/* 빈 상태 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #94a3b8;
}
.empty-state .icon { font-size: 4rem; margin-bottom: 15px; }
.empty-state .text { font-size: 1.1rem; }
/* 액션 버튼 */
.action-bar {
background: #f8fafc;
padding: 15px 25px;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
padding: 10px 25px;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary { background: #7c3aed; color: #fff; }
.btn-primary:hover { background: #6d28d9; }
.btn-secondary { background: #e2e8f0; color: #475569; }
.btn-secondary:hover { background: #cbd5e1; }
/* 로딩 */
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top-color: #7c3aed;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* 이전 처방 비교 영역 */
.history-section {
border-top: 3px solid #e2e8f0;
background: #fafafa;
}
.history-header {
background: linear-gradient(135deg, #64748b, #94a3b8);
color: #fff;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.history-header .title { font-weight: 600; }
.history-nav {
display: flex;
align-items: center;
gap: 10px;
}
.history-nav button {
background: rgba(255,255,255,0.2);
border: none;
color: #fff;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s;
}
.history-nav button:hover:not(:disabled) { background: rgba(255,255,255,0.4); }
.history-nav button:disabled { opacity: 0.4; cursor: not-allowed; }
.history-nav .index { font-size: 0.9rem; }
.history-info {
padding: 10px 20px;
background: #f1f5f9;
font-size: 0.85rem;
color: #64748b;
display: flex;
gap: 15px;
}
.history-meds {
max-height: 200px;
overflow-y: auto;
padding: 10px 20px;
}
.history-meds table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.history-meds th {
background: #e2e8f0;
padding: 8px 10px;
text-align: left;
font-weight: 600;
color: #475569;
}
.history-meds td {
padding: 8px 10px;
border-bottom: 1px solid #e2e8f0;
}
.history-meds .med-name { font-weight: 500; }
.history-empty {
padding: 30px;
text-align: center;
color: #94a3b8;
}
/* 처방 비교 상태 */
.med-status {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
margin-left: 8px;
}
.status-added { background: #dcfce7; color: #166534; }
.status-removed { background: #fee2e2; color: #991b1b; }
.status-changed { background: #fef3c7; color: #92400e; }
.status-same { background: #f1f5f9; color: #64748b; }
tr.row-added { background: #f0fdf4 !important; }
tr.row-removed { background: #fef2f2 !important; opacity: 0.7; }
tr.row-changed { background: #fffbeb !important; }
.change-arrow {
color: #94a3b8;
margin: 0 4px;
}
.change-from {
text-decoration: line-through;
color: #94a3b8;
}
.change-to {
background: #fbbf24;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}
/* 비교 모드 토글 */
.compare-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 15px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
font-size: 0.85rem;
}
.compare-toggle input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.compare-legend {
display: flex;
gap: 12px;
margin-left: auto;
font-size: 0.75rem;
}
.compare-legend span {
display: flex;
align-items: center;
gap: 4px;
}
</style>
</head>
<body>
<!-- 헤더 -->
<header class="header">
<h1>💊 조제관리 <span>청춘라벨 v2</span></h1>
<div class="controls">
<input type="date" id="dateSelect" class="date-picker">
<div class="stats-box">
<div class="stat-item">
<div class="num" id="statPrescriptions">-</div>
<div class="label">처방</div>
</div>
<div class="stat-item">
<div class="num" id="statPatients">-</div>
<div class="label">환자</div>
</div>
<div class="stat-item">
<div class="num" id="statMedications">-</div>
<div class="label">금액</div>
</div>
</div>
</div>
</header>
<!-- 메인 -->
<div class="main-container">
<!-- 왼쪽: 환자 목록 -->
<div class="patient-list">
<div class="patient-list-header">
<span>📋 환자 목록</span>
<span class="count" id="patientCount">0명</span>
</div>
<div class="patient-items" id="patientItems">
<div class="loading"><div class="spinner"></div></div>
</div>
</div>
<!-- 오른쪽: 처방 상세 -->
<div class="prescription-detail">
<div class="detail-header" id="detailHeader" style="display:none;">
<div class="patient-name" id="detailName">-</div>
<div class="patient-info" id="detailInfo">-</div>
<div class="rx-info" id="rxInfo"></div>
</div>
<div class="medication-list" id="medicationList">
<div class="empty-state">
<div class="icon">👈</div>
<div class="text">환자를 선택하세요</div>
</div>
</div>
<div class="compare-toggle" id="compareToggle" style="display:none;">
<input type="checkbox" id="compareMode" onchange="toggleCompareMode()">
<label for="compareMode">이전 처방과 비교</label>
<div class="compare-legend" id="compareLegend" style="display:none;">
<span><span class="med-status status-added">🆕 추가</span></span>
<span><span class="med-status status-changed">🔄 변경</span></span>
<span><span class="med-status status-removed">❌ 중단</span></span>
<span><span class="med-status status-same">✓ 동일</span></span>
</div>
</div>
<div class="action-bar" id="actionBar" style="display:none;">
<button class="btn btn-secondary" onclick="selectAll()">전체 선택</button>
<button class="btn btn-secondary" onclick="previewLabels()" style="background:#3b82f6;color:#fff;">👁️ 미리보기</button>
<button class="btn btn-primary" onclick="printLabels()">🖨️ 라벨 인쇄</button>
</div>
<!-- PAAI 분석 모달 -->
<div class="paai-modal" id="paaiModal">
<div class="paai-modal-content">
<div class="paai-modal-header">
<h3>🤖 PAAI 분석 결과</h3>
<button class="paai-modal-close" onclick="closePaaiModal()">×</button>
</div>
<div class="paai-modal-body" id="paaiBody">
<div class="paai-loading">
<div class="spinner"></div>
<div>AI 분석 중...</div>
<div style="font-size:0.85rem;color:#9ca3af;margin-top:10px;">KIMS 상호작용 확인 + AI 분석</div>
</div>
</div>
<div class="paai-modal-footer" id="paaiFooter" style="display:none;">
<div class="paai-feedback">
<span>도움이 되셨나요?</span>
<button onclick="sendPaaiFeedback(true)" id="paaiUseful">👍 유용해요</button>
<button onclick="sendPaaiFeedback(false)" id="paaiNotUseful">👎 아니요</button>
</div>
<div class="paai-timing" id="paaiTiming"></div>
</div>
</div>
</div>
<!-- OTC 구매 이력 모달 -->
<div class="otc-modal" id="otcModal">
<div class="otc-modal-content">
<div class="otc-modal-header">
<h3>💊 OTC 구매 이력</h3>
<button class="otc-modal-close" onclick="closeOtcModal()">×</button>
</div>
<div class="otc-summary" id="otcSummary"></div>
<div class="otc-frequent" id="otcFrequent"></div>
<div class="otc-purchases" id="otcPurchases"></div>
</div>
</div>
<!-- 미리보기 모달 -->
<div id="previewModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:1000;overflow-y:auto;">
<div style="max-width:400px;margin:50px auto;background:#fff;border-radius:12px;padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h3 style="margin:0;color:#4c1d95;">🏷️ 라벨 미리보기</h3>
<button onclick="closePreview()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;">×</button>
</div>
<div id="previewContent" style="display:flex;flex-direction:column;gap:15px;align-items:center;">
</div>
</div>
</div>
<!-- 이전 처방 비교 -->
<div class="history-section" id="historySection" style="display:none;">
<div class="history-header">
<span class="title">📜 이전 처방</span>
<div class="history-nav">
<button onclick="prevHistory()" id="historyPrev" disabled></button>
<span class="index" id="historyIndex">-</span>
<button onclick="nextHistory()" id="historyNext" disabled></button>
</div>
</div>
<div class="history-info" id="historyInfo"></div>
<div class="history-meds" id="historyMeds">
<div class="history-empty">이전 처방 없음</div>
</div>
</div>
</div>
</div>
<script>
let currentPrescriptionId = null;
let currentPatientCode = null;
let currentMedications = [];
let historyData = [];
let historyIndex = 0;
let compareMode = false;
let otcData = null;
// HTML 이스케이프
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 초기화
document.addEventListener('DOMContentLoaded', () => {
const today = new Date().toISOString().split('T')[0];
document.getElementById('dateSelect').value = today;
loadPatients(today);
loadStats(today);
});
// 날짜 변경
document.getElementById('dateSelect').addEventListener('change', (e) => {
loadPatients(e.target.value);
loadStats(e.target.value);
clearDetail();
});
// 처방전 목록 로드
async function loadPatients(date) {
const container = document.getElementById('patientItems');
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
try {
const res = await fetch(`/pmr/api/prescriptions?date=${date}`);
const data = await res.json();
if (data.success && data.prescriptions.length > 0) {
container.innerHTML = data.prescriptions.map(p => `
<div class="patient-card" onclick="selectPatient('${p.prescription_id}', this)">
<div class="top">
<span class="name">${p.patient_name || '이름없음'}</span>
<span class="order">${p.order_number || '-'}</span>
</div>
<div class="info">
${p.age ? p.age + '세' : ''} ${p.gender || ''}
${p.time ? '• ' + p.time : ''}
</div>
<div class="hospital">${p.hospital || ''} ${p.doctor ? '(' + p.doctor + ')' : ''}</div>
</div>
`).join('');
document.getElementById('patientCount').textContent = data.count + '명';
} else {
container.innerHTML = `
<div class="empty-state">
<div class="icon">📭</div>
<div class="text">해당 날짜에 처방이 없습니다</div>
</div>
`;
document.getElementById('patientCount').textContent = '0명';
}
} catch (err) {
container.innerHTML = `<div class="empty-state"><div class="text">오류: ${err.message}</div></div>`;
}
}
// 통계 로드
async function loadStats(date) {
try {
const res = await fetch(`/pmr/api/stats?date=${date}`);
const data = await res.json();
if (data.success) {
document.getElementById('statPatients').textContent = '-';
document.getElementById('statPrescriptions').textContent = data.stats.total_prescriptions;
document.getElementById('statMedications').textContent = Math.round(data.stats.total_amount / 10000) + '만';
}
} catch (err) {
console.error('Stats error:', err);
}
}
// 환자 선택
async function selectPatient(prescriptionId, element) {
// UI 활성화
document.querySelectorAll('.patient-card').forEach(c => c.classList.remove('active'));
element.classList.add('active');
currentPrescriptionId = prescriptionId;
const medList = document.getElementById('medicationList');
medList.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
try {
const res = await fetch(`/pmr/api/prescription/${prescriptionId}`);
const data = await res.json();
if (data.success) {
// 헤더 업데이트
document.getElementById('detailHeader').style.display = 'block';
document.getElementById('detailName').textContent = data.patient.name || '이름없음';
document.getElementById('detailInfo').textContent =
`${data.patient.age || '-'}세 / ${data.patient.gender || '-'} / ${data.patient.birthdate || '-'}`;
// 질병 정보 표시 (각각 별도 뱃지)
let diseaseHtml = '';
if (data.disease_info) {
const d = data.disease_info;
if (d.name_1) {
diseaseHtml += `<span class="disease-badge">🩺 ${d.name_1}</span>`;
}
if (d.name_2) {
diseaseHtml += `<span class="disease-badge">🩺 ${d.name_2}</span>`;
}
}
document.getElementById('rxInfo').innerHTML = `
<span>🏥 ${data.prescription.hospital || '-'}</span>
<span>👨‍⚕️ ${data.prescription.doctor || '-'}</span>
<span>📅 ${data.prescription.date}</span>
<span>💊 ${data.medication_count}종</span>
${diseaseHtml}
`;
// 약품 테이블
if (data.medications.length > 0) {
medList.innerHTML = `
<table class="med-table">
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" id="checkAll" onchange="toggleAll(this)"></th>
<th>약품명</th>
<th>제형</th>
<th>용량</th>
<th>횟수</th>
<th>일수</th>
</tr>
</thead>
<tbody>
${data.medications.map(m => `
<tr data-add-info="${escapeHtml(m.add_info || '')}">
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}" ${m.is_auto_print ? 'checked' : ''}></td>
<td>
<div class="med-name">${m.med_name || m.medication_code}</div>
<div class="med-code">${m.medication_code}</div>
${m.add_info ? `<div style="font-size:0.75rem;color:#6b7280;">${escapeHtml(m.add_info)}</div>` : ''}
</td>
<td>${m.formulation ? `<span class="med-form">${m.formulation}</span>` : '-'}</td>
<td><span class="med-dosage">${m.dosage || '-'}</span></td>
<td>${m.frequency || '-'}회</td>
<td>${m.duration || '-'}일</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} else {
medList.innerHTML = '<div class="empty-state"><div class="text">처방 약품이 없습니다</div></div>';
}
document.getElementById('actionBar').style.display = 'flex';
// 현재 약품 저장
currentMedications = data.medications;
// 이전 처방 로드
currentPatientCode = data.patient.code;
if (currentPatientCode) {
loadPatientHistory(currentPatientCode, prescriptionId);
checkOtcHistory(currentPatientCode);
}
}
} catch (err) {
medList.innerHTML = `<div class="empty-state"><div class="text">오류: ${err.message}</div></div>`;
}
}
// 환자 이전 처방 로드
async function loadPatientHistory(cusCode, excludeSerial) {
const section = document.getElementById('historySection');
const medsDiv = document.getElementById('historyMeds');
try {
const res = await fetch(`/pmr/api/patient/${cusCode}/history?limit=10&exclude=${excludeSerial}`);
const data = await res.json();
if (data.success && data.history.length > 0) {
historyData = data.history;
historyIndex = 0;
section.style.display = 'block';
document.getElementById('compareToggle').style.display = 'flex';
renderHistory();
} else {
section.style.display = 'none';
document.getElementById('compareToggle').style.display = 'none';
historyData = [];
}
} catch (err) {
console.error('History error:', err);
section.style.display = 'none';
}
}
// 이전 처방 렌더링
function renderHistory() {
if (historyData.length === 0) return;
const h = historyData[historyIndex];
// 인덱스 표시
document.getElementById('historyIndex').textContent =
`${historyIndex + 1} / ${historyData.length}`;
// 네비게이션 버튼
document.getElementById('historyPrev').disabled = historyIndex <= 0;
document.getElementById('historyNext').disabled = historyIndex >= historyData.length - 1;
// 정보
document.getElementById('historyInfo').innerHTML = `
<span>📅 ${h.date}</span>
<span>🏥 ${h.hospital || '-'}</span>
<span>👨‍⚕️ ${h.doctor || '-'}</span>
<span>💊 ${h.medication_count}종</span>
<span>💰 ${(h.copayment || 0).toLocaleString()}원</span>
`;
// 약품 테이블
if (h.medications && h.medications.length > 0) {
document.getElementById('historyMeds').innerHTML = `
<table>
<thead>
<tr>
<th>약품명</th>
<th>용량</th>
<th>횟수</th>
<th>일수</th>
</tr>
</thead>
<tbody>
${h.medications.map(m => `
<tr>
<td>
<div class="med-name">${m.med_name || m.medication_code}</div>
${m.add_info ? `<div style="font-size:0.7rem;color:#94a3b8;">${escapeHtml(m.add_info)}</div>` : ''}
</td>
<td>${m.dosage || '-'}</td>
<td>${m.frequency || '-'}회</td>
<td>${m.duration || '-'}일</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} else {
document.getElementById('historyMeds').innerHTML = '<div class="history-empty">약품 정보 없음</div>';
}
}
// 이전 처방 네비게이션
function prevHistory() {
if (historyIndex > 0) {
historyIndex--;
renderHistory();
if (compareMode) applyCompareMode();
}
}
function nextHistory() {
if (historyIndex < historyData.length - 1) {
historyIndex++;
renderHistory();
if (compareMode) applyCompareMode();
}
}
// 비교 모드 토글
function toggleCompareMode() {
compareMode = document.getElementById('compareMode').checked;
document.getElementById('compareLegend').style.display = compareMode ? 'flex' : 'none';
if (compareMode) {
applyCompareMode();
} else {
// 비교 모드 해제 - 테이블 다시 렌더링
rerenderMedicationTable();
}
}
// 처방 비교 로직
function comparePrescriptions(current, previous) {
const result = [];
const prevMap = new Map(previous.map(m => [m.medication_code, m]));
const currCodes = new Set(current.map(m => m.medication_code));
// 현재 처방 약품 처리
for (const curr of current) {
const prev = prevMap.get(curr.medication_code);
if (!prev) {
// 추가된 약
result.push({ ...curr, status: 'added' });
} else {
// 비교
const changes = [];
if (parseFloat(curr.dosage) !== parseFloat(prev.dosage)) {
changes.push({ field: 'dosage', from: prev.dosage, to: curr.dosage });
}
if (parseInt(curr.frequency) !== parseInt(prev.frequency)) {
changes.push({ field: 'frequency', from: prev.frequency, to: curr.frequency });
}
if (parseInt(curr.duration) !== parseInt(prev.duration)) {
changes.push({ field: 'duration', from: prev.duration, to: curr.duration });
}
if (changes.length > 0) {
result.push({ ...curr, status: 'changed', changes });
} else {
result.push({ ...curr, status: 'same' });
}
}
}
// 중단된 약 (이전에 있었는데 현재 없음)
for (const prev of previous) {
if (!currCodes.has(prev.medication_code)) {
result.push({ ...prev, status: 'removed' });
}
}
return result;
}
// 비교 모드 적용
function applyCompareMode() {
if (historyData.length === 0 || !currentMedications.length) return;
const prevMeds = historyData[historyIndex].medications || [];
const compared = comparePrescriptions(currentMedications, prevMeds);
// 테이블 다시 렌더링
renderComparedTable(compared);
}
// 비교 결과 테이블 렌더링
function renderComparedTable(compared) {
const medList = document.getElementById('medicationList');
// 상태별 정렬: 추가 > 변경 > 동일 > 중단
const order = { added: 0, changed: 1, same: 2, removed: 3 };
compared.sort((a, b) => order[a.status] - order[b.status]);
medList.innerHTML = `
<table class="med-table">
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" id="checkAll" onchange="toggleAll(this)"></th>
<th>약품명</th>
<th>상태</th>
<th>용량</th>
<th>횟수</th>
<th>일수</th>
</tr>
</thead>
<tbody>
${compared.map(m => {
const rowClass = 'row-' + m.status;
const statusLabel = {
added: '<span class="med-status status-added">🆕 추가</span>',
removed: '<span class="med-status status-removed">❌ 중단</span>',
changed: '<span class="med-status status-changed">🔄 변경</span>',
same: '<span class="med-status status-same">✓ 동일</span>'
}[m.status];
// 변경된 필드 찾기
const getChangeValue = (field, value) => {
if (m.status !== 'changed' || !m.changes) return value || '-';
const change = m.changes.find(c => c.field === field);
if (change) {
return `<span class="change-from">${change.from || '-'}</span>` +
`<span class="change-arrow">→</span>` +
`<span class="change-to">${change.to || '-'}</span>`;
}
return value || '-';
};
const disabled = m.status === 'removed' ? 'disabled' : '';
return `
<tr class="${rowClass}" data-add-info="${escapeHtml(m.add_info || '')}">
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}" ${disabled}></td>
<td>
<div class="med-name">${m.med_name || m.medication_code}</div>
<div class="med-code">${m.medication_code}</div>
${m.add_info ? `<div style="font-size:0.75rem;color:#6b7280;">${escapeHtml(m.add_info)}</div>` : ''}
</td>
<td>${statusLabel}</td>
<td>${m.status === 'changed' ? getChangeValue('dosage', m.dosage) : (m.dosage || '-')}</td>
<td>${m.status === 'changed' ? getChangeValue('frequency', m.frequency) : (m.frequency || '-')}회</td>
<td>${m.status === 'changed' ? getChangeValue('duration', m.duration) : (m.duration || '-')}일</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
// 일반 테이블로 복원
function rerenderMedicationTable() {
if (!currentMedications.length) return;
const medList = document.getElementById('medicationList');
medList.innerHTML = `
<table class="med-table">
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" id="checkAll" onchange="toggleAll(this)"></th>
<th>약품명</th>
<th>제형</th>
<th>용량</th>
<th>횟수</th>
<th>일수</th>
</tr>
</thead>
<tbody>
${currentMedications.map(m => `
<tr data-add-info="${escapeHtml(m.add_info || '')}">
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}"></td>
<td>
<div class="med-name">${m.med_name || m.medication_code}</div>
<div class="med-code">${m.medication_code}</div>
${m.add_info ? `<div style="font-size:0.75rem;color:#6b7280;">${escapeHtml(m.add_info)}</div>` : ''}
</td>
<td>${m.formulation ? `<span class="med-form">${m.formulation}</span>` : '-'}</td>
<td><span class="med-dosage">${m.dosage || '-'}</span></td>
<td>${m.frequency || '-'}회</td>
<td>${m.duration || '-'}일</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// OTC 구매 이력 체크
async function checkOtcHistory(cusCode) {
try {
const res = await fetch(`/pmr/api/patient/${cusCode}/otc?limit=20`);
const data = await res.json();
if (data.success && data.count > 0) {
otcData = data;
// OTC 뱃지 추가 (질병 뱃지 앞에)
const rxInfo = document.getElementById('rxInfo');
const otcBadge = `<span class="otc-badge" onclick="showOtcModal()">💊 OTC ${data.count}</span>`;
rxInfo.innerHTML = otcBadge + rxInfo.innerHTML;
} else {
otcData = null;
}
// PAAI 버튼 추가 (항상 표시)
addPaaiButton();
} catch (err) {
console.error('OTC check error:', err);
otcData = null;
// OTC 오류여도 PAAI 버튼은 추가
addPaaiButton();
}
}
// OTC 모달 표시
function showOtcModal() {
if (!otcData) return;
const modal = document.getElementById('otcModal');
const summary = document.getElementById('otcSummary');
const frequent = document.getElementById('otcFrequent');
const purchases = document.getElementById('otcPurchases');
// 요약
summary.innerHTML = `
<div class="otc-summary-item">
<div class="num">${otcData.summary.total_visits}</div>
<div class="label">방문</div>
</div>
<div class="otc-summary-item">
<div class="num">${(otcData.summary.total_amount / 10000).toFixed(1)}만</div>
<div class="label">총 구매액</div>
</div>
`;
// 자주 구매하는 품목
if (otcData.summary.frequent_items && otcData.summary.frequent_items.length > 0) {
frequent.innerHTML = `
<h4>🔥 자주 구매하는 품목</h4>
<div class="otc-frequent-list">
${otcData.summary.frequent_items.map(item =>
`<span class="otc-frequent-item">${item.name} (${item.count}회)</span>`
).join('')}
</div>
`;
frequent.style.display = 'block';
} else {
frequent.style.display = 'none';
}
// 구매 이력
purchases.innerHTML = otcData.purchases.map(p => `
<div class="otc-purchase">
<div class="otc-purchase-header">
<span class="date">📅 ${p.date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3') || p.datetime}</span>
<span class="amount">${p.amount.toLocaleString()}원</span>
</div>
<div class="otc-purchase-items">
${p.items.map(item => `
<div class="otc-purchase-item">
${item.image
? `<img class="thumb" src="data:image/jpeg;base64,${item.image}" alt="">`
: `<div class="thumb-placeholder"><svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-7l-3 3.72L9 13l-3 4h12l-4-5z"/></svg></div>`
}
<div class="info">
<div class="name">${item.name}</div>
${item.category ? `<div class="category">${item.category}</div>` : ''}
</div>
<span class="qty">${item.quantity}개 / ${item.price.toLocaleString()}원</span>
</div>
`).join('')}
</div>
</div>
`).join('');
modal.style.display = 'block';
}
// OTC 모달 닫기
function closeOtcModal() {
document.getElementById('otcModal').style.display = 'none';
}
// ─────────────────────────────────────────────────────────────
// PAAI (Pharmacist Assistant AI) 함수들
// ─────────────────────────────────────────────────────────────
let currentPaaiLogId = null;
function addPaaiButton() {
const rxInfo = document.getElementById('rxInfo');
if (!rxInfo || rxInfo.querySelector('.paai-badge')) return;
const paaiBtn = document.createElement('span');
paaiBtn.className = 'paai-badge';
paaiBtn.textContent = '🤖 PAAI 분석';
paaiBtn.onclick = showPaaiModal;
rxInfo.appendChild(paaiBtn);
}
async function showPaaiModal() {
if (!currentPrescription) return;
const modal = document.getElementById('paaiModal');
const body = document.getElementById('paaiBody');
const footer = document.getElementById('paaiFooter');
// 초기화
body.innerHTML = `
<div class="paai-loading">
<div class="spinner"></div>
<div>AI 분석 중...</div>
<div style="font-size:0.85rem;color:#9ca3af;margin-top:10px;">KIMS 상호작용 확인 + AI 분석</div>
</div>
`;
footer.style.display = 'none';
modal.classList.add('show');
try {
// 요청 데이터 구성
const requestData = {
pre_serial: currentPrescription.pre_serial,
cus_code: currentPrescription.cus_code,
patient_name: currentPrescription.name,
disease_info: {
code_1: currentPrescription.st1 || '',
name_1: currentPrescription.st1_name || '',
code_2: currentPrescription.st2 || '',
name_2: currentPrescription.st2_name || ''
},
current_medications: (currentPrescription.medications || []).map(med => ({
code: med.medication_code,
name: med.med_name,
dosage: med.dosage,
frequency: med.frequency,
days: med.days
})),
previous_serial: currentPrescription.previous_serial || '',
previous_medications: (currentPrescription.previous_medications || []).map(med => ({
code: med.medication_code,
name: med.med_name,
dosage: med.dosage,
frequency: med.frequency,
days: med.days
})),
otc_history: otcData ? {
visit_count: otcData.summary?.total_visits || 0,
frequent_items: otcData.summary?.frequent_items || [],
purchases: otcData.purchases || []
} : {}
};
const response = await fetch('/pmr/api/paai/analyze', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(requestData)
});
const result = await response.json();
if (result.success) {
currentPaaiLogId = result.log_id;
displayPaaiResult(result);
} else {
throw new Error(result.error || '분석 실패');
}
} catch (err) {
console.error('PAAI error:', err);
body.innerHTML = `
<div style="text-align:center;padding:40px;color:#ef4444;">
<div style="font-size:2rem;margin-bottom:15px;">⚠️</div>
<div>분석 중 오류가 발생했습니다</div>
<div style="font-size:0.85rem;color:#9ca3af;margin-top:10px;">${err.message}</div>
</div>
`;
}
}
function displayPaaiResult(result) {
const body = document.getElementById('paaiBody');
const footer = document.getElementById('paaiFooter');
const timing = document.getElementById('paaiTiming');
const analysis = result.analysis || {};
const kims = result.kims_summary || {};
let html = '';
// KIMS 상호작용 요약
if (kims.interaction_count > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">⚠️ KIMS 상호작용 (${kims.interaction_count}건)</div>
<div class="paai-section-content ${kims.has_severe ? 'paai-kims-severe' : 'paai-caution'}">
${analysis.kims_analysis || 'KIMS 상호작용이 감지되었습니다. 상세 내용을 확인하세요.'}
</div>
</div>
`;
}
// 처방 분석
if (analysis.prescription_insight) {
html += `
<div class="paai-section">
<div class="paai-section-title">📋 처방 분석</div>
<div class="paai-section-content">${analysis.prescription_insight}</div>
</div>
`;
}
// 복용 주의사항
if (analysis.cautions && analysis.cautions.length > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">⚡ 복용 주의사항</div>
<div class="paai-section-content paai-caution">
<ul class="paai-list">
${analysis.cautions.map(c => `<li>${c}</li>`).join('')}
</ul>
</div>
</div>
`;
}
// OTC 추천
if (analysis.otc_recommendations && analysis.otc_recommendations.length > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">💊 OTC 추천</div>
<div>
${analysis.otc_recommendations.map(rec => `
<div class="paai-otc-rec">
<div class="product">${rec.product}</div>
<div class="reason">${rec.reason}</div>
</div>
`).join('')}
</div>
</div>
`;
}
// 상담 포인트
if (analysis.counseling_points && analysis.counseling_points.length > 0) {
html += `
<div class="paai-section">
<div class="paai-section-title">💬 상담 포인트</div>
<div class="paai-section-content">
<ul class="paai-list">
${analysis.counseling_points.map(p => `<li>${p}</li>`).join('')}
</ul>
</div>
</div>
`;
}
// fallback 메시지
if (analysis._fallback) {
html += `
<div style="text-align:center;padding:20px;color:#9ca3af;font-size:0.9rem;">
⚠️ AI 서비스 연결 불가 - KIMS 데이터만 표시됨
</div>
`;
}
body.innerHTML = html || '<div style="text-align:center;padding:40px;color:#9ca3af;">분석 결과가 없습니다.</div>';
// 타이밍 정보
if (result.timing) {
timing.textContent = `KIMS: ${result.timing.kims_ms}ms / AI: ${result.timing.ai_ms}ms / 총: ${result.timing.total_ms}ms`;
}
// 피드백 버튼 초기화
document.getElementById('paaiUseful').classList.remove('selected');
document.getElementById('paaiNotUseful').classList.remove('selected');
footer.style.display = 'flex';
}
function closePaaiModal() {
document.getElementById('paaiModal').classList.remove('show');
}
async function sendPaaiFeedback(useful) {
if (!currentPaaiLogId) return;
try {
await fetch('/pmr/api/paai/feedback', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
log_id: currentPaaiLogId,
useful: useful
})
});
// 버튼 표시 업데이트
document.getElementById('paaiUseful').classList.toggle('selected', useful);
document.getElementById('paaiNotUseful').classList.toggle('selected', !useful);
} catch (err) {
console.error('Feedback error:', err);
}
}
// ─────────────────────────────────────────────────────────────
// 상세 초기화
function clearDetail() {
document.getElementById('detailHeader').style.display = 'none';
document.getElementById('actionBar').style.display = 'none';
document.getElementById('historySection').style.display = 'none';
document.getElementById('compareToggle').style.display = 'none';
document.getElementById('compareMode').checked = false;
document.getElementById('compareLegend').style.display = 'none';
document.getElementById('otcModal').style.display = 'none';
document.getElementById('medicationList').innerHTML = `
<div class="empty-state">
<div class="icon">👈</div>
<div class="text">환자를 선택하세요</div>
</div>
`;
currentPrescriptionId = null;
currentPatientCode = null;
currentMedications = [];
historyData = [];
historyIndex = 0;
compareMode = false;
otcData = null;
}
// 전체 선택 토글
function toggleAll(checkbox) {
document.querySelectorAll('.med-check').forEach(c => c.checked = checkbox.checked);
}
function selectAll() {
document.querySelectorAll('.med-check').forEach(c => c.checked = true);
const checkAll = document.getElementById('checkAll');
if (checkAll) checkAll.checked = true;
}
// 라벨 미리보기
async function previewLabels() {
const checkboxes = document.querySelectorAll('.med-check:checked');
if (checkboxes.length === 0) {
alert('미리보기할 약품을 선택하세요');
return;
}
const container = document.getElementById('previewContent');
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
document.getElementById('previewModal').style.display = 'block';
// 현재 선택된 환자명
const patientName = document.getElementById('detailName').textContent;
container.innerHTML = '';
for (const checkbox of checkboxes) {
const tr = checkbox.closest('tr');
const cells = tr.querySelectorAll('td');
// 약품명: 두 번째 셀의 .med-name
const medName = tr.querySelector('.med-name')?.textContent?.trim() || '';
const addInfo = tr.dataset.addInfo || '';
// 용량: 네 번째 셀 (index 3)
const dosageText = cells[3]?.textContent?.replace(/[^0-9.]/g, '') || '0';
const dosage = parseFloat(dosageText) || 0;
// 횟수: 다섯 번째 셀 (index 4)
const freqText = cells[4]?.textContent?.replace(/[^0-9]/g, '') || '0';
const frequency = parseInt(freqText) || 0;
// 일수: 여섯 번째 셀 (index 5)
const durText = cells[5]?.textContent?.replace(/[^0-9]/g, '') || '0';
const duration = parseInt(durText) || 0;
console.log('Preview data:', { patientName, medName, addInfo, dosage, frequency, duration });
try {
const res = await fetch('/pmr/api/label/preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
patient_name: patientName,
med_name: medName,
add_info: addInfo,
dosage: dosage,
frequency: frequency,
duration: duration,
unit: '정'
})
});
const data = await res.json();
console.log('Preview response:', data.success, data.error);
if (data.success && data.image) {
const img = document.createElement('img');
img.src = data.image;
img.style.cssText = 'max-width:100%;border:1px solid #ddd;border-radius:8px;';
container.appendChild(img);
} else {
console.error('Preview failed:', data.error);
}
} catch (err) {
console.error('Preview error:', err);
}
}
if (container.children.length === 0) {
container.innerHTML = '<p style="color:#999;">미리보기 생성 실패 - 콘솔(F12) 확인</p>';
}
}
function closePreview() {
document.getElementById('previewModal').style.display = 'none';
}
// 라벨 인쇄 (TODO: 구현)
function printLabels() {
const selected = Array.from(document.querySelectorAll('.med-check:checked')).map(c => c.dataset.code);
if (selected.length === 0) {
alert('인쇄할 약품을 선택하세요');
return;
}
alert(`선택된 약품 ${selected.length}개 인쇄 기능은 추후 구현 예정입니다.\n\n${selected.join('\n')}`);
}
</script>
</body>
</html>