Files
pharmacy-stats-api/PMPLUS20_MIGRATION_GUIDE.md
root bc0c282eaf docs: V1 PS_Main_Sub 선별급여/상한제 스키마 분석 추가
V1 PharmIT3000 구조 분석:
- PS_Main: 처방헤더 (PRICE_N만, 합산 안됨)
- PS_Main_Sub: 처방 부가정보 (SE_PRICE_P_80 등 선별급여 컬럼 있음!)
- CD_SUNAB: 수납 (ETC_CARD에 합산 결과 저장)

V1 버그 원인:
- 수납 화면에서는 PS_Main + PS_Main_Sub JOIN해서 계산
- CD_SUNAB.ETC_CARD에 정확히 저장
- 하지만 PS_Main.PRICE_N은 업데이트 안 함

SE_PRICE 컬럼 이력:
- PS_Main_Sub 테이블: 2010년 초기 스키마에 생성
- SE_PRICE_P_* 컬럼: 선별급여 제도 시행 후 추가
- 실제 데이터: 2026년부터 6건 존재

V2 PMPLUS20 개선:
- REAL_PRICE에 모든 항목 합산해서 저장
- 수납테이블 RECP_AMT와 일치 보장
2026-04-01 15:26:58 +00:00

12 KiB

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 관계

-- 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 + MPRE_TYPE_GUBUN)

v2에서 MPRE_TYPE='4'는 특수보험 그룹이며, MPRE_TYPE_GUBUN으로 세부 구분함.

v1 PreGubun v2 MPRE_TYPE v2 MPRE_TYPE_GUBUN 의미 건수 (202603)
'0' '0' '0' 건강보험 3,325건
'1' '1' '0' 의료급여1종 259건
'2' '2' '0' 산재 17건
'3' '3' '0' 의료급여2종 6건
'7' '4' 'C' 차상위1 7건
'E' '4' 'E' 보훈 32건
'F' '4' 'F' 차상위2 11건
'9' '9' '0' 비급여 34건

v2에서 PreGubun 변환 SQL:

CASE
    WHEN m.MPRE_TYPE = '4' AND m.MPRE_TYPE_GUBUN = 'E' THEN 'E'  -- 보훈
    WHEN m.MPRE_TYPE = '4' AND m.MPRE_TYPE_GUBUN = 'F' THEN 'F'  -- 차상위2
    WHEN m.MPRE_TYPE = '4' AND m.MPRE_TYPE_GUBUN = 'C' THEN '7'  -- 차상위1
    ELSE ISNULL(m.MPRE_TYPE, '0')
END AS PreGubun

주의: 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 계산 공식:

# 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 쿼리 (변환 완료)

SELECT
    m.DRUG_SEQ       AS PreSerial,
    -- 보험구분 변환: MPRE_TYPE + MPRE_TYPE_GUBUN → PreGubun
    CASE
        WHEN m.MPRE_TYPE = '4' AND m.MPRE_TYPE_GUBUN = 'E' THEN 'E'  -- 보훈
        WHEN m.MPRE_TYPE = '4' AND m.MPRE_TYPE_GUBUN = 'F' THEN 'F'  -- 차상위2
        WHEN m.MPRE_TYPE = '4' AND m.MPRE_TYPE_GUBUN = 'C' THEN '7'  -- 차상위1
        ELSE ISNULL(m.MPRE_TYPE, '0')
    END 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 매핑 상세 분석

결론: REAL_PRICE = 진짜 수납금액

PMPLUS20의 REAL_PRICE는 수납테이블(TBSIR000_01)의 RECP_AMT, SUNAB_PRICE와 정확히 일치함.

검증 케이스 (20260324)

환자 보험구분 v1 PRICE_N v2 REAL_PRICE v2 수납(RECP_AMT) 설명
박명순 건강보험+비급여 89,600 89,600 89,600 일반 케이스
김상훈 건강보험+비급여 70,900 70,900 70,900 일반 케이스
지성윤 차상위2(F) 500 11,920 11,920 특수 케이스 ⚠️
임해자 건강보험+선별급여 19,700 31,700 31,700 특수 케이스 ⚠️

일반 환자 (건강보험 + 비급여)

REAL_PRICE = EXE_PRICE(본인부담) + NON_DRUG_PRICE(비급여약) + EXP_EXE_PRICE(비급여조제)
         = PRICE_N (정확히 일치)

특수 보험 환자 (차상위/선별급여)

지성윤 (차상위2):

EXE_PRICE = 500 (기본 본인부담)
EXP_UNDER_EXE_PRICE = 11,420 (상한제초과 본인부담)
REAL_PRICE = 500 + 11,420 = 11,920 (진짜 수납액)
v1 PRICE_N = 500 ← 상한제초과분 누락!

임해자 (건강보험 + 선별급여 80%):

EXE_PRICE = 19,700 (기본 본인부담)
SE_PRICE_P_80 = 12,000 (선별급여 80% 본인부담)
REAL_PRICE = 19,700 + 12,000 = 31,700 (진짜 수납액)
v1 PRICE_N = 19,700 ← 선별급여분 누락!

v1 스키마 분석: 선별급여/상한제 데이터는 어디에?

V1 PharmIT3000에도 데이터는 있다! PS_Main_Sub 테이블에 저장됨:

-- V1 PharmIT3000에서 선별급여/상한제 확인
SELECT 
    m.PreSerial, m.PRICE_N,
    s.SE_PRICE_P_80,     -- 선별급여 80% 본인부담
    s.SE_PRICE_P,        -- 선별급여 본인부담 합계
    c.ETC_CARD           -- 실제 수납액
FROM PS_Main m
JOIN PS_Main_Sub s ON s.PreSerial = m.PreSerial
JOIN CD_SUNAB c ON c.PRESERIAL = m.PreSerial
WHERE s.SE_PRICE_P > 0

V1 테이블 구조:

테이블 역할 선별급여/상한제
PS_Main 처방 헤더 PRICE_N만 (합산 안됨)
PS_Main_Sub 처방 부가정보 SE_PRICE_P_80, SE_PRICE_P
CD_SUNAB 수납 ETC_CARD에 합산 결과

검증 케이스 (V1):

환자 PS_Main.PRICE_N PS_Main_Sub.SE_PRICE_P_80 CD_SUNAB.ETC_CARD
지성윤(차상위2) 500 11,420 11,920
임해자(선별급여) 19,700 12,000 31,700

문제점: PS_Main.PRICE_NPS_Main_Sub의 선별급여/상한제 금액을 합산하지 않음.

선별급여 컨럼 추가 이력

  • PS_Main_Sub 테이블: 2010년 초기 스키마에 생성
  • SE_PRICE_P_* 커럼들: 나중에 추가됨 (선별급여 제도 시행 후)
  • 실제 데이터: 2026년부터 6건 존재 (최근 사용 시작)

V1 수납 프로세스 (추정)

  1. 수납 화면에서 PS_Main + PS_Main_Sub JOIN해서 총액 계산
  2. 계산된 금액을 CD_SUNAB.ETC_CARD에 저장
  3. 하지만 PS_Main.PRICE_N은 업데이트 안 함 (버그)

V2 PMPLUS20 개선 사항

PMPLUS20은 이 문제를 해결:

  • TBSID040_03.REAL_PRICE모든 항목 합산해서 저장
  • EXP_UNDER_EXE_PRICE, SE_PRICE_P_80 등 별도 컨럼도 유지
  • 수납테이블(TBSIR000_01)의 RECP_AMT와 일치 보장

결론: REAL_PRICE 매핑 유지가 맞음. V1의 PRICE_N 버그를 V2에서 수정한 것.

보험구분별 (2026년 3월 전체 검증)

보험구분 v1 건수 v2 건수 일치
0 (건강보험) 3,359 3,359
1 (의료급여1종) 271 271
2 (산재) 17 17
3 (의료급여2종) 6 6
7 (차상위1) 8 8
9 (비급여) 35 35
E (보훈) 32 32
F (차상위2) 11 11

결제수단별

구분 v1 건수 v2 건수 일치
9 (카드) 179 179
A 24 24
0 5 5
2 (현금) 3 3

6. 작업 이력

  • 테이블 매핑 확정 (PS_MAIN → TBSID040_03)
  • 컬럼 매핑 확정 (13개 컬럼 전체)
  • 수납 테이블 매핑 확정 (CD_SUNAB → TBSIR000_01)
  • 변환 쿼리 작성
  • queries/v2_pmplus20.py 코드 수정 (2026-04-01)
  • 보험구분 매핑 완료 - 보훈/차상위1/차상위2 정확히 변환 (2026-04-01)
  • 2026년 3월 전체 기간 보험구분별 건수 검증 완료 (V1=V2 일치)
  • /api/compare로 UI 수치 검증
  • 시간별/결제별/병원별 세부 수치 검증

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)