Initial commit: Flask stats API (v1 PharmIT3000, v2 PMPLUS20)
This commit is contained in:
75
README.md
Normal file
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Pharmacy Stats API
|
||||||
|
|
||||||
|
QT-POS 통계 다이얼로그 API 버전
|
||||||
|
|
||||||
|
## 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
pharmacy-stats-api/
|
||||||
|
├── app.py # Flask 메인
|
||||||
|
├── config.py # DB 연결 설정
|
||||||
|
├── queries/
|
||||||
|
│ ├── v1_pharmit3000.py # PharmIT3000 쿼리
|
||||||
|
│ └── v2_pmplus20.py # PMPLUS20 쿼리
|
||||||
|
└── templates/
|
||||||
|
└── stats.html # 비교 UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 엔드포인트
|
||||||
|
|
||||||
|
### v1 - PharmIT3000
|
||||||
|
|
||||||
|
| Endpoint | 설명 |
|
||||||
|
|----------|------|
|
||||||
|
| `/v1/api/stats` | 전체 통계 |
|
||||||
|
| `/v1/api/stats/insurance` | 보험별 통계 |
|
||||||
|
| `/v1/api/stats/time` | 시간가산별 통계 |
|
||||||
|
| `/v1/api/stats/payment` | 결제수단별 통계 |
|
||||||
|
| `/v1/api/stats/hospital` | 병원별 통계 |
|
||||||
|
|
||||||
|
### v2 - PMPLUS20
|
||||||
|
|
||||||
|
동일한 구조의 `/v2/api/stats/...` 엔드포인트
|
||||||
|
|
||||||
|
### 비교
|
||||||
|
|
||||||
|
| Endpoint | 설명 |
|
||||||
|
|----------|------|
|
||||||
|
| `/api/compare` | v1 vs v2 비교 |
|
||||||
|
|
||||||
|
## 쿼리 파라미터
|
||||||
|
|
||||||
|
- `from`: 시작일 (YYYYMMDD)
|
||||||
|
- `to`: 종료일 (YYYYMMDD)
|
||||||
|
|
||||||
|
예: `/v1/api/stats?from=20260401&to=20260401`
|
||||||
|
|
||||||
|
## 테이블 매핑
|
||||||
|
|
||||||
|
| 역할 | PharmIT3000 (v1) | PMPLUS20 (v2) |
|
||||||
|
|------|-----------------|---------------|
|
||||||
|
| 판매 헤더 | PM_PRES.PS_MAIN | PM_MAIN.TBSIM040_28 |
|
||||||
|
| 판매 상세 | PM_PRES.PS_SUB_PHARM | PM_MAIN.TBSIM040_29 |
|
||||||
|
| 수납 정보 | PM_PRES.CD_SUNAB | PM_MAIN.TBSIR000_01 |
|
||||||
|
| 약품 마스터 | PM_DRUG.CD_GOODS | PM_MAIN.TBSIM040_01 |
|
||||||
|
|
||||||
|
## 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
서버: http://0.0.0.0:5060
|
||||||
|
|
||||||
|
## 환경변수
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PHARMIT_SERVER=192.168.0.4\PM2014
|
||||||
|
PHARMIT_USER=sa
|
||||||
|
PHARMIT_PASS=password
|
||||||
|
|
||||||
|
PMPLUS_SERVER=192.168.0.4\PM2014
|
||||||
|
PMPLUS_USER=sa
|
||||||
|
PMPLUS_PASS=password
|
||||||
|
```
|
||||||
308
app.py
Normal file
308
app.py
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Pharmacy Stats API - Flask 서버
|
||||||
|
|
||||||
|
QT-POS 통계 다이얼로그 API 버전
|
||||||
|
v1: PharmIT3000
|
||||||
|
v2: PMPLUS20
|
||||||
|
"""
|
||||||
|
from flask import Flask, jsonify, request, render_template
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from queries import v1_pharmit3000 as v1
|
||||||
|
from queries import v2_pmplus20 as v2
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# 보험구분 라벨
|
||||||
|
GUBUN_LABEL = {
|
||||||
|
'0': '건강보험', '1': '의료급여', '2': '산재', '3': '자동차',
|
||||||
|
'4': '보훈', '5': '공상', '6': '본인', '7': '차상위1',
|
||||||
|
'8': '희귀', '9': '비급여', 'E': '차상위2', 'F': '차상위2',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('stats.html')
|
||||||
|
|
||||||
|
|
||||||
|
# ============== v1 API (PharmIT3000) ==============
|
||||||
|
|
||||||
|
@app.route('/v1/api/stats')
|
||||||
|
def v1_stats():
|
||||||
|
"""v1 전체 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = v1.get_sales_stats(date_from, date_to)
|
||||||
|
result['version'] = 'v1'
|
||||||
|
result['source'] = 'PharmIT3000'
|
||||||
|
result['date_from'] = date_from
|
||||||
|
result['date_to'] = date_to
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v1/api/stats/insurance')
|
||||||
|
def v1_stats_insurance():
|
||||||
|
"""v1 보험별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v1.get_sales_stats(date_from, date_to)
|
||||||
|
|
||||||
|
by_gubun = []
|
||||||
|
for code, data in result.get('by_gubun', {}).items():
|
||||||
|
by_gubun.append({
|
||||||
|
'code': code,
|
||||||
|
'label': GUBUN_LABEL.get(code, code),
|
||||||
|
**data
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v1',
|
||||||
|
'source': 'PharmIT3000',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_gubun': sorted(by_gubun, key=lambda x: x['code'])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v1/api/stats/time')
|
||||||
|
def v1_stats_time():
|
||||||
|
"""v1 시간가산별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v1.get_sales_stats(date_from, date_to)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v1',
|
||||||
|
'source': 'PharmIT3000',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_time': result['by_time']
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v1/api/stats/payment')
|
||||||
|
def v1_stats_payment():
|
||||||
|
"""v1 결제수단별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v1.get_sales_stats(date_from, date_to)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v1',
|
||||||
|
'source': 'PharmIT3000',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_pay': result['by_pay']
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v1/api/stats/hospital')
|
||||||
|
def v1_stats_hospital():
|
||||||
|
"""v1 병원별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v1.get_sales_stats(date_from, date_to)
|
||||||
|
|
||||||
|
by_hosp = []
|
||||||
|
for name, data in result.get('by_hosp', {}).items():
|
||||||
|
by_hosp.append({'name': name, **data})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v1',
|
||||||
|
'source': 'PharmIT3000',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_hosp': sorted(by_hosp, key=lambda x: -x['cnt'])[:50] # 상위 50개
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ============== v2 API (PMPLUS20) ==============
|
||||||
|
|
||||||
|
@app.route('/v2/api/stats')
|
||||||
|
def v2_stats():
|
||||||
|
"""v2 전체 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = v2.get_sales_stats(date_from, date_to)
|
||||||
|
result['version'] = 'v2'
|
||||||
|
result['source'] = 'PMPLUS20'
|
||||||
|
result['date_from'] = date_from
|
||||||
|
result['date_to'] = date_to
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v2/api/stats/insurance')
|
||||||
|
def v2_stats_insurance():
|
||||||
|
"""v2 보험별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v2.get_sales_stats(date_from, date_to)
|
||||||
|
|
||||||
|
if 'error' in result:
|
||||||
|
return jsonify(result), 500
|
||||||
|
|
||||||
|
by_gubun = []
|
||||||
|
for code, data in result.get('by_gubun', {}).items():
|
||||||
|
by_gubun.append({
|
||||||
|
'code': code,
|
||||||
|
'label': GUBUN_LABEL.get(code, code),
|
||||||
|
**data
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v2',
|
||||||
|
'source': 'PMPLUS20',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_gubun': sorted(by_gubun, key=lambda x: x['code'])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v2/api/stats/time')
|
||||||
|
def v2_stats_time():
|
||||||
|
"""v2 시간가산별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v2.get_sales_stats(date_from, date_to)
|
||||||
|
if 'error' in result:
|
||||||
|
return jsonify(result), 500
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v2',
|
||||||
|
'source': 'PMPLUS20',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_time': result['by_time']
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v2/api/stats/payment')
|
||||||
|
def v2_stats_payment():
|
||||||
|
"""v2 결제수단별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v2.get_sales_stats(date_from, date_to)
|
||||||
|
if 'error' in result:
|
||||||
|
return jsonify(result), 500
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v2',
|
||||||
|
'source': 'PMPLUS20',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_pay': result['by_pay']
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/v2/api/stats/hospital')
|
||||||
|
def v2_stats_hospital():
|
||||||
|
"""v2 병원별 통계"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
result = v2.get_sales_stats(date_from, date_to)
|
||||||
|
if 'error' in result:
|
||||||
|
return jsonify(result), 500
|
||||||
|
|
||||||
|
by_hosp = []
|
||||||
|
for name, data in result.get('by_hosp', {}).items():
|
||||||
|
by_hosp.append({'name': name, **data})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'version': 'v2',
|
||||||
|
'source': 'PMPLUS20',
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'total': result['total'],
|
||||||
|
'by_hosp': sorted(by_hosp, key=lambda x: -x['cnt'])[:50]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ============== 비교 API ==============
|
||||||
|
|
||||||
|
@app.route('/api/compare')
|
||||||
|
def compare():
|
||||||
|
"""v1 vs v2 비교"""
|
||||||
|
date_from = request.args.get('from', _default_from())
|
||||||
|
date_to = request.args.get('to', _default_to())
|
||||||
|
|
||||||
|
v1_result = v1.get_sales_stats(date_from, date_to)
|
||||||
|
v2_result = v2.get_sales_stats(date_from, date_to)
|
||||||
|
|
||||||
|
# 차이 계산
|
||||||
|
diff = {}
|
||||||
|
if 'total' in v1_result and 'total' in v2_result:
|
||||||
|
v1_total = v1_result['total']
|
||||||
|
v2_total = v2_result['total']
|
||||||
|
for key in v1_total:
|
||||||
|
v1_val = v1_total.get(key, 0)
|
||||||
|
v2_val = v2_total.get(key, 0)
|
||||||
|
diff[key] = v2_val - v1_val
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
'v1': {
|
||||||
|
'source': 'PharmIT3000',
|
||||||
|
'total': v1_result.get('total', {}),
|
||||||
|
'error': v1_result.get('error')
|
||||||
|
},
|
||||||
|
'v2': {
|
||||||
|
'source': 'PMPLUS20',
|
||||||
|
'total': v2_result.get('total', {}),
|
||||||
|
'error': v2_result.get('error')
|
||||||
|
},
|
||||||
|
'diff': diff
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _default_from():
|
||||||
|
"""기본 시작일: 이번 달 1일"""
|
||||||
|
today = date.today()
|
||||||
|
return today.replace(day=1).strftime('%Y%m%d')
|
||||||
|
|
||||||
|
|
||||||
|
def _default_to():
|
||||||
|
"""기본 종료일: 오늘"""
|
||||||
|
return date.today().strftime('%Y%m%d')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("🏥 Pharmacy Stats API")
|
||||||
|
print("http://0.0.0.0:5060")
|
||||||
|
print("")
|
||||||
|
print("Endpoints:")
|
||||||
|
print(" /v1/api/stats - PharmIT3000 전체 통계")
|
||||||
|
print(" /v1/api/stats/insurance - PharmIT3000 보험별")
|
||||||
|
print(" /v1/api/stats/time - PharmIT3000 시간가산별")
|
||||||
|
print(" /v1/api/stats/payment - PharmIT3000 결제수단별")
|
||||||
|
print(" /v1/api/stats/hospital - PharmIT3000 병원별")
|
||||||
|
print("")
|
||||||
|
print(" /v2/api/stats/... - PMPLUS20 (동일 구조)")
|
||||||
|
print("")
|
||||||
|
print(" /api/compare - v1 vs v2 비교")
|
||||||
|
app.run(host='0.0.0.0', port=5060, debug=True)
|
||||||
19
config.py
Normal file
19
config.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""DB 연결 설정"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# PharmIT3000 (v1)
|
||||||
|
PHARMIT3000_CONFIG = {
|
||||||
|
'server': os.getenv('PHARMIT_SERVER', '192.168.0.4\\PM2014'),
|
||||||
|
'database': 'PM_PRES',
|
||||||
|
'username': os.getenv('PHARMIT_USER', 'sa'),
|
||||||
|
'password': os.getenv('PHARMIT_PASS', 'tmddls214!%('),
|
||||||
|
}
|
||||||
|
|
||||||
|
# PMPLUS20 (v2)
|
||||||
|
PMPLUS20_CONFIG = {
|
||||||
|
'server': os.getenv('PMPLUS_SERVER', '192.168.0.4\\PM2014'),
|
||||||
|
'database': 'PM_MAIN',
|
||||||
|
'username': os.getenv('PMPLUS_USER', 'sa'),
|
||||||
|
'password': os.getenv('PMPLUS_PASS', 'tmddls214!%('),
|
||||||
|
}
|
||||||
1
queries/__init__.py
Normal file
1
queries/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
200
queries/v1_pharmit3000.py
Normal file
200
queries/v1_pharmit3000.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
PharmIT3000 통계 쿼리 (v1)
|
||||||
|
|
||||||
|
테이블:
|
||||||
|
- PM_PRES.PS_MAIN: 처방 헤더
|
||||||
|
- PM_PRES.PS_SUB_PHARM: 처방 상세
|
||||||
|
- PM_PRES.CD_SUNAB: 수납 정보
|
||||||
|
- PM_DRUG.CD_GOODS: 약품 마스터
|
||||||
|
|
||||||
|
QT-POS sales_stats_dialog.py 기반
|
||||||
|
"""
|
||||||
|
import pyodbc
|
||||||
|
from datetime import date
|
||||||
|
from config import PHARMIT3000_CONFIG as CFG
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
conn_str = (
|
||||||
|
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||||
|
f"SERVER={CFG['server']};"
|
||||||
|
f"DATABASE={CFG['database']};"
|
||||||
|
f"UID={CFG['username']};"
|
||||||
|
f"PWD={CFG['password']};"
|
||||||
|
f"TrustServerCertificate=yes;"
|
||||||
|
f"Connection Timeout=30;"
|
||||||
|
)
|
||||||
|
return pyodbc.connect(conn_str)
|
||||||
|
|
||||||
|
|
||||||
|
def query(sql, params=None):
|
||||||
|
"""쿼리 실행 후 dict 리스트 반환"""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(sql, params or ())
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def get_sales_stats(date_from: str, date_to: str) -> dict:
|
||||||
|
"""
|
||||||
|
매출 통계 조회 (QT-POS sales_stats_dialog.py 기준)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
date_from: 시작일 (YYYYMMDD)
|
||||||
|
date_to: 종료일 (YYYYMMDD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {total, by_gubun, by_age, by_time, by_pay, by_hosp}
|
||||||
|
"""
|
||||||
|
sql = """
|
||||||
|
SELECT
|
||||||
|
m.PreSerial,
|
||||||
|
m.PreGubun,
|
||||||
|
m.PaNum,
|
||||||
|
m.OrderName,
|
||||||
|
m.Holiday,
|
||||||
|
m.PRICE_T,
|
||||||
|
m.Drug_T4,
|
||||||
|
m.S_Prep,
|
||||||
|
m.PRICE_C,
|
||||||
|
m.PRICE_P,
|
||||||
|
m.PRICE_N,
|
||||||
|
ISNULL(n.Appr_Gubun, '') AS Appr_Gubun,
|
||||||
|
ISNULL(n.nAPPROVAL_NUM, '') AS nAPPROVAL_NUM
|
||||||
|
FROM PM_PRES..PS_MAIN m
|
||||||
|
LEFT JOIN PM_PRES..CD_SUNAB n ON n.PRESERIAL = m.PreSerial
|
||||||
|
WHERE m.INDATE BETWEEN ? AND ?
|
||||||
|
"""
|
||||||
|
rows = query(sql, (date_from, date_to))
|
||||||
|
|
||||||
|
ref_date = date(
|
||||||
|
int(date_to[:4]),
|
||||||
|
int(date_to[4:6]),
|
||||||
|
int(date_to[6:8]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 집계
|
||||||
|
result = {
|
||||||
|
'total': _empty_row(),
|
||||||
|
'by_gubun': {},
|
||||||
|
'by_age': {'old': _empty_row(), 'infant': _empty_row(), 'mid': _empty_row()},
|
||||||
|
'by_time': {'overtime': _empty_row(), 'saturday': _empty_row(), 'normal': _empty_row()},
|
||||||
|
'by_pay': {'card': _empty_row(), 'cash': _empty_row(), 'paper': _empty_row()},
|
||||||
|
'by_hosp': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
gubun = str(r.get('PreGubun') or '0')
|
||||||
|
holiday = str(r.get('Holiday') or '1')
|
||||||
|
price_t = int(r.get('PRICE_T') or 0)
|
||||||
|
drug_t4 = int(r.get('Drug_T4') or 0)
|
||||||
|
s_prep = int(r.get('S_Prep') or 0)
|
||||||
|
price_c = int(r.get('PRICE_C') or 0)
|
||||||
|
price_p = int(r.get('PRICE_P') or 0)
|
||||||
|
price_n = int(r.get('PRICE_N') or 0)
|
||||||
|
appr_gubun = str(r.get('Appr_Gubun') or '')
|
||||||
|
order_name = str(r.get('OrderName') or '기타')
|
||||||
|
panum = str(r.get('PaNum') or '')
|
||||||
|
|
||||||
|
sales_amt = price_t + drug_t4
|
||||||
|
|
||||||
|
# 급여/비급여 분리
|
||||||
|
if gubun == '9':
|
||||||
|
ins_prep, ins_drug = 0, 0
|
||||||
|
nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0
|
||||||
|
nonins_drug = drug_t4
|
||||||
|
else:
|
||||||
|
ins_prep, ins_drug = s_prep, price_t - s_prep
|
||||||
|
nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0
|
||||||
|
nonins_drug = drug_t4
|
||||||
|
|
||||||
|
args = (sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug, 0, price_c, price_p, price_n)
|
||||||
|
|
||||||
|
# 전체
|
||||||
|
_add(result['total'], *args)
|
||||||
|
|
||||||
|
# 보험별
|
||||||
|
if gubun not in result['by_gubun']:
|
||||||
|
result['by_gubun'][gubun] = _empty_row()
|
||||||
|
_add(result['by_gubun'][gubun], *args)
|
||||||
|
|
||||||
|
# 연령가산별
|
||||||
|
age = _calc_age(panum, ref_date)
|
||||||
|
if age is None or (6 <= age < 65):
|
||||||
|
_add(result['by_age']['mid'], *args)
|
||||||
|
elif age >= 65:
|
||||||
|
_add(result['by_age']['old'], *args)
|
||||||
|
else:
|
||||||
|
_add(result['by_age']['infant'], *args)
|
||||||
|
|
||||||
|
# 시간가산별
|
||||||
|
hd_add = holiday in ('2', '4') # 공휴가산
|
||||||
|
overtime = holiday in ('3', '4') # 시간외
|
||||||
|
if hd_add:
|
||||||
|
_add(result['by_time']['saturday'], *args)
|
||||||
|
elif overtime:
|
||||||
|
_add(result['by_time']['overtime'], *args)
|
||||||
|
else:
|
||||||
|
_add(result['by_time']['normal'], *args)
|
||||||
|
|
||||||
|
# 결제수단별
|
||||||
|
if appr_gubun == '1':
|
||||||
|
_add(result['by_pay']['card'], *args)
|
||||||
|
elif appr_gubun == '2':
|
||||||
|
_add(result['by_pay']['cash'], *args)
|
||||||
|
else:
|
||||||
|
_add(result['by_pay']['paper'], *args)
|
||||||
|
|
||||||
|
# 병원별
|
||||||
|
if order_name not in result['by_hosp']:
|
||||||
|
result['by_hosp'][order_name] = _empty_row()
|
||||||
|
_add(result['by_hosp'][order_name], *args)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_row():
|
||||||
|
return dict(cnt=0, sales_amt=0, ins_prep=0, ins_drug=0,
|
||||||
|
nonins_prep=0, nonins_drug=0, nonins_margin=0,
|
||||||
|
claim_amt=0, copay=0, receipt=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _add(d, sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug,
|
||||||
|
nonins_margin, claim_amt, copay, receipt):
|
||||||
|
d['cnt'] += 1
|
||||||
|
d['sales_amt'] += sales_amt
|
||||||
|
d['ins_prep'] += ins_prep
|
||||||
|
d['ins_drug'] += ins_drug
|
||||||
|
d['nonins_prep'] += nonins_prep
|
||||||
|
d['nonins_drug'] += nonins_drug
|
||||||
|
d['nonins_margin'] += nonins_margin
|
||||||
|
d['claim_amt'] += claim_amt
|
||||||
|
d['copay'] += copay
|
||||||
|
d['receipt'] += receipt
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_age(panum: str, ref_date: date) -> int | None:
|
||||||
|
"""주민번호 → 나이"""
|
||||||
|
if not panum or len(panum) < 7:
|
||||||
|
return None
|
||||||
|
birth6 = panum[:6]
|
||||||
|
gender = panum[6]
|
||||||
|
if not birth6.isdigit():
|
||||||
|
return None
|
||||||
|
yy, mm, dd = int(birth6[:2]), int(birth6[2:4]), int(birth6[4:6])
|
||||||
|
if gender in ('1', '2'):
|
||||||
|
year = 1900 + yy
|
||||||
|
elif gender in ('3', '4'):
|
||||||
|
year = 2000 + yy
|
||||||
|
else:
|
||||||
|
year = 1900 + yy
|
||||||
|
try:
|
||||||
|
bday = date(year, mm, dd)
|
||||||
|
return ref_date.year - bday.year - (
|
||||||
|
(ref_date.month, ref_date.day) < (bday.month, bday.day))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
207
queries/v2_pmplus20.py
Normal file
207
queries/v2_pmplus20.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
PMPLUS20 통계 쿼리 (v2)
|
||||||
|
|
||||||
|
테이블 매핑 (PharmIT3000 → PMPLUS20):
|
||||||
|
- PM_PRES.PS_MAIN → PM_MAIN.TBSIM040_28
|
||||||
|
- PM_PRES.PS_SUB_PHARM → PM_MAIN.TBSIM040_29
|
||||||
|
- PM_PRES.CD_SUNAB → PM_MAIN.TBSIR000_01
|
||||||
|
- PM_DRUG.CD_GOODS → PM_MAIN.TBSIM040_01
|
||||||
|
|
||||||
|
주의:
|
||||||
|
- DrugCode vs Drug_Code 대소문자 차이
|
||||||
|
- TBSIR000_01.RECP_DT는 datetime (변환 필요)
|
||||||
|
"""
|
||||||
|
import pyodbc
|
||||||
|
from datetime import date
|
||||||
|
from config import PMPLUS20_CONFIG as CFG
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
conn_str = (
|
||||||
|
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||||
|
f"SERVER={CFG['server']};"
|
||||||
|
f"DATABASE={CFG['database']};"
|
||||||
|
f"UID={CFG['username']};"
|
||||||
|
f"PWD={CFG['password']};"
|
||||||
|
f"TrustServerCertificate=yes;"
|
||||||
|
f"Connection Timeout=30;"
|
||||||
|
)
|
||||||
|
return pyodbc.connect(conn_str)
|
||||||
|
|
||||||
|
|
||||||
|
def query(sql, params=None):
|
||||||
|
"""쿼리 실행 후 dict 리스트 반환"""
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(sql, params or ())
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def get_sales_stats(date_from: str, date_to: str) -> dict:
|
||||||
|
"""
|
||||||
|
매출 통계 조회 (PMPLUS20 버전)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
date_from: 시작일 (YYYYMMDD)
|
||||||
|
date_to: 종료일 (YYYYMMDD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {total, by_gubun, by_age, by_time, by_pay, by_hosp}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
컬럼명/테이블명은 실제 PMPLUS20 스키마 확인 후 수정 필요
|
||||||
|
"""
|
||||||
|
# TODO: 실제 PMPLUS20 스키마 확인 후 쿼리 수정
|
||||||
|
# 현재는 PharmIT3000과 동일한 컬럼명 가정
|
||||||
|
sql = """
|
||||||
|
SELECT
|
||||||
|
m.PreSerial,
|
||||||
|
m.PreGubun,
|
||||||
|
m.PaNum,
|
||||||
|
m.OrderName,
|
||||||
|
m.Holiday,
|
||||||
|
m.PRICE_T,
|
||||||
|
m.Drug_T4,
|
||||||
|
m.S_Prep,
|
||||||
|
m.PRICE_C,
|
||||||
|
m.PRICE_P,
|
||||||
|
m.PRICE_N,
|
||||||
|
ISNULL(n.Appr_Gubun, '') AS Appr_Gubun,
|
||||||
|
ISNULL(n.nAPPROVAL_NUM, '') AS nAPPROVAL_NUM
|
||||||
|
FROM PM_MAIN..TBSIM040_28 m
|
||||||
|
LEFT JOIN PM_MAIN..TBSIR000_01 n ON n.PRESERIAL = m.PreSerial
|
||||||
|
WHERE m.INDATE BETWEEN ? AND ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
rows = query(sql, (date_from, date_to))
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'error': str(e),
|
||||||
|
'note': 'PMPLUS20 스키마 확인 필요. 컬럼명/테이블명이 다를 수 있음.'
|
||||||
|
}
|
||||||
|
|
||||||
|
ref_date = date(
|
||||||
|
int(date_to[:4]),
|
||||||
|
int(date_to[4:6]),
|
||||||
|
int(date_to[6:8]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 집계
|
||||||
|
result = {
|
||||||
|
'total': _empty_row(),
|
||||||
|
'by_gubun': {},
|
||||||
|
'by_age': {'old': _empty_row(), 'infant': _empty_row(), 'mid': _empty_row()},
|
||||||
|
'by_time': {'overtime': _empty_row(), 'saturday': _empty_row(), 'normal': _empty_row()},
|
||||||
|
'by_pay': {'card': _empty_row(), 'cash': _empty_row(), 'paper': _empty_row()},
|
||||||
|
'by_hosp': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
gubun = str(r.get('PreGubun') or '0')
|
||||||
|
holiday = str(r.get('Holiday') or '1')
|
||||||
|
price_t = int(r.get('PRICE_T') or 0)
|
||||||
|
drug_t4 = int(r.get('Drug_T4') or 0)
|
||||||
|
s_prep = int(r.get('S_Prep') or 0)
|
||||||
|
price_c = int(r.get('PRICE_C') or 0)
|
||||||
|
price_p = int(r.get('PRICE_P') or 0)
|
||||||
|
price_n = int(r.get('PRICE_N') or 0)
|
||||||
|
appr_gubun = str(r.get('Appr_Gubun') or '')
|
||||||
|
order_name = str(r.get('OrderName') or '기타')
|
||||||
|
panum = str(r.get('PaNum') or '')
|
||||||
|
|
||||||
|
sales_amt = price_t + drug_t4
|
||||||
|
|
||||||
|
if gubun == '9':
|
||||||
|
ins_prep, ins_drug = 0, 0
|
||||||
|
nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0
|
||||||
|
nonins_drug = drug_t4
|
||||||
|
else:
|
||||||
|
ins_prep, ins_drug = s_prep, price_t - s_prep
|
||||||
|
nonins_prep = max(0, price_n - drug_t4 - price_p) if drug_t4 > 0 else 0
|
||||||
|
nonins_drug = drug_t4
|
||||||
|
|
||||||
|
args = (sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug, 0, price_c, price_p, price_n)
|
||||||
|
|
||||||
|
_add(result['total'], *args)
|
||||||
|
|
||||||
|
if gubun not in result['by_gubun']:
|
||||||
|
result['by_gubun'][gubun] = _empty_row()
|
||||||
|
_add(result['by_gubun'][gubun], *args)
|
||||||
|
|
||||||
|
age = _calc_age(panum, ref_date)
|
||||||
|
if age is None or (6 <= age < 65):
|
||||||
|
_add(result['by_age']['mid'], *args)
|
||||||
|
elif age >= 65:
|
||||||
|
_add(result['by_age']['old'], *args)
|
||||||
|
else:
|
||||||
|
_add(result['by_age']['infant'], *args)
|
||||||
|
|
||||||
|
hd_add = holiday in ('2', '4')
|
||||||
|
overtime = holiday in ('3', '4')
|
||||||
|
if hd_add:
|
||||||
|
_add(result['by_time']['saturday'], *args)
|
||||||
|
elif overtime:
|
||||||
|
_add(result['by_time']['overtime'], *args)
|
||||||
|
else:
|
||||||
|
_add(result['by_time']['normal'], *args)
|
||||||
|
|
||||||
|
if appr_gubun == '1':
|
||||||
|
_add(result['by_pay']['card'], *args)
|
||||||
|
elif appr_gubun == '2':
|
||||||
|
_add(result['by_pay']['cash'], *args)
|
||||||
|
else:
|
||||||
|
_add(result['by_pay']['paper'], *args)
|
||||||
|
|
||||||
|
if order_name not in result['by_hosp']:
|
||||||
|
result['by_hosp'][order_name] = _empty_row()
|
||||||
|
_add(result['by_hosp'][order_name], *args)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_row():
|
||||||
|
return dict(cnt=0, sales_amt=0, ins_prep=0, ins_drug=0,
|
||||||
|
nonins_prep=0, nonins_drug=0, nonins_margin=0,
|
||||||
|
claim_amt=0, copay=0, receipt=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _add(d, sales_amt, ins_prep, ins_drug, nonins_prep, nonins_drug,
|
||||||
|
nonins_margin, claim_amt, copay, receipt):
|
||||||
|
d['cnt'] += 1
|
||||||
|
d['sales_amt'] += sales_amt
|
||||||
|
d['ins_prep'] += ins_prep
|
||||||
|
d['ins_drug'] += ins_drug
|
||||||
|
d['nonins_prep'] += nonins_prep
|
||||||
|
d['nonins_drug'] += nonins_drug
|
||||||
|
d['nonins_margin'] += nonins_margin
|
||||||
|
d['claim_amt'] += claim_amt
|
||||||
|
d['copay'] += copay
|
||||||
|
d['receipt'] += receipt
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_age(panum: str, ref_date: date) -> int | None:
|
||||||
|
"""주민번호 → 나이"""
|
||||||
|
if not panum or len(panum) < 7:
|
||||||
|
return None
|
||||||
|
birth6 = panum[:6]
|
||||||
|
gender = panum[6]
|
||||||
|
if not birth6.isdigit():
|
||||||
|
return None
|
||||||
|
yy, mm, dd = int(birth6[:2]), int(birth6[2:4]), int(birth6[4:6])
|
||||||
|
if gender in ('1', '2'):
|
||||||
|
year = 1900 + yy
|
||||||
|
elif gender in ('3', '4'):
|
||||||
|
year = 2000 + yy
|
||||||
|
else:
|
||||||
|
year = 1900 + yy
|
||||||
|
try:
|
||||||
|
bday = date(year, mm, dd)
|
||||||
|
return ref_date.year - bday.year - (
|
||||||
|
(ref_date.month, ref_date.day) < (bday.month, bday.day))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask>=2.0
|
||||||
|
pyodbc>=4.0
|
||||||
308
templates/stats.html
Normal file
308
templates/stats.html
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<!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>
|
||||||
Reference in New Issue
Block a user