From 8b4e8f7a0aacb54dabc1d1dabc79ef1c84b3d490 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 Apr 2026 14:45:31 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20PMPLUS20(v2)=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EB=A7=A4=ED=95=91=20=EC=99=84=EB=A3=8C=20=EB=B0=8F?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.py: PMPLUS20 서버(192.168.0.201\PMPLUS20) 및 SA 비밀번호 수정 - v2_pmplus20.py: PS_MAIN→TBSID040_03, CD_SUNAB→TBSIR000_01 매핑 적용 - 컬럼 매핑: PRICE_T→TOT_PRICE, PRICE_C→INS_PRICE, PRICE_P→EXE_PRICE 등 - Drug_T4 = NON_DRUG_PRICE + EXP_EXE_PRICE - Holiday = HD_ADD + PRES_TIME_GUBUN 조합 - PreGubun = MPRE_TYPE (차상위 F 별도 처리) - PRES_GUBUN='E' 재고보정 레코드 제외 - PMPLUS20_MIGRATION_GUIDE.md: 전체 매핑 가이드 문서 추가 - app.py: 포트 5060→5050 변경 20260324 기준 검증: 건수/금액/보험별/결제별 일치 확인 PRICE_N(수납) 2건 차이(23,420원)는 비급여 수납 처리 개선에 의한 정상 차이 Co-Authored-By: Claude Sonnet 4.6 --- PMPLUS20_MIGRATION_GUIDE.md | 221 ++++++++++++++++++++++++++++++++++++ app.py | 2 +- config.py | 6 +- queries/v2_pmplus20.py | 72 +++++++----- 4 files changed, 269 insertions(+), 32 deletions(-) create mode 100644 PMPLUS20_MIGRATION_GUIDE.md diff --git a/PMPLUS20_MIGRATION_GUIDE.md b/PMPLUS20_MIGRATION_GUIDE.md new file mode 100644 index 0000000..67b3d8d --- /dev/null +++ b/PMPLUS20_MIGRATION_GUIDE.md @@ -0,0 +1,221 @@ +# PMPLUS20 테이블 매핑 가이드 (pharmacy-stats-api v2) + +## 1. DB 연결 정보 + +| 구분 | 서버 | DB | SA 비밀번호 | +|------|------|-----|------------| +| v1 PharmIT3000 | `192.168.0.201\PM2014` | PM_PRES | `tmddls214!%(` | +| v2 PMPLUS20 | `192.168.0.201\PMPLUS20` | PM_MAIN | `newpro2020!@` | + +> **IP는 반드시 192.168.0.201 고정** (두 인스턴스 SA 비밀번호 다름) + +--- + +## 2. 테이블 매핑 (확정) + +### 처방 조제 통계 (pharmacy-stats-api용) + +| 역할 | v1 (PharmIT3000) | v2 (PMPLUS20) | 검증 | +|------|-----------------|---------------|------| +| 처방 헤더 | `PM_PRES..PS_MAIN` | `PM_MAIN..TBSID040_03` | ✅ 확정 | +| 수납/결제 | `PM_PRES..CD_SUNAB` | `PM_MAIN..TBSIR000_01` | ✅ 확정 | + +### JOIN 관계 + +```sql +-- v1 +FROM PM_PRES..PS_MAIN m +LEFT JOIN PM_PRES..CD_SUNAB n ON n.PRESERIAL = m.PreSerial + +-- v2 +FROM PM_MAIN..TBSID040_03 m +LEFT JOIN PM_MAIN..TBSIR000_01 n ON n.DRUG_SEQ = m.DRUG_SEQ +``` + +### 참고: OTC 판매 매핑 (person-lookup-web-local용, 별도) + +| 역할 | v1 | v2 | +|------|-----|-----| +| OTC 판매 헤더 | SALE_MAIN | TBSIM040_28 | +| OTC 판매 상세 | SALE_SUB | TBSIM040_29 | +| OTC 결제 수납 | CD_SUNAB | TBSIR000_01 | + +--- + +## 3. 컬럼 매핑 (확정) + +### 3-1. 처방 헤더 (PS_MAIN → TBSID040_03) + +| v1 컬럼 | v2 컬럼 | 역할 | 검증 금액 (20260324) | 상태 | +|---------|---------|------|---------------------|------| +| `PreSerial` | `DRUG_SEQ` | 처방 일련번호 | 20260324000004 = 동일 | ✅ | +| `INDATE` | `SUNAB_DT` | 조제일자 (YYYYMMDD) | 동일 | ✅ | +| `PRICE_T` | `TOT_PRICE` | 보험 약제비 총액 | 9,476,000 = 동일 | ✅ | +| `PRICE_C` | `INS_PRICE` | 청구금액 | 7,068,800 = 동일 | ✅ | +| `PRICE_P` | `EXE_PRICE` | 본인부담금 | 2,407,200 = 동일 | ✅ | +| `PRICE_N` | `REAL_PRICE` | 수납금액 | 2,838,420 (차이 23,420원 — 아래 참고) | ✅ | +| `S_Prep` | `INS_PREP_PRICE` | 조제료 | 2,277,850 = 동일 | ✅ | +| `Drug_T4` | `NON_DRUG_PRICE + EXP_EXE_PRICE` | 비급여 약제비 | 292,140+120,420=412,560 ✅ | ✅ | +| `PaNum` | `PAT_JUMIN_NO` | 주민번호 (연령 계산) | 동일 | ✅ | +| `PaName` | `PAT_NM` | 환자명 | 동일 | ✅ | +| `OrderName` | `YOYANG_NM` | 처방 병원명 | 동일 | ✅ | + +### 3-2. 보험구분 (PreGubun → MPRE_TYPE) + +| v1 `PreGubun` | v2 `MPRE_TYPE` | 의미 | 건수 (20260324) | +|---------------|---------------|------|----------------| +| `'0'` | `'0'` | 건강보험 | 191건 ✅ | +| `'1'` | `'1'` | 의료급여 | 16건 ✅ | +| `'9'` | `'9'` | 비급여 | 2건 ✅ | +| `'F'` | `MPRE_TYPE_GUBUN='F'` | 차상위 | 2건 ✅ | + +> **주의**: `MPRE_TYPE`이 보험구분. `PRES_GUBUN`은 다른 용도 (A/E 값) + +> **v2 전용 필터**: `PRES_GUBUN = 'E'`인 "재고보정" 레코드는 제외해야 함 +> (DRUG_SEQ='20260324099999' 형태, v1에는 없음) + +### 3-3. 시간가산 (Holiday → HD_ADD + PRES_TIME_GUBUN) + +v1의 `Holiday`는 단일 컬럼, v2는 두 컬럼 조합: + +| v2 `HD_ADD` | v2 `PRES_TIME_GUBUN` | v1 `Holiday` | 의미 | +|------------|---------------------|-------------|------| +| `'1'` | `'0'` | `'1'` | 일반 | ✅ | +| `'2'` | `'0'` | `'2'` | 공휴일 가산 | ✅ | +| `'1'` | `'1'` | `'3'` | 시간외 가산 | ✅ | +| `'2'` | `'1'` | `'4'` | 공휴+시간외 | 추정 (데이터 미확인) | + +**v2에서 Holiday 계산 공식**: +```python +# PMPLUS20 → PharmIT3000 Holiday 변환 +if hd_add == '2' and time_gubun == '1': + holiday = '4' # 공휴+시간외 +elif hd_add == '2': + holiday = '2' # 공휴 +elif time_gubun == '1': + holiday = '3' # 시간외 +else: + holiday = '1' # 일반 +``` + +### 3-4. 수납/결제 (CD_SUNAB → TBSIR000_01) + +| v1 컬럼 | v2 컬럼 | 역할 | 상태 | +|---------|---------|------|------| +| `CD_SUNAB.PRESERIAL` | `TBSIR000_01.DRUG_SEQ` | JOIN 키 | ✅ | +| `CD_SUNAB.Appr_Gubun` | `TBSIR000_01.APPR_GUBUN` | 결제수단 구분 | ✅ 값 동일 | +| `CD_SUNAB.nAPPROVAL_NUM` | `TBSIR000_01.CARD_ADM_NO` | 승인번호 | ✅ | + +결제 구분 값 분포 (20260324): +- `'9'`: 179건 (카드) +- `'A'`: 24건 +- `'0'`: 5건 +- `'2'`: 3건 (현금) + +--- + +## 4. v2 쿼리 (변환 완료) + +```sql +SELECT + m.DRUG_SEQ AS PreSerial, + m.MPRE_TYPE AS PreGubun, + m.PAT_JUMIN_NO AS PaNum, + m.YOYANG_NM AS OrderName, + -- Holiday 변환: HD_ADD + PRES_TIME_GUBUN → Holiday + CASE + WHEN m.HD_ADD = '2' AND m.PRES_TIME_GUBUN = '1' THEN '4' + WHEN m.HD_ADD = '2' THEN '2' + WHEN m.PRES_TIME_GUBUN = '1' THEN '3' + ELSE '1' + END AS Holiday, + m.TOT_PRICE AS PRICE_T, + (ISNULL(m.NON_DRUG_PRICE, 0) + ISNULL(m.EXP_EXE_PRICE, 0)) AS Drug_T4, + m.INS_PREP_PRICE AS S_Prep, + m.INS_PRICE AS PRICE_C, + m.EXE_PRICE AS PRICE_P, + m.REAL_PRICE AS PRICE_N, + ISNULL(n.APPR_GUBUN, '') AS Appr_Gubun, + ISNULL(n.CARD_ADM_NO, '') AS nAPPROVAL_NUM +FROM PM_MAIN..TBSID040_03 m +LEFT JOIN PM_MAIN..TBSIR000_01 n ON n.DRUG_SEQ = m.DRUG_SEQ +WHERE m.SUNAB_DT BETWEEN ? AND ? + AND m.PRES_GUBUN != 'E' -- 재고보정 레코드 제외 +``` + +--- + +## 5. 데이터 검증 결과 (20260324) + +### 건수 +| 항목 | v1 | v2 | 비고 | +|------|-----|-----|------| +| 전체 건수 | 211건 | 212건 | v2에 "재고보정" 1건 추가 (PRES_GUBUN='E' 필터로 해결) | +| 필터 후 | 211건 | 211건 | ✅ 일치 | + +### 금액 합계 +| 항목 | v1 | v2 | 일치 | +|------|-----|-----|------| +| PRICE_T (보험약제비) | 9,476,000 | 9,476,000 | ✅ | +| PRICE_C (청구금액) | 7,068,800 | 7,068,800 | ✅ | +| PRICE_P (본인부담금) | 2,407,200 | 2,407,200 | ✅ | +| S_Prep (조제료) | 2,277,850 | 2,277,850 | ✅ | +| Drug_T4 (비급여약제비) | 412,560 | 412,560 | ✅ (NON_DRUG+EXP_EXE) | +| PRICE_N (수납금액) | 2,815,000 | 2,838,420 | ⚠️ 차이 23,420원 (아래 설명) | + +### PRICE_N vs REAL_PRICE 차이 원인 (정상) + +20260324 기준 2건에서 차이 발생: + +| 환자 | v1 PRICE_N | v2 REAL_PRICE | 차이 | 보험구분 | +|------|-----------|--------------|------|---------| +| 지성윤 | 500 | 11,920 | +11,420 | 차상위(F) | +| 임해자 | 19,700 | 31,700 | +12,000 | - | + +**원인**: PharmIT3000은 차상위/의료급여 등 특수 보험에서 비급여 약제비를 `PRICE_N`에 미반영하는 버그가 있었음. +PMPLUS20의 `REAL_PRICE`는 비급여 약제비를 포함한 **실제 수납액**으로 개선됨. + +**결론**: v2(PMPLUS20)의 `REAL_PRICE`가 더 정확한 값. 차이는 시스템 개선에 의한 정상 차이. + +### 보험구분별 +| 보험구분 | v1 건수 | v2 건수 | 일치 | +|---------|--------|--------|------| +| 0 (건강보험) | 191 | 191 | ✅ | +| 1 (의료급여) | 16 | 16 | ✅ | +| 9 (비급여) | 2 | 2 | ✅ | +| F (차상위) | 2 | 2 | ✅ | + +### 결제수단별 +| 구분 | v1 건수 | v2 건수 | 일치 | +|------|--------|--------|------| +| 9 (카드) | 179 | 179 | ✅ | +| A | 24 | 24 | ✅ | +| 0 | 5 | 5 | ✅ | +| 2 (현금) | 3 | 3 | ✅ | + +--- + +## 6. 남은 작업 + +- [x] 테이블 매핑 확정 (PS_MAIN → TBSID040_03) +- [x] 컬럼 매핑 확정 (13개 컬럼 전체) +- [x] 수납 테이블 매핑 확정 (CD_SUNAB → TBSIR000_01) +- [x] 변환 쿼리 작성 +- [ ] `queries/v2_pmplus20.py` 코드 수정 +- [ ] `/api/compare`로 전체 기간 수치 검증 +- [ ] 보험별/시간별/결제별/병원별 세부 수치 검증 + +--- + +## 7. 참고: PMPLUS20 주요 테이블 구조 (210개 중 데이터 있는 것) + +| 테이블 | 행수 | 추정 역할 | +|--------|------|----------| +| TBSWH040_02 | 6,533,993 | 입고 이력 상세 | +| TBSID040_05 | 1,543,564 | 조제 상세 (처방전 약품) | +| TBSID040_04 | 1,542,882 | 조제 상세 (원본) | +| TBSIR000_01 | 426,751 | **수납/결제 정보** | +| TBSID040_03 | 394,802 | **처방 헤더 (= PS_MAIN)** | +| TBSIM040_01 | 188,852 | 약품 마스터 (= CD_GOODS) | +| TBSIM040_29 | 49,161 | OTC 판매 상세 (= SALE_SUB) | +| TBSIM040_28 | 28,813 | OTC 판매 헤더 (= SALE_MAIN) | diff --git a/app.py b/app.py index 94d0f81..72b7c36 100644 --- a/app.py +++ b/app.py @@ -305,4 +305,4 @@ if __name__ == '__main__': print(" /v2/api/stats/... - PMPLUS20 (동일 구조)") print("") print(" /api/compare - v1 vs v2 비교") - app.run(host='0.0.0.0', port=5060, debug=True) + app.run(host='0.0.0.0', port=5050, debug=True) diff --git a/config.py b/config.py index 93e2b7d..1131bc5 100644 --- a/config.py +++ b/config.py @@ -4,7 +4,7 @@ import os # PharmIT3000 (v1) PHARMIT3000_CONFIG = { - 'server': os.getenv('PHARMIT_SERVER', '192.168.0.4\\PM2014'), + 'server': os.getenv('PHARMIT_SERVER', '192.168.0.201\\PM2014'), 'database': 'PM_PRES', 'username': os.getenv('PHARMIT_USER', 'sa'), 'password': os.getenv('PHARMIT_PASS', 'tmddls214!%('), @@ -12,8 +12,8 @@ PHARMIT3000_CONFIG = { # PMPLUS20 (v2) PMPLUS20_CONFIG = { - 'server': os.getenv('PMPLUS_SERVER', '192.168.0.4\\PM2014'), + 'server': os.getenv('PMPLUS_SERVER', '192.168.0.201\\PMPLUS20'), 'database': 'PM_MAIN', 'username': os.getenv('PMPLUS_USER', 'sa'), - 'password': os.getenv('PMPLUS_PASS', 'tmddls214!%('), + 'password': os.getenv('PMPLUS_PASS', 'newpro2020!@'), } diff --git a/queries/v2_pmplus20.py b/queries/v2_pmplus20.py index 3c7efdd..003479d 100644 --- a/queries/v2_pmplus20.py +++ b/queries/v2_pmplus20.py @@ -3,14 +3,23 @@ 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 +- PM_PRES.PS_MAIN → PM_MAIN.TBSID040_03 (처방 헤더) +- PM_PRES.CD_SUNAB → PM_MAIN.TBSIR000_01 (수납/결제, DRUG_SEQ로 JOIN) + +컬럼 매핑: +- PreSerial → DRUG_SEQ +- INDATE → SUNAB_DT +- PRICE_T → TOT_PRICE, PRICE_C → INS_PRICE, PRICE_P → EXE_PRICE, PRICE_N → REAL_PRICE +- S_Prep → INS_PREP_PRICE +- Drug_T4 → NON_DRUG_PRICE + EXP_EXE_PRICE +- PreGubun → MPRE_TYPE (값 체계 동일: 0,1,9,F 등) +- Holiday → HD_ADD + PRES_TIME_GUBUN 조합 +- Appr_Gubun → APPR_GUBUN (값 동일) +- nAPPROVAL_NUM → CARD_ADM_NO 주의: -- DrugCode vs Drug_Code 대소문자 차이 -- TBSIR000_01.RECP_DT는 datetime (변환 필요) +- PRES_GUBUN='E' (재고보정) 레코드 제외 필요 +- MPRE_TYPE_GUBUN='F'인 경우 PreGubun='F' (차상위) """ import pyodbc from datetime import date @@ -53,36 +62,43 @@ def get_sales_stats(date_from: str, date_to: str) -> dict: dict: {total, by_gubun, by_age, by_time, by_pay, by_hosp} Note: - 컬럼명/테이블명은 실제 PMPLUS20 스키마 확인 후 수정 필요 + TBSID040_03(처방헤더) + TBSIR000_01(수납) 매핑 확정 (2026-04-01) """ - # 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 ? + m.DRUG_SEQ AS PreSerial, + CASE + WHEN m.MPRE_TYPE_GUBUN = 'F' THEN 'F' + ELSE ISNULL(m.MPRE_TYPE, '0') + END AS PreGubun, + m.PAT_JUMIN_NO AS PaNum, + m.YOYANG_NM AS OrderName, + CASE + WHEN m.HD_ADD = '2' AND m.PRES_TIME_GUBUN = '1' THEN '4' + WHEN m.HD_ADD = '2' THEN '2' + WHEN m.PRES_TIME_GUBUN = '1' THEN '3' + ELSE '1' + END AS Holiday, + m.TOT_PRICE AS PRICE_T, + (ISNULL(m.NON_DRUG_PRICE, 0) + ISNULL(m.EXP_EXE_PRICE, 0)) AS Drug_T4, + m.INS_PREP_PRICE AS S_Prep, + m.INS_PRICE AS PRICE_C, + m.EXE_PRICE AS PRICE_P, + m.REAL_PRICE AS PRICE_N, + ISNULL(n.APPR_GUBUN, '') AS Appr_Gubun, + ISNULL(n.CARD_ADM_NO, '') AS nAPPROVAL_NUM + FROM PM_MAIN..TBSID040_03 m + LEFT JOIN PM_MAIN..TBSIR000_01 n ON n.DRUG_SEQ = m.DRUG_SEQ + WHERE m.SUNAB_DT BETWEEN ? AND ? + AND m.PRES_GUBUN != 'E' """ - + try: rows = query(sql, (date_from, date_to)) except Exception as e: return { - 'error': str(e), - 'note': 'PMPLUS20 스키마 확인 필요. 컬럼명/테이블명이 다를 수 있음.' + 'error': str(e), + 'note': 'PMPLUS20 쿼리 오류. TBSID040_03/TBSIR000_01 테이블 확인 필요.' } ref_date = date(