Files
pharmacy-stats-api/docs/REFACTOR_PLAN.md
청춘약국 e674c775b5 docs: 모듈 분리 리팩토링 계획서 추가
2026-04-08 v1 QT-POS 동기화 작업을 통해 드러난 근본 문제 (pharmon-web
과 stats-api 간 통계 로직 중복) 를 해결하기 위한 3단계 리팩토링 로드맵.

주요 내용:
- 현재 구조의 7가지 문제점 (P1~P7) 분석
- 4레이어 목표 아키텍처 (db → core → queries → api)
- pharma-stats-core 공통 패키지 분리 전략 (옵션 A/B/C 비교)
- 1단계: pharmacy-stats-api 내부 정돈 (pharmon-web 손대지 않음)
- 2단계: pharma-stats-core 분리 + pharmon-web 통합
- 3단계: v2 (PMPLUS20) 재동기화 + 선택적 독립 repo
- 리스크 & 완화 방안 (한글 경로 editable install 이슈 등)
- 이번 마이그레이션에서 학습한 설계 근거 (GPPOS 7항 공식,
  CD_SUNAB 1:N 함정, 보훈 세분화 문자열 키 등)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 20:58:43 +09:00

15 KiB
Raw Blame History

pharmacy-stats-api 모듈화 리팩토링 계획서

작성일: 2026-04-08 작성 시점 배경: v1_pharmit3000.py 를 QT-POS sales_stats_dialog.py 와 1:1 동기화 완료 직후. 이번 마이그레이션을 통해 "두 프로젝트 간 통계 로직 중복" 이라는 근본 문제가 드러났고, 다음 동기화 때는 수동 포팅이 반복되지 않도록 구조를 재설계한다.


1. 현재 구조와 문제점

1.1 현재 레이아웃

pharmacy-stats-api/
├── app.py                      322 lines  ← Flask 라우트 + 라벨 dict + 유틸
├── config.py                    22 lines  ← DB 커넥션 설정
├── queries/
│   ├── v1_pharmit3000.py       368 lines  ← 커넥션/캐시/쿼리/집계/유틸 전부
│   └── v2_pmplus20.py          ~250 lines ← v1 구버전 복제 (미동기화)
└── templates/stats.html

그리고 외부 의존 중복:

pharmon-web/
└── sales_stats_dialog.py       (_GUBUN_LABEL, _build_margin_cache,
                                 _query, _calc_age, _empty_row, _add …)

1.2 구체적 문제

# 문제 증상
P1 로직 중복 — pharmon-web sales_stats_dialog.py 의 쿼리/공식/캐시 빌더가 queries/v1_pharmit3000.py 에 통째로 복제됨 QT-POS 에서 버그 고치면 stats-api 도 수동 포팅 필요 (이번 CD_SUNAB 이중계산 버그가 정확히 이 케이스)
P2 라벨 dict 3중 중복_GUBUN_LABEL (pharmon-web) / GUBUN_LABEL (app.py) / 프론트 js 보훈 세분화(4_1~4_4) 추가 때 한 군데 놓침 → 날 UI 노출
P3 단일 파일 비대화v1_pharmit3000.py 368줄에 커넥션, 캐시, SQL, 집계, 유틸이 섞임 테스트 단위 분리 불가, 변경 시 diff 가독성 낮음
P4 엔드포인트 6개 × 2버전 중복app.py 의 v1/v2 라우트가 거의 동일 (day_from/to 파싱, 에러처리, label 붙이기) 라우트 추가 시 12곳 수정
P5 v2 (PMPLUS20) 미동기화 — v1 은 2026-04-08 재작성했지만 v2 는 구버전 로직 그대로 v1/v2 비교 API 가 사실상 무의미
P6 테스트 없음 — GPPOS 공식 (sales_amt, ins_drug, claim_amt), _calc_age 등이 regression 없이 수동 검증에만 의존 다음 리팩토링 때 수치 틀어져도 못 잡음
P7 DB 연결 구문 하드코드v1_* / v2_* 각자 get_connection() 작성 드라이버 변경 시 양쪽 수정

2. 목표 아키텍처

2.1 설계 원칙

  1. Single Source of Truth: 통계 공식·라벨·나이계산 같은 "도메인 로직" 은 한 곳에서만 정의하고, pharmon-web(PyQt) 과 pharmacy-stats-api(Flask) 가 동일 모듈을 import.
  2. Version 경계를 버전 모듈 안으로 가둔다: v1(PharmIT3000) / v2(PMPLUS20) 차이는 "SQL 과 컬럼 매핑" 뿐 — 공통 집계/공식 로직은 버전 무관.
  3. 레이어 분리: dbcorequeriesapi 의 단방향 의존. 상위 레이어만 하위를 import.
  4. 과도한 추상화 금지: 당장 중복 제거에 필요한 만큼만 쪼갠다. ORM 이나 Repository 패턴은 도입하지 않는다 (MSSQL 읽기 전용, 쿼리 복잡도 낮음).

2.2 목표 레이아웃

pharmacy-stats-api/
├── app.py                  ← create_app() factory + main. 라우트 직접 정의 안함
├── config.py               ← PHARMIT3000_CONFIG, PMPLUS20_CONFIG
├── requirements.txt
├── docs/
│   ├── REFACTOR_PLAN.md            (이 문서)
│   ├── PMPLUS20_MIGRATION_GUIDE.md
│   └── MODULE_BOUNDARY.md          (레이어 경계 설명, 2단계 때 작성)
│
├── db/                     ← 레이어 1. 순수 DB 커넥션
│   ├── __init__.py
│   └── connection.py       ← get_connection(cfg), query(conn_cfg, sql, params)
│
├── core/                   ← 레이어 2. 버전 독립 도메인 로직 (★재사용 핵심★)
│   ├── __init__.py
│   ├── labels.py           ← GUBUN_LABEL, TIME_LABEL, PAY_LABEL, AGE_LABEL
│   ├── formula.py          ← GPPOS 공식: sales_amt, ins_drug, ins_prep, claim_amt
│   ├── aggregator.py       ← empty_row(), add_row(), StatsAccumulator 클래스
│   ├── margin.py           ← build_margin_cache() — 쿼리 주입형 (Callable[[sql,params],rows])
│   ├── age.py              ← calc_age(panum, ref_date)
│   └── classifier.py       ← classify_gubun(), classify_time(), classify_pay()
│
├── queries/                ← 레이어 3. 버전별 SQL + core 조합
│   ├── __init__.py
│   ├── v1_pharmit3000.py   ← PS_MAIN SQL + get_sales_stats() (core 호출)
│   └── v2_pmplus20.py      ← TBSID040_03 SQL + get_sales_stats() (core 호출)
│
├── api/                    ← 레이어 4. Flask Blueprint
│   ├── __init__.py
│   ├── common.py           ← _default_from/to, _parse_date, 공통 응답 래퍼
│   ├── v1.py               ← Blueprint("v1", url_prefix="/v1")
│   ├── v2.py               ← Blueprint("v2", url_prefix="/v2")
│   └── compare.py          ← Blueprint("compare", url_prefix="/api")
│
├── templates/
│   └── stats.html
│
└── tests/                  ← 레이어 2 로직 regression 테스트
    ├── test_formula.py     ← sales_amt/ins_drug 공식 (QT-POS 스크린샷 고정값)
    ├── test_age.py         ← 1900/2000 세기, 윤년
    ├── test_labels.py      ← 보훈 4_1~4_4 매핑
    └── test_margin.py      ← last_cost > user_price*5 경계

2.3 의존 그래프

api/*  ──┐
         ▼
    queries/v1, v2
         │
         ▼
    core/*  ──┐
              ▼
           db/*
              ▼
         config.py

core/ 는 DB 를 직접 호출하지 않고, margin.build_margin_cache(rows, query_fn) 처럼 콜러블을 주입받는다. → core 는 pharmon-web 에서도 그대로 import 가능 (핵심 재사용 포인트).


3. pharmon-web 과의 공유 전략

3.1 3가지 옵션 비교

옵션 방법 장점 단점 권장
A. 복붙 유지 지금처럼 수동 동기화 0 작업 또 틀어짐. 매번 이번 같은 사고 ×
B. 로컬 경로 import pharma-stats-core 를 별도 폴더로 두고 양쪽에서 sys.path 또는 editable install (pip install -e ../pharma-stats-core) 저비용, 즉시 동기화, PyInstaller 에서도 hidden-import 처리만 추가하면 됨 배포 환경에서 경로 신경써야 함 ★ 2단계에서 채택
C. 독립 패키지 + git submodule pharma-stats-core 독립 repo, 둘 다 submodule 로 포함 버전 고정 가능, 장기적으로 깔끔 초기 설정 비용, PyInstaller 빌드 스크립트 수정 3단계 (선택적)

3.2 최종 그림 (옵션 B 채택 시)

c:\Users\청춘약국\source\
├── pharma-stats-core\          ← 신규 (core/ 를 여기로 이전)
│   ├── pyproject.toml          (editable install 용)
│   └── pharma_stats_core/
│       ├── __init__.py
│       ├── labels.py
│       ├── formula.py
│       ├── aggregator.py
│       ├── margin.py
│       ├── age.py
│       └── classifier.py
│
├── pharmacy-stats-api\
│   └── queries/v1_pharmit3000.py   ← `from pharma_stats_core import ...`
│
└── pharmon-web\
    └── sales_stats_dialog.py       ← `from pharma_stats_core import ...`

pharmon-web PyInstaller 영향:

  • pos_gui.spechiddenimports=['pharma_stats_core.formula', ...] 추가
  • datas=[('../pharma-stats-core/pharma_stats_core', 'pharma_stats_core')] 로 번들 포함
  • 또는 editable install 후 PyInstaller 가 정상 탐색하도록 --collect-all pharma_stats_core

4. 마이그레이션 로드맵 (3단계)

▶ 1단계: pharmacy-stats-api 내부 정리 (독립 진행, pharmon-web 손대지 않음)

목표: P2, P3, P4, P7 해결. pharmon-web 과의 중복(P1)은 아직 손대지 않음.

순서 작업 영향 파일 검증
1-1 db/connection.py 생성, get_connection(cfg) 하나로 통합 신규 스모크: v1/api/stats 기존과 동일 응답
1-2 core/labels.py 로 GUBUN_LABEL 이전, app.py 에서 import app.py, core/labels.py /v1/api/stats/insurance 라벨 동일
1-3 core/age.py, core/aggregator.py 추출 (_calc_age, _empty_row, _add) v1_pharmit3000.py 에서 제거 수치 동일
1-4 core/formula.py 에 GPPOS 공식 함수로 추출 — calc_sales_amt(row), calc_ins_drug(row) v1_pharmit3000.py 기존 응답 diff 0
1-5 core/margin.pybuild_margin_cache(rows, query_fn) 추출 (DB 주입형) v1_pharmit3000.py 1/3 스크린샷 수치 재검증
1-6 core/classifier.py 에 gubun/time/pay 분류 로직 추출 v1_pharmit3000.py 보험별/시간별/결제별 모두 동일
1-7 api/v1.py, api/v2.py, api/compare.py Blueprint 로 분리. app.py 는 factory 만 app.py, api/*.py 모든 엔드포인트 동일 응답
1-8 api/common.py_default_from/to, stats_response(result, version) 헬퍼 v1.py/v2.py 중복 제거 회귀 없음
1-9 tests/test_formula.py 추가 — QT-POS 1/3 스크린샷 수치를 고정값 assert tests/ pytest tests/ 통과

1단계 완료 조건:

  • v1_pharmit3000.py 가 < 150 줄 (현재 368 줄)
  • app.py 가 < 50 줄
  • 모든 엔드포인트 응답이 리팩토링 전과 1:1 동일 (기준: 2026-04-08 기준 1/3~4/8 응답)
  • pytest green

1단계에서 하지 않는 것:

  • pharmon-web 수정 — 아직 core/ 는 stats-api 내부에 머무름
  • v2 로직 재작성 — 구조만 맞추고 내용은 유지

▶ 2단계: pharma-stats-core 분리 (pharmon-web 통합)

목표: P1 해결. 양쪽이 같은 코어를 import.

순서 작업 검증
2-1 c:\Users\청춘약국\source\pharma-stats-core\ 신규 폴더, pharma_stats_core/ 패키지 + pyproject.toml pip install -e . 성공
2-2 pharmacy-stats-api/core/*pharma_stats_core/* 로 이동, stats-api 에서 editable install stats-api 전 엔드포인트 회귀 0
2-3 pharmon-web sales_stats_dialog.py_GUBUN_LABEL, _calc_age, _empty_row, _add, _build_margin_cache, GPPOS 공식을 pharma_stats_core import 로 교체 QT-POS 통계 다이얼로그 기존 스크린샷과 1:1 동일
2-4 pharmon-web PyInstaller spec 업데이트 (hiddenimports / --collect-all) 번들 exe 에서 통계 정상 동작
2-5 pharma-stats-coretests/ 이관, CI 없이 로컬 pytest 만 green
2-6 docs/MODULE_BOUNDARY.md 작성 — "core 는 Qt/Flask 둘 다 의존 금지" 원칙 문서화 문서 리뷰

2단계 완료 조건:

  • pharmon-web 에서 grep "GUBUN_LABEL\|_calc_age\|_build_margin_cache"sales_stats_dialog.py 에 없음 (전부 core 경유)
  • stats-api v1/v2 응답 회귀 0
  • QT-POS 통계 다이얼로그 회귀 0 (스크린샷 수치 동일)
  • PyInstaller 빌드 성공 + 런타임 정상

▶ 3단계: v2 (PMPLUS20) 재동기화 + 선택적 확장

목표: P5, P6 해결.

순서 작업
3-1 queries/v2_pmplus20.py 를 v1 과 동일 패턴으로 재작성. SQL 만 v2 테이블(TBSID040_03) 로 바뀌고, 집계/공식은 pharma_stats_core 그대로 사용
3-2 v2 의 테이블/컬럼 매핑 차이 (MPRE_TYPEPreGubun, DRUG_SEQPreSerial 등) 를 queries/v2_pmplus20.py 상단에서 row 딕셔너리 normalize 로 흡수 → core 에는 "v1 스키마" 하나만 존재
3-3 /api/compare 가 실제로 v1/v2 매출 비교 의미있는 수치 반환
3-4 (선택) pharma-stats-core 를 git submodule 또는 별도 repo 로 승격 — 옵션 C 전환

5. 리스크 & 완화

리스크 영향 완화
1단계 리팩토링 중 수치 틀어짐 매출 통계 오차 각 단계마다 1/3 스크린샷 기준값을 tests/test_formula.py 에 고정 후 비교
pharmon-web PyInstaller 에서 core 못 찾음 QT-POS 통계 다이얼로그 에러 2-4 단계에서 dev 빌드로 먼저 검증, hidden import 리스트는 ANALYSIS_02_libraries.md 패턴 참고
v2 포팅이 미뤄져 compare API 망가짐 비교 기능 부정확 3단계 전까지 compare 엔드포인트에 "v2 is legacy, may differ" 경고 플래그 붙여둠
1단계에서 너무 많이 쪼개서 과설계 읽기 어려워짐 core 파일당 < 100줄 목표, 기능별로 1파일 1책임. 억지로 클래스화 하지 않음
editable install 이 한글 경로 문제 일으킴 pip install -e 실패 실패 시 sys.path.append('../pharma-stats-core') fallback (pharmon-web 에 이미 패턴 있음)

6. 이번 마이그레이션에서 배운 것 (설계 근거)

2026-04-08 작업을 통해 확인한 사실:

  1. QT-POS 가 Source of Truth: 통계 로직은 pharmon-web 에서 먼저 수정되고, stats-api 는 후행 동기화. → core 는 pharmon-web 의 인터페이스를 기준으로 설계한다.
  2. GPPOS 공식은 절대 단순화 금지: sales_amt = PRICE_C+PRICE_P+S_FASTMON+S_TEMP3+SE_PRICE_C+SE_PRICE_P+SE_BOHUN_C — 7항 가산 공식. 중간에 하나라도 빠지면 1원 단위 오차 발생. → core/formula.py 는 docstring 에 GPPOS2 델파이 레거시 근거 명시.
  3. CD_SUNAB 1:N 함정: LEFT JOIN 하면 행 중복 → 금액 이중계산. 반드시 OUTER APPLY TOP 1 패턴. → core 가 아닌 queries/v1 쪽 SQL 에 고정.
  4. 보훈 세분화 키는 문자열 '4_1': 숫자 아님. dict 키. → core/labels.py + core/classifier.py 둘 다에서 문자열로만 처리.
  5. 라벨 누락은 UI 에 raw 코드 노출: dict.get(code, code) fallback 이 역설적으로 버그 은폐. → 2-2 단계에서 라벨 통합 후, 미매핑 코드는 ? 대신 로그 경고 남기는 것도 검토.

7. 의사결정 필요 항목

다음 사항은 1단계 시작 전에 사용자 확답 필요:

  • 1단계를 언제 시작할지 — 지금 바로? 아니면 QT-POS 추가 기능 작업 후?
  • editable install vs sys.path — pharmon-web 한글 경로(청춘약국) 에서 pip install -e 검증 필요. 실패 시 sys.path 경로 주입 방식으로 폴백.
  • 테스트 기준값 스냅샷 — 2026-04-08 1/3 데이터를 "황금 데이터" 로 고정할지, 아니면 매 분기 갱신할지.
  • v2 폐기 여부 — PMPLUS20 마이그레이션이 현재 진행 중이 아니라면 v2 모듈을 stub 으로 두는 것도 옵션.

8. 참고

  • pharmon-web 분석 문서: pharmon-web/docs/ANALYSIS_03_database.md (MSSQL 스키마)
  • pharmon-web 분석 문서: pharmon-web/docs/ANALYSIS_02_libraries.md (PyInstaller hidden imports)
  • 이번 v1 동기화 변경점: queries/v1_pharmit3000.py 상단 docstring 1~32줄
  • GPPOS 델파이 원본 참고: Unit_Statistics.pas (pharmon-web 레거시)