309 lines
14 KiB
HTML
309 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Pharmacy Stats API - QT-POS 통계 비교</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { font-family: 'Malgun Gothic', sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
h1 { color: #2196F3; margin-bottom: 20px; }
|
|
.controls {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
input[type="date"] { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; }
|
|
button {
|
|
padding: 8px 16px;
|
|
background: #2196F3;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
}
|
|
button:hover { background: #1976D2; }
|
|
button.secondary { background: #4CAF50; }
|
|
button.secondary:hover { background: #388E3C; }
|
|
|
|
.container { display: flex; gap: 20px; }
|
|
.panel {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
flex: 1;
|
|
min-width: 400px;
|
|
}
|
|
.panel h2 { margin-top: 0; padding-bottom: 10px; border-bottom: 2px solid #eee; }
|
|
.v1 h2 { color: #4CAF50; border-bottom-color: #4CAF50; }
|
|
.v2 h2 { color: #FF9800; border-bottom-color: #FF9800; }
|
|
|
|
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }
|
|
th, td { padding: 8px 10px; text-align: right; border-bottom: 1px solid #eee; }
|
|
th { background: #f8f8f8; font-weight: 600; }
|
|
td:first-child, th:first-child { text-align: left; }
|
|
.total { font-weight: bold; background: #e3f2fd !important; }
|
|
.sub-total { background: #fff8e1; }
|
|
|
|
.tabs { display: flex; gap: 5px; margin-bottom: 15px; }
|
|
.tab {
|
|
padding: 8px 16px;
|
|
background: #eee;
|
|
border: none;
|
|
border-radius: 4px 4px 0 0;
|
|
cursor: pointer;
|
|
}
|
|
.tab.active { background: #2196F3; color: white; }
|
|
|
|
.loading { color: #666; font-style: italic; padding: 20px; text-align: center; }
|
|
.error { color: #f44336; padding: 20px; }
|
|
.diff-positive { color: #f44336; }
|
|
.diff-negative { color: #4CAF50; }
|
|
.diff-zero { color: #888; }
|
|
|
|
.compare-table { margin-top: 20px; }
|
|
.compare-table th { background: #e3f2fd; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🏥 Pharmacy Stats API - QT-POS 통계 비교</h1>
|
|
|
|
<div class="controls">
|
|
<label>기간:</label>
|
|
<input type="date" id="dateFrom">
|
|
<span>~</span>
|
|
<input type="date" id="dateTo">
|
|
<button onclick="loadStats()">조회</button>
|
|
<button class="secondary" onclick="loadCompare()">v1 vs v2 비교</button>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="setMode('total')">전체</button>
|
|
<button class="tab" onclick="setMode('insurance')">보험별</button>
|
|
<button class="tab" onclick="setMode('time')">시간가산별</button>
|
|
<button class="tab" onclick="setMode('payment')">결제수단별</button>
|
|
<button class="tab" onclick="setMode('hospital')">병원별</button>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="panel v1">
|
|
<h2>v1 - PharmIT3000</h2>
|
|
<div id="v1-result" class="loading">조회 버튼을 클릭하세요</div>
|
|
</div>
|
|
<div class="panel v2">
|
|
<h2>v2 - PMPLUS20</h2>
|
|
<div id="v2-result" class="loading">조회 버튼을 클릭하세요</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="compare-result"></div>
|
|
|
|
<script>
|
|
let currentMode = 'total';
|
|
|
|
// 기본 날짜 설정
|
|
const today = new Date();
|
|
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
document.getElementById('dateFrom').value = firstDay.toISOString().split('T')[0];
|
|
document.getElementById('dateTo').value = today.toISOString().split('T')[0];
|
|
|
|
function formatMoney(n) {
|
|
return (n || 0).toLocaleString();
|
|
}
|
|
|
|
function formatDate(d) {
|
|
return d.replace(/-/g, '');
|
|
}
|
|
|
|
function setMode(mode) {
|
|
currentMode = mode;
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
loadStats();
|
|
}
|
|
|
|
async function loadStats() {
|
|
const from = formatDate(document.getElementById('dateFrom').value);
|
|
const to = formatDate(document.getElementById('dateTo').value);
|
|
|
|
document.getElementById('v1-result').innerHTML = '<div class="loading">로딩 중...</div>';
|
|
document.getElementById('v2-result').innerHTML = '<div class="loading">로딩 중...</div>';
|
|
document.getElementById('compare-result').innerHTML = '';
|
|
|
|
const endpoint = currentMode === 'total' ? 'stats' : `stats/${currentMode}`;
|
|
|
|
// v1
|
|
try {
|
|
const v1Resp = await fetch(`/v1/api/${endpoint}?from=${from}&to=${to}`);
|
|
const v1Data = await v1Resp.json();
|
|
document.getElementById('v1-result').innerHTML = renderResult(v1Data, currentMode);
|
|
} catch(e) {
|
|
document.getElementById('v1-result').innerHTML = `<div class="error">에러: ${e}</div>`;
|
|
}
|
|
|
|
// v2
|
|
try {
|
|
const v2Resp = await fetch(`/v2/api/${endpoint}?from=${from}&to=${to}`);
|
|
const v2Data = await v2Resp.json();
|
|
if (v2Data.error) {
|
|
document.getElementById('v2-result').innerHTML =
|
|
`<div class="error">에러: ${v2Data.error}<br><small>${v2Data.note || ''}</small></div>`;
|
|
} else {
|
|
document.getElementById('v2-result').innerHTML = renderResult(v2Data, currentMode);
|
|
}
|
|
} catch(e) {
|
|
document.getElementById('v2-result').innerHTML = `<div class="error">에러: ${e}</div>`;
|
|
}
|
|
}
|
|
|
|
function renderResult(data, mode) {
|
|
if (mode === 'total') return renderTotal(data.total);
|
|
if (mode === 'insurance') return renderInsurance(data);
|
|
if (mode === 'time') return renderTime(data);
|
|
if (mode === 'payment') return renderPayment(data);
|
|
if (mode === 'hospital') return renderHospital(data);
|
|
return renderTotal(data.total);
|
|
}
|
|
|
|
function renderTotal(t) {
|
|
if (!t) return '<div class="error">데이터 없음</div>';
|
|
return `
|
|
<table>
|
|
<tr><th>항목</th><th>값</th></tr>
|
|
<tr class="total"><td>총 건수</td><td>${formatMoney(t.cnt)}건</td></tr>
|
|
<tr class="total"><td>매출금액</td><td>${formatMoney(t.sales_amt)}원</td></tr>
|
|
<tr><td>급여조제료</td><td>${formatMoney(t.ins_prep)}원</td></tr>
|
|
<tr><td>급여약가</td><td>${formatMoney(t.ins_drug)}원</td></tr>
|
|
<tr><td>비급여조제료</td><td>${formatMoney(t.nonins_prep)}원</td></tr>
|
|
<tr><td>비급여약가</td><td>${formatMoney(t.nonins_drug)}원</td></tr>
|
|
<tr><td>청구액</td><td>${formatMoney(t.claim_amt)}원</td></tr>
|
|
<tr><td>본인부담</td><td>${formatMoney(t.copay)}원</td></tr>
|
|
<tr><td>수납금액</td><td>${formatMoney(t.receipt)}원</td></tr>
|
|
</table>
|
|
`;
|
|
}
|
|
|
|
function renderInsurance(data) {
|
|
const items = data.by_gubun || [];
|
|
let html = `<table>
|
|
<tr><th>보험구분</th><th>건수</th><th>매출금액</th><th>청구액</th><th>본인부담</th></tr>`;
|
|
items.forEach(item => {
|
|
html += `<tr>
|
|
<td>${item.label}</td>
|
|
<td>${formatMoney(item.cnt)}</td>
|
|
<td>${formatMoney(item.sales_amt)}</td>
|
|
<td>${formatMoney(item.claim_amt)}</td>
|
|
<td>${formatMoney(item.copay)}</td>
|
|
</tr>`;
|
|
});
|
|
html += `<tr class="total">
|
|
<td>합계</td>
|
|
<td>${formatMoney(data.total?.cnt)}</td>
|
|
<td>${formatMoney(data.total?.sales_amt)}</td>
|
|
<td>${formatMoney(data.total?.claim_amt)}</td>
|
|
<td>${formatMoney(data.total?.copay)}</td>
|
|
</tr></table>`;
|
|
return html;
|
|
}
|
|
|
|
function renderTime(data) {
|
|
const t = data.by_time || {};
|
|
return `<table>
|
|
<tr><th>시간구분</th><th>건수</th><th>매출금액</th></tr>
|
|
<tr><td>정상시간</td><td>${formatMoney(t.normal?.cnt)}</td><td>${formatMoney(t.normal?.sales_amt)}</td></tr>
|
|
<tr><td>시간외가산</td><td>${formatMoney(t.overtime?.cnt)}</td><td>${formatMoney(t.overtime?.sales_amt)}</td></tr>
|
|
<tr><td>토요/공휴가산</td><td>${formatMoney(t.saturday?.cnt)}</td><td>${formatMoney(t.saturday?.sales_amt)}</td></tr>
|
|
<tr class="total"><td>합계</td><td>${formatMoney(data.total?.cnt)}</td><td>${formatMoney(data.total?.sales_amt)}</td></tr>
|
|
</table>`;
|
|
}
|
|
|
|
function renderPayment(data) {
|
|
const p = data.by_pay || {};
|
|
return `<table>
|
|
<tr><th>결제수단</th><th>건수</th><th>매출금액</th><th>수납금액</th></tr>
|
|
<tr><td>카드</td><td>${formatMoney(p.card?.cnt)}</td><td>${formatMoney(p.card?.sales_amt)}</td><td>${formatMoney(p.card?.receipt)}</td></tr>
|
|
<tr><td>현금</td><td>${formatMoney(p.cash?.cnt)}</td><td>${formatMoney(p.cash?.sales_amt)}</td><td>${formatMoney(p.cash?.receipt)}</td></tr>
|
|
<tr><td>외상/기타</td><td>${formatMoney(p.paper?.cnt)}</td><td>${formatMoney(p.paper?.sales_amt)}</td><td>${formatMoney(p.paper?.receipt)}</td></tr>
|
|
<tr class="total"><td>합계</td><td>${formatMoney(data.total?.cnt)}</td><td>${formatMoney(data.total?.sales_amt)}</td><td>${formatMoney(data.total?.receipt)}</td></tr>
|
|
</table>`;
|
|
}
|
|
|
|
function renderHospital(data) {
|
|
const items = data.by_hosp || [];
|
|
let html = `<table>
|
|
<tr><th>병원명</th><th>건수</th><th>매출금액</th></tr>`;
|
|
items.slice(0, 20).forEach(item => {
|
|
html += `<tr><td>${item.name}</td><td>${formatMoney(item.cnt)}</td><td>${formatMoney(item.sales_amt)}</td></tr>`;
|
|
});
|
|
html += `</table>`;
|
|
if (items.length > 20) html += `<p>...외 ${items.length - 20}개</p>`;
|
|
return html;
|
|
}
|
|
|
|
async function loadCompare() {
|
|
const from = formatDate(document.getElementById('dateFrom').value);
|
|
const to = formatDate(document.getElementById('dateTo').value);
|
|
|
|
document.getElementById('compare-result').innerHTML = '<div class="loading">비교 중...</div>';
|
|
|
|
try {
|
|
const resp = await fetch(`/api/compare?from=${from}&to=${to}`);
|
|
const data = await resp.json();
|
|
|
|
const v1 = data.v1?.total || {};
|
|
const v2 = data.v2?.total || {};
|
|
const diff = data.diff || {};
|
|
|
|
let html = `<div class="panel" style="margin-top:20px;">
|
|
<h2>📊 v1 vs v2 비교 결과</h2>
|
|
<table class="compare-table">
|
|
<tr><th>항목</th><th>v1 (PharmIT3000)</th><th>v2 (PMPLUS20)</th><th>차이</th></tr>`;
|
|
|
|
const fields = [
|
|
['건수', 'cnt', '건'],
|
|
['매출금액', 'sales_amt', '원'],
|
|
['급여조제료', 'ins_prep', '원'],
|
|
['급여약가', 'ins_drug', '원'],
|
|
['비급여약가', 'nonins_drug', '원'],
|
|
['청구액', 'claim_amt', '원'],
|
|
['본인부담', 'copay', '원'],
|
|
['수납금액', 'receipt', '원'],
|
|
];
|
|
|
|
fields.forEach(([label, key, unit]) => {
|
|
const v1Val = v1[key] || 0;
|
|
const v2Val = v2[key] || 0;
|
|
const d = diff[key] || 0;
|
|
const diffClass = d > 0 ? 'diff-positive' : (d < 0 ? 'diff-negative' : 'diff-zero');
|
|
const diffSign = d > 0 ? '+' : '';
|
|
html += `<tr>
|
|
<td>${label}</td>
|
|
<td>${formatMoney(v1Val)}${unit}</td>
|
|
<td>${formatMoney(v2Val)}${unit}</td>
|
|
<td class="${diffClass}">${diffSign}${formatMoney(d)}${unit}</td>
|
|
</tr>`;
|
|
});
|
|
|
|
html += `</table></div>`;
|
|
|
|
if (data.v2?.error) {
|
|
html += `<div class="error">v2 에러: ${data.v2.error}</div>`;
|
|
}
|
|
|
|
document.getElementById('compare-result').innerHTML = html;
|
|
|
|
} catch(e) {
|
|
document.getElementById('compare-result').innerHTML = `<div class="error">에러: ${e}</div>`;
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|