Compare commits

..

54 Commits

Author SHA1 Message Date
2ca5622bbd feat: 의약품 마스터 DB 연동 및 한약재/OTC 구분 체계 구축
- herb_items 테이블에 product_type, standard_code 컬럼 추가
- POST /api/purchase-receipts/from-cart API 구현 (표준코드 기반 입고)
- 5개 API에 product_type/standard_code 필드 추가
- 프론트엔드 전역 구분 표시: 한약재/OTC 배지, 보험코드/표준코드 구분
- 경방신약 주문 매핑 문서 작성 (38건, 총액 1,561,800원)
- DB 스키마 백업 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 13:39:59 +00:00
9dd1f41bbb feat: 의약품 마스터 입고 장바구니 UI 구현
검색 결과에서 제품을 장바구니에 담고, 종이 입고장 기준으로
수량/단가 입력 시 g당단가·금액을 자동 계산하는 프론트엔드 플로우.
DB 연동은 추후 구현 예정.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:11:56 +00:00
c0d55f8e16 feat: 의약품 마스터 DB 연동 (ATTACH DATABASE)
- medicine_master.db (305,522행) CSV→SQLite 변환 완료
- get_db()에서 ATTACH DATABASE로 자동 연결
- GET /api/medicine-master/search: 상품명/업체명/표준코드 검색
- GET /api/medicine-master/categories: 전문일반구분별 통계
- config.py에 MEDICINE_MASTER_PATH 추가
- 취소된 제품 자동 필터링, 카테고리 필터 지원

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:19:14 +00:00
725f14c59a feat: 조제 원가 미리보기 및 재고 상태 표시 개선
원가 미리보기:
- 조제 실행 전 약재별 예상 원가(용량×단가) 및 합계 표시
- 용량/원산지/로트 변경 시 실시간 갱신
- 추가 약재의 이름 표시 오류 수정 (select 내 전체 옵션 텍스트 → 선택값만)

원산지 자동 선택:
- 처방 로드 시 재고 충분한 최저가 원산지를 자동 선택
- "자동 선택" 상태가 아닌 실제 원산지가 선택되어 원가 즉시 계산

재고 상태 표시:
- checkStockForCompound() TODO 제거, 실제 API 호출로 재고 확인
- 기존 원산지 선택을 덮어쓰지 않고 재고 상태만 갱신
- 선택 가능한 원산지가 2개 이상이면 "N종" 뱃지 표시

조제 폼 초기화:
- 새 조제 시 제수 기본값(1)으로 총 첩수(20)/파우치(30) 자동 설정
- 처방 선택 시 총 첩수가 비어있으면 자동 계산

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:50:21 +00:00
974ce5f655 feat: 한퓨어 엑셀 형식 지원 및 조제 용도 구분(usage_type) 추가
한퓨어 엑셀:
- ExcelProcessor에 hanpure 형식 자동 감지 및 처리 추가
- 옵션항목에서 중량 파싱 (600g*5개 → 3000g 등)
- 주문번호에서 입고일 추출, ingredient_code 직접 활용

조제 용도 구분:
- compounds.usage_type 컬럼 추가 (SALE/SELF_USE/SAMPLE/DISPOSAL)
- 조제 실행 시 용도 선택 드롭다운
- 조제 목록에서 용도 뱃지 클릭으로 사후 변경 가능
- 비판매 용도 시 sell_price_total=0, 매출 통계 제외
- PUT /api/compounds/:id/usage-type API 추가
- 용도 구분 설계 문서 (docs/조제_용도구분_usage_type.md)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:34:10 +00:00
69be63d00d docs: 제품 3단계분류 문서 추가, DB 초기화/복원 스크립트
- 제품 3단계분류.md: 성분→제품→로트 분류 체계, AI display_name 채우기 절차
- reset_operational_data.py: 마스터 보존 + 운영 데이터 초기화
- restore_backup.py: 백업 선택 복원 스크립트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:42:31 +00:00
50883a6a84 feat: 100처방 구성약재 수 표시, 등록 여부 백엔드 판정, 이름매칭 개선
- official_formulas API에 ingredient_count 추가 (LEFT JOIN COUNT)
- 등록 여부를 백엔드에서 판정 (official_formula_id FK 1차 + 이름 포함 매칭 fallback)
- 내 처방 100처방 뱃지 매칭: startsWith → includes 변경
- 100처방 목록에 구성약재 수 뱃지 컬럼 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:41:40 +00:00
1679f75d33 feat: 100처방 UI, 처방 가감 표시, 어울림 스타일링
- 처방 관리 페이지에 100처방 원방 마스터 섹션 추가 (검색 포함)
- 100처방 상세 모달 (구성약재, 참고자료 편집, 내 처방으로 등록)
- 내 처방 목록에 100처방 뱃지 및 가감 정보 표시
  - 변경: 파란 뱃지, 추가: 초록 뱃지, 제거: 빨간 뱃지
  - 원방 그대로: 회색 뱃지
- "어울림" 접두어 초록색 볼드 스타일링
- stock_ledger에 RETURN(반환)/DISCARD(폐기) 한글 라벨 추가
- 수동입고 원산지 드롭다운 변경, 재고 상세 유통기한 표시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:15:44 +00:00
51e0c99c77 feat: 100처방 API, 조제취소 재고복원, 처방 가감 비교 기능
- 100처방 마스터 CRUD API (목록조회/수정/구성약재조회)
- 100처방 시드 데이터 100개 처방 로드 (init_db)
- formulas API에 official_formula_id 및 가감(추가/제거/변경) 정보 포함
- create_formula: herb_item_id → ingredient_code 기반으로 수정
- create_formula: official_formula_id 저장 지원
- 조제 취소(CANCELLED) 시 inventory_lots 재고 복원 + stock_ledger RETURN 기록
- 입고장 삭제 시 취소된 조제의 compound_consumptions 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:15:33 +00:00
87e839be14 feat: 100처방 마스터 테이블 스키마 및 관련 문서 추가
- official_formulas, official_formula_ingredients 테이블 스키마 추가
- 100처방 마스터데이터 등록 가이드 (Agent용 절차/규칙/코드 템플릿)
- 한약국 첩제 vs OTC 상담 가이드
- 한약국 AI데이터 기본이해 문서
- 가미패독산 업셀링 칼럼, 입고장 수정기능 구현 문서
- CLAUDE.md에 참고 문서 경로 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:15:25 +00:00
3a39951fdc feat: 수동입고 기능 구현 및 입고일 날짜 포맷 버그 수정
- 수동입고 API (POST /api/purchase-receipts/manual) 추가
- 수동입고 모달 UI 구현 (도매상 선택, 품목 동적 추가, 금액 자동계산)
- 도매상 등록 모달 z-index 처리 (수동입고 모달 위에 표시)
- Excel 입고 시 receipt_date 튜플/대시 없는 날짜 포맷 정규화
- inventory_lots에 lot_number, expiry_date 저장 누락 수정
- CLAUDE.md 추가 (lot_id vs lot_number 구분 가이드)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:34:56 +00:00
3d13c0b1f3 feat: 전화번호/주민번호 포맷팅 및 대시보드 매출 통계 추가
- 전화번호 포맷팅 (010-1234-5678 형식) 전역 적용
- 주민번호 마스킹 포맷팅 (980520-1****** 형식)
- 대시보드에 총 마일리지, 이번달 매출, 마진, 마진율 통계 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:41:28 +00:00
605db69daa fix: 판매 모달에서 마일리지 표시 오류 수정
- patient_id를 통한 직접 조회로 변경
- /api/patients/search 엔드포인트 추가
- 판매 버튼에 patient_id 데이터 속성 추가
- loadPatientMileage 함수 개선 (patient_id 기반 조회)

이제 박주호 회원의 50,000 마일리지가 판매 모달에서 정상 표시됨

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 06:32:31 +00:00
3f96b286d3 fix: 환자 편집 버튼 기능 수정
- 환자 편집 버튼 클릭 이벤트 핸들러 추가
- 편집 모드에서 환자 정보 불러오기 구현
- 마일리지 정보 표시 (현재 잔액, 총 적립, 총 사용)
- 환자 정보 수정 API 연동 (PUT 메소드)
- API에 마일리지 정보 포함하도록 수정
- 모달 닫힐 때 폼 초기화 처리

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 05:49:00 +00:00
ee15d8e45e feat: 회원 관리 페이지에 마일리지 표시 추가
- 환자 목록에 마일리지 잔액 컬럼 추가
- 마일리지가 있는 회원은 돼지저금통 아이콘 표시
- 환자 편집 모달에 마일리지 관리 섹션 추가
  - 현재 잔액, 총 적립, 총 사용 표시
  - 마일리지 적립/차감 버튼 (추후 구현 예정)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 05:44:34 +00:00
f3f1efd8c2 feat: 판매관리 시스템 Phase 1 및 마일리지 시스템 구현
- 판매 관리 기능 추가
  - compounds 테이블에 판매 관련 컬럼 추가 (payment_method, discount_rate, delivery_method 등)
  - 판매 상태 관리 (조제완료→결제대기→결제완료→배송대기→배송완료)
  - 판매 처리 모달 UI 구현
  - 9가지 상태별 뱃지 표시

- 마일리지 시스템 구축
  - patients 테이블에 마일리지 컬럼 추가 (balance, earned, used)
  - mileage_transactions 테이블 생성 (거래 이력 관리)
  - 마일리지 사용/적립 기능 구현

- 복합 결제 기능
  - 할인율(%) / 할인액(원) 직접 입력 선택 가능
  - 마일리지 + 현금 + 카드 + 계좌이체 복합 결제
  - 결제 금액 자동 검증
  - 결제 방법 자동 분류 (복합결제 지원)

- API 엔드포인트 추가
  - POST /api/compounds/<id>/status (상태 업데이트)
  - PUT /api/compounds/<id>/price (가격 조정)
  - GET /api/sales/statistics (판매 통계)

- 데이터베이스 설정 통합
  - config.py 생성하여 DB 경로 중앙화

TODO: 처방별 기본가격 정책 시스템 (price_policies 테이블 활용)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 05:42:29 +00:00
ad9ac396e2 chore: 개발 파일 정리 및 구조화
- 개발/테스트 스크립트를 dev_scripts/ 폴더로 이동
- 스크린샷을 screenshots/ 폴더로 이동
- 백업 파일 보존 (.backup)
- 처방 관련 추가 스크립트 포함

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 04:44:48 +00:00
124bc5eaf8 feat: 처방 주요 효능(efficacy) 필드 추가 및 UI 개선
- DB: formulas 테이블에 efficacy 칼럼 추가
- API: 처방 생성/수정/조회 시 efficacy 필드 처리
- UI: 처방 등록/수정 모달에 주요 효능 입력 필드 추가
- UI: 처방 상세 화면에 주요 효능 표시
- 기존 처방들의 주요 효능 데이터 입력 완료

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 04:39:05 +00:00
95df32c14d fix: 기존 처방 약재 및 효능 정보 수정
- 쌍화탕: 당귀 → 일당귀로 수정
- 월비탕: 진피초 → 진피(陳皮)로 수정
- 십전대보탕: 각 약재별 효능 설명 추가
  - 보음보혈, 보혈지통, 대보원기 등 11개 약재 효능 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 04:37:16 +00:00
f1034c197f feat: 월비탕 및 삼소음 처방 추가
- 월비탕 1차~4차 단계별 처방 추가 (WBT001-1 ~ WBT001-4)
- 삼소음 처방 추가 (SSE001)
- 처방 추가 가이드 문서 작성
- 약재 성분 코드 확인 및 검증 스크립트 추가

월비탕: 단계별 비만치료 처방 (1차~4차)
삼소음: 리기화담, 해표산한 효능의 기침/가래 치료 처방

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 04:36:38 +00:00
831584f752 docs: 입고 테스트 가이드라인 및 개발 규칙 문서화
- 입고장 → 입고 라인 → LOT 생성 프로세스 명시
- inventory_lots 직접 INSERT 금지 규칙
- 재고 계산 모드별 설명
- 데이터베이스 구조 및 흐름도 추가
- 개발 시 주의사항 정리

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 02:46:27 +00:00
7314c0075d docs: 데이터 구조 및 흐름 문서 완성
- 전체 시스템 데이터 흐름 문서 추가
- 테이블 관계 및 JOIN 경로 명확화
- ingredient_code 중심 설계 반영
- claude.md 메인 문서 추가
- 한약재 정보 관리 시스템 설계 문서 개선

주요 내용:
- 코드 체계 (성분코드 vs 보험코드) 설명
- 개선된 JOIN 구조 (5단계 → 3단계)
- 효능 태그 시스템 리팩토링 반영
- 개발 가이드 및 주의사항 포함
2026-02-17 03:26:37 +00:00
28991c5743 refactor: herb_item_tags를 ingredient_code 기반으로 개선
- herb_id 대신 ingredient_code 사용 (더 직관적)
- 복잡한 JOIN 체인 제거
  Before: items → products → masters → extended → tags (5단계)
  After:  items → products → tags (3단계)
- 성능 개선 및 코드 가독성 향상
- 모든 API 정상 작동 확인
2026-02-17 03:20:35 +00:00
13b56bc1e9 fix: 효능 태그 JOIN 오류 수정 및 올바른 테이블 관계 구현
- herb_item_id ≠ herb_id 문제 해결
- herb_products를 통한 올바른 JOIN 경로 구현
  (herb_items → herb_products → herb_masters → herb_master_extended → tags)
- /api/herbs, /api/herbs/masters, /api/inventory/summary 모두 정상 작동
- 감초에 효능 태그 추가 (보기, 청열, 해독, 거담, 항염)

이제 조제 페이지 약재 추가 드롭다운 정상 작동
재고 현황 페이지 정상 표시
2026-02-17 03:17:24 +00:00
037e307637 feat: 약재 효능 태그 시스템 추가
- herb_efficacy_tags 테이블 생성 (효능 마스터)
- herb_item_tags 테이블 생성 (약재-효능 다대다 관계)
- 18개 기본 효능 태그 등록 (보혈, 활혈, 보기 등)
- Git 사용 가이드라인 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 02:57:57 +00:00
3f4b9c816a fix: 로트 배분 모달 필요량 0 표시 버그 수정
문제:
- 원산지 선택에서 "수동 배분" 선택 시 필요량이 0으로 표시
- loadOriginOptions 함수 호출 시점의 requiredQty를 그대로 사용

해결:
- 모달 열기 시점에 현재 행의 실제 필요량 재계산
- gramsPerCheop × cheopTotal로 실시간 계산
- 디버깅 로그 추가로 값 확인 가능

이제 첩수나 용량이 변경된 후에도 정확한 필요량이 모달에 표시됩니다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 02:23:37 +00:00
7d2b458e31 fix: 수동 로트 배분 시 remaining_qty 처리 수정
문제:
- 수동 로트 배분 후 remaining_qty를 감소시키지 않아 재고 부족 오류 발생
- 재고 부족 체크를 수동 배분에서 제외했던 임시 처리

해결:
- 수동 로트 배분 시에도 remaining_qty 감소 처리 추가
- 재고 부족 체크를 수동/자동 모두에 적용하도록 복원
- 이제 수동 배분도 정확한 재고 검증 수행

검증 테스트 추가:
- 배분 합계 불일치 시 오류
- 로트 재고 부족 시 오류
- 존재하지 않는 로트 사용 시 오류

이제 수동 로트 배분도 자동 선택과 동일한 수준의 재고 검증을 수행합니다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 02:18:34 +00:00
0f40cdfba7 feat: 복합 로트 사용 기능 구현 (수동 로트 배분)
## 구현 내용

### 1. 백엔드 (app.py)
- 수동 로트 배분 지원 (lot_assignments 배열 처리)
- 각 로트별 지정 수량만큼 재고 차감
- 검증: 배분 합계 확인, 재고 충분 확인
- compound_consumptions 테이블에 각 로트별 소비 기록

### 2. 프론트엔드 (app.js, index.html)
- 로트 배분 모달 UI 구현
  - 로트별 재고, 단가 표시
  - 수동 입력 및 자동 배분 기능
  - 실시간 합계 계산 및 검증
- 원산지 선택에 "수동 배분" 옵션 추가 (로트 2개 이상 시)
- 조제 저장 시 lot_assignments 포함

### 3. 테스트
- 테스트용 당귀 로트 추가 (한국산)
- E2E 테스트 성공
  - 당귀 100g을 2개 로트(중국산 60g + 한국산 40g)로 배분
  - 각 로트별 재고 정확히 차감
  - 소비 내역 올바르게 기록

## 장점
- DB 스키마 변경 없음
- 기존 자동 선택과 호환
- 재고 부족 시 여러 로트 조합 가능
- 원가 최적화 가능

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 02:16:02 +00:00
6ad8bac5c2 fix: 커스텀 처방 감지 로직 개선 및 기존 데이터 업데이트
- 문제: ingredient_code 기준 비교 누락으로 인삼 제거가 감지되지 않음
  - 인삼(3400H1AHM)은 herb_items에 없어서 herb_item_id 매핑 실패
  - 원 처방에만 있고 실제 재고가 없는 약재 처리 불가

- 해결: ingredient_code 기준 비교 로직 추가
  - original_by_code 딕셔너리로 원 처방 구성 저장
  - actual_by_code 딕셔너리로 실제 조제 구성 저장
  - 제거된 약재를 ingredient_code 기준으로 감지

- 조제 상세 모달에 가감방 표시 추가
  - 처방명 옆에 "가감" 뱃지 표시
  - 변경 내용 요약 표시

- 기존 데이터 업데이트 스크립트 추가
  - 십전대보탕에서 인삼 제거 감지 성공
  - compound #4를 가감방으로 정상 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 01:38:23 +00:00
1441c01fb4 feat: 실시간 커스텀 처방(가감방) 감지 시스템 구현
- 프론트엔드: 조제 시 실시간 커스텀 처방 감지
  - 처방 선택 시 원래 구성 약재 저장
  - 약재 추가/삭제/변경 시 즉시 감지
  - 가감방 뱃지 및 변경 내용 표시

- 백엔드: 커스텀 처방 자동 감지 및 저장
  - compounds 테이블에 커스텀 관련 필드 추가
  - 조제 시 원 처방과 비교하여 변경사항 자동 감지
  - 커스텀 처방 정보 저장 (추가/제거/변경된 약재)

- 환자 조제 내역에 커스텀 처방 표시
  - 가감방 뱃지 표시
  - 변경 내용 상세 표시

- DB 마이그레이션 스크립트 추가
  - is_custom, custom_summary, custom_type 필드 추가
  - compound_ingredients에 modification_type, original_grams 필드 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-17 01:28:44 +00:00
d6410fa273 test: variant 시스템 적용 테스트 스크립트 추가
- inventory_lots 현황 확인
- supplier_product_catalog 샘플 데이터 추가
- 가격 기반 매칭 테스트
- variant 속성 파싱 로직 검증

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 16:06:55 +00:00
fe19a72686 feat: 약재명 기반 직접 매핑 스크립트 추가
- 휴먼허브 약재와 한의사랑 display_name 1:1 매핑
- 29개 품목 완전 매핑 성공
- lot_variants 테이블 자동 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 16:06:38 +00:00
111c173692 feat: 한의사랑 카탈로그 import 스크립트 추가
- 마이페이지 데이터 파싱 및 DB 저장
- supplier_product_catalog 테이블 관리
- 가격 기반 매칭 시도 기능 포함

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 16:06:20 +00:00
490553881f feat: UI에 display_name 및 variant 정보 표시
- 입고 상세 모달에 display_name과 variant 뱃지 표시
- 재고 상세 모달에 품명 컬럼 추가
- 조제 시 원산지 선택에 display_name 표시
- 형태, 가공, 등급 정보를 색상별 뱃지로 구분

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 16:05:31 +00:00
4d230a2ca8 feat: API에 variant 정보 추가
- 입고 상세 API에 display_name 및 variant 속성 추가
- 재고 상세 API에 lot_variants 테이블 조인
- 조제용 재고 조회 API에 display_name 포함

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 16:05:05 +00:00
a03f344635 docs: 제품 세부분류(variant) 시스템 설계 문서 추가
- 동일 보험코드 내 세부 제품 구분 방안
- 형태, 가공법, 선별상태, 등급 등 variant 속성 정의
- display_name 활용 방안 및 가격 기반 매칭 전략

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 16:04:31 +00:00
29efa3d3c2 chore: uploads 폴더를 .gitignore에 추가 2026-02-16 14:41:30 +00:00
ae0d093044 test: 입고 프로세스 테스트 및 유틸리티 추가
추가된 파일:
1. reset_purchase_data.py
   - 입고 및 관련 데이터 초기화 스크립트
   - 조제, 재고, 입고장 데이터 완전 초기화
   - 잘못된 herb_items 정리 기능

2. test_improved_import.py
   - Excel 보험코드 9자리 패딩 테스트
   - 한의사랑/한의정보 형식 처리 확인

3. test_upload_api.py
   - API를 통한 Excel 업로드 테스트
   - 도매상 생성 및 입고 처리 검증
   - 재고 현황 확인

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 14:39:44 +00:00
f367781031 docs: 프로젝트 분석 및 개선 방안 문서 추가
추가된 문서:
1. 프로젝트_전체_분석.md
   - 시스템 아키텍처 분석
   - 디렉토리 구조 정리
   - 데이터베이스 설계 상세
   - 주요 기능 및 비즈니스 로직

2. 보험코드_매핑_문제_분석.md
   - Excel 입고 시 보험코드 처리 문제 분석
   - 앞자리 0 누락 문제 원인과 해결방안
   - 영향 범위 및 수정 방법

3. 입고_프로세스_개선방안.md
   - 성분코드-보험코드 매핑 구조 설명
   - 개선된 입고 프로세스 설계
   - 성분코드 기준 재고 관리 방법
   - 구현 우선순위 및 기대 효과

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 14:38:52 +00:00
849c2dd561 feat: 입고 시 herb_products에서 자동 정보 매핑
주요 개선사항:
1. 보험코드로 herb_products 테이블에서 자동 정보 조회
   - 성분코드(ingredient_code) 자동 매핑
   - 회사명(specification) 자동 입력
   - 표준 제품명 자동 적용

2. 입고 날짜 처리 버그 수정
   - pandas groupby 튜플 문제 해결
   - receipt_date 문자열 변환 처리
   - 입고번호 정상 생성 (PR-YYYYMMDD-XXXX)

3. 데이터 타입 문제 수정
   - total_amount numpy 타입을 float로 변환
   - JSON 직렬화 오류 방지

이제 Excel 입고 시 보험코드만으로 모든 정보 자동 입력

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 14:37:52 +00:00
1198c22083 fix: Excel 입고 시 보험코드 9자리 패딩 처리
- Excel 읽기 시 제품코드를 문자열로 처리하여 앞자리 0 보존
- 한의사랑/한의정보 형식 모두 보험코드 9자리 패딩 적용
- 예: 60600420 → 060600420 자동 변환

이제 Excel 파일의 보험코드가 숫자로 읽혀도 정상 처리됨

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-16 14:36:52 +00:00
116712aa24 docs: 데이터베이스 스키마 및 API 문서 추가
- database_schema.md: 전체 테이블 구조 상세 설명
  - 27개 테이블의 컬럼 정의 및 설명
  - 테이블 간 관계 설명
  - 주요 비즈니스 규칙 문서화

- database_erd.md: ER 다이어그램 및 데이터 플로우
  - Mermaid 다이어그램으로 시각화
  - 재고 흐름도, 처방-조제 흐름 설명
  - 인덱스 전략 및 데이터 무결성 규칙

- api_documentation.md: REST API 상세 명세
  - 약재, 처방, 조제, 재고, 환자 관리 API
  - 요청/응답 형식 예시
  - 에러 처리 방식

- README 업데이트: 문서 링크 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 19:07:34 +00:00
0bf0772864 fix: 제품 선택 드롭다운에서 마스터 약재명 표시
- /api/herbs/by-ingredient API에서 마스터 약재명(herb_masters) 조회 추가
- 제품별 개별명(신흥인삼) 대신 통일된 성분명(인삼) 표시
- product_name 필드에 원래 제품명 보존, herb_name에 마스터명 제공
- 프론트엔드에서 약재명 [회사명] (재고) 형식으로 표시

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 18:38:08 +00:00
8d03e85648 fix: 조제 화면 제품 선택 드롭다운 '기타' 표시 문제 수정
- /api/herbs/by-ingredient 엔드포인트에서 specification이 '기타'로 표시되는 문제 수정
- specification 값이 없을 때 '일반'으로 표시
- '세화' 제조사 추가 인식
- 실제 specification 값을 그대로 표시하도록 개선
- 칼럼 헤더 '원산지 선택' → '제품/로트 선택'으로 변경

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 18:34:39 +00:00
a4861dc1b8 fix: 조제 관리 약재 추가 시 마스터 약재명 표시 및 2단계 선택 구조 개선
- 약재 추가 드롭다운에서 제품명 대신 마스터 약재명 표시
- /api/herbs/masters 엔드포인트 사용하여 ingredient_code 기반 약재 목록 로드
- /api/herbs/by-ingredient/<code> 엔드포인트 추가 (제품 목록 조회)
- 2단계 선택 구조: 약재(마스터) → 제품 → 원산지/롯트
- 기존 처방 약재와 새로 추가하는 약재의 테이블 구조 통일 (6칼럼)
- 원산지 선택 칼럼에 제품/원산지 드롭다운 함께 표시

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 18:31:15 +00:00
bfc5c992de docs: 셀프체크 문서 추가
개발 진행 상황과 체크리스트 문서화
2026-02-15 17:47:07 +00:00
0af715b2c2 feat: 환자 문진표 기능 추가 (미완성)
- 설문 테이블 스키마 추가 (survey_templates, survey_progress, survey_responses)
- 설문 템플릿 데이터 추가 스크립트
- 모바일 문진표 HTML 템플릿

※ 아직 개발 중인 기능으로 추가 구현 필요
2026-02-15 17:46:58 +00:00
eac5bb72dd test: 조제 화면 인삼 선택 E2E 테스트 추가
- Playwright를 사용한 E2E 테스트 구현
- 쌍화탕 조제 시 인삼 선택 가능 여부 확인
- 제품 드롭다운과 원산지 드롭다운 동작 테스트

테스트 시나리오:
1. 조제관리 화면 진입
2. 처방 선택 (쌍화탕)
3. 인삼 제품 선택 가능 확인
4. 원산지/로트 선택 가능 확인
2026-02-15 17:46:39 +00:00
55974423ea feat: 조제 화면에 2단계 선택 시스템 구현
- 1단계: 제품 선택 (같은 주성분의 여러 제품 중 선택)
- 2단계: 원산지/로트 선택 (선택한 제품의 원산지별 로트 중 선택)
- formula_ingredients API 응답 구조 변경에 따른 프론트엔드 수정
  - available_products 배열 기반 렌더링
  - 제품 선택 드롭다운과 원산지 선택 드롭다운 분리
  - 제품 변경 시 원산지/로트 옵션 동적 로드

예시:
- 인삼 → 신흥인삼 선택 → [한국산|중국산] 선택 가능
- 인삼 → 세화인삼 선택 → [한국산|미국산] 선택 가능
2026-02-15 17:46:04 +00:00
9fa1f7a031 refactor: formula_ingredients를 ingredient_code 기반으로 변경
- formula_ingredients 테이블이 herb_item_id 대신 ingredient_code 사용하도록 변경
- GET /api/formulas/<id>/ingredients API 개선
  - ingredient_code 기반 조회로 변경
  - available_products 배열 추가 (재고 있는 모든 제품 포함)
  - total_available_stock, product_count 필드 추가
- 같은 주성분을 가진 모든 제품의 재고 정보를 반환하도록 수정

이제 처방에서 특정 제품이 아닌 주성분을 지정하여
재고가 있는 대체 제품을 자동으로 선택 가능
2026-02-15 17:45:19 +00:00
dbd6f4f841 feat: 십전대보탕 처방 추가
- 십전대보탕 약재 데이터 파싱 (12개 약재)
- herb_items 테이블과 자동 매칭 (11/12개 성공)
- 처방 등록: 십전대보탕 (SJDB01)
- 구성 약재 11개, 총 49.4g/첩
- 보험코드 및 주성분코드 100% 매핑 완료

참고: 계피는 미매칭, 생강은 반하생강백반제로 매칭됨

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 15:16:11 +00:00
041d1d81c4 feat: 환자 처방 내역에서 조제 상세 정보 인라인 펼침 기능 추가
- 환자 처방 내역 모달에서 처방 행 클릭 시 상세 정보 펼침
- 중첩 모달 문제 해결을 위해 인라인 표시 방식으로 변경
- 구성 약재 정보 테이블 표시 (약재명, 보험코드, 첩당용량, 총용량)
- 재고 소비 내역 테이블 표시 (약재명, 원산지, 도매상, 사용량, 단가, 원가)
- 총 원가 자동 계산 및 표시
- chevron 아이콘으로 펼침/접힘 상태 표시
- 다른 행 클릭 시 자동으로 이전 행 닫기

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 12:54:22 +00:00
83f9f10f3d fix: 환자 처방 내역 조회 API 응답 형식 수정
- API 응답에서 'data' 대신 'compounds' 사용하도록 수정
- 에러 방지를 위해 기본값 빈 배열 설정

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 12:48:03 +00:00
45672a125f feat: 환자 처방 내역 조회 기능 추가
- 환자 목록에 처방 횟수 표시 (배지)
- 환자별 처방 내역 조회 버튼 추가
- 환자 처방 내역 모달 추가 (통계 + 상세 내역)
  - 총 처방 횟수, 최근 방문일, 총 제수, 총 처방비
  - 전체 처방 내역 테이블 (조제일, 처방명, 상태 등)
  - 각 처방의 상세 보기 기능 연동
- 환자 개별 조회 API 엔드포인트 추가 (GET /api/patients/<id>)
- 환자 편집 버튼 UI 추가 (기능 준비)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-15 12:45:21 +00:00
127 changed files with 32343 additions and 292 deletions

123
.claude/claude.md Normal file
View File

@@ -0,0 +1,123 @@
# 한약 재고관리 시스템 - 개발 가이드라인
## 📋 테스트 데이터 입력 규칙
### 🔴 중요: 입고 테스트 시 필수 준수사항
**모든 재고 입고 테스트는 반드시 다음 프로세스를 따라야 합니다:**
1. **입고장 생성 (purchase_receipts)**
- 공급업체, 날짜, 총액 등 기본 정보 등록
- VAT 포함/미포함 구분
2. **입고 라인 생성 (purchase_receipt_lines)**
- 각 약재별 상세 입고 정보
- 수량, 단가, 원산지 등 기록
3. **재고 LOT 자동 생성 (inventory_lots)**
- 입고 라인에 따라 자동으로 LOT 생성
- `receipt_line_id`로 입고장과 연결
- 재고 추적 및 이력 관리
### ❌ 금지사항
- inventory_lots 테이블에 직접 데이터 INSERT 금지
- 입고장 없이 재고만 추가하는 것은 테스트 목적 외 금지
### ✅ 올바른 예시
```python
# 1. 입고장 생성
INSERT INTO purchase_receipts (supplier_id, receipt_date, receipt_no, ...)
VALUES (1, '2024-02-18', 'PR-20240218-001', ...);
# 2. 입고 라인 추가
INSERT INTO purchase_receipt_lines (receipt_id, herb_item_id, quantity_g, ...)
VALUES (1, 47, 1000, ...);
# 3. LOT은 자동 생성되거나 트리거로 처리
INSERT INTO inventory_lots (receipt_line_id, ...) # receipt_line_id 필수!
```
### 📌 특수 케이스
**입고장 없는 재고 테스트가 필요한 경우:**
- `receipt_line_id = 0` 사용 (입고장 없음 표시)
- 반드시 테스트 완료 후 삭제 또는 원복
- 실제 운영 환경에서는 사용 금지
---
## 🗂️ 데이터베이스 구조
### 핵심 테이블 관계
```
purchase_receipts (입고장)
purchase_receipt_lines (입고 상세)
inventory_lots (재고 LOT) - receipt_line_id로 연결
compound_consumptions (소비 내역)
```
### 재고 계산 방식
1. **전체 재고 (all)**
- 모든 LOT 포함
- `receipt_line_id = 0` 포함
2. **입고장 기준 (receipt_only)**
- `receipt_line_id > 0`인 LOT만
- 정식 입고된 재고만 계산
3. **검증된 재고 (verified)**
- 현재는 입고장 기준과 동일
- 향후 별도 검증 플래그 추가 예정
---
## 🔄 재고 흐름
```mermaid
graph LR
A[입고장 등록] --> B[입고 라인 생성]
B --> C[LOT 자동 생성]
C --> D[재고 보유]
D --> E1[복합제 소비]
D --> E2[처방 출고]
D --> E3[재고 보정]
```
---
## 💡 개발 시 주의사항
1. **재고 자산 계산**
- 효능 태그 JOIN 시 중복 주의
- GROUP BY 전에 DISTINCT 사용
- 태그는 별도 쿼리로 조회 권장
2. **LOT 관리**
- receipt_line_id는 NOT NULL 제약
- 0 = 입고장 없음 (특수 케이스)
- NULL 사용 불가
3. **단가 처리**
- 입고 시점 단가 저장
- 출고 시 LOT의 단가 사용
- 가중평균 계산 시 주의
---
## 📝 테스트 체크리스트
- [ ] 입고장 생성 확인
- [ ] 입고 라인과 LOT 연결 확인
- [ ] 재고 자산 계산 정확성
- [ ] 소비 후 재고 차감 확인
- [ ] 재고 보정 처리 확인
---
*Last Updated: 2024-02-18*
*작성자: Claude & User*

1
.gitignore vendored
View File

@@ -66,3 +66,4 @@ Thumbs.db
# Excel temporary files
~$*.xlsx
~$*.xls
uploads/

13
CLAUDE.md Normal file
View File

@@ -0,0 +1,13 @@
# CLAUDE.md - AI 개발 가이드
## 핵심 참고 문서
- `docs/한약국_첩제_vs_OTC_상담가이드.md` — 첩제 vs OTC 차별점, 업셀링 근거, 100처방 reference_notes 작성 가이드. AI 상담/알림톡/웹 설명 자료 작성 시 반드시 참고.
- `docs/100처방_마스터데이터_등록_가이드.md`**100처방 마스터 데이터 등록 절차/규칙/코드 템플릿**. official_formulas·official_formula_ingredients 테이블에 처방 데이터를 채울 때 반드시 이 문서의 절차와 규칙을 따를 것. 성분코드(ingredient_code) 조회법, 동명이약 구분, 용량 기준, description/reference_notes 포맷, Python 코드 템플릿 포함.
## DB 주의사항
### inventory_lots 테이블: lot_id vs lot_number
- `lot_id` (INTEGER PK): 시스템이 자동 생성하는 내부 식별자. 재고 추적/조제/원장 등 모든 로직에서 로트를 참조할 때 사용.
- `lot_number` (TEXT, nullable): 도매상이 부여한 납품 로트번호. 사용자가 직접 입력하는 참고용 텍스트.
- INSERT 시 `lot_number``expiry_date`를 빠뜨리지 말 것. 둘 다 nullable이지만 사용자가 입력했으면 반드시 저장해야 함.

View File

@@ -53,24 +53,33 @@
## 데이터베이스 구조
### 핵심 테이블
- `herb_masters` - 약재 마스터 (성분코드 기준, 454개 표준 약재)
- `herb_items` - 약재 제품 (제조사별 개별 제품)
- `patients` - 환자 정보
- `herb_items` - 약재 마스터 (보험코드 기준)
- `suppliers` - 도매상 정보
- `purchase_receipts` - 입고장 헤더
- `purchase_receipt_lines` - 입고장 상세
- `inventory_lots` - 로트별 재고
- `formulas` - 처방 마스터
- `formula_ingredients` - 처방 구성 약재
- `formula_ingredients` - 처방 구성 약재 (ingredient_code 기반)
- `compounds` - 조제 작업
- `compound_consumptions` - 로트별 차감 내역
- `stock_ledger` - 재고 원장 (모든 변동 기록)
### 핵심 개념
- **성분코드 (ingredient_code)**: 표준 약재 식별자
- **2단계 약재 체계**: 마스터(성분) → 제품(제조사별)
- **1제 = 20첩 = 30파우치** (기본값, 조정 가능)
- **로트 관리**: 입고 시점별로 재고를 구분 관리
- **FIFO 차감**: 오래된 재고부터 우선 사용
- **원가 추적**: 로트별 단가 기준 정확한 원가 계산
## 📚 문서
- [데이터베이스 스키마](docs/database_schema.md) - 전체 테이블 구조 상세 설명
- [ER 다이어그램](docs/database_erd.md) - 엔티티 관계도 및 데이터 플로우
- [API 문서](docs/api_documentation.md) - REST API 엔드포인트 상세 명세
## 설치 방법
### 1. 필수 요구사항

103
add_efficacy_column.py Normal file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
formulas 테이블에 efficacy(주요 효능) 칼럼 추가
"""
import sqlite3
def add_efficacy_column():
"""formulas 테이블에 efficacy 칼럼 추가 및 데이터 입력"""
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 1. efficacy 칼럼이 이미 있는지 확인
cursor.execute("PRAGMA table_info(formulas)")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
if 'efficacy' not in column_names:
print("📝 efficacy 칼럼 추가 중...")
cursor.execute("""
ALTER TABLE formulas
ADD COLUMN efficacy TEXT
""")
print("✅ efficacy 칼럼 추가 완료")
else:
print(" efficacy 칼럼이 이미 존재합니다")
# 2. 기존 처방들의 주요 효능 데이터 업데이트
print("\n📋 처방별 주요 효능 데이터 추가:")
print("-"*60)
formula_efficacies = {
"십전대보탕": "기혈양허(氣血兩虛)를 치료, 대보기혈(大補氣血), 병후 회복, 수술 후 회복, 만성 피로 개선",
"소청룡탕": "외감풍한(外感風寒), 내정수음(內停水飮)으로 인한 기침, 천식 치료, 해표산한, 온폐화음",
"갈근탕": "외감풍한으로 인한 두통, 발열, 오한, 항강 치료, 해표발한, 생진지갈",
"쌍화탕": "기혈허약, 피로회복, 감기예방, 면역력 증강, 원기회복",
"월비탕 1차": "비만치료 초기단계, 대사촉진, 체중감량, 부종개선",
"월비탕 2차": "비만치료 중기단계, 대사촉진 강화, 체중감량, 부종개선",
"월비탕 3차": "비만치료 후기단계, 대사촉진 최대화, 체중감량, 체질개선",
"월비탕 4차": "비만치료 마무리단계, 체중유지, 체질개선, 요요방지",
"삼소음": "리기화담(理氣化痰), 해표산한(解表散寒), 외감풍한과 내상식적으로 인한 기침, 가래 치료"
}
for formula_name, efficacy in formula_efficacies.items():
cursor.execute("""
UPDATE formulas
SET efficacy = ?
WHERE formula_name = ?
""", (efficacy, formula_name))
if cursor.rowcount > 0:
print(f"{formula_name}: 효능 추가됨")
else:
print(f"⚠️ {formula_name}: 처방을 찾을 수 없음")
conn.commit()
# 3. 업데이트 결과 확인
print("\n📊 업데이트 결과 확인:")
print("-"*60)
cursor.execute("""
SELECT formula_name, efficacy
FROM formulas
WHERE efficacy IS NOT NULL
ORDER BY formula_id
""")
results = cursor.fetchall()
for name, efficacy in results:
print(f"\n{name}:")
print(f" {efficacy[:80]}...")
# 4. 테이블 구조 최종 확인
print("\n📋 formulas 테이블 최종 구조:")
print("-"*60)
cursor.execute("PRAGMA table_info(formulas)")
columns = cursor.fetchall()
for col in columns:
if col[1] in ['formula_name', 'description', 'efficacy']:
print(f" {col[1]:20}: {col[2]}")
except sqlite3.Error as e:
print(f"❌ 데이터베이스 오류: {e}")
conn.rollback()
return False
finally:
conn.close()
return True
if __name__ == "__main__":
print("🌿 처방 효능 칼럼 추가 프로그램")
print("="*60)
if add_efficacy_column():
print("\n✅ efficacy 칼럼 추가 및 데이터 업데이트 완료!")
else:
print("\n❌ 작업 중 오류가 발생했습니다.")

194
add_mileage_system.py Normal file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""
마일리지 시스템 구축 - patients 테이블에 마일리지 컬럼 추가 및 이력 테이블 생성
"""
import sqlite3
from datetime import datetime
from config import DATABASE_PATH
def add_mileage_system():
"""환자 테이블에 마일리지 컬럼 추가 및 이력 테이블 생성"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
# 1. patients 테이블에 마일리지 관련 컬럼 추가
try:
cursor.execute("""
ALTER TABLE patients
ADD COLUMN mileage_balance INTEGER DEFAULT 0
""")
print("✓ mileage_balance 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- mileage_balance 컬럼 이미 존재")
else:
raise
try:
cursor.execute("""
ALTER TABLE patients
ADD COLUMN total_mileage_earned INTEGER DEFAULT 0
""")
print("✓ total_mileage_earned 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- total_mileage_earned 컬럼 이미 존재")
else:
raise
try:
cursor.execute("""
ALTER TABLE patients
ADD COLUMN total_mileage_used INTEGER DEFAULT 0
""")
print("✓ total_mileage_used 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- total_mileage_used 컬럼 이미 존재")
else:
raise
# 2. 마일리지 거래 이력 테이블 생성
cursor.execute("""
CREATE TABLE IF NOT EXISTS mileage_transactions (
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
patient_id INTEGER REFERENCES patients(patient_id),
compound_id INTEGER REFERENCES compounds(compound_id),
transaction_type TEXT NOT NULL, -- EARNED, USED, EXPIRED, ADMIN_ADJUST
amount INTEGER NOT NULL,
balance_after INTEGER NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by TEXT
)
""")
print("✓ mileage_transactions 테이블 생성 완료")
# 인덱스 생성
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_mileage_transactions_patient
ON mileage_transactions(patient_id)
""")
print("✓ 인덱스 생성 완료")
# 3. compounds 테이블에 마일리지 사용 컬럼 추가
try:
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN mileage_used INTEGER DEFAULT 0
""")
print("✓ compounds.mileage_used 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- compounds.mileage_used 컬럼 이미 존재")
else:
raise
try:
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN mileage_earned INTEGER DEFAULT 0
""")
print("✓ compounds.mileage_earned 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- compounds.mileage_earned 컬럼 이미 존재")
else:
raise
# 4. 복합 결제를 위한 컬럼 추가
try:
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN payment_cash INTEGER DEFAULT 0
""")
print("✓ payment_cash 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- payment_cash 컬럼 이미 존재")
else:
raise
try:
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN payment_card INTEGER DEFAULT 0
""")
print("✓ payment_card 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- payment_card 컬럼 이미 존재")
else:
raise
try:
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN payment_transfer INTEGER DEFAULT 0
""")
print("✓ payment_transfer 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- payment_transfer 컬럼 이미 존재")
else:
raise
try:
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN discount_amount INTEGER DEFAULT 0
""")
print("✓ discount_amount 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- discount_amount 컬럼 이미 존재")
else:
raise
conn.commit()
# 5. 테스트용 마일리지 데이터 추가 (박주호 회원)
cursor.execute("""
SELECT patient_id, name FROM patients
WHERE name LIKE '%박주호%' OR name LIKE '%주호%'
""")
patients = cursor.fetchall()
if patients:
for patient in patients:
print(f"\n박주호 회원 발견: ID={patient[0]}, 이름={patient[1]}")
# 초기 마일리지 부여
cursor.execute("""
UPDATE patients
SET mileage_balance = 50000,
total_mileage_earned = 50000,
total_mileage_used = 0
WHERE patient_id = ?
""", (patient[0],))
# 마일리지 이력 추가
cursor.execute("""
INSERT INTO mileage_transactions
(patient_id, transaction_type, amount, balance_after, description, created_by)
VALUES (?, 'ADMIN_ADJUST', 50000, 50000, '초기 마일리지 부여', 'system')
""", (patient[0],))
print(f" → 50,000 마일리지 부여 완료")
else:
print("\n박주호 회원을 찾을 수 없습니다.")
conn.commit()
# 현재 patients 테이블 구조 확인
cursor.execute("PRAGMA table_info(patients)")
columns = cursor.fetchall()
print("\n현재 patients 테이블 구조 (마일리지 관련):")
for col in columns:
if 'mileage' in col[1].lower():
print(f" - {col[1]}: {col[2]}")
conn.close()
print("\n마일리지 시스템 구축 완료!")
if __name__ == "__main__":
add_mileage_system()

168
add_prescription_data.py Normal file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
처방 데이터 추가 스크립트
- 소청룡탕, 갈근탕 등 처방 데이터 추가
"""
import sqlite3
from datetime import datetime
def get_connection():
"""데이터베이스 연결"""
return sqlite3.connect('database/kdrug.db')
def add_prescriptions():
"""소청룡탕과 갈근탕 처방 추가"""
conn = get_connection()
cursor = conn.cursor()
# 처방 데이터 정의
prescriptions = [
{
'formula_code': 'SCR001',
'formula_name': '소청룡탕',
'formula_type': 'STANDARD',
'base_cheop': 1,
'base_pouches': 1,
'description': '외감풍한, 내정수음으로 인한 기침, 천식을 치료하는 처방. 한담을 풀어내고 기침을 멎게 함.',
'ingredients': [
{'code': '3147H1AHM', 'amount': 6.0, 'notes': '발한해표'}, # 마황
{'code': '3419H1AHM', 'amount': 6.0, 'notes': '화영지통'}, # 백작약
{'code': '3342H1AHM', 'amount': 6.0, 'notes': '렴폐지해'}, # 오미자
{'code': '3182H1AHM', 'amount': 6.0, 'notes': '화담지구'}, # 반하
{'code': '3285H1AHM', 'amount': 4.0, 'notes': '온폐산한'}, # 세신
{'code': '3017H1AHM', 'amount': 4.0, 'notes': '온중산한'}, # 건강
{'code': '3033H1AHM', 'amount': 4.0, 'notes': '해표발한'}, # 계지
{'code': '3007H1AHM', 'amount': 4.0, 'notes': '조화제약'}, # 감초
]
},
{
'formula_code': 'GGT001',
'formula_name': '갈근탕',
'formula_type': 'STANDARD',
'base_cheop': 1,
'base_pouches': 1,
'description': '외감풍한으로 인한 두통, 발열, 오한, 항강을 치료하는 처방. 발한해표하고 승진해기함.',
'ingredients': [
{'code': '3002H1AHM', 'amount': 8.0, 'notes': '승진해기'}, # 갈근
{'code': '3147H1AHM', 'amount': 6.0, 'notes': '발한해표'}, # 마황
{'code': '3115H1AHM', 'amount': 6.0, 'notes': '보중익기'}, # 대조(대추)
{'code': '3033H1AHM', 'amount': 4.0, 'notes': '해표발한'}, # 계지
{'code': '3419H1AHM', 'amount': 4.0, 'notes': '화영지통'}, # 작약
{'code': '3007H1AHM', 'amount': 4.0, 'notes': '조화제약'}, # 감초
{'code': '3017H1AHM', 'amount': 2.0, 'notes': '온중산한'}, # 건강
]
}
]
try:
for prescription in prescriptions:
# 1. formulas 테이블에 처방 추가
cursor.execute("""
INSERT INTO formulas (
formula_code, formula_name, formula_type, base_cheop, base_pouches,
description, is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (
prescription['formula_code'],
prescription['formula_name'],
prescription['formula_type'],
prescription['base_cheop'],
prescription['base_pouches'],
prescription['description']
))
formula_id = cursor.lastrowid
print(f"[추가됨] {prescription['formula_name']} 처방 추가 완료 (ID: {formula_id})")
# 2. formula_ingredients 테이블에 구성 약재 추가
for ingredient in prescription['ingredients']:
# 약재 이름 조회 (로그용)
cursor.execute("""
SELECT herb_name FROM herb_masters
WHERE ingredient_code = ?
""", (ingredient['code'],))
herb_name_result = cursor.fetchone()
herb_name = herb_name_result[0] if herb_name_result else 'Unknown'
cursor.execute("""
INSERT INTO formula_ingredients (
formula_id, ingredient_code, grams_per_cheop, notes,
sort_order, created_at
) VALUES (?, ?, ?, ?, 0, CURRENT_TIMESTAMP)
""", (
formula_id,
ingredient['code'],
ingredient['amount'],
ingredient['notes']
))
print(f" - {herb_name}({ingredient['code']}): {ingredient['amount']}g - {ingredient['notes']}")
conn.commit()
print("\n[완료] 모든 처방 데이터가 성공적으로 추가되었습니다!")
except Exception as e:
conn.rollback()
print(f"\n[오류] 오류 발생: {e}")
import traceback
traceback.print_exc()
finally:
conn.close()
def verify_prescriptions():
"""추가된 처방 데이터 확인"""
conn = get_connection()
cursor = conn.cursor()
print("\n" + "="*80)
print("추가된 처방 데이터 확인")
print("="*80)
# 추가된 처방 목록 확인
cursor.execute("""
SELECT f.formula_id, f.formula_code, f.formula_name, f.formula_type, f.description,
COUNT(fi.ingredient_id) as ingredient_count,
SUM(fi.grams_per_cheop) as total_amount
FROM formulas f
LEFT JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
WHERE f.formula_code IN ('SCR001', 'GGT001')
GROUP BY f.formula_id
""")
for row in cursor.fetchall():
print(f"\n[처방] {row[2]} ({row[1]})")
print(f" 타입: {row[3]}")
print(f" 설명: {row[4]}")
print(f" 구성약재: {row[5]}가지")
print(f" 총 용량: {row[6]}g")
# 구성 약재 상세
cursor.execute("""
SELECT hm.herb_name, fi.ingredient_code, fi.grams_per_cheop, fi.notes
FROM formula_ingredients fi
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
WHERE fi.formula_id = ?
ORDER BY fi.grams_per_cheop DESC
""", (row[0],))
print(" 구성 약재:")
for ingredient in cursor.fetchall():
print(f" - {ingredient[0]}({ingredient[1]}): {ingredient[2]}g - {ingredient[3]}")
conn.close()
def main():
print("="*80)
print("처방 데이터 추가 스크립트")
print("="*80)
# 처방 추가
add_prescriptions()
# 추가된 데이터 확인
verify_prescriptions()
if __name__ == "__main__":
main()

131
add_sales_columns.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
판매 관리 시스템 Phase 1 - compounds 테이블에 판매 관련 컬럼 추가
"""
import sqlite3
from datetime import datetime
def add_sales_columns():
"""compounds 테이블에 판매 관련 컬럼 추가"""
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 1. payment_method 컬럼 추가
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN payment_method TEXT
""")
print("✓ payment_method 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- payment_method 컬럼 이미 존재")
else:
raise
try:
# 2. payment_date 컬럼 추가
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN payment_date DATETIME
""")
print("✓ payment_date 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- payment_date 컬럼 이미 존재")
else:
raise
try:
# 3. discount_rate 컬럼 추가
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN discount_rate REAL DEFAULT 0
""")
print("✓ discount_rate 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- discount_rate 컬럼 이미 존재")
else:
raise
try:
# 4. discount_reason 컬럼 추가
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN discount_reason TEXT
""")
print("✓ discount_reason 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- discount_reason 컬럼 이미 존재")
else:
raise
try:
# 5. delivery_method 컬럼 추가
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN delivery_method TEXT
""")
print("✓ delivery_method 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- delivery_method 컬럼 이미 존재")
else:
raise
try:
# 6. delivery_date 컬럼 추가
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN delivery_date DATETIME
""")
print("✓ delivery_date 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- delivery_date 컬럼 이미 존재")
else:
raise
try:
# 7. invoice_number 컬럼 추가
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN invoice_number TEXT
""")
print("✓ invoice_number 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- invoice_number 컬럼 이미 존재")
else:
raise
try:
# 8. actual_payment_amount 컬럼 추가 (실제 결제 금액)
cursor.execute("""
ALTER TABLE compounds
ADD COLUMN actual_payment_amount REAL
""")
print("✓ actual_payment_amount 컬럼 추가 완료")
except sqlite3.OperationalError as e:
if "duplicate column name" in str(e):
print("- actual_payment_amount 컬럼 이미 존재")
else:
raise
conn.commit()
print("\n판매 관련 컬럼 추가 완료!")
# 현재 compounds 테이블 구조 확인
cursor.execute("PRAGMA table_info(compounds)")
columns = cursor.fetchall()
print("\n현재 compounds 테이블 구조:")
for col in columns:
print(f" - {col[1]}: {col[2]}")
conn.close()
if __name__ == "__main__":
add_sales_columns()

317
add_sample_herb_data.py Normal file
View File

@@ -0,0 +1,317 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
한약재 샘플 데이터 추가 - 십전대보탕 구성 약재
"""
import sqlite3
from datetime import datetime
def get_connection():
"""데이터베이스 연결"""
return sqlite3.connect('database/kdrug.db')
def add_herb_extended_data():
"""약재 확장 정보 추가"""
conn = get_connection()
cursor = conn.cursor()
# 십전대보탕 구성 약재들의 실제 ingredient_code 사용
herb_data = [
{
'ingredient_code': '3400H1AHM', # 인삼
'property': '온(溫)',
'taste': '감(甘), 미고(微苦)',
'meridian_tropism': '폐(肺), 비(脾), 심(心)',
'main_effects': '대보원기, 보비익폐, 생진지갈, 안신익지',
'indications': '기허증, 피로, 식욕부진, 설사, 호흡곤란, 자한, 양위, 소갈, 건망, 불면',
'dosage_range': '1~3돈(3~9g)',
'precautions': '실증, 열증자 신중 투여',
'preparation_method': '수치법: 홍삼, 백삼, 당삼 등으로 가공',
'tags': [
('보기', 5),
('보혈', 3),
('안신', 4),
]
},
{
'ingredient_code': '3007H1AHM', # 감초
'property': '평(平)',
'taste': '감(甘)',
'meridian_tropism': '심(心), 폐(肺), 비(脾), 위(胃)',
'main_effects': '화중완급, 윤폐지해, 해독',
'indications': '복통, 기침, 인후통, 소화불량, 약물중독',
'dosage_range': '1~3돈(3~9g)',
'precautions': '장기복용시 부종 주의',
'preparation_method': '자감초(炙甘草) 등',
'tags': [
('보기', 3),
('해독', 4),
('윤조', 3),
('청열', 2),
('항염', 3),
]
},
{
'ingredient_code': '3204H1AHM', # 백출
'property': '온(溫)',
'taste': '감(甘), 고(苦)',
'meridian_tropism': '비(脾), 위(胃)',
'main_effects': '건비익기, 조습이수, 지한, 안태',
'indications': '비허설사, 수종, 담음, 자한, 태동불안',
'dosage_range': '2~4돈(6~12g)',
'precautions': '음허내열자 신중',
'preparation_method': '토백출, 생백출',
'tags': [
('보기', 4),
('이수', 4),
('건비', 5),
]
},
{
'ingredient_code': '3215H1AHM', # 복령
'property': '평(平)',
'taste': '감(甘), 담(淡)',
'meridian_tropism': '심(心), 폐(肺), 비(脾), 신(腎)',
'main_effects': '이수삼습, 건비영심, 안신',
'indications': '소변불리, 수종, 설사, 불면, 심계',
'dosage_range': '3~5돈(9~15g)',
'precautions': '음허자 신중',
'preparation_method': '백복령, 적복령',
'tags': [
('이수', 5),
('안신', 3),
('건비', 3),
]
},
{
'ingredient_code': '3419H1AHM', # 작약
'property': '미한(微寒)',
'taste': '고(苦), 산(酸)',
'meridian_tropism': '간(肝), 비(脾)',
'main_effects': '양혈렴음, 유간지통, 평간양',
'indications': '혈허, 복통, 사지경련, 두훈, 월경불순',
'dosage_range': '2~4돈(6~12g)',
'precautions': '비허설사자 신중',
'preparation_method': '백작약, 적작약',
'tags': [
('보혈', 4),
('진경', 4),
('평간', 3),
]
},
{
'ingredient_code': '3475H1AHM', # 천궁
'property': '온(溫)',
'taste': '신(辛)',
'meridian_tropism': '간(肝), 담(膽), 심포(心包)',
'main_effects': '활혈행기, 거풍지통',
'indications': '혈체, 두통, 현훈, 월경불순, 복통',
'dosage_range': '1~2돈(3~6g)',
'precautions': '음허화왕자 신중',
'preparation_method': '주천궁',
'tags': [
('활혈', 5),
('거풍', 3),
('지통', 4),
]
},
{
'ingredient_code': '3105H1AHM', # 당귀
'property': '온(溫)',
'taste': '감(甘), 신(辛)',
'meridian_tropism': '간(肝), 심(心), 비(脾)',
'main_effects': '보혈활혈, 조경지통, 윤장통변',
'indications': '혈허, 월경불순, 복통, 변비, 타박상',
'dosage_range': '2~4돈(6~12g)',
'precautions': '습성설사자 신중',
'preparation_method': '주당귀, 당귀신, 당귀미',
'tags': [
('보혈', 5),
('활혈', 4),
('윤조', 3),
]
},
{
'ingredient_code': '3583H1AHM', # 황기
'property': '온(溫)',
'taste': '감(甘)',
'meridian_tropism': '폐(肺), 비(脾)',
'main_effects': '보기승양, 고표지한, 이수소종, 탈독생기',
'indications': '기허, 자한, 설사, 탈항, 수종, 창양',
'dosage_range': '3~6돈(9~18g)',
'precautions': '표실사 및 음허자 신중',
'preparation_method': '밀자황기',
'tags': [
('보기', 5),
('승양', 4),
('고표', 4),
]
},
{
'ingredient_code': '3384H1AHM', # 육계
'property': '대열(大熱)',
'taste': '감(甘), 신(辛)',
'meridian_tropism': '신(腎), 비(脾), 심(心), 간(肝)',
'main_effects': '보화조양, 산한지통, 온경통맥',
'indications': '양허, 냉증, 요통, 복통, 설사',
'dosage_range': '0.5~1돈(1.5~3g)',
'precautions': '음허화왕자, 임신부 금기',
'preparation_method': '육계심, 계피',
'tags': [
('보양', 5),
('온리', 5),
('산한', 4),
]
},
{
'ingredient_code': '3299H1AHM', # 숙지황
'property': '온(溫)',
'taste': '감(甘)',
'meridian_tropism': '간(肝), 신(腎)',
'main_effects': '자음보혈, 익정전수',
'indications': '혈허, 음허, 요슬산연, 유정, 붕루',
'dosage_range': '3~6돈(9~18g)',
'precautions': '비허설사, 담다자 신중',
'preparation_method': '숙지황 제법',
'tags': [
('보혈', 5),
('자음', 5),
('보신', 4),
]
},
]
for herb in herb_data:
# herb_master_extended 업데이트
cursor.execute("""
UPDATE herb_master_extended
SET property = ?,
taste = ?,
meridian_tropism = ?,
main_effects = ?,
indications = ?,
dosage_range = ?,
precautions = ?,
preparation_method = ?,
updated_at = CURRENT_TIMESTAMP
WHERE ingredient_code = ?
""", (
herb['property'],
herb['taste'],
herb['meridian_tropism'],
herb['main_effects'],
herb['indications'],
herb['dosage_range'],
herb['precautions'],
herb['preparation_method'],
herb['ingredient_code']
))
# 효능 태그 매핑
for tag_name, strength in herb.get('tags', []):
# 태그 ID 조회
cursor.execute("""
SELECT tag_id FROM herb_efficacy_tags
WHERE tag_name = ?
""", (tag_name,))
tag_result = cursor.fetchone()
if tag_result:
tag_id = tag_result[0]
# 기존 태그 삭제
cursor.execute("""
DELETE FROM herb_item_tags
WHERE ingredient_code = ? AND tag_id = ?
""", (herb['ingredient_code'], tag_id))
# 태그 매핑 추가
cursor.execute("""
INSERT INTO herb_item_tags
(ingredient_code, tag_id, strength)
VALUES (?, ?, ?)
""", (herb['ingredient_code'], tag_id, strength))
print(f"{herb['ingredient_code']} 데이터 추가 완료")
conn.commit()
conn.close()
def add_prescription_rules():
"""처방 배합 규칙 추가"""
conn = get_connection()
cursor = conn.cursor()
# 몇 가지 대표적인 배합 규칙 추가
rules = [
{
'herb1': '인삼',
'herb2': '황기',
'rule_type': '상수',
'description': '보기작용 상승효과',
'clinical_note': '기허증에 병용시 효과 증대'
},
{
'herb1': '당귀',
'herb2': '천궁',
'rule_type': '상수',
'description': '활혈작용 상승효과',
'clinical_note': '혈허, 혈체에 병용'
},
{
'herb1': '반하',
'herb2': '생강',
'rule_type': '상수',
'description': '반하의 독성 감소, 진토작용 증강',
'clinical_note': '구토, 오심에 병용'
},
{
'herb1': '감초',
'herb2': '감수',
'rule_type': '상반',
'description': '효능 상반',
'clinical_note': '병용 금지'
},
{
'herb1': '인삼',
'herb2': '오령지',
'rule_type': '상외',
'description': '효능 감소',
'clinical_note': '병용시 주의'
}
]
for rule in rules:
cursor.execute("""
INSERT OR IGNORE INTO prescription_rules
(herb1_name, herb2_name, rule_type, description, clinical_notes)
VALUES (?, ?, ?, ?, ?)
""", (rule['herb1'], rule['herb2'], rule['rule_type'],
rule['description'], rule['clinical_note']))
print(f"{rule['herb1']} - {rule['herb2']} 규칙 추가")
conn.commit()
conn.close()
def main():
print("=" * 80)
print("한약재 샘플 데이터 추가 - 십전대보탕 구성 약재")
print("=" * 80)
try:
print("\n1. 약재 확장 정보 추가 중...")
add_herb_extended_data()
print("\n2. 처방 배합 규칙 추가 중...")
add_prescription_rules()
print("\n✨ 모든 샘플 데이터가 성공적으로 추가되었습니다!")
except Exception as e:
print(f"\n❌ 오류 발생: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

163
add_samsoeun_formula.py Normal file
View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
삼소음 처방 추가 스크립트
처방 추가 가이드 문서의 방식에 따라 삼소음을 추가합니다.
"""
import sqlite3
from datetime import datetime
def add_samsoeun():
"""삼소음 처방 추가"""
# 처방 데이터 준비
prescription_data = {
'formula_code': 'SSE001', # 삼소음 코드
'formula_name': '삼소음',
'formula_type': 'STANDARD',
'base_cheop': 1,
'base_pouches': 1,
'description': '리기화담, 해표산한의 효능으로 외감풍한과 내상식적으로 인한 기침, 가래를 치료하는 처방',
'ingredients': [
{'code': '3400H1AHM', 'herb': '인삼', 'amount': 4.0, 'notes': '대보원기'},
{'code': '3411H1AHM', 'herb': '소엽(자소엽)', 'amount': 4.0, 'notes': '해표산한'},
{'code': '3433H1AHM', 'herb': '전호', 'amount': 4.0, 'notes': '강기화담'},
{'code': '3182H1AHM', 'herb': '반하', 'amount': 4.0, 'notes': '화담지구'},
{'code': '3002H1AHM', 'herb': '갈근', 'amount': 4.0, 'notes': '승진해기'},
{'code': '3215H1AHM', 'herb': '적복령(복령)', 'amount': 4.0, 'notes': '건비이수'},
{'code': '3115H1AHM', 'herb': '대조(대추)', 'amount': 4.0, 'notes': '보중익기'},
{'code': '3466H1AHM', 'herb': '진피', 'amount': 3.0, 'notes': '리기화담'},
{'code': '3077H1AHM', 'herb': '길경', 'amount': 3.0, 'notes': '선폐거담'},
{'code': '3454H1AHM', 'herb': '지각', 'amount': 3.0, 'notes': '파기소적'},
{'code': '3007H1AHM', 'herb': '감초', 'amount': 3.0, 'notes': '조화제약'},
{'code': '3017H1AHM', 'herb': '건강', 'amount': 1.0, 'notes': '온중산한'}
]
}
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 기존 삼소음 처방 확인
cursor.execute("""
SELECT formula_id, formula_code, formula_name
FROM formulas
WHERE formula_code = ? OR formula_name = ?
""", (prescription_data['formula_code'], prescription_data['formula_name']))
existing = cursor.fetchone()
if existing:
print(f"⚠️ 이미 존재하는 처방: {existing[2]} ({existing[1]})")
print("기존 처방을 삭제하고 새로 추가하시겠습니까? (이 스크립트는 자동으로 진행합니다)")
# 기존 처방과 관련 데이터 삭제
cursor.execute("DELETE FROM formula_ingredients WHERE formula_id = ?", (existing[0],))
cursor.execute("DELETE FROM formulas WHERE formula_id = ?", (existing[0],))
print(f"✅ 기존 처방 삭제 완료")
print(f"\n{'='*60}")
print(f"📝 {prescription_data['formula_name']} 처방 추가 중...")
# 1. formulas 테이블에 처방 추가
cursor.execute("""
INSERT INTO formulas (
formula_code, formula_name, formula_type,
base_cheop, base_pouches, description,
is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (
prescription_data['formula_code'],
prescription_data['formula_name'],
prescription_data['formula_type'],
prescription_data['base_cheop'],
prescription_data['base_pouches'],
prescription_data['description']
))
formula_id = cursor.lastrowid
print(f"✅ 처방 기본 정보 등록 (ID: {formula_id})")
# 2. formula_ingredients 테이블에 약재 추가
print(f"\n약재 구성:")
sort_order = 0
total_amount = 0
for ingredient in prescription_data['ingredients']:
cursor.execute("""
INSERT INTO formula_ingredients (
formula_id, ingredient_code,
grams_per_cheop, notes,
sort_order, created_at
) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
""", (
formula_id,
ingredient['code'],
ingredient['amount'],
ingredient['notes'],
sort_order
))
sort_order += 1
total_amount += ingredient['amount']
print(f" - {ingredient['herb']:15s}: {ingredient['amount']:5.1f}g ({ingredient['notes']})")
print(f"\n총 약재: {len(prescription_data['ingredients'])}")
print(f"1첩 총 용량: {total_amount:.1f}g")
conn.commit()
print(f"\n{prescription_data['formula_name']} 처방 추가 완료!")
# 추가된 처방 확인
print(f"\n{'='*60}")
print("📊 추가된 처방 확인:")
cursor.execute("""
SELECT f.formula_id, f.formula_code, f.formula_name,
COUNT(fi.ingredient_id) as herb_count,
SUM(fi.grams_per_cheop) as total_grams
FROM formulas f
LEFT JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
WHERE f.formula_code = ?
GROUP BY f.formula_id
""", (prescription_data['formula_code'],))
result = cursor.fetchone()
if result:
print(f"ID {result[0]}: {result[1]} - {result[2]}")
print(f" 약재 {result[3]}개, 총 {result[4]:.1f}g")
# 상세 구성 확인
print(f"\n상세 구성:")
cursor.execute("""
SELECT hm.herb_name, fi.grams_per_cheop, fi.notes
FROM formula_ingredients fi
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
WHERE fi.formula_id = ?
ORDER BY fi.sort_order
""", (result[0],))
for herb, amount, notes in cursor.fetchall():
print(f" - {herb:15s}: {amount:5.1f}g ({notes})")
except sqlite3.IntegrityError as e:
print(f"❌ 중복 오류: {e}")
conn.rollback()
return False
except sqlite3.Error as e:
print(f"❌ 데이터베이스 오류: {e}")
conn.rollback()
return False
finally:
conn.close()
return True
if __name__ == "__main__":
print("🌿 삼소음 처방 추가 프로그램")
print("="*60)
if add_samsoeun():
print("\n✅ 삼소음 처방 추가 작업이 완료되었습니다.")
else:
print("\n❌ 처방 추가 중 오류가 발생했습니다.")

81
add_test_dangui_lot.py Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
테스트용 당귀 로트 추가 - 복합 로트 테스트를 위함
"""
import sqlite3
from datetime import datetime, timedelta
def add_test_lot():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 휴먼일당귀 herb_item_id 확인
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = '휴먼일당귀'")
herb_item_id = cursor.fetchone()[0]
# 공급업체 ID 확인
cursor.execute("SELECT supplier_id FROM suppliers WHERE name = '한의사랑' LIMIT 1")
supplier_id = cursor.fetchone()[0]
# 기존 로트의 receipt_line_id 복사
cursor.execute("""
SELECT receipt_line_id
FROM inventory_lots
WHERE herb_item_id = ?
LIMIT 1
""", (herb_item_id,))
receipt_line_id = cursor.fetchone()[0]
# 새 로트 추가 (한국산, 다른 가격)
cursor.execute("""
INSERT INTO inventory_lots (
herb_item_id, supplier_id, receipt_line_id, received_date, origin_country,
unit_price_per_g, quantity_received, quantity_onhand,
expiry_date, lot_number, is_depleted, display_name
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
herb_item_id, # herb_item_id (휴먼일당귀)
supplier_id, # supplier_id
receipt_line_id, # receipt_line_id (기존 로트에서 복사)
datetime.now().strftime('%Y-%m-%d'), # received_date
'한국', # origin_country (기존은 중국)
18.5, # unit_price_per_g (기존은 12.9)
3000.0, # quantity_received
3000.0, # quantity_onhand
(datetime.now() + timedelta(days=365)).strftime('%Y-%m-%d'), # expiry_date
'TEST-DG-2024-001', # lot_number
0, # is_depleted
'일당귀(한국산)' # display_name
))
new_lot_id = cursor.lastrowid
conn.commit()
print(f"✅ 테스트용 당귀 로트 추가 완료!")
print(f" - Lot ID: {new_lot_id}")
print(f" - 약재: 휴먼일당귀")
print(f" - 원산지: 한국")
print(f" - 재고: 3000g")
print(f" - 단가: 18.5원/g")
# 현재 당귀 로트 현황 표시
print("\n=== 현재 휴먼일당귀 로트 현황 ===")
cursor.execute("""
SELECT lot_id, origin_country, quantity_onhand, unit_price_per_g
FROM inventory_lots
WHERE herb_item_id = ? AND is_depleted = 0
ORDER BY lot_id
""", (herb_item_id,))
for row in cursor.fetchall():
print(f"Lot #{row[0]}: {row[1]}산, 재고 {row[2]}g, 단가 {row[3]}원/g")
except Exception as e:
conn.rollback()
print(f"❌ 오류: {e}")
finally:
conn.close()
if __name__ == "__main__":
add_test_lot()

213
add_wolbitang_formulas.py Normal file
View File

@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
월비탕 단계별 처방 추가 스크립트
월비탕 1차부터 4차까지 단계별로 처방을 등록합니다.
각 단계마다 약재의 용량이 다릅니다.
"""
import sqlite3
from datetime import datetime
def add_wolbitang_formulas():
"""월비탕 단계별 처방 추가"""
# 약재 성분 코드 매핑
herb_codes = {
"마황": "3147H1AHM",
"석고": "3265H1AHM",
"감초": "3007H1AHM",
"진피": "3632H1AHM",
"복령": "3215H1AHM",
"갈근": "3002H1AHM",
"지황": "3463H1AHM", # 건지황 대신 지황 사용
"창출": "3472H1AHM"
}
# 월비탕 단계별 처방 데이터
wolbitang_prescriptions = [
{
'formula_code': 'WBT001-1',
'formula_name': '월비탕 1차',
'formula_type': 'CUSTOM',
'base_cheop': 1,
'base_pouches': 1,
'description': '월비탕 1차 - 단계별 처방의 첫 번째 단계',
'ingredients': [
{'herb': '마황', 'amount': 4.0, 'notes': '발한해표'},
{'herb': '석고', 'amount': 3.0, 'notes': '청열사화'},
{'herb': '감초', 'amount': 3.0, 'notes': '조화제약'},
{'herb': '진피', 'amount': 3.333, 'notes': '리기화담'},
{'herb': '복령', 'amount': 4.0, 'notes': '건비이수'},
{'herb': '갈근', 'amount': 3.333, 'notes': '승진해기'},
{'herb': '지황', 'amount': 3.333, 'notes': '보음청열'},
{'herb': '창출', 'amount': 3.333, 'notes': '건비조습'}
]
},
{
'formula_code': 'WBT001-2',
'formula_name': '월비탕 2차',
'formula_type': 'CUSTOM',
'base_cheop': 1,
'base_pouches': 1,
'description': '월비탕 2차 - 단계별 처방의 두 번째 단계',
'ingredients': [
{'herb': '마황', 'amount': 5.0, 'notes': '발한해표'},
{'herb': '석고', 'amount': 4.0, 'notes': '청열사화'},
{'herb': '감초', 'amount': 3.0, 'notes': '조화제약'},
{'herb': '진피', 'amount': 3.75, 'notes': '리기화담'},
{'herb': '복령', 'amount': 4.0, 'notes': '건비이수'},
{'herb': '갈근', 'amount': 3.333, 'notes': '승진해기'},
{'herb': '지황', 'amount': 3.333, 'notes': '보음청열'},
{'herb': '창출', 'amount': 3.333, 'notes': '건비조습'}
]
},
{
'formula_code': 'WBT001-3',
'formula_name': '월비탕 3차',
'formula_type': 'CUSTOM',
'base_cheop': 1,
'base_pouches': 1,
'description': '월비탕 3차 - 단계별 처방의 세 번째 단계',
'ingredients': [
{'herb': '마황', 'amount': 6.0, 'notes': '발한해표'},
{'herb': '석고', 'amount': 4.17, 'notes': '청열사화'},
{'herb': '감초', 'amount': 3.0, 'notes': '조화제약'},
{'herb': '진피', 'amount': 4.17, 'notes': '리기화담'},
{'herb': '복령', 'amount': 4.17, 'notes': '건비이수'},
{'herb': '갈근', 'amount': 3.75, 'notes': '승진해기'},
{'herb': '지황', 'amount': 3.75, 'notes': '보음청열'},
{'herb': '창출', 'amount': 3.333, 'notes': '건비조습'}
]
},
{
'formula_code': 'WBT001-4',
'formula_name': '월비탕 4차',
'formula_type': 'CUSTOM',
'base_cheop': 1,
'base_pouches': 1,
'description': '월비탕 4차 - 단계별 처방의 네 번째 단계',
'ingredients': [
{'herb': '마황', 'amount': 7.0, 'notes': '발한해표'},
{'herb': '석고', 'amount': 5.0, 'notes': '청열사화'},
{'herb': '감초', 'amount': 3.0, 'notes': '조화제약'},
{'herb': '진피', 'amount': 4.17, 'notes': '리기화담'},
{'herb': '복령', 'amount': 5.0, 'notes': '건비이수'},
{'herb': '갈근', 'amount': 3.75, 'notes': '승진해기'},
{'herb': '지황', 'amount': 4.0, 'notes': '보음청열'},
{'herb': '창출', 'amount': 3.333, 'notes': '건비조습'}
]
}
]
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 기존 월비탕 처방 확인
cursor.execute("""
SELECT formula_code, formula_name
FROM formulas
WHERE formula_code LIKE 'WBT%'
ORDER BY formula_code
""")
existing = cursor.fetchall()
if existing:
print("⚠️ 기존 월비탕 처방 발견:")
for code, name in existing:
print(f" - {code}: {name}")
print()
# 각 처방 추가
for prescription in wolbitang_prescriptions:
print(f"\n{'='*60}")
print(f"📝 {prescription['formula_name']} 추가 중...")
# 1. formulas 테이블에 처방 추가
cursor.execute("""
INSERT INTO formulas (
formula_code, formula_name, formula_type,
base_cheop, base_pouches, description,
is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (
prescription['formula_code'],
prescription['formula_name'],
prescription['formula_type'],
prescription['base_cheop'],
prescription['base_pouches'],
prescription['description']
))
formula_id = cursor.lastrowid
print(f" ✅ 처방 기본 정보 등록 (ID: {formula_id})")
# 2. formula_ingredients 테이블에 약재 추가
sort_order = 0
for ingredient in prescription['ingredients']:
herb_name = ingredient['herb']
ingredient_code = herb_codes[herb_name]
cursor.execute("""
INSERT INTO formula_ingredients (
formula_id, ingredient_code,
grams_per_cheop, notes,
sort_order, created_at
) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
""", (
formula_id,
ingredient_code,
ingredient['amount'],
ingredient['notes'],
sort_order
))
sort_order += 1
print(f" - {herb_name}: {ingredient['amount']}g ({ingredient['notes']})")
print(f"{prescription['formula_name']} 추가 완료!")
conn.commit()
print(f"\n{'='*60}")
print("🎉 월비탕 1차~4차 모든 처방이 성공적으로 추가되었습니다!")
# 추가된 처방 확인
print("\n📊 추가된 월비탕 처방 목록:")
print("-"*60)
cursor.execute("""
SELECT f.formula_id, f.formula_code, f.formula_name,
COUNT(fi.ingredient_id) as herb_count,
SUM(fi.grams_per_cheop) as total_grams
FROM formulas f
LEFT JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
WHERE f.formula_code LIKE 'WBT%'
GROUP BY f.formula_id
ORDER BY f.formula_code
""")
for row in cursor.fetchall():
print(f"ID {row[0]}: {row[1]} - {row[2]}")
print(f" 약재 {row[3]}개, 총 {row[4]:.3f}g")
except sqlite3.IntegrityError as e:
print(f"❌ 중복 오류: {e}")
print(" 이미 동일한 처방 코드가 존재합니다.")
conn.rollback()
return False
except sqlite3.Error as e:
print(f"❌ 데이터베이스 오류: {e}")
conn.rollback()
return False
finally:
conn.close()
return True
if __name__ == "__main__":
print("🌿 월비탕 단계별 처방 추가 프로그램")
print("="*60)
if add_wolbitang_formulas():
print("\n✅ 월비탕 처방 추가 작업이 완료되었습니다.")
else:
print("\n❌ 처방 추가 중 오류가 발생했습니다.")

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
월비탕 단계별 처방 추가 스크립트
월비탕 1차부터 4차까지 단계별로 처방을 등록합니다.
각 단계마다 약재의 용량이 다릅니다.
"""
import sqlite3
import json
from datetime import datetime
def add_wolbitang_prescriptions():
"""월비탕 단계별 처방 추가"""
# 월비탕 단계별 데이터
wolbitang_data = {
"월비탕 1차": {
"마황": 4,
"석고": 3,
"감초": 3,
"진피": 3.333,
"복령": 4,
"갈근": 3.333,
"건지황": 3.333,
"창출": 3.333
},
"월비탕 2차": {
"마황": 5,
"석고": 4,
"감초": 3,
"진피": 3.75,
"복령": 4,
"갈근": 3.333,
"건지황": 3.333,
"창출": 3.333
},
"월비탕 3차": {
"마황": 6,
"석고": 4.17,
"감초": 3,
"진피": 4.17,
"복령": 4.17,
"갈근": 3.75,
"건지황": 3.75,
"창출": 3.333
},
"월비탕 4차": {
"마황": 7,
"석고": 5,
"감초": 3,
"진피": 4.17,
"복령": 5,
"갈근": 3.75,
"건지황": 4,
"창출": 3.333
}
}
conn = sqlite3.connect('kdrug.db')
cursor = conn.cursor()
try:
# 약재명-코드 매핑
herb_code_mapping = {
"마황": "H004",
"석고": "H025",
"감초": "H001",
"진피": "H022",
"복령": "H010",
"갈근": "H024",
"건지황": "H026",
"창출": "H014"
}
# 각 단계별로 처방 추가
for prescription_name, herbs in wolbitang_data.items():
print(f"\n{'='*50}")
print(f"{prescription_name} 추가 중...")
# 1. 처방 기본 정보 추가
cursor.execute("""
INSERT INTO prescriptions (
name,
description,
source,
category,
created_at
) VALUES (?, ?, ?, ?, ?)
""", (
prescription_name,
f"{prescription_name} - 월비탕의 단계별 처방",
"임상처방",
"단계별처방",
datetime.now().isoformat()
))
prescription_id = cursor.lastrowid
print(f" 처방 ID {prescription_id}로 등록됨")
# 2. 처방 구성 약재 추가
ingredients = []
for herb_name, amount in herbs.items():
herb_code = herb_code_mapping.get(herb_name)
if not herb_code:
print(f" ⚠️ {herb_name}의 코드를 찾을 수 없습니다.")
continue
# prescription_ingredients 테이블에 추가
cursor.execute("""
INSERT INTO prescription_ingredients (
prescription_id,
ingredient_code,
amount,
unit
) VALUES (?, ?, ?, ?)
""", (prescription_id, herb_code, amount, 'g'))
ingredients.append({
'code': herb_code,
'name': herb_name,
'amount': amount,
'unit': 'g'
})
print(f" - {herb_name}({herb_code}): {amount}g 추가됨")
# 3. prescription_details 테이블에 JSON 형태로도 저장
cursor.execute("""
INSERT INTO prescription_details (
prescription_id,
ingredients_json,
total_herbs,
default_packets,
preparation_method
) VALUES (?, ?, ?, ?, ?)
""", (
prescription_id,
json.dumps(ingredients, ensure_ascii=False),
len(ingredients),
20, # 기본 첩수
"1일 2회, 1회 1포"
))
print(f"{prescription_name} 처방 추가 완료 (총 {len(ingredients)}개 약재)")
conn.commit()
print(f"\n{'='*50}")
print("✅ 월비탕 1차~4차 처방이 모두 성공적으로 추가되었습니다!")
# 추가된 처방 확인
print("\n📊 추가된 처방 목록:")
cursor.execute("""
SELECT p.id, p.name, pd.total_herbs
FROM prescriptions p
LEFT JOIN prescription_details pd ON p.id = pd.prescription_id
WHERE p.name LIKE '월비탕%'
ORDER BY p.id
""")
for row in cursor.fetchall():
print(f" ID {row[0]}: {row[1]} - {row[2]}개 약재")
except sqlite3.Error as e:
print(f"❌ 데이터베이스 오류: {e}")
conn.rollback()
return False
finally:
conn.close()
return True
if __name__ == "__main__":
print("🌿 월비탕 단계별 처방 추가 프로그램")
print("="*50)
if add_wolbitang_prescriptions():
print("\n✅ 월비탕 처방 추가 작업이 완료되었습니다.")
else:
print("\n❌ 처방 추가 중 오류가 발생했습니다.")

2715
app.py

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

428
apply_variants_to_lots.py Normal file
View File

@@ -0,0 +1,428 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
기존 inventory_lots에 variant 정보 적용
"""
import sqlite3
import json
from datetime import datetime
def get_connection():
"""데이터베이스 연결"""
return sqlite3.connect('database/kdrug.db')
def check_current_lots():
"""현재 inventory_lots 상태 확인"""
conn = get_connection()
cursor = conn.cursor()
print("\n" + "="*80)
print("현재 입고된 Inventory Lots 현황")
print("="*80)
cursor.execute("""
SELECT
l.lot_id,
l.herb_item_id,
h.herb_name,
h.insurance_code,
l.quantity_onhand,
l.unit_price_per_g,
l.origin_country,
l.received_date,
s.name as supplier_name,
l.display_name
FROM inventory_lots l
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
JOIN purchase_receipt_lines prl ON l.receipt_line_id = prl.line_id
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
JOIN suppliers s ON pr.supplier_id = s.supplier_id
ORDER BY l.received_date DESC, h.herb_name
""")
lots = cursor.fetchall()
print(f"\n{len(lots)}개의 로트가 있습니다.\n")
for lot in lots[:10]: # 처음 10개만 출력
print(f"Lot #{lot[0]}: {lot[2]} (보험코드: {lot[3]})")
print(f" - 재고: {lot[4]:.1f}g, 단가: {lot[5]:.1f}원/g")
print(f" - 원산지: {lot[6]}, 공급처: {lot[8]}")
print(f" - display_name: {lot[9] if lot[9] else '없음'}")
print()
conn.close()
return lots
def insert_sample_catalog_data():
"""공급처 카탈로그 샘플 데이터 추가"""
conn = get_connection()
cursor = conn.cursor()
print("\n" + "="*80)
print("공급처 카탈로그 샘플 데이터 추가")
print("="*80)
# 휴먼허브 supplier_id 조회
cursor.execute("SELECT supplier_id FROM suppliers WHERE name = '(주)휴먼허브'")
supplier_id = cursor.fetchone()
if not supplier_id:
print("휴먼허브 공급처를 찾을 수 없습니다.")
conn.close()
return
supplier_id = supplier_id[0]
# 휴먼허브 실제 가격 기반 데이터 (원산지별로 구분)
catalog_items = [
('신흥숙지황(9증)', 20.0, '1kg'),
('휴먼갈근[한국산]', 16.8, '1kg'),
('휴먼감초', 22.1, '1kg'),
('휴먼건강[페루산]', 12.4, '1kg'),
('휴먼건강.土[한국산]', 51.4, '1kg'),
('휴먼계지', 5.8, '1kg'),
('휴먼구기자(영하)', 17.9, '1kg'),
('휴먼길경.片', 10.6, '1kg'),
('휴먼대추(한국산)', 20.0, '1kg'),
('휴먼마황', 9.6, '1kg'),
('휴먼반하(생강백반제)', 33.7, '1kg'),
('휴먼백출', 11.8, '1kg'),
('휴먼복령', 11.5, '1kg'),
('휴먼석고', 4.7, '1kg'),
('휴먼세신.中', 129.0, '1kg'),
('휴먼오미자<토매지>', 17.5, '1kg'),
('휴먼용안육', 20.7, '1kg'),
('휴먼육계', 14.6, '1kg'),
('휴먼일당귀', 12.9, '1kg'),
('휴먼자소엽.土', 13.8, '1kg'),
('휴먼작약', 18.7, '1kg'),
('휴먼작약(주자.酒炙)', 24.6, '1kg'),
('휴먼전호[재배]', 14.0, '1kg'),
('휴먼지각', 10.0, '1kg'),
('휴먼지황.건', 11.5, '1kg'),
('휴먼진피', 13.7, '1kg'),
('휴먼창출[북창출]', 13.5, '1kg'),
('휴먼천궁.일', 11.9, '1kg'),
('휴먼황기(직절.小)', 9.9, '1kg'),
]
# 기존 데이터 삭제
cursor.execute("DELETE FROM supplier_product_catalog WHERE supplier_id = ?", (supplier_id,))
# 새 데이터 삽입
for item in catalog_items:
raw_name, unit_price, package_unit = item
try:
cursor.execute("""
INSERT INTO supplier_product_catalog
(supplier_id, raw_name, unit_price, package_unit, stock_status, last_updated)
VALUES (?, ?, ?, ?, '재고있음', date('now'))
""", (supplier_id, raw_name, unit_price, package_unit))
print(f" 추가: {raw_name} - {unit_price:.1f}원/g")
except sqlite3.IntegrityError:
print(f" 중복: {raw_name} (이미 존재)")
conn.commit()
# 추가된 항목 수 확인
cursor.execute("SELECT COUNT(*) FROM supplier_product_catalog WHERE supplier_id = ?", (supplier_id,))
count = cursor.fetchone()[0]
print(f"\n{count}개의 카탈로그 항목이 등록되었습니다.")
conn.close()
def match_lots_with_catalog():
"""가격 기반으로 lot과 카탈로그 매칭"""
conn = get_connection()
cursor = conn.cursor()
print("\n" + "="*80)
print("가격 기반 Lot-Catalog 매칭")
print("="*80)
# 한의사랑에서 입고된 lot들 조회
cursor.execute("""
SELECT DISTINCT
l.lot_id,
h.herb_name,
l.unit_price_per_g,
s.supplier_id,
s.name as supplier_name
FROM inventory_lots l
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
JOIN purchase_receipt_lines prl ON l.receipt_line_id = prl.line_id
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
JOIN suppliers s ON pr.supplier_id = s.supplier_id
WHERE s.name = '한의사랑'
AND l.display_name IS NULL
""")
lots = cursor.fetchall()
matched_count = 0
for lot in lots:
lot_id, herb_name, unit_price, supplier_id, supplier_name = lot
# 가격 기반 매칭 (±0.5원 허용)
cursor.execute("""
SELECT raw_name, unit_price
FROM supplier_product_catalog
WHERE supplier_id = ?
AND ABS(unit_price - ?) < 0.5
""", (supplier_id, unit_price))
matches = cursor.fetchall()
if matches:
if len(matches) == 1:
# 정확히 1개 매칭
raw_name = matches[0][0]
# display_name 업데이트
cursor.execute("""
UPDATE inventory_lots
SET display_name = ?
WHERE lot_id = ?
""", (raw_name, lot_id))
# lot_variants 생성
cursor.execute("""
INSERT OR REPLACE INTO lot_variants
(lot_id, raw_name, parsed_at, parsed_method)
VALUES (?, ?, datetime('now'), 'price_match')
""", (lot_id, raw_name))
print(f" 매칭 성공: Lot #{lot_id} {herb_name} -> {raw_name}")
matched_count += 1
else:
# 여러 개 매칭 - 약재명으로 추가 필터링
print(f" 다중 매칭: Lot #{lot_id} {herb_name} (단가: {unit_price:.1f}원)")
for match in matches:
print(f" - {match[0]} ({match[1]:.1f}원/g)")
# 약재명이 포함된 것 선택
if herb_name in match[0]:
raw_name = match[0]
cursor.execute("""
UPDATE inventory_lots
SET display_name = ?
WHERE lot_id = ?
""", (raw_name, lot_id))
cursor.execute("""
INSERT OR REPLACE INTO lot_variants
(lot_id, raw_name, parsed_at, parsed_method)
VALUES (?, ?, datetime('now'), 'price_herb_match')
""", (lot_id, raw_name))
print(f" -> 선택: {raw_name}")
matched_count += 1
break
else:
print(f" 매칭 실패: Lot #{lot_id} {herb_name} (단가: {unit_price:.1f}원)")
conn.commit()
print(f"\n{matched_count}/{len(lots)}개의 로트가 매칭되었습니다.")
conn.close()
return matched_count
def parse_variant_attributes():
"""raw_name에서 variant 속성 파싱"""
conn = get_connection()
cursor = conn.cursor()
print("\n" + "="*80)
print("Variant 속성 파싱")
print("="*80)
cursor.execute("""
SELECT variant_id, raw_name
FROM lot_variants
WHERE form IS NULL AND processing IS NULL
""")
variants = cursor.fetchall()
for variant_id, raw_name in variants:
# 기본 파싱 로직
form = None
processing = None
selection_state = None
grade = None
# 형태 파싱 (각, 片, 절편, 직절, 土 등)
if '.각' in raw_name:
form = ''
elif '.片' in raw_name or '[片]' in raw_name:
form = ''
elif '절편' in raw_name:
form = '절편'
elif '직절' in raw_name:
form = '직절'
elif '.土' in raw_name:
form = ''
# 가공 파싱 (9증, 酒炙, 비열, 회 등)
if '9증' in raw_name:
processing = '9증'
elif '酒炙' in raw_name or '주자' in raw_name:
processing = '酒炙'
elif '비열' in raw_name or '非熱' in raw_name:
processing = '비열'
elif '생강백반제' in raw_name:
processing = '생강백반제'
elif '.건[회]' in raw_name or '[회]' in raw_name:
processing = ''
# 선별상태 파싱 (야생, 토매지, 재배 등)
if '야생' in raw_name:
selection_state = '야생'
elif '토매지' in raw_name:
selection_state = '토매지'
elif '재배' in raw_name:
selection_state = '재배'
elif '영하' in raw_name:
selection_state = '영하'
# 등급 파싱 (특, 名品, 中, 小, 1호, YB2, 당 등)
if '[특]' in raw_name or '.특' in raw_name:
grade = ''
elif '名品' in raw_name:
grade = '名品'
elif '.中' in raw_name:
grade = ''
elif '.小' in raw_name:
grade = ''
elif '1호' in raw_name:
grade = '1호'
elif 'YB2' in raw_name:
grade = 'YB2'
elif '.당' in raw_name:
grade = ''
elif '[완]' in raw_name:
grade = ''
# 업데이트
cursor.execute("""
UPDATE lot_variants
SET form = ?, processing = ?, selection_state = ?, grade = ?
WHERE variant_id = ?
""", (form, processing, selection_state, grade, variant_id))
attributes = []
if form: attributes.append(f"형태:{form}")
if processing: attributes.append(f"가공:{processing}")
if selection_state: attributes.append(f"선별:{selection_state}")
if grade: attributes.append(f"등급:{grade}")
if attributes:
print(f" {raw_name}")
print(f" -> {', '.join(attributes)}")
conn.commit()
conn.close()
def show_results():
"""최종 결과 확인"""
conn = get_connection()
cursor = conn.cursor()
print("\n" + "="*80)
print("Variant 적용 결과")
print("="*80)
cursor.execute("""
SELECT
l.lot_id,
h.herb_name,
l.display_name,
v.form,
v.processing,
v.selection_state,
v.grade,
l.unit_price_per_g,
l.quantity_onhand
FROM inventory_lots l
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
LEFT JOIN lot_variants v ON l.lot_id = v.lot_id
WHERE l.display_name IS NOT NULL
ORDER BY h.herb_name, l.lot_id
""")
results = cursor.fetchall()
current_herb = None
for result in results:
lot_id, herb_name, display_name, form, processing, selection_state, grade, price, qty = result
if herb_name != current_herb:
print(f"\n[{herb_name}]")
current_herb = herb_name
print(f" Lot #{lot_id}: {display_name or herb_name}")
print(f" 재고: {qty:.0f}g, 단가: {price:.1f}원/g")
attributes = []
if form: attributes.append(f"형태:{form}")
if processing: attributes.append(f"가공:{processing}")
if selection_state: attributes.append(f"선별:{selection_state}")
if grade: attributes.append(f"등급:{grade}")
if attributes:
print(f" 속성: {', '.join(attributes)}")
# 통계
cursor.execute("""
SELECT
COUNT(DISTINCT l.lot_id) as total_lots,
COUNT(DISTINCT CASE WHEN l.display_name IS NOT NULL THEN l.lot_id END) as lots_with_display,
COUNT(DISTINCT v.lot_id) as lots_with_variants
FROM inventory_lots l
LEFT JOIN lot_variants v ON l.lot_id = v.lot_id
""")
stats = cursor.fetchone()
print("\n" + "-"*40)
print(f"전체 로트: {stats[0]}")
print(f"display_name 설정됨: {stats[1]}")
print(f"variant 정보 있음: {stats[2]}")
conn.close()
def main():
"""메인 실행 함수"""
print("\n" + "="*80)
print("Inventory Lots Variant System 적용")
print("="*80)
# 1. 현재 lot 상태 확인
lots = check_current_lots()
if not lots:
print("입고된 로트가 없습니다.")
return
# 2. 공급처 카탈로그 데이터 추가
insert_sample_catalog_data()
# 3. 가격 기반 매칭
matched = match_lots_with_catalog()
if matched > 0:
# 4. variant 속성 파싱
parse_variant_attributes()
# 5. 결과 확인
show_results()
print("\n완료!")
if __name__ == "__main__":
main()

102
check_compound_detail.py Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
조제 데이터 상세 확인 - formula_id가 NULL인 케이스 분석
"""
import sqlite3
def check_compound_details():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
print("=" * 80)
print("조제 데이터 상세 분석")
print("=" * 80)
# formula_id가 NULL인 조제건 확인
print("\n1. Formula ID가 NULL인 조제건:")
print("-" * 80)
cursor.execute("""
SELECT compound_id, patient_id, formula_id, is_custom, custom_type,
custom_summary, compound_date, status
FROM compounds
WHERE formula_id IS NULL
ORDER BY compound_id DESC
""")
null_formulas = cursor.fetchall()
for comp in null_formulas:
print(f" ID: {comp[0]}")
print(f" 환자ID: {comp[1]}, is_custom: {comp[3]}, custom_type: {comp[4]}")
print(f" custom_summary: {comp[5]}")
print(f" 조제일: {comp[6]}, 상태: {comp[7]}")
print()
# 전체 조제 데이터 요약
print("\n2. 전체 조제 데이터 요약:")
print("-" * 80)
cursor.execute("""
SELECT
COUNT(*) as total,
COUNT(formula_id) as with_formula,
COUNT(*) - COUNT(formula_id) as without_formula,
SUM(CASE WHEN is_custom = 1 THEN 1 ELSE 0 END) as custom_count
FROM compounds
""")
summary = cursor.fetchone()
print(f" 총 조제건수: {summary[0]}")
print(f" 처방 있음: {summary[1]}")
print(f" 처방 없음(직접조제): {summary[2]}")
print(f" 커스텀 플래그: {summary[3]}")
# compound_items 테이블 확인
print("\n3. Compound_items 테이블 구조 및 데이터:")
print("-" * 80)
# 테이블 구조 확인
cursor.execute("PRAGMA table_info(compound_items)")
columns = cursor.fetchall()
if columns:
print(" 테이블 구조:")
for col in columns:
print(f" {col[1]:20s} {col[2]:15s}")
# 샘플 데이터
cursor.execute("""
SELECT * FROM compound_items
LIMIT 5
""")
items = cursor.fetchall()
if items:
print("\n 샘플 데이터:")
for item in items:
print(f" {item}")
else:
print(" compound_items 테이블이 비어있거나 없습니다.")
# 직접조제의 약재 구성 확인 (compound_items와 연결)
print("\n4. 직접조제(formula_id=NULL) 약재 구성:")
print("-" * 80)
for comp_id in [10, 8, 7]: # NULL인 compound_id들
cursor.execute("""
SELECT ci.*, h.herb_name
FROM compound_items ci
LEFT JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
WHERE ci.compound_id = ?
LIMIT 5
""", (comp_id,))
items = cursor.fetchall()
if items:
print(f" Compound ID {comp_id}의 약재:")
for item in items:
print(f" {item}")
else:
print(f" Compound ID {comp_id}: 약재 데이터 없음")
conn.close()
if __name__ == "__main__":
check_compound_details()

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
compound_ingredients 테이블 확인 - 직접조제 데이터 분석
"""
import sqlite3
def analyze_compound_ingredients():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
print("=" * 80)
print("Compound_ingredients 테이블 분석")
print("=" * 80)
# 테이블 구조
print("\n1. 테이블 구조:")
print("-" * 80)
cursor.execute("PRAGMA table_info(compound_ingredients)")
for col in cursor.fetchall():
print(f" {col[1]:25s} {col[2]:15s} {'NOT NULL' if col[3] else 'NULL':10s}")
# formula_id가 NULL인 compound의 약재 구성
print("\n2. Formula_id가 NULL인 조제건의 약재 구성:")
print("-" * 80)
cursor.execute("""
SELECT
c.compound_id,
c.formula_id,
c.is_custom,
c.custom_type,
COUNT(ci.compound_ingredient_id) as ingredient_count,
GROUP_CONCAT(h.herb_name || ':' || ci.grams_per_cheop || 'g', ', ') as ingredients
FROM compounds c
LEFT JOIN compound_ingredients ci ON c.compound_id = ci.compound_id
LEFT JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
WHERE c.formula_id IS NULL
GROUP BY c.compound_id
ORDER BY c.compound_id DESC
""")
results = cursor.fetchall()
for row in results:
print(f"\n Compound ID: {row[0]}")
print(f" Formula ID: {row[1]}, is_custom: {row[2]}, custom_type: {row[3]}")
print(f" 약재 수: {row[4]}")
print(f" 구성: {row[5]}")
# 전체 통계
print("\n3. 전체 조제 통계:")
print("-" * 80)
cursor.execute("""
SELECT
c.formula_id IS NOT NULL as has_formula,
c.is_custom,
COUNT(DISTINCT c.compound_id) as compound_count,
COUNT(ci.compound_ingredient_id) as total_ingredients
FROM compounds c
LEFT JOIN compound_ingredients ci ON c.compound_id = ci.compound_id
GROUP BY has_formula, is_custom
""")
print(f" {'처방있음':10s} {'커스텀':8s} {'조제수':8s} {'총약재수':10s}")
print(" " + "-" * 40)
for row in cursor.fetchall():
has_formula = "" if row[0] else "아니오"
is_custom = "" if row[1] else "아니오"
print(f" {has_formula:10s} {is_custom:8s} {row[2]:8d} {row[3]:10d}")
# 특정 조제건 상세
print("\n4. Compound ID 10번 상세 (formula_id=NULL):")
print("-" * 80)
cursor.execute("""
SELECT
ci.herb_item_id,
h.herb_name,
ci.grams_per_cheop,
c.cheop_total,
ci.total_grams,
ci.notes
FROM compound_ingredients ci
LEFT JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
LEFT JOIN compounds c ON ci.compound_id = c.compound_id
WHERE ci.compound_id = 10
ORDER BY ci.compound_ingredient_id
""")
ingredients = cursor.fetchall()
if ingredients:
print(f" {'약재코드':15s} {'약재명':15s} {'1첩용량':10s} {'첩수':8s} {'총용량':10s}")
print(" " + "-" * 60)
for ing in ingredients:
print(f" {ing[0]:15s} {ing[1] or 'Unknown':15s} {ing[2]:10.1f}g {ing[3]:8.0f} {ing[4]:10.1f}g")
else:
print(" 약재 데이터 없음")
conn.close()
if __name__ == "__main__":
analyze_compound_ingredients()

65
check_compound_schema.py Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
조제(compound) 관련 테이블 스키마 확인
"""
import sqlite3
def check_compound_tables():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# compounds 테이블 구조 확인
print("=" * 80)
print("1. COMPOUNDS 테이블 구조:")
print("=" * 80)
cursor.execute("PRAGMA table_info(compounds)")
columns = cursor.fetchall()
for col in columns:
print(f" {col[1]:25s} {col[2]:15s} {'NOT NULL' if col[3] else 'NULL':10s} {col[4] or ''}")
# compound_items 테이블 구조 확인
print("\n" + "=" * 80)
print("2. COMPOUND_ITEMS 테이블 구조:")
print("=" * 80)
cursor.execute("PRAGMA table_info(compound_items)")
columns = cursor.fetchall()
for col in columns:
print(f" {col[1]:25s} {col[2]:15s} {'NOT NULL' if col[3] else 'NULL':10s} {col[4] or ''}")
# 관련 테이블 확인
print("\n" + "=" * 80)
print("3. 관련 테이블 목록:")
print("=" * 80)
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND (name LIKE '%sale%' OR name LIKE '%payment%' OR name LIKE '%invoice%')
ORDER BY name
""")
related_tables = cursor.fetchall()
if related_tables:
for table in related_tables:
print(f" - {table[0]}")
else:
print(" 판매/결제 관련 테이블 없음")
# 샘플 데이터 확인
print("\n" + "=" * 80)
print("4. 최근 조제 데이터 샘플:")
print("=" * 80)
cursor.execute("""
SELECT c.compound_id, p.name, f.formula_name, c.compound_date, c.created_at, c.status
FROM compounds c
LEFT JOIN patients p ON c.patient_id = p.patient_id
LEFT JOIN formulas f ON c.formula_id = f.formula_id
ORDER BY c.created_at DESC
LIMIT 5
""")
compounds = cursor.fetchall()
for comp in compounds:
print(f" ID:{comp[0]} | 환자:{comp[1]} | 처방:{comp[2]} | 조제일:{comp[3]} | 상태:{comp[5]}")
conn.close()
if __name__ == "__main__":
check_compound_tables()

241
claude.md Normal file
View File

@@ -0,0 +1,241 @@
# 한약 재고관리 시스템 (kdrug)
## 프로젝트 개요
한의원에서 한약재 재고를 관리하고 처방 조제를 추적하는 통합 시스템입니다.
### 주요 기능
- 📦 **재고 관리**: 한약재 입고/출고/재고 추적
- 💊 **처방 조제**: 표준 처방 및 가감방 조제
- 🏥 **보험 청구**: 건강보험 급여 약재 코드 관리
- 🔍 **효능 검색**: 약재 효능별 검색 및 분류
- 📊 **통계 분석**: 재고 현황 및 소비 패턴 분석
## 기술 스택
- **Backend**: Python Flask
- **Database**: SQLite
- **Frontend**: Bootstrap 5 + jQuery
- **Excel**: pandas, openpyxl
## 핵심 개념
### 코드 체계
```
성분코드 (ingredient_code) 보험코드 (insurance_code)
━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━
"3400H1AHM" (인삼) "062400740" (휴먼감초)
- 454개 표준 약재 - 실제 청구/재고 단위
- 한의학적 속성 기준 - 9자리 제품 코드
```
### 테이블 관계
```
herb_items (재고)
↓ insurance_code
herb_products (제품)
↓ ingredient_code
herb_masters (마스터)
↓ ingredient_code
herb_item_tags (효능)
```
## 프로젝트 구조
```
kdrug/
├── app.py # Flask 메인 애플리케이션
├── database/
│ ├── kdrug.db # SQLite 데이터베이스
│ └── schema.sql # 스키마 정의
├── static/
│ ├── app.js # 프론트엔드 로직
│ └── style.css # 스타일시트
├── templates/
│ └── index.html # SPA 템플릿
├── docs/
│ ├── 데이터_구조_및_흐름.md
│ ├── 조제_프로세스_및_커스텀_처방.md
│ └── 한약재_정보_관리_시스템_설계.md
└── migrations/
└── add_herb_extended_info_tables.py
```
## 주요 API 엔드포인트
### 약재 관리
- `GET /api/herbs` - 약재 목록 (효능 태그 포함)
- `GET /api/herbs/masters` - 전체 약재 마스터 (454개)
- `GET /api/herbs/{id}/extended` - 약재 상세 정보
- `GET /api/herbs/{id}/tags` - 효능 태그 조회
- `POST /api/herbs/{id}/tags` - 효능 태그 추가
### 재고 관리
- `GET /api/inventory/summary` - 재고 현황 요약
- `POST /api/purchase-receipts` - 입고 처리
- `GET /api/herbs/{id}/available-lots` - 사용 가능한 로트
### 처방 조제
- `GET /api/formulas` - 처방 목록
- `GET /api/formulas/{id}/ingredients` - 처방 구성
- `POST /api/compounds` - 조제 실행
- `GET /api/patients/{id}/compounds` - 환자별 조제 이력
### 효능 검색
- `GET /api/efficacy-tags` - 모든 효능 태그
- `GET /api/herbs/search-by-efficacy` - 효능별 약재 검색
- `POST /api/prescription-check` - 처방 안전성 검증
## 최근 개선사항 (2026-02-17)
### 1. 효능 태그 시스템 리팩토링 ✅
```sql
-- Before: 복잡한 5단계 JOIN
herb_items products masters extended tags
-- After: 간단한 3단계 JOIN
herb_items products tags (ingredient_code )
```
### 2. 가감방 실시간 감지 ✅
- 처방 수정 시 자동으로 "가감방" 배지 표시
- `ingredient_code` 기준 비교로 정확도 향상
### 3. 복합 로트 지원 ✅
- 한 약재에 여러 로트 사용 가능
- 수동 로트 배분 UI 제공
- FIFO 자동 배분 옵션
## 데이터베이스 설계 특징
### 정규화된 구조
- 성분코드와 보험코드 분리
- 재고는 로트 단위 관리
- 처방 구성은 성분코드 기준
### 확장 가능한 설계
- 효능 태그 시스템 (18개 기본 태그)
- 안전성 정보 테이블
- 연구 문헌 관리
- AI/API 업데이트 로그
### 성능 최적화
- `ingredient_code` 인덱싱
- 집계 쿼리용 서브쿼리 활용
- GROUP_CONCAT으로 태그 조회
## 설치 및 실행
```bash
# 1. 의존성 설치
pip install -r requirements.txt
# 2. 데이터베이스 초기화
python init_db.py
# 3. 서버 실행
python app.py
# 4. 브라우저에서 접속
http://localhost:5001
```
## 개발 가이드
### 새 약재 추가
1. `herb_masters`에 성분코드 확인
2. `herb_products`에 보험코드 매핑
3. `herb_items`에 재고 단위 생성
4. 입고 처리로 `inventory_lots` 생성
### 효능 태그 추가
```python
# ingredient_code로 직접 추가 (개선됨!)
INSERT INTO herb_item_tags (ingredient_code, tag_id, strength)
VALUES ('3400H1AHM', 1, 5) # 인삼에 보기(5) 추가
```
### API 개발 원칙
- `ingredient_code` 중심 JOIN
- `herb_products` 테이블 활용
- COALESCE로 안전한 처리
- 효능 태그는 선택적 로드
## 주의사항
### ID 체계
- ⚠️ `herb_item_id``herb_id`
- `herb_item_id`: 재고 관리 (1~31)
- `herb_id`: 단순 인덱스 (1~454)
- 실제 KEY: `ingredient_code`
### 코드 매핑
- 입력: 보험코드 (9자리)
- 중간: `herb_products` 매핑
- 최종: `ingredient_code` 연결
### 재고 처리
- FIFO 원칙
- 로트별 추적
- 복합 로트 지원
## 향후 계획
### Phase 2: AI 통합
- 한의학연구원 API 연동
- PubMed 문헌 자동 수집
- ChatGPT 기반 정보 추출
### Phase 3: 임상 지원
- DUR 시스템 구현
- 처방 최적화 제안
- 부작용 모니터링
### Phase 4: 분석 강화
- 소비 패턴 분석
- 재고 예측 모델
- 비용 최적화
## 문제 해결
### 약재 드롭다운이 안 나올 때
```python
# /api/herbs 엔드포인트 확인
# herb_products 매핑 확인
# ingredient_code 연결 확인
```
### 효능 태그가 안 보일 때
```python
# herb_item_tags 테이블 확인
# ingredient_code 매핑 확인
# JOIN 경로 확인
```
### 가감방이 감지 안 될 때
```python
# originalFormulaIngredients 전역 변수 확인
# ingredient_code 비교 로직 확인
# 약재 추가/삭제 이벤트 확인
```
## 기여 가이드
1. 기존 시스템 이해
2. 영향도 분석
3. 테스트 작성
4. 문서 업데이트
5. PR 제출
## 라이선스
Private Project - All rights reserved
## 문의
개발팀 연락처: [이메일/슬랙]
---
*Last updated: 2026-02-17*
*Version: 1.0.0*

43
config.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""
kdrug 프로젝트 공통 설정 파일
모든 Python 스크립트에서 이 설정을 import하여 사용
"""
import os
from pathlib import Path
# 프로젝트 루트 디렉토리
PROJECT_ROOT = Path(__file__).parent
# 데이터베이스 경로 - 항상 절대 경로 사용
DATABASE_PATH = PROJECT_ROOT / 'database' / 'kdrug.db'
MEDICINE_MASTER_PATH = PROJECT_ROOT / 'database' / 'medicine_master.db'
# 기타 자주 사용하는 경로들
STATIC_PATH = PROJECT_ROOT / 'static'
TEMPLATES_PATH = PROJECT_ROOT / 'templates'
DOCS_PATH = PROJECT_ROOT / 'docs'
BACKUP_PATH = PROJECT_ROOT / 'backups'
# 데이터베이스 연결 헬퍼 함수
def get_db_connection():
"""표준 데이터베이스 연결 반환"""
import sqlite3
conn = sqlite3.connect(str(DATABASE_PATH))
conn.row_factory = sqlite3.Row # 컬럼명으로 접근 가능하도록 설정
return conn
# 설정 확인용 (디버그)
if __name__ == "__main__":
print(f"프로젝트 루트: {PROJECT_ROOT}")
print(f"데이터베이스 경로: {DATABASE_PATH}")
print(f"데이터베이스 존재: {DATABASE_PATH.exists()}")
if DATABASE_PATH.exists():
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'")
table_count = cursor.fetchone()[0]
print(f"테이블 개수: {table_count}")
conn.close()

94
create_sales_tables.py Normal file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""
판매 관리 시스템 - 신규 테이블 생성
"""
import sqlite3
from datetime import datetime
def create_sales_tables():
"""판매 관련 신규 테이블 생성"""
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# 1. sales_transactions 테이블 생성
cursor.execute("""
CREATE TABLE IF NOT EXISTS sales_transactions (
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
compound_id INTEGER REFERENCES compounds(compound_id),
transaction_date DATETIME NOT NULL,
transaction_type TEXT NOT NULL, -- SALE, REFUND, CANCEL
amount REAL NOT NULL,
payment_method TEXT,
payment_status TEXT, -- PENDING, COMPLETED, FAILED
notes TEXT,
created_by TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
print("✓ sales_transactions 테이블 생성 완료")
# 2. price_policies 테이블 생성
cursor.execute("""
CREATE TABLE IF NOT EXISTS price_policies (
policy_id INTEGER PRIMARY KEY AUTOINCREMENT,
formula_id INTEGER REFERENCES formulas(formula_id),
base_price REAL NOT NULL,
dispensing_fee REAL DEFAULT 0,
is_active BOOLEAN DEFAULT 1,
effective_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
print("✓ price_policies 테이블 생성 완료")
# 3. sales_status_history 테이블 생성
cursor.execute("""
CREATE TABLE IF NOT EXISTS sales_status_history (
history_id INTEGER PRIMARY KEY AUTOINCREMENT,
compound_id INTEGER REFERENCES compounds(compound_id),
old_status TEXT,
new_status TEXT NOT NULL,
changed_by TEXT,
change_reason TEXT,
changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
print("✓ sales_status_history 테이블 생성 완료")
# 인덱스 생성
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_sales_transactions_compound
ON sales_transactions(compound_id)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_price_policies_formula
ON price_policies(formula_id)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_sales_status_history_compound
ON sales_status_history(compound_id)
""")
print("✓ 인덱스 생성 완료")
conn.commit()
# 생성된 테이블 확인
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table'
AND name IN ('sales_transactions', 'price_policies', 'sales_status_history')
""")
tables = cursor.fetchall()
print("\n생성된 테이블:")
for table in tables:
print(f" - {table[0]}")
conn.close()
print("\n판매 관련 테이블 생성 완료!")
if __name__ == "__main__":
create_sales_tables()

View File

@@ -0,0 +1,70 @@
-- 환자 사전 문진표 시스템 테이블
-- 1. 문진 메인 테이블
CREATE TABLE IF NOT EXISTS patient_surveys (
survey_id INTEGER PRIMARY KEY AUTOINCREMENT,
patient_id INTEGER,
survey_token TEXT UNIQUE NOT NULL, -- QR/링크용 고유 토큰
survey_date DATE DEFAULT (date('now')),
status TEXT DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'REVIEWED')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
reviewed_at DATETIME,
reviewed_by TEXT,
notes TEXT,
FOREIGN KEY (patient_id) REFERENCES patients(patient_id)
);
CREATE INDEX IF NOT EXISTS idx_survey_token ON patient_surveys(survey_token);
CREATE INDEX IF NOT EXISTS idx_survey_patient ON patient_surveys(patient_id);
-- 2. 문진 응답 테이블
CREATE TABLE IF NOT EXISTS survey_responses (
response_id INTEGER PRIMARY KEY AUTOINCREMENT,
survey_id INTEGER NOT NULL,
category TEXT NOT NULL, -- 카테고리 (TEMPERATURE, BODY_TEMP, DIGESTION, etc)
question_code TEXT NOT NULL, -- 질문 코드
question_text TEXT, -- 질문 내용
answer_value TEXT, -- 응답 값 (JSON 또는 TEXT)
answer_type TEXT, -- SINGLE, MULTIPLE, TEXT, RANGE
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (survey_id) REFERENCES patient_surveys(survey_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_response_survey ON survey_responses(survey_id);
CREATE INDEX IF NOT EXISTS idx_response_category ON survey_responses(category);
-- 3. 문진 템플릿 테이블 (질문 정의)
CREATE TABLE IF NOT EXISTS survey_templates (
template_id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
category_name TEXT NOT NULL, -- 카테고리 한글명
question_code TEXT NOT NULL UNIQUE,
question_text TEXT NOT NULL,
question_subtext TEXT, -- 부가 설명
input_type TEXT NOT NULL CHECK(input_type IN ('RADIO', 'CHECKBOX', 'RANGE', 'TEXT', 'TEXTAREA', 'NUMBER')),
options TEXT, -- JSON 배열 형태의 선택지
is_required INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_template_category ON survey_templates(category);
CREATE INDEX IF NOT EXISTS idx_template_active ON survey_templates(is_active);
-- 4. 문진 진행 상태 추적 테이블
CREATE TABLE IF NOT EXISTS survey_progress (
progress_id INTEGER PRIMARY KEY AUTOINCREMENT,
survey_id INTEGER NOT NULL,
category TEXT NOT NULL,
total_questions INTEGER DEFAULT 0,
answered_questions INTEGER DEFAULT 0,
is_completed INTEGER DEFAULT 0,
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (survey_id) REFERENCES patient_surveys(survey_id) ON DELETE CASCADE,
UNIQUE(survey_id, category)
);
CREATE INDEX IF NOT EXISTS idx_progress_survey ON survey_progress(survey_id);

View File

@@ -0,0 +1,596 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
문진표 템플릿 데이터 입력 스크립트
"""
import sqlite3
import json
# 문진 항목 데이터
survey_data = [
# 1. 체온 감각 (TEMPERATURE)
{
'category': 'TEMPERATURE',
'category_name': '체온 감각',
'questions': [
{
'code': 'COLD_SENSITIVITY',
'text': '추위를 타시나요?',
'type': 'RADIO',
'options': ['심하게 탄다', '타는 편', '약간 탄다', '안탄다', '겨울이 싫다'],
'required': True,
'order': 1
},
{
'code': 'HEAT_SENSITIVITY',
'text': '더위를 타시나요?',
'type': 'RADIO',
'options': ['심하게 탄다', '타는 편', '약간 탄다', '안탄다', '선풍기/에어컨 바람이 싫다'],
'required': True,
'order': 2
},
{
'code': 'SWEAT_AMOUNT',
'text': '땀을 흘리는 정도는?',
'type': 'RADIO',
'options': ['건조하다/없다', '약간', '보통', '많다', '아주 많다'],
'required': True,
'order': 3
},
{
'code': 'SWEAT_LOCATION',
'text': '땀이 나는 부위는? (복수선택)',
'type': 'CHECKBOX',
'options': ['얼굴', '몸전체', '손발', '하체', '머리', '이마', '겨드랑이'],
'required': False,
'order': 4
},
{
'code': 'SWEAT_TIMING',
'text': '주로 언제 땀이 나나요? (복수선택)',
'type': 'CHECKBOX',
'options': ['잘 때', '식사할 때', '긴장할 때', '여름에', '일할 때', '수시로'],
'required': False,
'order': 5
}
]
},
# 2. 신체 부위별 온도 (BODY_TEMP)
{
'category': 'BODY_TEMP',
'category_name': '신체 부위별 온도',
'questions': [
{
'code': 'HAND_TEMP',
'text': '손의 온도는?',
'type': 'RADIO',
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '화끈거림', '저림', ''],
'required': True,
'order': 1
},
{
'code': 'FOOT_TEMP',
'text': '발의 온도는?',
'type': 'RADIO',
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '화끈거림', '저림', ''],
'required': True,
'order': 2
},
{
'code': 'UPPER_ABDOMEN_TEMP',
'text': '윗배의 온도는?',
'type': 'RADIO',
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '시림'],
'required': True,
'order': 3
},
{
'code': 'LOWER_ABDOMEN_TEMP',
'text': '아랫배의 온도는?',
'type': 'RADIO',
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '복부비만'],
'required': True,
'order': 4
},
{
'code': 'WHOLE_BODY_TEMP',
'text': '몸 전체의 느낌은?',
'type': 'CHECKBOX',
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '무겁다', '아프다', '부종'],
'required': True,
'order': 5
}
]
},
# 3. 식성 및 소화 (DIGESTION)
{
'category': 'DIGESTION',
'category_name': '식성 및 소화',
'questions': [
{
'code': 'FOOD_TEMP_PREF',
'text': '음식 온도 선호는?',
'type': 'RADIO',
'options': ['찬 것', '시원한 것', '보통', '따뜻한 것', '뜨거운 것', '모두'],
'required': True,
'order': 1
},
{
'code': 'TASTE_PREF',
'text': '맛 선호는?',
'type': 'RADIO',
'options': ['신 것', '단 것', '매운 것', '짠 것', '쓴 것', '담백한 것', '모두'],
'required': True,
'order': 2
},
{
'code': 'FOOD_HABITS',
'text': '식습관 (복수선택)',
'subtext': '주 1회 이상 섭취하는 것을 선택하세요',
'type': 'CHECKBOX',
'options': ['된장', '채식', '육류', '해물', '커피', '', '담배'],
'required': False,
'order': 3
},
{
'code': 'WATER_INTAKE',
'text': '물을 마시는 정도는?',
'type': 'RADIO',
'options': ['많이 마심', '자주', '보통', '거의 안마심'],
'required': True,
'order': 4
},
{
'code': 'APPETITE',
'text': '식욕은 어떠신가요?',
'type': 'RADIO',
'options': ['없다', '별로', '보통', '좋다', '왕성', '아침 생략'],
'required': True,
'order': 5
},
{
'code': 'MEAL_AMOUNT',
'text': '식사량은?',
'type': 'RADIO',
'options': ['적다', '보통', '많다', '1공기 이하', '1공기 이상', '일정치 않다', '저녁에 많이'],
'required': True,
'order': 6
},
{
'code': 'DIGESTION_POWER',
'text': '소화력은?',
'type': 'RADIO',
'options': ['잘 된다', '보통', '약하다', '잘 안됨', '잘 체한다'],
'required': True,
'order': 7
},
{
'code': 'INDIGESTION_SYMPTOMS',
'text': '소화불량 증상 (복수선택)',
'type': 'CHECKBOX',
'options': ['막힌듯함', '답답함', '걸린듯함', '더부룩함', '그득함', '속쓰림',
'헛배부름', '가스참', '느글거림', '트림', '구토', '헛구역', '방귀',
'꾸룩소리남', '명치아픔', '복통', '딸꾹질', '하품'],
'required': False,
'order': 8
}
]
},
# 4. 대소변 (EXCRETION)
{
'category': 'EXCRETION',
'category_name': '대소변',
'questions': [
{
'code': 'STOOL_FREQUENCY',
'text': '대변 빈도는?',
'subtext': '일 _회 / 매일 / 아침 / 불규칙',
'type': 'TEXT',
'required': True,
'order': 1
},
{
'code': 'STOOL_CONDITION',
'text': '대변 상태는? (복수선택)',
'type': 'CHECKBOX',
'options': ['변비', '된편', '굵다', '토끼똥', '설사', '물변', '보통', '가늘다', '퍼진다', '냄새심함'],
'required': True,
'order': 2
},
{
'code': 'STOOL_DIFFICULTY',
'text': '배변 시 불편함은? (복수선택)',
'type': 'CHECKBOX',
'options': ['잘 나옴', '잘 안나옴', '시원치 않다', '오래봄', '힘들게 나옴',
'조금 나옴', '남아있는 듯함', '조금씩 자주', '지림', '못 참음'],
'required': False,
'order': 3
},
{
'code': 'URINE_FREQUENCY_DAY',
'text': '낮 소변 빈도는?',
'subtext': '낮에 몇 시간마다 1회?',
'type': 'NUMBER',
'required': True,
'order': 4
},
{
'code': 'URINE_FREQUENCY_NIGHT',
'text': '밤 소변 빈도는?',
'subtext': '자다가 몇 회?',
'type': 'NUMBER',
'required': True,
'order': 5
},
{
'code': 'URINE_FREQUENCY_LEVEL',
'text': '전체적인 소변 빈도는?',
'type': 'RADIO',
'options': ['거의 안봄', '가끔', '보통', '자주', '매우 자주', '밤에 오줌 싼다'],
'required': True,
'order': 6
},
{
'code': 'URINE_COLOR',
'text': '소변 색은?',
'type': 'RADIO',
'options': ['붉다', '노랗다', '보통', '탁하다', '맑다', '커피색'],
'required': True,
'order': 7
},
{
'code': 'URINE_ABNORMAL',
'text': '소변 이상 증상 (복수선택)',
'type': 'CHECKBOX',
'options': ['거품이 남', '기름이 뜸', '단내남', '뿌옇다', '정액이 나옴'],
'required': False,
'order': 8
}
]
},
# 5. 수면 (SLEEP)
{
'category': 'SLEEP',
'category_name': '수면',
'questions': [
{
'code': 'SLEEP_HOURS',
'text': '하루 수면 시간은?',
'subtext': '예: 7시간',
'type': 'NUMBER',
'required': True,
'order': 1
},
{
'code': 'SLEEP_TIME',
'text': '수면 시간대는?',
'subtext': '예: 23시 ~ 06시',
'type': 'TEXT',
'required': False,
'order': 2
},
{
'code': 'SLEEP_SATISFACTION',
'text': '수면이 충분한가요?',
'type': 'RADIO',
'options': ['잠 부족', '잠 충분'],
'required': True,
'order': 3
},
{
'code': 'SLEEP_QUALITY',
'text': '수면 상태는? (복수선택)',
'type': 'CHECKBOX',
'options': ['잘잠', '잘못잠', '거의 못잠', '가끔 못잠', '뒤척임', '곧 잔다',
'잠들기 어렵다', '깊이 잠', '얕은 잠', '잠귀 밝음', '잘 깸', '깨면 안옴'],
'required': True,
'order': 4
},
{
'code': 'DREAM_FREQUENCY',
'text': '꿈을 꾸는 빈도는?',
'type': 'RADIO',
'options': ['밤새 꿈', '자주 꾼다', '가끔 꿈', '거의 없다', '안꾼다', '잠꼬대'],
'required': True,
'order': 5
},
{
'code': 'DREAM_TYPE',
'text': '꾸는 꿈의 종류는? (복수선택)',
'type': 'CHECKBOX',
'options': ['무서운 꿈', '죽은 사람 꿈', '쫓기는 꿈', '개꿈', '기억 안남', '기억 남'],
'required': False,
'order': 6
}
]
},
# 6. 순환 및 정신 (CIRCULATION)
{
'category': 'CIRCULATION',
'category_name': '순환 및 정신 증상',
'questions': [
{
'code': 'HEART_SYMPTOMS',
'text': '심장 관련 증상 (복수선택)',
'type': 'CHECKBOX',
'options': ['가슴뜀', '가슴답답', '가슴뻐근', '한숨쉼', '호흡곤란', '숨참', '뒷목뻐근'],
'required': False,
'order': 1
},
{
'code': 'HEAT_FLASH',
'text': '열이 달아오르는 증상이 있나요?',
'subtext': '있다면 빈도를 알려주세요 (예: 일 3회, 주 2회)',
'type': 'TEXT',
'required': False,
'order': 2
},
{
'code': 'MENTAL_SYMPTOMS',
'text': '정신적 증상 (복수선택)',
'type': 'CHECKBOX',
'options': ['잘 놀람', '불안', '초조', '우울', '비관', '신경질', '짜증',
'매사 귀찮다', '손떨림', '졸도', '가슴 막힌 듯', '조이는 듯',
'기억력 격감', '건망증', '현기증', '눈 피로감'],
'required': False,
'order': 3
},
{
'code': 'FATIGUE_SYMPTOMS',
'text': '피로 증상 (복수선택)',
'type': 'CHECKBOX',
'options': ['피로', '기운이 없다', '아침에 잘 못일어난다', '의욕이 없다', '무겁다'],
'required': False,
'order': 4
}
]
},
# 7. 여성 건강 (WOMEN_HEALTH)
{
'category': 'WOMEN_HEALTH',
'category_name': '여성 건강',
'questions': [
{
'code': 'MARITAL_STATUS',
'text': '결혼 상태는?',
'type': 'RADIO',
'options': ['미혼', '기혼'],
'required': False,
'order': 1
},
{
'code': 'MARRIAGE_YEARS',
'text': '결혼한 지 몇 년?',
'type': 'NUMBER',
'required': False,
'order': 2
},
{
'code': 'BIRTH_COUNT',
'text': '출산 횟수',
'type': 'NUMBER',
'required': False,
'order': 3
},
{
'code': 'INFERTILITY_YEARS',
'text': '불임 기간 (년)',
'type': 'NUMBER',
'required': False,
'order': 4
},
{
'code': 'MISCARRIAGE',
'text': '유산 경험',
'subtext': '자연유산 _회, 인공유산 _회',
'type': 'TEXT',
'required': False,
'order': 5
},
{
'code': 'MENSTRUAL_CYCLE',
'text': '생리 주기는?',
'subtext': '_일 간격',
'type': 'NUMBER',
'required': False,
'order': 6
},
{
'code': 'MENSTRUAL_REGULARITY',
'text': '생리 규칙성은?',
'type': 'RADIO',
'options': ['정상', '부정확', '건넘', '중단', '폐경', '계속 나옴'],
'required': False,
'order': 7
},
{
'code': 'MENSTRUAL_DURATION',
'text': '생리 기간은?',
'subtext': '_일간 (_일 많고 _일 적다)',
'type': 'TEXT',
'required': False,
'order': 8
},
{
'code': 'MENSTRUAL_AMOUNT',
'text': '생리량은?',
'type': 'RADIO',
'options': ['너무 많다', '많다', '보통', '약간 적다', '아주 적다', '줄어듬', '늦어짐', '빨라짐'],
'required': False,
'order': 9
},
{
'code': 'MENSTRUAL_COLOR',
'text': '생리 색은? (복수선택)',
'type': 'CHECKBOX',
'options': ['검붉다', '검다', '일부 덩어리', '찌꺼기', '묽다'],
'required': False,
'order': 10
},
{
'code': 'MENSTRUAL_PAIN',
'text': '생리통은?',
'type': 'RADIO',
'options': ['없다', '약간', '심하다', '극심'],
'required': False,
'order': 11
},
{
'code': 'MENSTRUAL_PAIN_TIMING',
'text': '생리통 시기는?',
'subtext': '생리 _일 부터 _일간',
'type': 'TEXT',
'required': False,
'order': 12
},
{
'code': 'MENSTRUAL_PAIN_LOCATION',
'text': '생리통 부위 (복수선택)',
'type': 'CHECKBOX',
'options': ['아랫배', '허리', '허벅지', '가슴', '머리', '전신', '몸살', '과민'],
'required': False,
'order': 13
},
{
'code': 'VAGINAL_DISCHARGE',
'text': '냉대하는?',
'type': 'RADIO',
'options': ['없다', '약간', '많다', '심하다'],
'required': False,
'order': 14
},
{
'code': 'DISCHARGE_SYMPTOMS',
'text': '냉대하 증상 (복수선택)',
'type': 'CHECKBOX',
'options': ['투명', '누렇다', '희다', '묽다', '냄새', '악취', '가렵다'],
'required': False,
'order': 15
}
]
},
# 8. 피부 (SKIN)
{
'category': 'SKIN',
'category_name': '피부',
'questions': [
{
'code': 'SKIN_COLOR',
'text': '피부 색은?',
'type': 'RADIO',
'options': ['보통', '약간 황색', '약간 검다', '희다', '창백', '누렇다', '약간 붉다'],
'required': True,
'order': 1
},
{
'code': 'SKIN_TYPE',
'text': '피부 타입은?',
'type': 'RADIO',
'options': ['보통', '섬세', '얇다', '약간 두텁다', '두텁다', '지성', '중성', '건성'],
'required': True,
'order': 2
}
]
},
# 9. 성품/체질 (PERSONALITY)
{
'category': 'PERSONALITY',
'category_name': '성품 및 체질',
'questions': [
{
'code': 'PERSONALITY_TYPE1',
'text': '성격 유형 1 - 태양인 성향 (복수선택)',
'type': 'CHECKBOX',
'options': ['저돌적이다', '기세가 강하다', '남의 말을 잘 듣지 않는다',
'거침이 없다', '독불장군이다', '안하무인이다', '뚜렷하다',
'말과 행동 빠름', '음식을 빨리 먹음', '눈매 예리/날카롭다',
'부지런함', '적극적', '활동적', '소변 자주 본다', '일을 안미룬다',
'나다니기를 좋아함', '분명하다', '나서기 잘함', '질투가 심하다'],
'required': False,
'order': 1
},
{
'code': 'PERSONALITY_TYPE2',
'text': '성격 유형 2 - 태음인 성향 (복수선택)',
'type': 'CHECKBOX',
'options': ['눕기 좋아함', '엉덩이 무겁다', '느긋함', '땀이 많다', '과묵하다',
'사람 좋다', '무던하다', '부드럽다', '가정적이다', '씻기를 싫어함',
'원만', '정중', '은근', '꾸준함', '우유부단', '된장/쓴 것 좋아함'],
'required': False,
'order': 2
},
{
'code': 'PERSONALITY_TYPE3',
'text': '성격 유형 3 - 소양인/소음인 성향 (복수선택)',
'type': 'CHECKBOX',
'options': ['약해 보인다', '겁이 많음', '세심하다', '소심', '차분함', '자상함',
'연약', '영민', '잘 미룬다', '깐깐', '치밀', '궁리는 많으나 실행은 적음'],
'required': False,
'order': 3
}
]
}
]
def insert_survey_templates():
conn = sqlite3.connect('/root/kdrug/database/kdrug.db')
cursor = conn.cursor()
# 기존 템플릿 삭제
cursor.execute("DELETE FROM survey_templates")
inserted_count = 0
for category_data in survey_data:
category = category_data['category']
category_name = category_data['category_name']
for question in category_data['questions']:
cursor.execute("""
INSERT INTO survey_templates
(category, category_name, question_code, question_text, question_subtext,
input_type, options, is_required, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
category,
category_name,
question['code'],
question['text'],
question.get('subtext'),
question['type'],
json.dumps(question.get('options', []), ensure_ascii=False),
1 if question.get('required', False) else 0,
question['order']
))
inserted_count += 1
conn.commit()
# 결과 확인
cursor.execute("""
SELECT category, category_name, COUNT(*) as cnt
FROM survey_templates
GROUP BY category
ORDER BY MIN(template_id)
""")
results = cursor.fetchall()
print(f"✅ 문진 템플릿 {inserted_count}개 항목 입력 완료\n")
print("카테고리별 질문 수:")
for row in results:
print(f" {row[0]:20s} ({row[1]:15s}): {row[2]:2d}")
conn.close()
return inserted_count
if __name__ == '__main__':
insert_survey_templates()

View File

@@ -114,18 +114,46 @@ CREATE TABLE IF NOT EXISTS stock_ledger (
);
-- 8) 처방 마스터 (약속 처방)
-- 8-1) 100처방 원방 마스터
CREATE TABLE IF NOT EXISTS official_formulas (
official_formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
formula_number INTEGER NOT NULL UNIQUE, -- 연번 (1~100)
formula_name TEXT NOT NULL, -- 처방명 (예: 쌍화탕)
formula_name_hanja TEXT, -- 한자명 (예: 雙和湯)
source_text TEXT, -- 출전 (예: 화제국방)
description TEXT, -- 설명/효능
reference_notes TEXT, -- 상담참고자료 (OTC 대비 차별점, 구성 해설, 업셀링 포인트 등)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 8-2) 100처방 원방 구성 약재 (성분코드 기반)
CREATE TABLE IF NOT EXISTS official_formula_ingredients (
ingredient_id INTEGER PRIMARY KEY AUTOINCREMENT,
official_formula_id INTEGER NOT NULL,
ingredient_code TEXT NOT NULL, -- herb_masters.ingredient_code 기준
grams_per_cheop REAL NOT NULL, -- 1첩당 그램수
notes TEXT, -- 역할 (예: 군약, 신약, 좌약, 사약)
sort_order INTEGER DEFAULT 0,
FOREIGN KEY (official_formula_id) REFERENCES official_formulas(official_formula_id),
UNIQUE (official_formula_id, ingredient_code)
);
-- 8-3) 운영 처방 (조제에 사용)
CREATE TABLE IF NOT EXISTS formulas (
formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
formula_code TEXT UNIQUE, -- 처방 코드
formula_name TEXT NOT NULL, -- 처방명 (예: 쌍화탕)
formula_type TEXT DEFAULT 'CUSTOM', -- INSURANCE(보험), CUSTOM(약속처방)
formula_type TEXT DEFAULT 'CUSTOM', -- CUSTOM(약속처방), STANDARD 등
official_formula_id INTEGER, -- 100처방 마스터 참조 (원방 기반인 경우)
base_cheop INTEGER DEFAULT 20, -- 기본 첩수 (1제 기준)
base_pouches INTEGER DEFAULT 30, -- 기본 파우치수 (1제 기준)
description TEXT,
is_active INTEGER DEFAULT 1,
created_by TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (official_formula_id) REFERENCES official_formulas(official_formula_id)
);
-- 9) 처방 구성 약재

View File

@@ -0,0 +1,258 @@
-- 한약 재고관리 시스템 데이터베이스 스키마
-- SQLite 기준
-- 1) 도매상/공급업체
CREATE TABLE IF NOT EXISTS suppliers (
supplier_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
business_no TEXT,
contact_person TEXT,
phone TEXT,
address TEXT,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 2) 약재 마스터 (보험코드 9자리 기준)
CREATE TABLE IF NOT EXISTS herb_items (
herb_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
insurance_code TEXT UNIQUE, -- 보험코드 (9자리)
herb_name TEXT NOT NULL, -- 약재명
specification TEXT, -- 규격/품질
default_unit TEXT DEFAULT 'g', -- 기본 단위
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 3) 환자 정보
CREATE TABLE IF NOT EXISTS patients (
patient_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT NOT NULL,
jumin_no TEXT, -- 주민번호 (암호화 필요)
gender TEXT CHECK(gender IN ('M', 'F')),
birth_date DATE,
address TEXT,
notes TEXT,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(phone, name)
);
-- 4) 입고장 헤더
CREATE TABLE IF NOT EXISTS purchase_receipts (
receipt_id INTEGER PRIMARY KEY AUTOINCREMENT,
supplier_id INTEGER NOT NULL,
receipt_date DATE NOT NULL,
receipt_no TEXT, -- 입고 번호/전표번호
vat_included INTEGER DEFAULT 1, -- 부가세 포함 여부
vat_rate REAL DEFAULT 0.10, -- 부가세율
total_amount REAL, -- 총 입고액
source_file TEXT, -- Excel 파일명
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id)
);
-- 5) 입고장 상세 라인
CREATE TABLE IF NOT EXISTS purchase_receipt_lines (
line_id INTEGER PRIMARY KEY AUTOINCREMENT,
receipt_id INTEGER NOT NULL,
herb_item_id INTEGER NOT NULL,
origin_country TEXT, -- 원산지
quantity_g REAL NOT NULL, -- 구입량(g)
unit_price_per_g REAL NOT NULL, -- g당 단가 (VAT 포함)
line_total REAL, -- 라인 총액
expiry_date DATE, -- 유효기간
lot_number TEXT, -- 로트번호
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (receipt_id) REFERENCES purchase_receipts(receipt_id),
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id)
);
-- 6) 재고 로트 (입고 라인별 재고 관리)
CREATE TABLE IF NOT EXISTS inventory_lots (
lot_id INTEGER PRIMARY KEY AUTOINCREMENT,
herb_item_id INTEGER NOT NULL,
supplier_id INTEGER NOT NULL,
receipt_line_id INTEGER NOT NULL,
received_date DATE NOT NULL,
origin_country TEXT,
unit_price_per_g REAL NOT NULL,
quantity_received REAL NOT NULL, -- 입고 수량
quantity_onhand REAL NOT NULL, -- 현재 재고
expiry_date DATE,
lot_number TEXT,
is_depleted INTEGER DEFAULT 0, -- 소진 여부
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id),
FOREIGN KEY (receipt_line_id) REFERENCES purchase_receipt_lines(line_id)
);
-- 7) 재고 원장 (모든 재고 변동 기록)
CREATE TABLE IF NOT EXISTS stock_ledger (
ledger_id INTEGER PRIMARY KEY AUTOINCREMENT,
event_time DATETIME DEFAULT CURRENT_TIMESTAMP,
event_type TEXT NOT NULL CHECK(event_type IN ('RECEIPT', 'CONSUME', 'ADJUST', 'DISCARD', 'RETURN')),
herb_item_id INTEGER NOT NULL,
lot_id INTEGER,
quantity_delta REAL NOT NULL, -- 증감량 (+입고, -사용)
unit_cost_per_g REAL,
reference_table TEXT, -- 참조 테이블 (compounds, adjustments 등)
reference_id INTEGER, -- 참조 ID
notes TEXT,
created_by TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
FOREIGN KEY (lot_id) REFERENCES inventory_lots(lot_id)
);
-- 8) 처방 마스터 (약속 처방)
-- 8-1) 100처방 원방 마스터
CREATE TABLE IF NOT EXISTS official_formulas (
official_formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
formula_number INTEGER NOT NULL UNIQUE, -- 연번 (1~100)
formula_name TEXT NOT NULL, -- 처방명 (예: 쌍화탕)
formula_name_hanja TEXT, -- 한자명 (예: 雙和湯)
source_text TEXT, -- 출전 (예: 화제국방)
description TEXT, -- 설명/효능
reference_notes TEXT, -- 상담참고자료 (OTC 대비 차별점, 구성 해설, 업셀링 포인트 등)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 8-2) 100처방 원방 구성 약재 (성분코드 기반)
CREATE TABLE IF NOT EXISTS official_formula_ingredients (
ingredient_id INTEGER PRIMARY KEY AUTOINCREMENT,
official_formula_id INTEGER NOT NULL,
ingredient_code TEXT NOT NULL, -- herb_masters.ingredient_code 기준
grams_per_cheop REAL NOT NULL, -- 1첩당 그램수
notes TEXT, -- 역할 (예: 군약, 신약, 좌약, 사약)
sort_order INTEGER DEFAULT 0,
FOREIGN KEY (official_formula_id) REFERENCES official_formulas(official_formula_id),
UNIQUE (official_formula_id, ingredient_code)
);
-- 8-3) 운영 처방 (조제에 사용)
CREATE TABLE IF NOT EXISTS formulas (
formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
formula_code TEXT UNIQUE, -- 처방 코드
formula_name TEXT NOT NULL, -- 처방명 (예: 쌍화탕)
formula_type TEXT DEFAULT 'CUSTOM', -- CUSTOM(약속처방), STANDARD 등
official_formula_id INTEGER, -- 100처방 마스터 참조 (원방 기반인 경우)
base_cheop INTEGER DEFAULT 20, -- 기본 첩수 (1제 기준)
base_pouches INTEGER DEFAULT 30, -- 기본 파우치수 (1제 기준)
description TEXT,
is_active INTEGER DEFAULT 1,
created_by TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (official_formula_id) REFERENCES official_formulas(official_formula_id)
);
-- 9) 처방 구성 약재
CREATE TABLE IF NOT EXISTS formula_ingredients (
ingredient_id INTEGER PRIMARY KEY AUTOINCREMENT,
formula_id INTEGER NOT NULL,
herb_item_id INTEGER NOT NULL,
grams_per_cheop REAL NOT NULL, -- 1첩당 그램수
notes TEXT,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (formula_id) REFERENCES formulas(formula_id),
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
UNIQUE (formula_id, herb_item_id)
);
-- 10) 조제 작업 (처방 실행)
CREATE TABLE IF NOT EXISTS compounds (
compound_id INTEGER PRIMARY KEY AUTOINCREMENT,
patient_id INTEGER,
formula_id INTEGER,
compound_date DATE NOT NULL,
je_count REAL NOT NULL, -- 제수 (1제, 0.5제 등)
cheop_total REAL NOT NULL, -- 총 첩수
pouch_total REAL NOT NULL, -- 총 파우치수
cost_total REAL, -- 원가 총액
sell_price_total REAL, -- 판매 총액
prescription_no TEXT, -- 처방전 번호
status TEXT DEFAULT 'PREPARED', -- PREPARED, DISPENSED, CANCELLED
notes TEXT,
created_by TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
FOREIGN KEY (formula_id) REFERENCES formulas(formula_id)
);
-- 11) 조제 약재 구성 (실제 조제시 사용된 약재 - 가감 포함)
CREATE TABLE IF NOT EXISTS compound_ingredients (
compound_ingredient_id INTEGER PRIMARY KEY AUTOINCREMENT,
compound_id INTEGER NOT NULL,
herb_item_id INTEGER NOT NULL,
grams_per_cheop REAL NOT NULL, -- 1첩당 그램수 (가감 반영)
total_grams REAL NOT NULL, -- 총 사용량
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (compound_id) REFERENCES compounds(compound_id),
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id)
);
-- 12) 조제 소비 내역 (로트별 차감)
CREATE TABLE IF NOT EXISTS compound_consumptions (
consumption_id INTEGER PRIMARY KEY AUTOINCREMENT,
compound_id INTEGER NOT NULL,
herb_item_id INTEGER NOT NULL,
lot_id INTEGER NOT NULL,
quantity_used REAL NOT NULL, -- 사용량(g)
unit_cost_per_g REAL NOT NULL, -- 단가
cost_amount REAL, -- 원가액
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (compound_id) REFERENCES compounds(compound_id),
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id),
FOREIGN KEY (lot_id) REFERENCES inventory_lots(lot_id)
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_herb_items_name ON herb_items(herb_name);
CREATE INDEX IF NOT EXISTS idx_herb_items_code ON herb_items(insurance_code);
CREATE INDEX IF NOT EXISTS idx_inventory_lots_herb ON inventory_lots(herb_item_id, is_depleted);
CREATE INDEX IF NOT EXISTS idx_stock_ledger_herb ON stock_ledger(herb_item_id, event_time);
CREATE INDEX IF NOT EXISTS idx_compounds_patient ON compounds(patient_id);
CREATE INDEX IF NOT EXISTS idx_compounds_date ON compounds(compound_date);
CREATE INDEX IF NOT EXISTS idx_patients_phone ON patients(phone);
-- 뷰 생성 (자주 사용되는 조회)
-- 현재 재고 현황
CREATE VIEW IF NOT EXISTS v_current_stock AS
SELECT
h.herb_item_id,
h.insurance_code,
h.herb_name,
SUM(il.quantity_onhand) as total_quantity,
COUNT(DISTINCT il.lot_id) as lot_count,
AVG(il.unit_price_per_g) as avg_unit_price
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name;
-- 처방별 구성 약재 뷰
CREATE VIEW IF NOT EXISTS v_formula_details AS
SELECT
f.formula_id,
f.formula_name,
f.formula_code,
h.herb_name,
fi.grams_per_cheop,
h.insurance_code
FROM formulas f
JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
WHERE f.is_active = 1
ORDER BY f.formula_id, fi.sort_order;

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
데이터베이스 구조 정확히 분석
"""
import sqlite3
def analyze_structure():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
print("=" * 80)
print("데이터베이스 구조 완전 분석")
print("=" * 80)
# 1. herb_items 분석
print("\n1. herb_items 테이블 (재고 관리):")
cursor.execute("SELECT COUNT(*) FROM herb_items")
count = cursor.fetchone()[0]
print(f" - 레코드 수: {count}")
cursor.execute("""
SELECT herb_item_id, insurance_code, herb_name, ingredient_code
FROM herb_items
WHERE herb_item_id IN (1, 2, 3)
ORDER BY herb_item_id
""")
print(" - 샘플 데이터:")
for row in cursor.fetchall():
print(f" ID={row[0]}: {row[2]} (보험코드: {row[1]}, 성분코드: {row[3]})")
# 2. herb_masters 분석
print("\n2. herb_masters 테이블 (성분코드 마스터):")
cursor.execute("SELECT COUNT(*) FROM herb_masters")
count = cursor.fetchone()[0]
print(f" - 레코드 수: {count}")
cursor.execute("""
SELECT ingredient_code, herb_name
FROM herb_masters
WHERE herb_name IN ('인삼', '감초', '당귀')
""")
print(" - 주요 약재:")
for row in cursor.fetchall():
print(f" {row[0]}: {row[1]}")
# 3. herb_master_extended 분석
print("\n3. herb_master_extended 테이블 (확장 정보):")
cursor.execute("SELECT COUNT(*) FROM herb_master_extended")
count = cursor.fetchone()[0]
print(f" - 레코드 수: {count}")
cursor.execute("""
SELECT herb_id, ingredient_code, name_korean
FROM herb_master_extended
WHERE name_korean IN ('인삼', '감초', '당귀')
""")
print(" - 주요 약재 herb_id:")
for row in cursor.fetchall():
print(f" herb_id={row[0]}: {row[2]} (성분코드: {row[1]})")
# 4. 관계 매핑 확인
print("\n4. 테이블 간 관계:")
print(" herb_items.ingredient_code → herb_masters.ingredient_code")
print(" herb_masters.ingredient_code → herb_master_extended.ingredient_code")
print(" herb_master_extended.herb_id → herb_item_tags.herb_id")
# 5. 올바른 JOIN 경로 제시
print("\n5. 올바른 JOIN 방법:")
print("""
방법 1: herb_items에서 시작 (재고 있는 약재만)
-----------------------------------------------
FROM herb_items hi
LEFT JOIN herb_masters hm ON hi.ingredient_code = hm.ingredient_code
LEFT JOIN herb_master_extended hme ON hm.ingredient_code = hme.ingredient_code
LEFT JOIN herb_item_tags hit ON hme.herb_id = hit.herb_id
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
방법 2: herb_masters에서 시작 (모든 약재)
-----------------------------------------------
FROM herb_masters hm
LEFT JOIN herb_master_extended hme ON hm.ingredient_code = hme.ingredient_code
LEFT JOIN herb_item_tags hit ON hme.herb_id = hit.herb_id
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
LEFT JOIN (재고 서브쿼리) inv ON hm.ingredient_code = inv.ingredient_code
""")
# 6. 실제 JOIN 테스트
print("\n6. JOIN 테스트 (인삼 예시):")
cursor.execute("""
SELECT
hi.herb_item_id,
hi.herb_name as item_name,
hi.ingredient_code,
hme.herb_id as master_herb_id,
hme.name_korean as master_name,
GROUP_CONCAT(het.tag_name) as tags
FROM herb_items hi
LEFT JOIN herb_masters hm ON hi.ingredient_code = hm.ingredient_code
LEFT JOIN herb_master_extended hme ON hm.ingredient_code = hme.ingredient_code
LEFT JOIN herb_item_tags hit ON hme.herb_id = hit.herb_id
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
WHERE hi.ingredient_code = '3400H1AHM'
GROUP BY hi.herb_item_id
""")
result = cursor.fetchone()
if result:
print(f" herb_item_id: {result[0]}")
print(f" 약재명: {result[1]}")
print(f" 성분코드: {result[2]}")
print(f" master_herb_id: {result[3]}")
print(f" master 약재명: {result[4]}")
print(f" 효능 태그: {result[5]}")
conn.close()
if __name__ == "__main__":
analyze_structure()

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
재고 자산 금액 불일치 분석 스크립트
"""
import sqlite3
from datetime import datetime
from decimal import Decimal, getcontext
# Decimal 정밀도 설정
getcontext().prec = 10
def analyze_inventory_discrepancy():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("재고 자산 금액 불일치 분석")
print("=" * 80)
print()
# 1. 현재 inventory_lots 기준 재고 자산 계산
print("1. 현재 시스템 재고 자산 계산 (inventory_lots 기준)")
print("-" * 60)
cursor.execute("""
SELECT
SUM(quantity_onhand * unit_price_per_g) as total_value,
COUNT(*) as lot_count,
SUM(quantity_onhand) as total_quantity
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
result = cursor.fetchone()
system_total = result['total_value'] or 0
print(f" 총 재고 자산: ₩{system_total:,.0f}")
print(f" 총 LOT 수: {result['lot_count']}")
print(f" 총 재고량: {result['total_quantity']:,.1f}g")
print()
# 2. 원본 입고장 데이터 분석
print("2. 입고장 기준 계산")
print("-" * 60)
# 전체 입고 금액
cursor.execute("""
SELECT
SUM(total_price) as total_purchase,
COUNT(*) as receipt_count,
SUM(quantity_g) as total_quantity
FROM purchase_receipts
""")
receipts = cursor.fetchone()
total_purchase = receipts['total_purchase'] or 0
print(f" 총 입고 금액: ₩{total_purchase:,.0f}")
print(f" 총 입고장 수: {receipts['receipt_count']}")
print(f" 총 입고량: {receipts['total_quantity']:,.1f}g")
print()
# 3. 출고 데이터 분석
print("3. 출고 데이터 분석")
print("-" * 60)
cursor.execute("""
SELECT
SUM(pd.quantity * il.unit_price_per_g) as total_dispensed_value,
SUM(pd.quantity) as total_dispensed_quantity,
COUNT(DISTINCT p.prescription_id) as prescription_count
FROM prescription_details pd
JOIN prescriptions p ON pd.prescription_id = p.prescription_id
JOIN inventory_lots il ON pd.lot_id = il.lot_id
WHERE p.status IN ('completed', 'dispensed')
""")
dispensed = cursor.fetchone()
total_dispensed_value = dispensed['total_dispensed_value'] or 0
print(f" 총 출고 금액: ₩{total_dispensed_value:,.0f}")
print(f" 총 출고량: {dispensed['total_dispensed_quantity'] or 0:,.1f}g")
print(f" 총 처방전 수: {dispensed['prescription_count']}")
print()
# 4. 재고 보정 데이터 분석
print("4. 재고 보정 데이터 분석")
print("-" * 60)
cursor.execute("""
SELECT
adjustment_type,
SUM(quantity) as total_quantity,
SUM(quantity * unit_price) as total_value,
COUNT(*) as count
FROM stock_adjustments
GROUP BY adjustment_type
""")
adjustments = cursor.fetchall()
total_adjustment_value = 0
for adj in adjustments:
adj_type = adj['adjustment_type']
value = adj['total_value'] or 0
# 보정 타입에 따른 금액 계산
if adj_type in ['disposal', 'loss', 'decrease']:
total_adjustment_value -= value
print(f" {adj_type}: -₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
else:
total_adjustment_value += value
print(f" {adj_type}: +₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
print(f" 순 보정 금액: ₩{total_adjustment_value:,.0f}")
print()
# 5. 예상 재고 자산 계산
print("5. 예상 재고 자산 계산")
print("-" * 60)
expected_value = total_purchase - total_dispensed_value + total_adjustment_value
print(f" 입고 금액: ₩{total_purchase:,.0f}")
print(f" - 출고 금액: ₩{total_dispensed_value:,.0f}")
print(f" + 보정 금액: ₩{total_adjustment_value:,.0f}")
print(f" = 예상 재고 자산: ₩{expected_value:,.0f}")
print()
# 6. 차이 분석
print("6. 차이 분석")
print("-" * 60)
discrepancy = system_total - expected_value
discrepancy_pct = (discrepancy / expected_value * 100) if expected_value != 0 else 0
print(f" 시스템 재고 자산: ₩{system_total:,.0f}")
print(f" 예상 재고 자산: ₩{expected_value:,.0f}")
print(f" 차이: ₩{discrepancy:,.0f} ({discrepancy_pct:+.2f}%)")
print()
# 7. 상세 불일치 원인 분석
print("7. 잠재적 불일치 원인 분석")
print("-" * 60)
# 7-1. LOT과 입고장 매칭 확인
cursor.execute("""
SELECT COUNT(*) as unmatched_lots
FROM inventory_lots il
WHERE il.receipt_id IS NULL AND il.is_depleted = 0
""")
unmatched = cursor.fetchone()
if unmatched['unmatched_lots'] > 0:
print(f" ⚠️ 입고장과 매칭되지 않은 LOT: {unmatched['unmatched_lots']}")
cursor.execute("""
SELECT
herb_name,
lot_number,
quantity_onhand,
unit_price_per_g,
quantity_onhand * unit_price_per_g as value
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE il.receipt_id IS NULL AND il.is_depleted = 0
ORDER BY value DESC
LIMIT 5
""")
unmatched_lots = cursor.fetchall()
for lot in unmatched_lots:
print(f" - {lot['herb_name']} (LOT: {lot['lot_number']}): ₩{lot['value']:,.0f}")
# 7-2. 단가 변동 확인
cursor.execute("""
SELECT
h.herb_name,
MIN(il.unit_price_per_g) as min_price,
MAX(il.unit_price_per_g) as max_price,
AVG(il.unit_price_per_g) as avg_price,
MAX(il.unit_price_per_g) - MIN(il.unit_price_per_g) as price_diff
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
GROUP BY h.herb_item_id, h.herb_name
HAVING price_diff > 0
ORDER BY price_diff DESC
LIMIT 5
""")
price_variations = cursor.fetchall()
if price_variations:
print(f"\n ⚠️ 단가 변동이 큰 약재 (동일 약재 다른 단가):")
for item in price_variations:
print(f" - {item['herb_name']}: ₩{item['min_price']:.2f} ~ ₩{item['max_price']:.2f} (차이: ₩{item['price_diff']:.2f})")
# 7-3. 입고장 없는 출고 확인
cursor.execute("""
SELECT COUNT(DISTINCT pd.lot_id) as orphan_dispenses
FROM prescription_details pd
LEFT JOIN inventory_lots il ON pd.lot_id = il.lot_id
WHERE il.lot_id IS NULL
""")
orphan = cursor.fetchone()
if orphan['orphan_dispenses'] > 0:
print(f"\n ⚠️ LOT 정보 없는 출고: {orphan['orphan_dispenses']}")
# 7-4. 음수 재고 확인
cursor.execute("""
SELECT COUNT(*) as negative_stock
FROM inventory_lots
WHERE quantity_onhand < 0
""")
negative = cursor.fetchone()
if negative['negative_stock'] > 0:
print(f"\n ⚠️ 음수 재고 LOT: {negative['negative_stock']}")
# 8. 권장사항
print("\n8. 권장사항")
print("-" * 60)
if abs(discrepancy) > 1000:
print(" 🔴 상당한 금액 차이가 발생했습니다. 다음 사항을 확인하세요:")
print(" 1) 모든 입고장이 inventory_lots에 정확히 반영되었는지 확인")
print(" 2) 출고 시 올바른 LOT과 단가가 적용되었는지 확인")
print(" 3) 재고 보정 내역이 정확히 기록되었는지 확인")
print(" 4) 초기 재고 입력 시 단가가 정확했는지 확인")
if unmatched['unmatched_lots'] > 0:
print(f" 5) 입고장과 매칭되지 않은 {unmatched['unmatched_lots']}개 LOT 확인 필요")
else:
print(" ✅ 재고 자산이 대체로 일치합니다.")
conn.close()
if __name__ == "__main__":
analyze_inventory_discrepancy()

View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
재고 자산 금액 불일치 상세 분석
"""
import sqlite3
from datetime import datetime
from decimal import Decimal, getcontext
# Decimal 정밀도 설정
getcontext().prec = 10
def analyze_inventory_discrepancy():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("재고 자산 금액 불일치 상세 분석")
print("분석 시간:", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print("=" * 80)
print()
# 1. 현재 inventory_lots 기준 재고 자산
print("1. 현재 시스템 재고 자산 (inventory_lots 테이블)")
print("-" * 60)
cursor.execute("""
SELECT
SUM(quantity_onhand * unit_price_per_g) as total_value,
COUNT(*) as lot_count,
SUM(quantity_onhand) as total_quantity,
COUNT(DISTINCT herb_item_id) as herb_count
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
result = cursor.fetchone()
system_total = result['total_value'] or 0
print(f" 💰 총 재고 자산: ₩{system_total:,.0f}")
print(f" 📦 활성 LOT 수: {result['lot_count']}")
print(f" ⚖️ 총 재고량: {result['total_quantity']:,.1f}g")
print(f" 🌿 약재 종류: {result['herb_count']}")
print()
# 2. 입고장 기준 분석
print("2. 입고장 데이터 분석 (purchase_receipts + purchase_receipt_lines)")
print("-" * 60)
# 전체 입고 금액 (purchase_receipt_lines 기준)
cursor.execute("""
SELECT
SUM(prl.line_total) as total_purchase,
COUNT(DISTINCT pr.receipt_id) as receipt_count,
COUNT(*) as line_count,
SUM(prl.quantity_g) as total_quantity
FROM purchase_receipt_lines prl
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
""")
receipts = cursor.fetchone()
total_purchase = receipts['total_purchase'] or 0
print(f" 📋 총 입고 금액: ₩{total_purchase:,.0f}")
print(f" 📑 입고장 수: {receipts['receipt_count']}")
print(f" 📝 입고 라인 수: {receipts['line_count']}")
print(f" ⚖️ 총 입고량: {receipts['total_quantity']:,.1f}g")
# 입고장별 요약도 확인
cursor.execute("""
SELECT
pr.receipt_id,
pr.receipt_no,
pr.receipt_date,
pr.total_amount as receipt_total,
SUM(prl.line_total) as lines_sum
FROM purchase_receipts pr
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
GROUP BY pr.receipt_id
ORDER BY pr.receipt_date DESC
LIMIT 5
""")
print("\n 최근 입고장 5건:")
recent_receipts = cursor.fetchall()
for r in recent_receipts:
print(f" - {r['receipt_no']} ({r['receipt_date']}): ₩{r['lines_sum']:,.0f}")
print()
# 3. inventory_lots와 purchase_receipt_lines 매칭 분석
print("3. LOT-입고장 매칭 분석")
print("-" * 60)
# receipt_line_id로 연결된 LOT 분석
cursor.execute("""
SELECT
COUNT(*) as total_lots,
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN 1 ELSE 0 END) as matched_lots,
SUM(CASE WHEN receipt_line_id IS NULL THEN 1 ELSE 0 END) as unmatched_lots,
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN quantity_onhand * unit_price_per_g ELSE 0 END) as matched_value,
SUM(CASE WHEN receipt_line_id IS NULL THEN quantity_onhand * unit_price_per_g ELSE 0 END) as unmatched_value
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
matching = cursor.fetchone()
print(f" ✅ 입고장과 연결된 LOT: {matching['matched_lots']}개 (₩{matching['matched_value']:,.0f})")
print(f" ❌ 입고장 없는 LOT: {matching['unmatched_lots']}개 (₩{matching['unmatched_value']:,.0f})")
if matching['unmatched_lots'] > 0:
print("\n 입고장 없는 LOT 상세:")
cursor.execute("""
SELECT
h.herb_name,
il.lot_number,
il.quantity_onhand,
il.unit_price_per_g,
il.quantity_onhand * il.unit_price_per_g as value,
il.received_date
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE il.receipt_line_id IS NULL
AND il.is_depleted = 0
AND il.quantity_onhand > 0
ORDER BY value DESC
LIMIT 5
""")
unmatched_lots = cursor.fetchall()
for lot in unmatched_lots:
print(f" - {lot['herb_name']} (LOT: {lot['lot_number']})")
print(f" 재고: {lot['quantity_onhand']:,.0f}g, 단가: ₩{lot['unit_price_per_g']:.2f}, 금액: ₩{lot['value']:,.0f}")
print()
# 4. 입고장 라인과 LOT 비교
print("4. 입고장 라인별 LOT 생성 확인")
print("-" * 60)
cursor.execute("""
SELECT
COUNT(*) as total_lines,
SUM(CASE WHEN il.lot_id IS NOT NULL THEN 1 ELSE 0 END) as lines_with_lot,
SUM(CASE WHEN il.lot_id IS NULL THEN 1 ELSE 0 END) as lines_without_lot
FROM purchase_receipt_lines prl
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
""")
line_matching = cursor.fetchone()
print(f" 📝 전체 입고 라인: {line_matching['total_lines']}")
print(f" ✅ LOT 생성된 라인: {line_matching['lines_with_lot']}")
print(f" ❌ LOT 없는 라인: {line_matching['lines_without_lot']}")
if line_matching['lines_without_lot'] > 0:
print("\n ⚠️ LOT이 생성되지 않은 입고 라인이 있습니다!")
cursor.execute("""
SELECT
pr.receipt_no,
pr.receipt_date,
h.herb_name,
prl.quantity_g,
prl.line_total
FROM purchase_receipt_lines prl
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
WHERE il.lot_id IS NULL
ORDER BY prl.line_total DESC
LIMIT 5
""")
missing_lots = cursor.fetchall()
for line in missing_lots:
print(f" - {line['receipt_no']} ({line['receipt_date']}): {line['herb_name']}")
print(f" 수량: {line['quantity_g']:,.0f}g, 금액: ₩{line['line_total']:,.0f}")
print()
# 5. 금액 차이 계산
print("5. 재고 자산 차이 분석")
print("-" * 60)
# 입고장 라인별로 생성된 LOT의 현재 재고 가치 합계
cursor.execute("""
SELECT
SUM(il.quantity_onhand * il.unit_price_per_g) as current_lot_value,
SUM(prl.line_total) as original_purchase_value
FROM purchase_receipt_lines prl
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
""")
value_comparison = cursor.fetchone()
if value_comparison['current_lot_value']:
print(f" 💰 현재 LOT 재고 가치: ₩{value_comparison['current_lot_value']:,.0f}")
print(f" 📋 원본 입고 금액: ₩{value_comparison['original_purchase_value']:,.0f}")
print(f" 📊 차이: ₩{(value_comparison['current_lot_value'] - value_comparison['original_purchase_value']):,.0f}")
print()
# 6. 출고 내역 확인
print("6. 출고 및 소비 내역")
print("-" * 60)
# 처방전을 통한 출고가 있는지 확인
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name IN ('prescriptions', 'prescription_details')
""")
prescription_tables = cursor.fetchall()
if len(prescription_tables) == 2:
cursor.execute("""
SELECT
SUM(pd.quantity * il.unit_price_per_g) as dispensed_value,
SUM(pd.quantity) as dispensed_quantity,
COUNT(DISTINCT p.prescription_id) as prescription_count
FROM prescription_details pd
JOIN prescriptions p ON pd.prescription_id = p.prescription_id
JOIN inventory_lots il ON pd.lot_id = il.lot_id
WHERE p.status IN ('completed', 'dispensed')
""")
dispensed = cursor.fetchone()
if dispensed and dispensed['dispensed_value']:
print(f" 💊 처방 출고 금액: ₩{dispensed['dispensed_value']:,.0f}")
print(f" ⚖️ 처방 출고량: {dispensed['dispensed_quantity']:,.1f}g")
print(f" 📋 처방전 수: {dispensed['prescription_count']}")
else:
print(" 처방전 테이블이 없습니다.")
# 복합제 소비 확인
cursor.execute("""
SELECT
SUM(cc.quantity_used * il.unit_price_per_g) as compound_value,
SUM(cc.quantity_used) as compound_quantity,
COUNT(DISTINCT cc.compound_id) as compound_count
FROM compound_consumptions cc
JOIN inventory_lots il ON cc.lot_id = il.lot_id
""")
compounds = cursor.fetchone()
if compounds and compounds['compound_value']:
print(f" 🏭 복합제 소비 금액: ₩{compounds['compound_value']:,.0f}")
print(f" ⚖️ 복합제 소비량: {compounds['compound_quantity']:,.1f}g")
print(f" 📦 복합제 수: {compounds['compound_count']}")
print()
# 7. 재고 보정 내역
print("7. 재고 보정 내역")
print("-" * 60)
cursor.execute("""
SELECT
adjustment_type,
SUM(quantity) as total_quantity,
SUM(quantity * unit_price) as total_value,
COUNT(*) as count
FROM stock_adjustments
GROUP BY adjustment_type
""")
adjustments = cursor.fetchall()
total_adjustment = 0
for adj in adjustments:
adj_type = adj['adjustment_type']
value = adj['total_value'] or 0
if adj_type in ['disposal', 'loss', 'decrease']:
total_adjustment -= value
print(f" {adj_type}: -₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
else:
total_adjustment += value
print(f" {adj_type}: +₩{value:,.0f} ({adj['count']}건, {adj['total_quantity']:,.1f}g)")
print(f"\n 📊 순 보정 금액: ₩{total_adjustment:,.0f}")
print()
# 8. 최종 분석 결과
print("8. 최종 분석 결과")
print("=" * 60)
print(f"\n 💰 화면 표시 재고 자산: ₩5,875,708")
print(f" 📊 실제 계산 재고 자산: ₩{system_total:,.0f}")
print(f" ❗ 차이: ₩{5875708 - system_total:,.0f}")
print("\n 🔍 불일치 원인:")
if matching['unmatched_lots'] > 0:
print(f" 1) 입고장과 연결되지 않은 LOT {matching['unmatched_lots']}개 (₩{matching['unmatched_value']:,.0f})")
if line_matching['lines_without_lot'] > 0:
print(f" 2) LOT이 생성되지 않은 입고 라인 {line_matching['lines_without_lot']}")
print(f" 3) 화면의 ₩5,875,708과 실제 DB의 ₩{system_total:,.0f} 차이")
# 화면에 표시되는 금액이 어디서 오는지 추가 확인
print("\n 💡 추가 확인 필요사항:")
print(" - 프론트엔드에서 재고 자산을 계산하는 로직 확인")
print(" - 캐시된 데이터나 별도 계산 로직이 있는지 확인")
print(" - inventory_lots_v2 테이블 데이터와 비교 필요")
conn.close()
if __name__ == "__main__":
analyze_inventory_discrepancy()

View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
입고 단가와 LOT 단가 차이 분석
"""
import sqlite3
def analyze_price_difference():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("입고 단가와 LOT 단가 차이 상세 분석")
print("=" * 80)
print()
# 1. 입고 라인과 LOT의 단가 차이 분석
print("1. 입고 라인 vs LOT 단가 비교")
print("-" * 60)
cursor.execute("""
SELECT
h.herb_name,
prl.line_id,
prl.quantity_g as purchase_qty,
prl.unit_price_per_g as purchase_price,
prl.line_total as purchase_total,
il.quantity_received as lot_received_qty,
il.quantity_onhand as lot_current_qty,
il.unit_price_per_g as lot_price,
il.quantity_received * il.unit_price_per_g as lot_original_value,
il.quantity_onhand * il.unit_price_per_g as lot_current_value,
ABS(prl.unit_price_per_g - il.unit_price_per_g) as price_diff,
prl.line_total - (il.quantity_received * il.unit_price_per_g) as value_diff
FROM purchase_receipt_lines prl
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
WHERE ABS(prl.unit_price_per_g - il.unit_price_per_g) > 0.01
OR ABS(prl.quantity_g - il.quantity_received) > 0.01
ORDER BY ABS(value_diff) DESC
""")
diffs = cursor.fetchall()
if diffs:
print(f" ⚠️ 단가 또는 수량이 다른 항목: {len(diffs)}\n")
total_value_diff = 0
for i, diff in enumerate(diffs[:10], 1):
print(f" {i}. {diff['herb_name']}")
print(f" 입고: {diff['purchase_qty']:,.0f}g ×{diff['purchase_price']:.2f} = ₩{diff['purchase_total']:,.0f}")
print(f" LOT: {diff['lot_received_qty']:,.0f}g ×{diff['lot_price']:.2f} = ₩{diff['lot_original_value']:,.0f}")
print(f" 차이: ₩{diff['value_diff']:,.0f}")
total_value_diff += diff['value_diff']
print()
cursor.execute("""
SELECT SUM(prl.line_total - (il.quantity_received * il.unit_price_per_g)) as total_diff
FROM purchase_receipt_lines prl
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
""")
total_diff = cursor.fetchone()['total_diff'] or 0
print(f" 총 차이 금액: ₩{total_diff:,.0f}")
else:
print(" ✅ 모든 입고 라인과 LOT의 단가/수량이 일치합니다.")
# 2. 입고 총액과 LOT 생성 총액 비교
print("\n2. 입고 총액 vs LOT 생성 총액")
print("-" * 60)
cursor.execute("""
SELECT
SUM(prl.line_total) as purchase_total,
SUM(il.quantity_received * il.unit_price_per_g) as lot_creation_total
FROM purchase_receipt_lines prl
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
""")
totals = cursor.fetchone()
print(f" 입고장 총액: ₩{totals['purchase_total']:,.0f}")
print(f" LOT 생성 총액: ₩{totals['lot_creation_total']:,.0f}")
print(f" 차이: ₩{totals['purchase_total'] - totals['lot_creation_total']:,.0f}")
# 3. 소비로 인한 차이 분석
print("\n3. 소비 내역 상세 분석")
print("-" * 60)
# 복합제 소비 상세
cursor.execute("""
SELECT
c.compound_name,
h.herb_name,
cc.quantity_used,
il.unit_price_per_g,
cc.quantity_used * il.unit_price_per_g as consumption_value,
cc.consumption_date
FROM compound_consumptions cc
JOIN inventory_lots il ON cc.lot_id = il.lot_id
JOIN compounds c ON cc.compound_id = c.compound_id
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
ORDER BY consumption_value DESC
LIMIT 10
""")
consumptions = cursor.fetchall()
print(" 복합제 소비 내역 (상위 10개):")
total_consumption = 0
for cons in consumptions:
print(f" - {cons['compound_name']} - {cons['herb_name']}")
print(f" {cons['quantity_used']:,.0f}g ×{cons['unit_price_per_g']:.2f} = ₩{cons['consumption_value']:,.0f}")
total_consumption += cons['consumption_value']
cursor.execute("""
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
FROM compound_consumptions cc
JOIN inventory_lots il ON cc.lot_id = il.lot_id
""")
total_consumed = cursor.fetchone()['total'] or 0
print(f"\n 총 소비 금액: ₩{total_consumed:,.0f}")
# 4. 재고 자산 흐름 요약
print("\n4. 재고 자산 흐름 요약")
print("=" * 60)
# 입고장 기준
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
receipt_total = cursor.fetchone()['total'] or 0
# LOT 생성 기준
cursor.execute("""
SELECT SUM(quantity_received * unit_price_per_g) as total
FROM inventory_lots
WHERE receipt_line_id IS NOT NULL
""")
lot_creation = cursor.fetchone()['total'] or 0
# 현재 LOT 재고
cursor.execute("""
SELECT SUM(quantity_onhand * unit_price_per_g) as total
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
current_inventory = cursor.fetchone()['total'] or 0
print(f" 1) 입고장 총액: ₩{receipt_total:,.0f}")
print(f" 2) LOT 생성 총액: ₩{lot_creation:,.0f}")
print(f" 차이 (1-2): ₩{receipt_total - lot_creation:,.0f}")
print()
print(f" 3) 복합제 소비: ₩{total_consumed:,.0f}")
print(f" 4) 현재 재고: ₩{current_inventory:,.0f}")
print()
print(f" 예상 재고 (2-3): ₩{lot_creation - total_consumed:,.0f}")
print(f" 실제 재고: ₩{current_inventory:,.0f}")
print(f" 차이: ₩{current_inventory - (lot_creation - total_consumed):,.0f}")
# 5. 차이 원인 설명
print("\n5. 차이 원인 분석")
print("-" * 60)
price_diff = receipt_total - lot_creation
if abs(price_diff) > 1000:
print(f"\n 💡 입고장과 LOT 생성 시 ₩{abs(price_diff):,.0f} 차이가 있습니다.")
print(" 가능한 원인:")
print(" - VAT 포함/제외 계산 차이")
print(" - 단가 반올림 차이")
print(" - 입고 시점의 환율 적용 차이")
consumption_diff = current_inventory - (lot_creation - total_consumed)
if abs(consumption_diff) > 1000:
print(f"\n 💡 예상 재고와 실제 재고 간 ₩{abs(consumption_diff):,.0f} 차이가 있습니다.")
print(" 가능한 원인:")
print(" - 재고 보정 내역")
print(" - 소비 시 반올림 오차 누적")
print(" - 초기 데이터 입력 오류")
conn.close()
if __name__ == "__main__":
analyze_price_difference()

View File

@@ -0,0 +1,224 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
커스텀 처방 감지 유틸리티
조제 시 원 처방과 다른 구성인지 확인
"""
import sqlite3
from typing import Dict, List, Tuple
def get_connection():
"""데이터베이스 연결"""
return sqlite3.connect('database/kdrug.db')
def check_custom_prescription(compound_id: int) -> Tuple[bool, Dict]:
"""
조제가 원 처방과 다른지 확인
Returns:
(is_custom, differences_dict)
"""
conn = get_connection()
cursor = conn.cursor()
# 1. compound의 formula_id 가져오기
cursor.execute("""
SELECT c.formula_id, f.formula_name
FROM compounds c
JOIN formulas f ON c.formula_id = f.formula_id
WHERE c.compound_id = ?
""", (compound_id,))
result = cursor.fetchone()
if not result:
conn.close()
return False, {"error": "Compound not found"}
formula_id, formula_name = result
# 2. 원 처방의 구성 약재
cursor.execute("""
SELECT
fi.herb_item_id,
h.herb_name,
fi.grams_per_cheop
FROM formula_ingredients fi
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
WHERE fi.formula_id = ?
ORDER BY fi.herb_item_id
""", (formula_id,))
original_ingredients = {row[0]: {
'herb_name': row[1],
'grams_per_cheop': row[2]
} for row in cursor.fetchall()}
# 3. 실제 조제된 구성 약재
cursor.execute("""
SELECT
ci.herb_item_id,
h.herb_name,
ci.grams_per_cheop
FROM compound_ingredients ci
JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
WHERE ci.compound_id = ?
ORDER BY ci.herb_item_id
""", (compound_id,))
actual_ingredients = {row[0]: {
'herb_name': row[1],
'grams_per_cheop': row[2]
} for row in cursor.fetchall()}
conn.close()
# 4. 비교 분석
differences = {
'formula_name': formula_name,
'added': [],
'removed': [],
'modified': [],
'is_custom': False
}
# 추가된 약재
for herb_id, info in actual_ingredients.items():
if herb_id not in original_ingredients:
differences['added'].append({
'herb_id': herb_id,
'herb_name': info['herb_name'],
'grams_per_cheop': info['grams_per_cheop']
})
differences['is_custom'] = True
# 제거된 약재
for herb_id, info in original_ingredients.items():
if herb_id not in actual_ingredients:
differences['removed'].append({
'herb_id': herb_id,
'herb_name': info['herb_name'],
'grams_per_cheop': info['grams_per_cheop']
})
differences['is_custom'] = True
# 용량 변경된 약재
for herb_id in set(original_ingredients.keys()) & set(actual_ingredients.keys()):
orig_grams = original_ingredients[herb_id]['grams_per_cheop']
actual_grams = actual_ingredients[herb_id]['grams_per_cheop']
if abs(orig_grams - actual_grams) > 0.01: # 부동소수점 오차 고려
differences['modified'].append({
'herb_id': herb_id,
'herb_name': original_ingredients[herb_id]['herb_name'],
'original_grams': orig_grams,
'actual_grams': actual_grams,
'difference': actual_grams - orig_grams
})
differences['is_custom'] = True
return differences['is_custom'], differences
def generate_custom_summary(differences: Dict) -> str:
"""커스텀 내역을 요약 문자열로 생성"""
summary_parts = []
# 추가
if differences['added']:
added_herbs = [f"{item['herb_name']} {item['grams_per_cheop']}g"
for item in differences['added']]
summary_parts.append(f"추가: {', '.join(added_herbs)}")
# 제거
if differences['removed']:
removed_herbs = [item['herb_name'] for item in differences['removed']]
summary_parts.append(f"제거: {', '.join(removed_herbs)}")
# 수정
if differences['modified']:
modified_herbs = [f"{item['herb_name']} {item['original_grams']}g→{item['actual_grams']}g"
for item in differences['modified']]
summary_parts.append(f"변경: {', '.join(modified_herbs)}")
return " | ".join(summary_parts) if summary_parts else "표준 처방"
def list_all_custom_prescriptions():
"""모든 커스텀 처방 찾기"""
conn = get_connection()
cursor = conn.cursor()
# 모든 조제 목록
cursor.execute("""
SELECT
c.compound_id,
c.compound_date,
p.name as patient_name,
f.formula_name
FROM compounds c
LEFT JOIN patients p ON c.patient_id = p.patient_id
JOIN formulas f ON c.formula_id = f.formula_id
ORDER BY c.compound_date DESC
""")
compounds = cursor.fetchall()
conn.close()
custom_compounds = []
for compound in compounds:
compound_id = compound[0]
is_custom, differences = check_custom_prescription(compound_id)
if is_custom:
custom_compounds.append({
'compound_id': compound_id,
'compound_date': compound[1],
'patient_name': compound[2],
'formula_name': compound[3],
'summary': generate_custom_summary(differences),
'differences': differences
})
return custom_compounds
def demo():
"""데모 실행"""
print("\n" + "="*80)
print("커스텀 처방 감지 시스템")
print("="*80)
# 전체 커스텀 처방 검색
custom_prescriptions = list_all_custom_prescriptions()
if not custom_prescriptions:
print("\n조제 내역이 없거나 모든 조제가 표준 처방입니다.")
# 테스트용 샘플 데이터 표시
print("\n[시뮬레이션] 만약 십전대보탕에 구기자를 추가했다면:")
print("-" * 60)
sample_diff = {
'formula_name': '십전대보탕',
'added': [{'herb_name': '구기자', 'grams_per_cheop': 3}],
'removed': [],
'modified': [{'herb_name': '인삼', 'original_grams': 5, 'actual_grams': 7}],
'is_custom': True
}
summary = generate_custom_summary(sample_diff)
print(f"처방: 십전대보탕 (가감방)")
print(f"변경 내역: {summary}")
print("\n환자 기록 표시:")
print(" 2024-02-17 십전대보탕 가감방 20첩")
print(f" └─ {summary}")
else:
print(f"\n{len(custom_prescriptions)}개의 커스텀 처방이 발견되었습니다.\n")
for cp in custom_prescriptions:
print(f"조제 #{cp['compound_id']} | {cp['compound_date']} | {cp['patient_name']}")
print(f" 처방: {cp['formula_name']} (가감방)")
print(f" 변경: {cp['summary']}")
print()
if __name__ == "__main__":
demo()

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
formulas 테이블의 칼럼 구조 확인
"""
import sqlite3
def check_formula_structure():
"""formulas 테이블의 전체 구조 확인"""
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
print("🔍 formulas 테이블 구조 확인")
print("="*70)
# 테이블 구조 확인
cursor.execute("PRAGMA table_info(formulas)")
columns = cursor.fetchall()
print("\n📊 formulas 테이블 칼럼 목록:")
print("-"*70)
print(f"{'번호':>4} | {'칼럼명':20} | {'타입':15} | {'NULL 허용':10} | {'기본값'}")
print("-"*70)
efficacy_columns = []
for col in columns:
cid, name, type_name, notnull, dflt_value, pk = col
null_str = "NOT NULL" if notnull else "NULL"
default_str = dflt_value if dflt_value else "-"
print(f"{cid:4d} | {name:20} | {type_name:15} | {null_str:10} | {default_str}")
# 효능 관련 칼럼 찾기
if 'efficacy' in name.lower() or 'indication' in name.lower() or '효능' in name:
efficacy_columns.append(name)
print("\n" + "="*70)
if efficacy_columns:
print(f"✅ 효능 관련 칼럼 발견: {', '.join(efficacy_columns)}")
else:
print("❌ 효능 관련 칼럼이 없습니다.")
# 실제 데이터 예시 확인
print("\n📋 십전대보탕 데이터 예시:")
print("-"*70)
cursor.execute("""
SELECT * FROM formulas
WHERE formula_code = 'SJDB01'
""")
row = cursor.fetchone()
if row:
col_names = [description[0] for description in cursor.description]
for i, (col_name, value) in enumerate(zip(col_names, row)):
if value and value != 0: # 값이 있는 경우만 표시
print(f"{col_name:25}: {str(value)[:100]}")
# prescription_details 테이블도 확인
print("\n\n🔍 prescription_details 테이블 확인 (혹시 여기 있는지)")
print("="*70)
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='prescription_details'
""")
if cursor.fetchone():
cursor.execute("PRAGMA table_info(prescription_details)")
columns = cursor.fetchall()
print("📊 prescription_details 테이블 칼럼:")
print("-"*70)
for col in columns:
cid, name, type_name, notnull, dflt_value, pk = col
if 'efficacy' in name.lower() or 'indication' in name.lower():
print(f"{name}: {type_name}")
# formula_details 테이블도 확인
print("\n\n🔍 formula_details 테이블 확인")
print("="*70)
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='formula_details'
""")
if cursor.fetchone():
cursor.execute("PRAGMA table_info(formula_details)")
columns = cursor.fetchall()
print("📊 formula_details 테이블 칼럼:")
print("-"*70)
for col in columns:
cid, name, type_name, notnull, dflt_value, pk = col
print(f" {name}: {type_name}")
# 실제 데이터 확인
cursor.execute("""
SELECT * FROM formula_details
WHERE formula_id = (SELECT formula_id FROM formulas WHERE formula_code = 'SJDB01')
""")
row = cursor.fetchone()
if row:
print("\n십전대보탕 상세 정보:")
col_names = [description[0] for description in cursor.description]
for col_name, value in zip(col_names, row):
if value:
print(f" {col_name}: {str(value)[:100]}")
else:
print("❌ formula_details 테이블이 없습니다.")
conn.close()
if __name__ == "__main__":
check_formula_structure()

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""약재 데이터 확인"""
import sqlite3
conn = sqlite3.connect('kdrug.db')
cur = conn.cursor()
# 확장 정보가 있는 약재 확인
cur.execute("""
SELECT COUNT(*) FROM herb_master_extended
WHERE nature IS NOT NULL OR taste IS NOT NULL
""")
extended_count = cur.fetchone()[0]
print(f"확장 정보가 있는 약재: {extended_count}")
# 효능 태그가 있는 약재 확인
cur.execute("SELECT COUNT(DISTINCT ingredient_code) FROM herb_item_tags")
tagged_count = cur.fetchone()[0]
print(f"효능 태그가 있는 약재: {tagged_count}")
# 구체적인 데이터 확인
cur.execute("""
SELECT hme.ingredient_code, hme.herb_name, hme.nature, hme.taste
FROM herb_master_extended hme
WHERE hme.nature IS NOT NULL OR hme.taste IS NOT NULL
LIMIT 5
""")
print("\n확장 정보 샘플:")
for row in cur.fetchall():
print(f" - {row[1]} ({row[0]}): {row[2]}/{row[3]}")
# herb_item_tags 데이터 확인
cur.execute("""
SELECT hit.ingredient_code, het.name, COUNT(*) as count
FROM herb_item_tags hit
JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
GROUP BY hit.ingredient_code
LIMIT 5
""")
print("\n효능 태그 샘플:")
for row in cur.fetchall():
print(f" - {row[0]}: {row[2]}개 태그")
conn.close()

View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LOT 생성 방법 분석 - 입고장 연결 vs 독립 생성
"""
import sqlite3
def check_lot_creation_methods():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("📦 LOT 생성 방법 분석")
print("=" * 80)
print()
# 1. 전체 LOT 현황
print("1. 전체 LOT 현황")
print("-" * 60)
cursor.execute("""
SELECT
COUNT(*) as total_lots,
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN 1 ELSE 0 END) as with_receipt,
SUM(CASE WHEN receipt_line_id IS NULL THEN 1 ELSE 0 END) as without_receipt,
SUM(CASE WHEN is_depleted = 0 THEN 1 ELSE 0 END) as active_lots
FROM inventory_lots
""")
stats = cursor.fetchone()
print(f" 전체 LOT 수: {stats['total_lots']}")
print(f" ✅ 입고장 연결: {stats['with_receipt']}")
print(f" ❌ 입고장 없음: {stats['without_receipt']}")
print(f" 활성 LOT: {stats['active_lots']}")
# 2. 입고장 없는 LOT 상세
if stats['without_receipt'] > 0:
print("\n2. 입고장 없이 생성된 LOT 상세")
print("-" * 60)
cursor.execute("""
SELECT
il.lot_id,
h.herb_name,
il.lot_number,
il.quantity_received,
il.quantity_onhand,
il.unit_price_per_g,
il.quantity_onhand * il.unit_price_per_g as value,
il.received_date,
il.created_at
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE il.receipt_line_id IS NULL
ORDER BY il.created_at DESC
""")
no_receipt_lots = cursor.fetchall()
for lot in no_receipt_lots:
print(f"\n LOT {lot['lot_id']}: {lot['herb_name']}")
print(f" LOT 번호: {lot['lot_number'] or 'None'}")
print(f" 수량: {lot['quantity_received']:,.0f}g → {lot['quantity_onhand']:,.0f}g")
print(f" 단가: ₩{lot['unit_price_per_g']:.2f}")
print(f" 재고 가치: ₩{lot['value']:,.0f}")
print(f" 입고일: {lot['received_date']}")
print(f" 생성일: {lot['created_at']}")
# 금액 합계
cursor.execute("""
SELECT
SUM(quantity_onhand * unit_price_per_g) as total_value,
SUM(quantity_onhand) as total_qty
FROM inventory_lots
WHERE receipt_line_id IS NULL
AND is_depleted = 0
AND quantity_onhand > 0
""")
no_receipt_total = cursor.fetchone()
if no_receipt_total['total_value']:
print(f"\n 📊 입고장 없는 LOT 합계:")
print(f" 총 재고량: {no_receipt_total['total_qty']:,.0f}g")
print(f" 총 재고 가치: ₩{no_receipt_total['total_value']:,.0f}")
# 3. LOT 생성 방법별 재고 자산
print("\n3. LOT 생성 방법별 재고 자산")
print("-" * 60)
cursor.execute("""
SELECT
CASE
WHEN receipt_line_id IS NOT NULL THEN '입고장 연결'
ELSE '직접 생성'
END as creation_type,
COUNT(*) as lot_count,
SUM(quantity_onhand) as total_qty,
SUM(quantity_onhand * unit_price_per_g) as total_value
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
GROUP BY creation_type
""")
by_type = cursor.fetchall()
total_value = 0
for row in by_type:
print(f"\n {row['creation_type']}:")
print(f" LOT 수: {row['lot_count']}")
print(f" 재고량: {row['total_qty']:,.0f}g")
print(f" 재고 가치: ₩{row['total_value']:,.0f}")
total_value += row['total_value']
print(f"\n 📊 전체 재고 자산: ₩{total_value:,.0f}")
# 4. 시스템 설계 분석
print("\n4. 시스템 설계 분석")
print("=" * 60)
print("\n 💡 현재 시스템은 두 가지 방법으로 LOT 생성 가능:")
print(" 1) 입고장 등록 시 자동 생성 (receipt_line_id 연결)")
print(" 2) 재고 직접 입력 (receipt_line_id = NULL)")
print()
print(" 📌 재고 자산 계산 로직:")
print(" - 입고장 연결 여부와 관계없이")
print(" - 모든 활성 LOT의 (수량 × 단가) 합계")
print()
if stats['without_receipt'] > 0:
print(" ⚠️ 주의사항:")
print(" - 입고장 없는 LOT이 존재합니다")
print(" - 초기 재고 입력이나 재고 조정으로 생성된 것으로 추정")
print(" - 회계 추적을 위해서는 입고장 연결 권장")
conn.close()
if __name__ == "__main__":
check_lot_creation_methods()

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LOT이 생성되지 않은 입고 라인 확인
"""
import sqlite3
def check_missing_lots():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("LOT이 생성되지 않은 입고 라인 분석")
print("=" * 80)
print()
# 1. 전체 입고 라인과 LOT 매칭 상태
print("1. 입고 라인 - LOT 매칭 현황")
print("-" * 60)
cursor.execute("""
SELECT
COUNT(*) as total_lines,
SUM(CASE WHEN il.lot_id IS NOT NULL THEN 1 ELSE 0 END) as lines_with_lot,
SUM(CASE WHEN il.lot_id IS NULL THEN 1 ELSE 0 END) as lines_without_lot,
SUM(prl.line_total) as total_purchase_amount,
SUM(CASE WHEN il.lot_id IS NOT NULL THEN prl.line_total ELSE 0 END) as amount_with_lot,
SUM(CASE WHEN il.lot_id IS NULL THEN prl.line_total ELSE 0 END) as amount_without_lot
FROM purchase_receipt_lines prl
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
""")
result = cursor.fetchone()
print(f" 총 입고 라인: {result['total_lines']}")
print(f" ✅ LOT 생성됨: {result['lines_with_lot']}개 (₩{result['amount_with_lot']:,.0f})")
print(f" ❌ LOT 없음: {result['lines_without_lot']}개 (₩{result['amount_without_lot']:,.0f})")
print()
print(f" 총 입고 금액: ₩{result['total_purchase_amount']:,.0f}")
print(f" LOT 없는 금액: ₩{result['amount_without_lot']:,.0f}")
if result['amount_without_lot'] > 0:
print(f"\n ⚠️ LOT이 생성되지 않은 입고 금액이 ₩{result['amount_without_lot']:,.0f} 있습니다!")
print(" 이것이 DB 재고와 예상 재고 차이(₩55,500)의 원인일 가능성이 높습니다.")
# 2. LOT이 없는 입고 라인 상세
if result['lines_without_lot'] > 0:
print("\n2. LOT이 생성되지 않은 입고 라인 상세")
print("-" * 60)
cursor.execute("""
SELECT
pr.receipt_no,
pr.receipt_date,
h.herb_name,
prl.quantity_g,
prl.unit_price_per_g,
prl.line_total,
prl.lot_number,
prl.line_id
FROM purchase_receipt_lines prl
JOIN purchase_receipts pr ON prl.receipt_id = pr.receipt_id
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
WHERE il.lot_id IS NULL
ORDER BY prl.line_total DESC
""")
missing_lots = cursor.fetchall()
total_missing_amount = 0
print("\n LOT이 생성되지 않은 입고 라인:")
for i, line in enumerate(missing_lots, 1):
print(f"\n {i}. {line['herb_name']}")
print(f" 입고장: {line['receipt_no']} ({line['receipt_date']})")
print(f" 수량: {line['quantity_g']:,.0f}g")
print(f" 단가: ₩{line['unit_price_per_g']:.2f}/g")
print(f" 금액: ₩{line['line_total']:,.0f}")
print(f" LOT번호: {line['lot_number'] or 'None'}")
print(f" Line ID: {line['line_id']}")
total_missing_amount += line['line_total']
print(f"\n 총 누락 금액: ₩{total_missing_amount:,.0f}")
# 3. 반대로 입고 라인 없는 LOT 확인
print("\n3. 입고 라인과 연결되지 않은 LOT")
print("-" * 60)
cursor.execute("""
SELECT
COUNT(*) as orphan_lots,
SUM(quantity_onhand * unit_price_per_g) as orphan_value,
SUM(quantity_onhand) as orphan_quantity
FROM inventory_lots
WHERE receipt_line_id IS NULL
AND is_depleted = 0
AND quantity_onhand > 0
""")
orphans = cursor.fetchone()
if orphans['orphan_lots'] > 0:
print(f" 입고 라인 없는 LOT: {orphans['orphan_lots']}")
print(f" 해당 재고 가치: ₩{orphans['orphan_value']:,.0f}")
print(f" 해당 재고량: {orphans['orphan_quantity']:,.0f}g")
cursor.execute("""
SELECT
h.herb_name,
il.lot_number,
il.quantity_onhand,
il.unit_price_per_g,
il.quantity_onhand * il.unit_price_per_g as value,
il.received_date
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE il.receipt_line_id IS NULL
AND il.is_depleted = 0
AND il.quantity_onhand > 0
ORDER BY value DESC
LIMIT 5
""")
orphan_lots = cursor.fetchall()
if orphan_lots:
print("\n 상위 5개 입고 라인 없는 LOT:")
for lot in orphan_lots:
print(f" - {lot['herb_name']} (LOT: {lot['lot_number']})")
print(f" 재고: {lot['quantity_onhand']:,.0f}g, 금액: ₩{lot['value']:,.0f}")
else:
print(" ✅ 모든 LOT이 입고 라인과 연결되어 있습니다.")
# 4. 금액 차이 분석
print("\n4. 금액 차이 최종 분석")
print("=" * 60)
# 현재 DB 재고
cursor.execute("""
SELECT SUM(quantity_onhand * unit_price_per_g) as total
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
db_total = cursor.fetchone()['total'] or 0
# 총 입고 - 소비
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
total_in = cursor.fetchone()['total'] or 0
cursor.execute("""
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
FROM compound_consumptions cc
JOIN inventory_lots il ON cc.lot_id = il.lot_id
""")
total_out = cursor.fetchone()['total'] or 0
expected = total_in - total_out
print(f" DB 재고 자산: ₩{db_total:,.0f}")
print(f" 예상 재고 (입고-소비): ₩{expected:,.0f}")
print(f" 차이: ₩{expected - db_total:,.0f}")
print()
if result['amount_without_lot'] > 0:
print(f" 💡 LOT 없는 입고 금액: ₩{result['amount_without_lot']:,.0f}")
adjusted_expected = (total_in - result['amount_without_lot']) - total_out
print(f" 📊 조정된 예상 재고: ₩{adjusted_expected:,.0f}")
print(f" 조정 후 차이: ₩{adjusted_expected - db_total:,.0f}")
if abs(adjusted_expected - db_total) < 1000:
print("\n ✅ LOT이 생성되지 않은 입고 라인을 제외하면 차이가 거의 없습니다!")
print(" 이것이 차이의 주요 원인입니다.")
conn.close()
if __name__ == "__main__":
check_missing_lots()

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
print("=== purchase_receipts 테이블 구조 ===")
cursor.execute("PRAGMA table_info(purchase_receipts)")
columns = cursor.fetchall()
for col in columns:
print(f" {col[1]}: {col[2]}")
print("\n=== purchase_receipt_lines 테이블 구조 ===")
cursor.execute("PRAGMA table_info(purchase_receipt_lines)")
columns = cursor.fetchall()
for col in columns:
print(f" {col[1]}: {col[2]}")
print("\n=== 입고장 데이터 샘플 ===")
cursor.execute("""
SELECT pr.receipt_id, pr.receipt_number, pr.receipt_date,
COUNT(prl.line_id) as line_count,
SUM(prl.quantity_g) as total_quantity,
SUM(prl.total_price) as total_amount
FROM purchase_receipts pr
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
GROUP BY pr.receipt_id
LIMIT 5
""")
rows = cursor.fetchall()
for row in rows:
print(f" 입고장 {row[0]}: {row[1]} ({row[2]})")
print(f" - 항목수: {row[3]}개, 총량: {row[4]}g, 총액: ₩{row[5]:,.0f}")
print("\n=== inventory_lots의 receipt_line_id 연결 확인 ===")
cursor.execute("""
SELECT
COUNT(*) as total_lots,
SUM(CASE WHEN receipt_line_id IS NOT NULL THEN 1 ELSE 0 END) as matched_lots,
SUM(CASE WHEN receipt_line_id IS NULL THEN 1 ELSE 0 END) as unmatched_lots
FROM inventory_lots
WHERE is_depleted = 0
""")
result = cursor.fetchone()
print(f" 전체 LOT: {result[0]}")
print(f" 입고장 연결된 LOT: {result[1]}")
print(f" 입고장 연결 안된 LOT: {result[2]}")
conn.close()

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# 테이블 목록 확인
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
tables = cursor.fetchall()
print("=== 전체 테이블 목록 ===")
for table in tables:
print(f" - {table[0]}")
print("\n=== inventory_lots 테이블 구조 ===")
cursor.execute("PRAGMA table_info(inventory_lots)")
columns = cursor.fetchall()
for col in columns:
print(f" {col[1]}: {col[2]}")
print("\n=== inventory_lots 샘플 데이터 ===")
cursor.execute("""
SELECT lot_id, lot_number, herb_item_id, quantity_onhand,
unit_price_per_g, received_date, receipt_id
FROM inventory_lots
WHERE is_depleted = 0
LIMIT 5
""")
rows = cursor.fetchall()
for row in rows:
print(f" LOT {row[0]}: {row[1]}, 재고:{row[3]}g, 단가:₩{row[4]}, 입고일:{row[5]}, receipt_id:{row[6]}")
conn.close()

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
삼소음에 사용되는 약재들의 성분 코드 확인
"""
import sqlite3
def check_herb_codes():
"""약재 성분 코드 확인"""
# 삼소음에 사용되는 약재들
herbs_to_check = [
"인삼",
"소엽", # 자소엽
"전호",
"반하",
"갈근",
"적복령", # 적복령 또는 복령
"대조", # 대추
"진피",
"길경",
"지각",
"감초",
"건강"
]
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
herb_codes = {}
print("🌿 삼소음 약재 성분 코드 확인")
print("="*60)
for herb in herbs_to_check:
# 정확한 이름으로 먼저 검색
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name = ?
""", (herb,))
result = cursor.fetchone()
# 정확한 이름이 없으면 포함된 이름으로 검색
if not result:
# 특수 케이스 처리
if herb == "소엽":
search_term = "자소엽"
elif herb == "대조":
search_term = "대추"
elif herb == "적복령":
search_term = "적복령"
else:
search_term = herb
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name LIKE ? OR herb_name = ?
ORDER BY
CASE WHEN herb_name = ? THEN 0 ELSE 1 END,
LENGTH(herb_name)
LIMIT 1
""", (f'%{search_term}%', search_term, search_term))
result = cursor.fetchone()
if result:
herb_codes[herb] = result[0]
print(f"{herb}: {result[0]} ({result[1]})")
else:
print(f"{herb}: 찾을 수 없음")
# 유사한 이름 검색
cursor.execute("""
SELECT herb_name
FROM herb_masters
WHERE herb_name LIKE ?
LIMIT 5
""", (f'%{herb[:2]}%',))
similar = cursor.fetchall()
if similar:
print(f" 유사한 약재: {', '.join([s[0] for s in similar])}")
# 복령 관련 추가 확인
if "적복령" not in herb_codes or not herb_codes.get("적복령"):
print("\n📌 복령 관련 약재 추가 검색:")
cursor.execute("""
SELECT ingredient_code, herb_name
FROM herb_masters
WHERE herb_name LIKE '%복령%'
ORDER BY herb_name
""")
bokryung_list = cursor.fetchall()
for code, name in bokryung_list:
print(f" - {code}: {name}")
conn.close()
return herb_codes
if __name__ == "__main__":
herb_codes = check_herb_codes()
print("\n📊 약재 코드 매핑 결과:")
print("-"*60)
for herb, code in herb_codes.items():
if code:
print(f'"{herb}": "{code}",')

View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
십전대보탕 데이터 조회 및 분석
"""
import sqlite3
def check_sipjeondaebotang():
"""십전대보탕 처방 상세 조회"""
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
print("🔍 십전대보탕 처방 조회")
print("="*70)
# 십전대보탕 처방 찾기
cursor.execute("""
SELECT formula_id, formula_code, formula_name, formula_type,
base_cheop, base_pouches, description, is_active
FROM formulas
WHERE formula_name LIKE '%십전대보%'
OR formula_name LIKE '%십전대보탕%'
OR formula_code LIKE '%SJDB%'
""")
formulas = cursor.fetchall()
if not formulas:
print("❌ 십전대보탕 처방을 찾을 수 없습니다.")
else:
for formula_id, code, name, f_type, cheop, pouches, desc, active in formulas:
print(f"\n📋 {name} ({code})")
print(f" ID: {formula_id}")
print(f" 타입: {f_type if f_type else '❌ 없음'}")
print(f" 기본 첩수: {cheop if cheop else '❌ 없음'}")
print(f" 기본 포수: {pouches if pouches else '❌ 없음'}")
print(f" 설명: {desc if desc else '❌ 없음'}")
print(f" 활성 상태: {'활성' if active else '비활성'}")
# 처방 구성 약재 확인
cursor.execute("""
SELECT hm.herb_name, hm.ingredient_code, fi.grams_per_cheop, fi.notes
FROM formula_ingredients fi
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
WHERE fi.formula_id = ?
ORDER BY fi.sort_order
""", (formula_id,))
ingredients = cursor.fetchall()
print(f"\n 구성 약재 ({len(ingredients)}개):")
print(" " + "-"*60)
print(f" {'약재명':15s} | {'용량(g)':>8s} | {'효능 설명'}")
print(" " + "-"*60)
total_amount = 0
for herb_name, code, amount, notes in ingredients:
total_amount += amount
notes_str = notes if notes else "❌ 효능 설명 없음"
print(f" {herb_name:15s} | {amount:8.1f} | {notes_str}")
print(" " + "-"*60)
print(f" {'총 용량':15s} | {total_amount:8.1f} |")
# 빠진 정보 체크
print(f"\n ⚠️ 빠진 정보 체크:")
missing = []
if not desc:
missing.append("처방 설명")
if not f_type:
missing.append("처방 타입")
if not cheop:
missing.append("기본 첩수")
if not pouches:
missing.append("기본 포수")
# 약재별 효능 설명 체크
missing_notes = []
for herb_name, code, amount, notes in ingredients:
if not notes:
missing_notes.append(herb_name)
if missing:
print(f" - 처방 기본 정보: {', '.join(missing)}")
if missing_notes:
print(f" - 약재 효능 설명 없음: {', '.join(missing_notes)}")
if not missing and not missing_notes:
print(" ✅ 모든 정보가 완비되어 있습니다.")
# 십전대보탕 표준 구성 확인
print(f"\n\n📚 십전대보탕 표준 구성 (참고용):")
print("="*70)
print("""
십전대보탕은 사군자탕(인삼, 백출, 복령, 감초)과
사물탕(당귀, 천궁, 백작약, 숙지황)을 합방한 처방으로,
황기와 육계를 추가하여 총 10개 약재로 구성됩니다.
주요 효능: 기혈양허(氣血兩虛)를 치료하는 대표 처방
- 대보기혈(大補氣血): 기와 혈을 크게 보함
- 병후 회복, 수술 후 회복, 만성 피로에 사용
표준 구성 (1첩 기준):
- 인삼 4g (대보원기)
- 황기 4g (보기승양)
- 백출 4g (보기건비)
- 복령 4g (건비이수)
- 감초 2g (조화제약)
- 당귀(일당귀) 4g (보혈)
- 천궁 4g (활혈)
- 백작약 4g (보혈)
- 숙지황 4g (보음보혈)
- 육계 2g (온양보화)
""")
conn.close()
if __name__ == "__main__":
check_sipjeondaebotang()

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
쌍화탕 처방 및 당귀 약재 확인
"""
import sqlite3
def check_ssanghwatang():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# 쌍화탕 처방 찾기
print("🔍 쌍화탕 처방 검색...")
print("="*60)
cursor.execute("""
SELECT formula_id, formula_code, formula_name
FROM formulas
WHERE formula_name LIKE '%쌍화%'
""")
formulas = cursor.fetchall()
if not formulas:
print("❌ 쌍화탕 처방을 찾을 수 없습니다.")
else:
for formula_id, code, name in formulas:
print(f"\n📋 {name} ({code})")
# 처방 구성 약재 확인
cursor.execute("""
SELECT hm.herb_name, hm.ingredient_code, fi.grams_per_cheop
FROM formula_ingredients fi
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
WHERE fi.formula_id = ?
ORDER BY fi.sort_order
""", (formula_id,))
ingredients = cursor.fetchall()
print(" 구성 약재:")
for herb_name, code, amount in ingredients:
if '당귀' in herb_name:
print(f" ⚠️ {herb_name} ({code}): {amount}g <-- 당귀 발견!")
else:
print(f" - {herb_name} ({code}): {amount}g")
# 당귀 관련 약재 검색
print("\n\n🌿 당귀 관련 약재 검색...")
print("="*60)
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name LIKE '%당귀%'
ORDER BY herb_name
""")
danggui_herbs = cursor.fetchall()
for code, name, hanja in danggui_herbs:
print(f"{code}: {name} ({hanja})")
# 일당귀 확인
print("\n✅ 일당귀 검색:")
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name = '일당귀'
OR herb_name LIKE '%일당귀%'
""")
result = cursor.fetchall()
if result:
for code, name, hanja in result:
print(f" {code}: {name} ({hanja})")
else:
print(" ❌ 일당귀를 찾을 수 없음")
conn.close()
if __name__ == "__main__":
check_ssanghwatang()

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cur = conn.cursor()
# herb_item_tags 테이블 구조 확인
cur.execute("PRAGMA table_info(herb_item_tags)")
print("herb_item_tags 테이블 구조:")
for row in cur.fetchall():
print(f" {row}")
# 실제 테이블 목록 확인
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'herb%' ORDER BY name")
print("\n약재 관련 테이블:")
for row in cur.fetchall():
print(f" - {row[0]}")
conn.close()

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
월비탕에 사용되는 약재들의 성분 코드 확인
"""
import sqlite3
def check_herb_codes():
"""약재 성분 코드 확인"""
# 월비탕에 사용되는 약재들
herbs_to_check = [
"마황",
"석고",
"감초",
"진피",
"복령",
"갈근",
"건지황",
"창출"
]
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
herb_codes = {}
print("🌿 월비탕 약재 성분 코드 확인")
print("="*50)
for herb in herbs_to_check:
# 정확한 이름으로 먼저 검색
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name = ?
""", (herb,))
result = cursor.fetchone()
# 정확한 이름이 없으면 포함된 이름으로 검색
if not result:
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name LIKE ?
ORDER BY LENGTH(herb_name)
LIMIT 1
""", (f'%{herb}%',))
result = cursor.fetchone()
if result:
herb_codes[herb] = result[0]
print(f"{herb}: {result[0]} ({result[1]}, {result[2]})")
else:
print(f"{herb}: 찾을 수 없음")
# 비슷한 이름 찾기
cursor.execute("""
SELECT herb_name
FROM herb_masters
WHERE herb_name LIKE ?
LIMIT 5
""", (f'%{herb[:2]}%',))
similar = cursor.fetchall()
if similar:
print(f" 유사한 약재: {', '.join([s[0] for s in similar])}")
conn.close()
return herb_codes
if __name__ == "__main__":
herb_codes = check_herb_codes()
print("\n📊 약재 코드 매핑 결과:")
print("-"*50)
for herb, code in herb_codes.items():
print(f'"{herb}": "{code}",')

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API 재고 계산 디버깅
"""
import sqlite3
def debug_api_calculation():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("API 재고 계산 디버깅")
print("=" * 80)
print()
# API와 동일한 쿼리 실행
cursor.execute("""
SELECT
h.herb_item_id,
h.insurance_code,
h.herb_name,
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
COUNT(DISTINCT il.lot_id) as lot_count,
COUNT(DISTINCT il.origin_country) as origin_count,
AVG(il.unit_price_per_g) as avg_price,
MIN(il.unit_price_per_g) as min_price,
MAX(il.unit_price_per_g) as max_price,
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name
HAVING total_quantity > 0
ORDER BY total_value DESC
""")
items = cursor.fetchall()
print("상위 10개 약재별 재고 가치:")
print("-" * 60)
total_api_value = 0
for i, item in enumerate(items[:10], 1):
value = item['total_value']
total_api_value += value
print(f"{i:2}. {item['herb_name']:15} 재고:{item['total_quantity']:8.0f}g 금액:₩{value:10,.0f}")
# 전체 합계 계산
total_api_value = sum(item['total_value'] for item in items)
print()
print(f"전체 약재 수: {len(items)}")
print(f"API 계산 총액: ₩{total_api_value:,.0f}")
print()
# 직접 inventory_lots에서 계산
cursor.execute("""
SELECT
SUM(quantity_onhand * unit_price_per_g) as direct_total
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
direct_total = cursor.fetchone()['direct_total'] or 0
print(f"직접 계산 총액: ₩{direct_total:,.0f}")
print(f"차이: ₩{total_api_value - direct_total:,.0f}")
print()
# 차이 원인 분석
if abs(total_api_value - direct_total) > 1:
print("차이 원인 분석:")
print("-" * 40)
# 중복 LOT 확인
cursor.execute("""
SELECT
h.herb_name,
COUNT(*) as lot_count,
SUM(il.quantity_onhand * il.unit_price_per_g) as total_value
FROM herb_items h
JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
GROUP BY h.herb_item_id
HAVING lot_count > 1
ORDER BY total_value DESC
LIMIT 5
""")
multi_lots = cursor.fetchall()
if multi_lots:
print("\n여러 LOT을 가진 약재:")
for herb in multi_lots:
print(f" - {herb['herb_name']}: {herb['lot_count']}개 LOT, ₩{herb['total_value']:,.0f}")
# 특이사항 확인 - LEFT JOIN으로 인한 NULL 처리
cursor.execute("""
SELECT COUNT(*) as herbs_without_lots
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id
AND il.is_depleted = 0
AND il.quantity_onhand > 0
WHERE il.lot_id IS NULL
""")
no_lots = cursor.fetchone()['herbs_without_lots']
if no_lots > 0:
print(f"\n재고가 없는 약재 수: {no_lots}")
conn.close()
if __name__ == "__main__":
debug_api_calculation()

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
재고 자산 금액 불일치 최종 분석
"""
import sqlite3
from datetime import datetime
def final_analysis():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("재고 자산 금액 불일치 최종 분석")
print("분석 시간:", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print("=" * 80)
print()
# 1. 현재 DB의 실제 재고 자산
print("📊 현재 데이터베이스 상태")
print("-" * 60)
cursor.execute("""
SELECT
SUM(quantity_onhand * unit_price_per_g) as total_value,
COUNT(*) as lot_count,
SUM(quantity_onhand) as total_quantity
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
current = cursor.fetchone()
db_total = current['total_value'] or 0
print(f" DB 재고 자산: ₩{db_total:,.0f}")
print(f" 활성 LOT: {current['lot_count']}")
print(f" 총 재고량: {current['total_quantity']:,.1f}g")
print()
# 2. 입고와 출고 분석
print("💼 입고/출고 분석")
print("-" * 60)
# 입고 총액
cursor.execute("""
SELECT SUM(line_total) as total_in
FROM purchase_receipt_lines
""")
total_in = cursor.fetchone()['total_in'] or 0
# 복합제 소비 금액
cursor.execute("""
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total_out
FROM compound_consumptions cc
JOIN inventory_lots il ON cc.lot_id = il.lot_id
""")
total_out = cursor.fetchone()['total_out'] or 0
print(f" 총 입고 금액: ₩{total_in:,.0f}")
print(f" 총 소비 금액: ₩{total_out:,.0f}")
print(f" 예상 잔액: ₩{total_in - total_out:,.0f}")
print()
# 3. 차이 분석
print("🔍 차이 분석 결과")
print("=" * 60)
print()
ui_value = 5875708 # 화면에 표시되는 금액
expected_value = total_in - total_out
print(f" 화면 표시 금액: ₩{ui_value:,.0f}")
print(f" DB 계산 금액: ₩{db_total:,.0f}")
print(f" 예상 금액 (입고-소비): ₩{expected_value:,.0f}")
print()
print(" 차이:")
print(f" 화면 vs DB: ₩{ui_value - db_total:,.0f}")
print(f" 화면 vs 예상: ₩{ui_value - expected_value:,.0f}")
print(f" DB vs 예상: ₩{db_total - expected_value:,.0f}")
print()
# 4. 가능한 원인 분석
print("❗ 불일치 원인 분석")
print("-" * 60)
# 4-1. 단가 차이 확인
cursor.execute("""
SELECT
prl.line_id,
h.herb_name,
prl.quantity_g as purchase_qty,
prl.unit_price_per_g as purchase_price,
prl.line_total as purchase_total,
il.quantity_onhand as current_qty,
il.unit_price_per_g as lot_price,
il.quantity_onhand * il.unit_price_per_g as current_value,
ABS(prl.unit_price_per_g - il.unit_price_per_g) as price_diff
FROM purchase_receipt_lines prl
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
WHERE il.is_depleted = 0 AND il.quantity_onhand > 0
AND ABS(prl.unit_price_per_g - il.unit_price_per_g) > 0.01
ORDER BY price_diff DESC
LIMIT 5
""")
price_diffs = cursor.fetchall()
if price_diffs:
print("\n ⚠️ 입고 단가와 LOT 단가가 다른 항목:")
for pd in price_diffs:
print(f" {pd['herb_name']}:")
print(f" 입고 단가: ₩{pd['purchase_price']:.2f}/g")
print(f" LOT 단가: ₩{pd['lot_price']:.2f}/g")
print(f" 차이: ₩{pd['price_diff']:.2f}/g")
# 4-2. 소비 후 남은 재고 확인
cursor.execute("""
SELECT
h.herb_name,
il.lot_number,
il.quantity_received as original_qty,
il.quantity_onhand as current_qty,
il.quantity_received - il.quantity_onhand as consumed_qty,
il.unit_price_per_g
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE il.is_depleted = 0
AND il.quantity_received > il.quantity_onhand
ORDER BY (il.quantity_received - il.quantity_onhand) DESC
LIMIT 5
""")
consumed_lots = cursor.fetchall()
if consumed_lots:
print("\n 📉 소비된 재고가 있는 LOT (상위 5개):")
for cl in consumed_lots:
print(f" {cl['herb_name']} (LOT: {cl['lot_number']})")
print(f" 원래: {cl['original_qty']:,.0f}g → 현재: {cl['current_qty']:,.0f}g")
print(f" 소비: {cl['consumed_qty']:,.0f}g (₩{cl['consumed_qty'] * cl['unit_price_per_g']:,.0f})")
# 4-3. JavaScript 계산 로직 확인 필요
print("\n 💡 추가 확인 필요사항:")
print(" 1) 프론트엔드 JavaScript에서 재고 자산을 계산하는 로직")
print(" 2) 캐시 또는 세션 스토리지에 저장된 이전 값")
print(" 3) inventory_lots_v2 테이블 사용 여부")
# inventory_lots_v2 확인
cursor.execute("""
SELECT
SUM(quantity_onhand * unit_price_per_g) as v2_total,
COUNT(*) as v2_count
FROM inventory_lots_v2
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
v2_result = cursor.fetchone()
if v2_result and v2_result['v2_count'] > 0:
v2_total = v2_result['v2_total'] or 0
print(f"\n ⚠️ inventory_lots_v2 테이블 데이터:")
print(f" 재고 자산: ₩{v2_total:,.0f}")
print(f" LOT 수: {v2_result['v2_count']}")
if abs(v2_total - ui_value) < 100:
print(f" → 화면 금액과 일치할 가능성 높음!")
print()
# 5. 결론
print("📝 결론")
print("=" * 60)
diff = ui_value - db_total
if diff > 0:
print(f" 화면에 표시되는 금액(₩{ui_value:,.0f})이")
print(f" 실제 DB 금액(₩{db_total:,.0f})보다")
print(f"{diff:,.0f} 더 많습니다.")
print()
print(" 가능한 원인:")
print(" 1) 프론트엔드에서 별도의 계산 로직 사용")
print(" 2) 캐시된 이전 데이터 표시")
print(" 3) inventory_lots_v2 테이블 참조")
print(" 4) 재고 보정 내역이 즉시 반영되지 않음")
else:
print(f" 실제 DB 금액이 화면 표시 금액보다 적습니다.")
conn.close()
if __name__ == "__main__":
final_analysis()

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
최종 가격 차이 분석
"""
import sqlite3
def final_price_analysis():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("📊 재고 자산 차이 최종 분석")
print("=" * 80)
print()
# 1. 핵심 차이 확인
print("1. 핵심 금액 차이")
print("-" * 60)
# 입고 라인과 LOT 차이
cursor.execute("""
SELECT
h.herb_name,
prl.quantity_g as receipt_qty,
prl.unit_price_per_g as receipt_price,
prl.line_total as receipt_total,
il.quantity_received as lot_qty,
il.unit_price_per_g as lot_price,
il.quantity_received * il.unit_price_per_g as lot_total,
prl.line_total - (il.quantity_received * il.unit_price_per_g) as diff
FROM purchase_receipt_lines prl
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
WHERE ABS(prl.line_total - (il.quantity_received * il.unit_price_per_g)) > 1
ORDER BY ABS(prl.line_total - (il.quantity_received * il.unit_price_per_g)) DESC
""")
differences = cursor.fetchall()
if differences:
print(" 입고장과 LOT 생성 시 차이가 있는 항목:")
print()
total_diff = 0
for diff in differences:
print(f" 📌 {diff['herb_name']}")
print(f" 입고장: {diff['receipt_qty']:,.0f}g ×{diff['receipt_price']:.2f} = ₩{diff['receipt_total']:,.0f}")
print(f" LOT: {diff['lot_qty']:,.0f}g ×{diff['lot_price']:.2f} = ₩{diff['lot_total']:,.0f}")
print(f" 차이: ₩{diff['diff']:,.0f}")
print()
total_diff += diff['diff']
print(f" 총 차이: ₩{total_diff:,.0f}")
# 2. 재고 자산 흐름
print("\n2. 재고 자산 흐름 정리")
print("=" * 60)
# 각 단계별 금액
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
receipt_total = cursor.fetchone()['total'] or 0
cursor.execute("""
SELECT SUM(quantity_received * unit_price_per_g) as total
FROM inventory_lots
""")
lot_creation_total = cursor.fetchone()['total'] or 0
cursor.execute("""
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
FROM compound_consumptions cc
JOIN inventory_lots il ON cc.lot_id = il.lot_id
""")
consumed_total = cursor.fetchone()['total'] or 0
cursor.execute("""
SELECT SUM(quantity_onhand * unit_price_per_g) as total
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
current_inventory = cursor.fetchone()['total'] or 0
print(f" 1⃣ 입고장 총액: ₩{receipt_total:,.0f}")
print(f" 2⃣ LOT 생성 총액: ₩{lot_creation_total:,.0f}")
print(f" 차이 (1-2): ₩{receipt_total - lot_creation_total:,.0f}")
print()
print(f" 3⃣ 소비 총액: ₩{consumed_total:,.0f}")
print(f" 4⃣ 현재 재고 자산: ₩{current_inventory:,.0f}")
print()
print(f" 📊 계산식:")
print(f" LOT 생성 - 소비 = ₩{lot_creation_total:,.0f} - ₩{consumed_total:,.0f}")
print(f" = ₩{lot_creation_total - consumed_total:,.0f} (예상)")
print(f" 실제 재고 = ₩{current_inventory:,.0f}")
print(f" 차이 = ₩{current_inventory - (lot_creation_total - consumed_total):,.0f}")
# 3. 차이 원인 분석
print("\n3. 차이 원인 설명")
print("-" * 60)
# 휴먼일당귀 특별 케이스 확인
cursor.execute("""
SELECT
prl.quantity_g as receipt_qty,
il.quantity_received as lot_received,
il.quantity_onhand as lot_current
FROM purchase_receipt_lines prl
JOIN inventory_lots il ON prl.line_id = il.receipt_line_id
JOIN herb_items h ON prl.herb_item_id = h.herb_item_id
WHERE h.herb_name = '휴먼일당귀'
""")
ildan = cursor.fetchone()
if ildan:
print("\n 💡 휴먼일당귀 케이스:")
print(f" 입고장 수량: {ildan['receipt_qty']:,.0f}g")
print(f" LOT 생성 수량: {ildan['lot_received']:,.0f}g")
print(f" 현재 재고: {ildan['lot_current']:,.0f}g")
print(f" → 입고 시 5,000g 중 3,000g만 LOT 생성됨")
print(f" → 나머지 2,000g는 별도 처리되었을 가능성")
print("\n 📝 결론:")
print(" 1. 입고장 총액 (₩1,616,400) vs LOT 생성 총액 (₩1,607,400)")
print(" → ₩9,000 차이 (휴먼일당귀 수량 차이로 인함)")
print()
print(" 2. 예상 재고 (₩1,529,434) vs 실제 재고 (₩1,529,434)")
print(" → 정확히 일치")
print()
print(" 3. 입고 기준 예상 (₩1,538,434) vs 실제 재고 (₩1,529,434)")
print(" → ₩9,000 차이 (입고와 LOT 생성 차이와 동일)")
# 4. 추가 LOT 확인
print("\n4. 추가 LOT 존재 여부")
print("-" * 60)
cursor.execute("""
SELECT
h.herb_name,
COUNT(*) as lot_count,
SUM(il.quantity_received) as total_received,
SUM(il.quantity_onhand) as total_onhand
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE h.herb_name = '휴먼일당귀'
GROUP BY h.herb_item_id
""")
ildan_lots = cursor.fetchone()
if ildan_lots:
print(f" 휴먼일당귀 LOT 현황:")
print(f" LOT 개수: {ildan_lots['lot_count']}")
print(f" 총 입고량: {ildan_lots['total_received']:,.0f}g")
print(f" 현재 재고: {ildan_lots['total_onhand']:,.0f}g")
# 상세 LOT 정보
cursor.execute("""
SELECT
lot_id,
lot_number,
quantity_received,
quantity_onhand,
unit_price_per_g,
receipt_line_id
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE h.herb_name = '휴먼일당귀'
""")
lots = cursor.fetchall()
for lot in lots:
print(f"\n LOT {lot['lot_id']}:")
print(f" LOT 번호: {lot['lot_number']}")
print(f" 입고량: {lot['quantity_received']:,.0f}g")
print(f" 현재: {lot['quantity_onhand']:,.0f}g")
print(f" 단가: ₩{lot['unit_price_per_g']:.2f}")
print(f" 입고라인: {lot['receipt_line_id']}")
conn.close()
if __name__ == "__main__":
final_price_analysis()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
최종 검증 - 문제 해결 확인
"""
import sqlite3
import json
import urllib.request
def final_verification():
print("=" * 80)
print("📊 재고 자산 문제 해결 최종 검증")
print("=" * 80)
print()
# 1. API 호출 결과
print("1. API 응답 확인")
print("-" * 60)
try:
with urllib.request.urlopen('http://localhost:5001/api/inventory/summary') as response:
data = json.loads(response.read())
api_value = data['summary']['total_value']
total_items = data['summary']['total_items']
print(f" API 재고 자산: ₩{api_value:,.0f}")
print(f" 총 약재 수: {total_items}")
except Exception as e:
print(f" API 호출 실패: {e}")
api_value = 0
# 2. 데이터베이스 직접 계산
print("\n2. 데이터베이스 직접 계산")
print("-" * 60)
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT
SUM(quantity_onhand * unit_price_per_g) as total_value,
COUNT(*) as lot_count,
SUM(quantity_onhand) as total_quantity
FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
""")
db_result = cursor.fetchone()
db_value = db_result['total_value'] or 0
print(f" DB 재고 자산: ₩{db_value:,.0f}")
print(f" 활성 LOT: {db_result['lot_count']}")
print(f" 총 재고량: {db_result['total_quantity']:,.1f}g")
# 3. 입고와 출고 기반 계산
print("\n3. 입고/출고 기반 계산")
print("-" * 60)
# 총 입고액
cursor.execute("SELECT SUM(line_total) as total FROM purchase_receipt_lines")
total_in = cursor.fetchone()['total'] or 0
# 총 소비액
cursor.execute("""
SELECT SUM(cc.quantity_used * il.unit_price_per_g) as total
FROM compound_consumptions cc
JOIN inventory_lots il ON cc.lot_id = il.lot_id
""")
total_out = cursor.fetchone()['total'] or 0
expected = total_in - total_out
print(f" 입고 총액: ₩{total_in:,.0f}")
print(f" 소비 총액: ₩{total_out:,.0f}")
print(f" 예상 재고: ₩{expected:,.0f}")
# 4. 결과 비교
print("\n4. 결과 비교")
print("=" * 60)
print(f"\n 🎯 API 재고 자산: ₩{api_value:,.0f}")
print(f" 🎯 DB 직접 계산: ₩{db_value:,.0f}")
print(f" 🎯 예상 재고액: ₩{expected:,.0f}")
# 차이 계산
api_db_diff = abs(api_value - db_value)
db_expected_diff = abs(db_value - expected)
print(f"\n API vs DB 차이: ₩{api_db_diff:,.0f}")
print(f" DB vs 예상 차이: ₩{db_expected_diff:,.0f}")
# 5. 결론
print("\n5. 결론")
print("=" * 60)
if api_db_diff < 100:
print("\n ✅ 문제 해결 완료!")
print(" API와 DB 계산이 일치합니다.")
print(f" 재고 자산: ₩{api_value:,.0f}")
else:
print("\n ⚠️ 아직 차이가 있습니다.")
print(f" 차이: ₩{api_db_diff:,.0f}")
if db_expected_diff > 100000:
print("\n 📌 참고: DB 재고와 예상 재고 간 차이는")
print(" 다음 요인들로 인해 발생할 수 있습니다:")
print(" - 입고 시점과 LOT 생성 시점의 단가 차이")
print(" - 재고 보정 내역")
print(" - 반올림 오차 누적")
# 6. 효능 태그 확인 (중복 문제가 해결되었는지)
print("\n6. 효능 태그 표시 확인")
print("-" * 60)
# API에서 효능 태그가 있는 약재 확인
try:
with urllib.request.urlopen('http://localhost:5001/api/inventory/summary') as response:
data = json.loads(response.read())
herbs_with_tags = [
item for item in data['data']
if item.get('efficacy_tags') and len(item['efficacy_tags']) > 0
]
print(f" 효능 태그가 있는 약재: {len(herbs_with_tags)}")
if herbs_with_tags:
sample = herbs_with_tags[0]
print(f"\n 예시: {sample['herb_name']}")
print(f" 태그: {', '.join(sample['efficacy_tags'])}")
print(f" 재고 가치: ₩{sample['total_value']:,.0f}")
except Exception as e:
print(f" 효능 태그 확인 실패: {e}")
conn.close()
print("\n" + "=" * 80)
print("검증 완료")
print("=" * 80)
if __name__ == "__main__":
final_verification()

View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
E2E 테스트: 조제 화면에서 쌍화탕 선택 후 인삼 선택 가능 확인
"""
from playwright.sync_api import sync_playwright, expect
import time
def test_compound_ginseng_selection():
"""쌍화탕 조제 시 인삼 선택 가능 테스트"""
with sync_playwright() as p:
# 브라우저 실행 (headless 모드)
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
print("=" * 80)
print("E2E 테스트: 쌍화탕 조제 시 인삼 선택 가능 확인")
print("=" * 80)
# 1. 메인 페이지 접속
print("\n[1] 메인 페이지 접속...")
page.goto('http://localhost:5001')
page.wait_for_load_state('networkidle')
print("✓ 페이지 로드 완료")
# 2. 조제관리 메뉴 클릭
print("\n[2] 조제관리 메뉴 클릭...")
# 사이드바에서 조제 관리 클릭
compound_menu = page.locator('text=조제 관리').first
compound_menu.click()
time.sleep(2)
print("✓ 조제관리 화면 진입")
# 조제 입력 섹션 표시
print("\n[2-1] 조제 입력 섹션 표시...")
show_compound_entry = page.locator('#showCompoundEntry')
if show_compound_entry.count() > 0:
show_compound_entry.click()
time.sleep(1)
print("✓ 조제 입력 섹션 표시")
# 3. 현재 화면 상태 확인
print("\n[3] 화면 상태 확인...")
# 스크린샷 저장
page.screenshot(path='/tmp/compound_screen_after_menu_click.png')
print("✓ 스크린샷: /tmp/compound_screen_after_menu_click.png")
# 페이지에 select 요소가 있는지 확인
all_selects = page.locator('select').all()
print(f"✓ 페이지 내 select 요소: {len(all_selects)}")
for idx, sel in enumerate(all_selects):
sel_id = sel.get_attribute('id')
sel_name = sel.get_attribute('name')
print(f" [{idx}] id={sel_id}, name={sel_name}")
# 처방 선택 시도
print("\n[4] 처방 선택...")
# compoundFormula select 요소 찾기 (ID로 정확히)
formula_select = page.locator('#compoundFormula')
if formula_select.count() > 0:
# select가 visible 될 때까지 기다리기
try:
formula_select.wait_for(state="visible", timeout=5000)
except:
print("⚠️ 처방 선택 드롭다운이 보이지 않음")
# 옵션 확인
options = formula_select.locator('option').all()
print(f"✓ 드롭다운 옵션: {len(options)}")
for opt in options:
print(f" - {opt.text_content()}")
# 쌍화탕 선택
try:
formula_select.select_option(label='쌍화탕')
time.sleep(3)
print("✓ 쌍화탕 선택 완료")
except Exception as e:
print(f"⚠️ label로 선택 실패: {e}")
# index로 시도 (첫 번째 옵션은 보통 placeholder이므로 index=1)
try:
formula_select.select_option(index=1)
time.sleep(3)
print("✓ 첫 번째 처방 선택 완료")
except Exception as e2:
print(f"❌ 처방 선택 실패: {e2}")
else:
print("❌ 처방 드롭다운을 찾을 수 없음")
# 5. 약재 추가 버튼 클릭
print("\n[5] 약재 추가 버튼 클릭...")
# 약재 추가 버튼 찾기
add_ingredient_btn = page.locator('#addIngredientBtn')
if add_ingredient_btn.count() > 0:
add_ingredient_btn.click()
time.sleep(1)
print("✓ 약재 추가 버튼 클릭 완료")
# 6. 새로 추가된 행에서 약재 선택 드롭다운 확인
print("\n[6] 약재 선택 드롭다운 확인...")
# 새로 추가된 행 찾기 (마지막 행)
new_row = page.locator('#compoundIngredients tr').last
# 약재 선택 드롭다운 찾기
herb_select = new_row.locator('.herb-select-compound')
if herb_select.count() > 0:
print("✓ 약재 선택 드롭다운 발견")
# 드롭다운 옵션 확인
time.sleep(1) # 드롭다운이 로드될 시간 확보
options = herb_select.locator('option').all()
print(f"✓ 약재 옵션: {len(options)}")
# 처음 10개 옵션 출력
for idx, option in enumerate(options[:10]):
text = option.text_content()
value = option.get_attribute('value')
print(f" [{idx}] {text} (value: {value})")
# 마스터 약재명이 표시되는지 확인
has_master_names = False
for option in options:
text = option.text_content()
# ingredient_code 형식의 value와 한글/한자 형식의 텍스트 확인
if '(' in text and ')' in text: # 한자 포함 형식
has_master_names = True
break
if has_master_names:
print("\n✅ 마스터 약재명이 드롭다운에 표시됨!")
# 인삼 선택 시도
try:
herb_select.select_option(label='인삼 (人蔘)')
print("✓ 인삼 선택 완료")
except:
# label이 정확히 일치하지 않으면 부분 매칭
for idx, option in enumerate(options):
if '인삼' in option.text_content():
herb_select.select_option(index=idx)
print(f"✓ 인삼 선택 완료 (index {idx})")
break
time.sleep(1)
# 제품 선택 드롭다운 확인
product_select = new_row.locator('.product-select')
if product_select.count() > 0:
print("\n[7] 제품 선택 드롭다운 확인...")
time.sleep(1) # 제품 목록 로드 대기
product_options = product_select.locator('option').all()
print(f"✓ 제품 옵션: {len(product_options)}")
for idx, option in enumerate(product_options):
print(f" [{idx}] {option.text_content()}")
else:
print("\n⚠️ 마스터 약재명 대신 제품명이 드롭다운에 표시됨")
print("(신흥생강, 신흥작약 등의 제품명이 보임)")
else:
print("❌ 약재 선택 드롭다운을 찾을 수 없음")
else:
print("❌ 약재 추가 버튼을 찾을 수 없음")
# 7. 최종 스크린샷
page.screenshot(path='/tmp/compound_screen_final.png')
print("\n✓ 최종 스크린샷: /tmp/compound_screen_final.png")
print("\n" + "=" * 80)
print("테스트 완료")
print("=" * 80)
# 완료
time.sleep(1)
except Exception as e:
print(f"\n❌ 에러 발생: {e}")
import traceback
traceback.print_exc()
# 에러 스크린샷
page.screenshot(path='/tmp/compound_error.png')
print("에러 스크린샷: /tmp/compound_error.png")
finally:
browser.close()
if __name__ == '__main__':
test_compound_ginseng_selection()

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
조제 페이지 드롭다운 테스트
"""
import requests
from datetime import datetime
BASE_URL = "http://localhost:5001"
print("\n" + "="*80)
print("조제 페이지 기능 테스트")
print("="*80)
# 1. 약재 마스터 목록 확인
print("\n1. /api/herbs/masters 테스트:")
response = requests.get(f"{BASE_URL}/api/herbs/masters")
if response.status_code == 200:
data = response.json()
print(f" ✅ 성공: {data['success']}")
print(f" 총 약재: {len(data['data'])}")
print(f" 재고 있는 약재: {data['stats']['herbs_with_stock']}")
print(f" 커버리지: {data['stats']['coverage_rate']}%")
else:
print(f" ❌ 실패: {response.status_code}")
# 2. 처방 목록 확인
print("\n2. /api/formulas 테스트:")
response = requests.get(f"{BASE_URL}/api/formulas")
if response.status_code == 200:
formulas = response.json()
print(f" ✅ 성공: {len(formulas)}개 처방")
# 십전대보탕 찾기
for f in formulas:
if '십전대보탕' in f.get('formula_name', ''):
print(f" 십전대보탕 ID: {f['formula_id']}")
# 처방 구성 확인
response2 = requests.get(f"{BASE_URL}/api/formulas/{f['formula_id']}/ingredients")
if response2.status_code == 200:
ingredients = response2.json()
print(f" 구성 약재: {len(ingredients)}")
for ing in ingredients[:3]:
print(f" - {ing['herb_name']} ({ing['ingredient_code']}): {ing['grams_per_cheop']}g")
break
else:
print(f" ❌ 실패: {response.status_code}")
# 3. 특정 약재(당귀)의 제품 목록 확인
print("\n3. /api/herbs/by-ingredient/3400H1ACD (당귀) 테스트:")
response = requests.get(f"{BASE_URL}/api/herbs/by-ingredient/3400H1ACD")
if response.status_code == 200:
data = response.json()
print(f" ✅ 성공: {data['success']}")
if data['data']:
print(f" 당귀 제품 수: {len(data['data'])}")
for product in data['data'][:3]:
print(f" - {product.get('herb_name', '제품명 없음')} ({product.get('insurance_code', '')})")
print(f" 재고: {product.get('total_stock', 0)}g, 로트: {product.get('lot_count', 0)}")
else:
print(f" ❌ 실패: {response.status_code}")
# 4. 재고 현황 페이지 API 확인
print("\n4. /api/herbs (재고현황 API) 테스트:")
response = requests.get(f"{BASE_URL}/api/herbs")
if response.status_code == 200:
data = response.json()
print(f" ✅ 성공: {data['success']}")
print(f" 약재 수: {len(data['data'])}")
# 재고가 있는 약재 필터링
herbs_with_stock = [h for h in data['data'] if h.get('current_stock', 0) > 0]
print(f" 재고 있는 약재: {len(herbs_with_stock)}")
for herb in herbs_with_stock[:3]:
print(f" - {herb['herb_name']} ({herb['insurance_code']}): {herb['current_stock']}g")
else:
print(f" ❌ 실패: {response.status_code}")
print("\n" + "="*80)
print("테스트 완료")
print("="*80)
print("\n결론:")
print("✅ 모든 API가 정상 작동하고 있습니다.")
print("✅ 약재 드롭다운이 정상적으로 로드될 것으로 예상됩니다.")
print("\n웹 브라우저에서 확인:")
print("1. 조제 탭으로 이동")
print("2. 처방 선택: 십전대보탕")
print("3. '약재 추가' 버튼 클릭")
print("4. 드롭다운에 약재 목록이 나타나는지 확인")

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API 함수 직접 테스트
"""
import os
import sqlite3
# Flask 앱과 동일한 설정
DATABASE = 'database/kdrug.db'
def get_inventory_summary():
"""app.py의 get_inventory_summary 함수와 동일"""
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT
h.herb_item_id,
h.insurance_code,
h.herb_name,
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
COUNT(DISTINCT il.lot_id) as lot_count,
COUNT(DISTINCT il.origin_country) as origin_count,
AVG(il.unit_price_per_g) as avg_price,
MIN(il.unit_price_per_g) as min_price,
MAX(il.unit_price_per_g) as max_price,
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value,
GROUP_CONCAT(DISTINCT et.tag_name) as efficacy_tags
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name
HAVING total_quantity > 0
ORDER BY h.herb_name
""")
inventory = []
for row in cursor.fetchall():
item = dict(row)
if item['efficacy_tags']:
item['efficacy_tags'] = item['efficacy_tags'].split(',')
else:
item['efficacy_tags'] = []
inventory.append(item)
# 전체 요약
total_value = sum(item['total_value'] for item in inventory)
total_items = len(inventory)
print("=" * 60)
print("API 함수 직접 실행 결과")
print("=" * 60)
print()
print(f"총 약재 수: {total_items}")
print(f"총 재고 자산: ₩{total_value:,.0f}")
print()
# 상세 내역
print("약재별 재고 가치 (상위 10개):")
print("-" * 40)
sorted_items = sorted(inventory, key=lambda x: x['total_value'], reverse=True)
for i, item in enumerate(sorted_items[:10], 1):
print(f"{i:2}. {item['herb_name']:15}{item['total_value']:10,.0f}")
conn.close()
return total_value
if __name__ == "__main__":
total = get_inventory_summary()
print()
print("=" * 60)
print(f"최종 결과: ₩{total:,.0f}")
if total == 5875708:
print("⚠️ API와 동일한 값이 나옴!")
else:
print(f"✅ 예상값: ₩1,529,434")
print(f" 차이: ₩{total - 1529434:,.0f}")

View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
신규 약재 추가 드롭다운 버그 테스트
십전대보탕 조제 시 새로운 약재 추가가 안되는 문제 확인
"""
import requests
import json
from datetime import datetime
BASE_URL = "http://localhost:5001"
def test_herb_dropdown_api():
"""약재 목록 API 테스트"""
print("\n" + "="*80)
print("1. 약재 목록 API 테스트")
print("="*80)
# 1. 전체 약재 목록 조회
response = requests.get(f"{BASE_URL}/api/herbs")
print(f"상태 코드: {response.status_code}")
if response.status_code == 200:
herbs = response.json()
print(f"총 약재 수: {len(herbs)}")
# 처음 5개만 출력
print("\n처음 5개 약재:")
for herb in herbs[:5]:
print(f" - ID: {herb.get('herb_item_id')}, 이름: {herb.get('herb_name')}, 코드: {herb.get('insurance_code')}")
else:
print(f"오류: {response.text}")
return response.status_code == 200
def test_formula_ingredients():
"""십전대보탕 처방 구성 테스트"""
print("\n" + "="*80)
print("2. 십전대보탕 처방 구성 조회")
print("="*80)
# 십전대보탕 ID 찾기
response = requests.get(f"{BASE_URL}/api/formulas")
formulas = response.json()
sipjeon_id = None
for formula in formulas:
if '십전대보탕' in formula.get('formula_name', ''):
sipjeon_id = formula['formula_id']
print(f"십전대보탕 ID: {sipjeon_id}")
break
if not sipjeon_id:
print("십전대보탕을 찾을 수 없습니다")
return False
# 처방 구성 조회
response = requests.get(f"{BASE_URL}/api/formulas/{sipjeon_id}/ingredients")
if response.status_code == 200:
ingredients = response.json()
print(f"\n십전대보탕 구성 약재 ({len(ingredients)}개):")
ingredient_codes = []
for ing in ingredients:
print(f" - {ing.get('herb_name')} ({ing.get('ingredient_code')}): {ing.get('grams_per_cheop')}g")
ingredient_codes.append(ing.get('ingredient_code'))
return ingredient_codes
else:
print(f"오류: {response.text}")
return []
def test_available_herbs_for_compound():
"""조제 시 사용 가능한 약재 목록 테스트"""
print("\n" + "="*80)
print("3. 조제용 약재 목록 API 테스트")
print("="*80)
# 재고가 있는 약재만 조회하는 API가 있는지 확인
endpoints = [
"/api/herbs",
"/api/herbs/available",
"/api/herbs-with-inventory"
]
for endpoint in endpoints:
print(f"\n테스트: {endpoint}")
try:
response = requests.get(f"{BASE_URL}{endpoint}")
if response.status_code == 200:
herbs = response.json()
print(f" ✓ 성공 - {len(herbs)}개 약재")
# 재고 정보 확인
if herbs and len(herbs) > 0:
sample = herbs[0]
print(f" 샘플 데이터: {sample}")
if 'quantity_onhand' in sample or 'total_quantity' in sample:
print(" → 재고 정보 포함됨")
else:
print(f" ✗ 실패 - 상태코드: {response.status_code}")
except Exception as e:
print(f" ✗ 오류: {e}")
def check_frontend_code():
"""프론트엔드 코드에서 약재 추가 부분 확인"""
print("\n" + "="*80)
print("4. 프론트엔드 코드 분석")
print("="*80)
print("""
app.js의 약재 추가 관련 주요 함수:
1. loadHerbOptions() - 약재 드롭다운 로드
2. addIngredientRow() - 약재 행 추가
3. loadOriginOptions() - 원산지 옵션 로드
문제 가능성:
- loadHerbOptions() 함수가 제대로 호출되지 않음
- API 엔드포인트가 잘못됨
- 드롭다운 element 선택자 오류
- 이벤트 바인딩 문제
""")
def test_with_playwright():
"""Playwright로 실제 UI 테스트"""
print("\n" + "="*80)
print("5. Playwright UI 테스트 스크립트 생성")
print("="*80)
test_code = '''from playwright.sync_api import sync_playwright
import time
def test_herb_dropdown():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
# 1. 조제 페이지로 이동
page.goto("http://localhost:5001")
page.click('a[href="#compound"]')
time.sleep(1)
# 2. 십전대보탕 선택
page.select_option('#compoundFormula', label='십전대보탕')
time.sleep(1)
# 3. 새 약재 추가 버튼 클릭
page.click('#addIngredientBtn')
time.sleep(1)
# 4. 드롭다운 확인
dropdown = page.locator('.herb-select').last
options = dropdown.locator('option').all_text_contents()
print(f"드롭다운 옵션 수: {len(options)}")
print(f"처음 5개: {options[:5]}")
browser.close()
if __name__ == "__main__":
test_herb_dropdown()
'''
print("Playwright 테스트 코드를 test_ui_dropdown.py 파일로 저장합니다.")
with open('/root/kdrug/test_ui_dropdown.py', 'w') as f:
f.write(test_code)
return True
def main():
"""메인 테스트 실행"""
print("\n" + "="*80)
print("신규 약재 추가 드롭다운 버그 테스트")
print("="*80)
# 1. API 테스트
if not test_herb_dropdown_api():
print("\n❌ 약재 목록 API에 문제가 있습니다")
return
# 2. 처방 구성 테스트
ingredient_codes = test_formula_ingredients()
# 3. 조제용 약재 테스트
test_available_herbs_for_compound()
# 4. 프론트엔드 코드 분석
check_frontend_code()
# 5. Playwright 테스트 생성
test_with_playwright()
print("\n" + "="*80)
print("테스트 완료 - app.js 파일을 확인하여 문제를 찾아보겠습니다")
print("="*80)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""약재 정보 페이지 테스트 - 렌더링 문제 수정 후 검증"""
import requests
import json
import re
from datetime import datetime
BASE_URL = "http://localhost:5001"
def test_html_structure():
"""HTML 구조 검증 - herb-info가 content-area 안에 있는지 확인"""
print("1. HTML 구조 검증...")
response = requests.get(f"{BASE_URL}/")
if response.status_code != 200:
print(f" FAIL: 페이지 로드 실패 {response.status_code}")
return False
content = response.text
# herb-info가 col-md-10 content-area 안에 있는지 확인
idx_content = content.find('col-md-10 content-area')
idx_herb_info = content.find('id="herb-info"')
if idx_content < 0:
print(" FAIL: col-md-10 content-area 찾을 수 없음")
return False
if idx_herb_info < 0:
print(" FAIL: herb-info div 찾을 수 없음")
return False
if idx_herb_info > idx_content:
print(" PASS: herb-info가 content-area 안에 올바르게 위치함")
else:
print(" FAIL: herb-info가 content-area 밖에 있음!")
return False
# efficacyFilter ID 중복 검사
count_efficacy = content.count('id="efficacyFilter"')
if count_efficacy > 1:
print(f" FAIL: id=\"efficacyFilter\" 중복 {count_efficacy}개 발견!")
return False
else:
print(f" PASS: id=\"efficacyFilter\" 중복 없음 (개수: {count_efficacy})")
# herbInfoEfficacyFilter 존재 확인
if 'id="herbInfoEfficacyFilter"' in content:
print(" PASS: herbInfoEfficacyFilter ID 정상 존재")
else:
print(" FAIL: herbInfoEfficacyFilter ID 없음!")
return False
return True
def test_efficacy_tags():
"""효능 태그 조회 API 검증"""
print("\n2. 효능 태그 목록 조회...")
response = requests.get(f"{BASE_URL}/api/efficacy-tags")
if response.status_code != 200:
print(f" FAIL: {response.status_code}")
return False
tags = response.json()
if not isinstance(tags, list):
print(f" FAIL: 응답이 리스트가 아님 - {type(tags)}")
return False
print(f" PASS: {len(tags)}개의 효능 태그 조회 성공")
for tag in tags[:3]:
print(f" - {tag.get('name', '')}: {tag.get('description', '')}")
return True
def test_herb_masters_api():
"""약재 마스터 목록 + herb_id 포함 여부 검증"""
print("\n3. 약재 마스터 목록 조회 (herb_id 포함 여부 확인)...")
response = requests.get(f"{BASE_URL}/api/herbs/masters")
if response.status_code != 200:
print(f" FAIL: {response.status_code}")
return False
result = response.json()
if not result.get('success'):
print(f" FAIL: success=False")
return False
herbs = result.get('data', [])
print(f" PASS: {len(herbs)}개의 약재 조회 성공")
if not herbs:
print(" FAIL: 약재 데이터 없음")
return False
first = herbs[0]
# herb_id 확인
if 'herb_id' in first:
print(f" PASS: herb_id 필드 존재 (값: {first['herb_id']})")
else:
print(f" FAIL: herb_id 필드 누락! 키 목록: {list(first.keys())}")
return False
# ingredient_code 확인
if 'ingredient_code' in first:
print(f" PASS: ingredient_code 필드 존재")
else:
print(" FAIL: ingredient_code 필드 누락!")
return False
# efficacy_tags가 리스트인지 확인
if isinstance(first.get('efficacy_tags'), list):
print(f" PASS: efficacy_tags가 리스트 형식")
else:
print(f" FAIL: efficacy_tags 형식 오류: {first.get('efficacy_tags')}")
return False
return True
def test_herb_extended_info():
"""약재 확장 정보 조회 API 검증"""
print("\n4. 약재 확장 정보 조회 (herb_id=1 기준)...")
response = requests.get(f"{BASE_URL}/api/herbs/1/extended")
if response.status_code != 200:
print(f" FAIL: {response.status_code}")
return False
info = response.json()
if not isinstance(info, dict):
print(f" FAIL: 응답이 dict가 아님")
return False
print(f" PASS: herb_id=1 확장 정보 조회 성공")
print(f" - herb_name: {info.get('herb_name', '-')}")
print(f" - name_korean: {info.get('name_korean', '-')}")
print(f" - property: {info.get('property', '-')}")
return True
def test_herb_masters_has_extended_fields():
"""약재 마스터 목록에 확장 정보(property, main_effects)가 포함되는지 검증"""
print("\n5. 약재 마스터에 확장 정보 필드 포함 여부...")
response = requests.get(f"{BASE_URL}/api/herbs/masters")
result = response.json()
herbs = result.get('data', [])
required_fields = ['ingredient_code', 'herb_name', 'herb_id', 'has_stock',
'efficacy_tags', 'property', 'main_effects']
first = herbs[0] if herbs else {}
missing = [f for f in required_fields if f not in first]
if missing:
print(f" FAIL: 누락된 필드: {missing}")
return False
print(f" PASS: 필수 필드 모두 존재: {required_fields}")
return True
def main():
print("=== 약재 정보 페이지 렌더링 수정 검증 테스트 ===")
print(f"시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"서버: {BASE_URL}")
print("-" * 50)
results = []
results.append(("HTML 구조 검증", test_html_structure()))
results.append(("효능 태그 API", test_efficacy_tags()))
results.append(("약재 마스터 API (herb_id)", test_herb_masters_api()))
results.append(("약재 확장 정보 API", test_herb_extended_info()))
results.append(("약재 마스터 필드 완전성", test_herb_masters_has_extended_fields()))
print("\n" + "=" * 50)
success = sum(1 for _, r in results if r)
total = len(results)
print(f"테스트 결과: {success}/{total} 성공")
for name, result in results:
status = "PASS" if result else "FAIL"
print(f" [{status}] {name}")
if success == total:
print("\n모든 테스트 통과. 약재 정보 페이지가 정상적으로 동작해야 합니다.")
else:
print(f"\n{total - success}개 테스트 실패. 추가 수정이 필요합니다.")
return success == total
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Playwright를 사용한 약재 정보 페이지 UI 테스트"""
import asyncio
from playwright.async_api import async_playwright
import time
async def test_herb_info_page():
async with async_playwright() as p:
# 브라우저 시작
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# 콘솔 메시지 캡처
console_messages = []
page.on("console", lambda msg: console_messages.append(f"{msg.type}: {msg.text}"))
# 페이지 에러 캡처
page_errors = []
page.on("pageerror", lambda err: page_errors.append(str(err)))
try:
print("=== Playwright 약재 정보 페이지 테스트 ===\n")
# 1. 메인 페이지 접속
print("1. 메인 페이지 접속...")
await page.goto("http://localhost:5001")
await page.wait_for_load_state("networkidle")
# 2. 약재 정보 메뉴 클릭
print("2. 약재 정보 메뉴 클릭...")
herb_info_link = page.locator('a[data-page="herb-info"]')
is_visible = await herb_info_link.is_visible()
print(f" - 약재 정보 메뉴 표시 여부: {is_visible}")
if is_visible:
await herb_info_link.click()
await page.wait_for_timeout(2000) # 2초 대기
# 3. herb-info 페이지 표시 확인
print("\n3. 약재 정보 페이지 요소 확인...")
herb_info_div = page.locator('#herb-info')
is_herb_info_visible = await herb_info_div.is_visible()
print(f" - herb-info div 표시: {is_herb_info_visible}")
if is_herb_info_visible:
# 검색 섹션 확인
search_section = page.locator('#herb-search-section')
is_search_visible = await search_section.is_visible()
print(f" - 검색 섹션 표시: {is_search_visible}")
# 약재 카드 그리드 확인
herb_grid = page.locator('#herbInfoGrid')
is_grid_visible = await herb_grid.is_visible()
print(f" - 약재 그리드 표시: {is_grid_visible}")
# 약재 카드 개수 확인
await page.wait_for_selector('.herb-info-card', timeout=5000)
herb_cards = await page.locator('.herb-info-card').count()
print(f" - 표시된 약재 카드 수: {herb_cards}")
if herb_cards > 0:
# 첫 번째 약재 카드 정보 확인
first_card = page.locator('.herb-info-card').first
card_title = await first_card.locator('.card-title').text_content()
print(f" - 첫 번째 약재: {card_title}")
# 카드 클릭으로 상세 보기 (카드 전체가 클릭 가능)
print("\n4. 약재 상세 정보 확인...")
# herb-info-card는 클릭 가능한 카드이므로 직접 클릭
if True:
await first_card.click()
await page.wait_for_timeout(1000)
# 상세 모달 확인
modal = page.locator('#herbDetailModal')
is_modal_visible = await modal.is_visible()
print(f" - 상세 모달 표시: {is_modal_visible}")
if is_modal_visible:
modal_title = await modal.locator('.modal-title').text_content()
print(f" - 모달 제목: {modal_title}")
# 모달 닫기
close_btn = modal.locator('button.btn-close')
if await close_btn.is_visible():
await close_btn.click()
await page.wait_for_timeout(500)
# 5. 검색 기능 테스트
print("\n5. 검색 기능 테스트...")
search_input = page.locator('#herbSearchInput')
if await search_input.is_visible():
await search_input.fill("감초")
await page.locator('#herbSearchBtn').click()
await page.wait_for_timeout(1000)
search_result_count = await page.locator('.herb-info-card').count()
print(f" - '감초' 검색 결과: {search_result_count}")
# 6. 효능별 보기 테스트
print("\n6. 효능별 보기 전환...")
efficacy_btn = page.locator('button[data-view="efficacy"]')
if await efficacy_btn.is_visible():
await efficacy_btn.click()
await page.wait_for_timeout(1000)
efficacy_section = page.locator('#herb-efficacy-section')
is_efficacy_visible = await efficacy_section.is_visible()
print(f" - 효능별 섹션 표시: {is_efficacy_visible}")
if is_efficacy_visible:
tag_buttons = await page.locator('.efficacy-tag-btn').count()
print(f" - 효능 태그 버튼 수: {tag_buttons}")
else:
print(" ⚠️ herb-info div가 표시되지 않음!")
# 디버깅: 현재 활성 페이지 확인
active_pages = await page.locator('.main-content.active').count()
print(f" - 활성 페이지 수: {active_pages}")
# 디버깅: herb-info의 display 스타일 확인
herb_info_style = await herb_info_div.get_attribute('style')
print(f" - herb-info style: {herb_info_style}")
# 디버깅: herb-info의 클래스 확인
herb_info_classes = await herb_info_div.get_attribute('class')
print(f" - herb-info classes: {herb_info_classes}")
# 7. 콘솔 에러 확인
print("\n7. 콘솔 메시지 확인...")
if console_messages:
print(" 콘솔 메시지:")
for msg in console_messages[:10]: # 처음 10개만 출력
print(f" - {msg}")
else:
print(" ✓ 콘솔 메시지 없음")
if page_errors:
print(" ⚠️ 페이지 에러:")
for err in page_errors:
print(f" - {err}")
else:
print(" ✓ 페이지 에러 없음")
# 스크린샷 저장
await page.screenshot(path="/root/kdrug/herb_info_page.png")
print("\n스크린샷 저장: /root/kdrug/herb_info_page.png")
except Exception as e:
print(f"\n❌ 테스트 실패: {e}")
# 에러 시 스크린샷
await page.screenshot(path="/root/kdrug/herb_info_error.png")
print("에러 스크린샷 저장: /root/kdrug/herb_info_error.png")
finally:
await browser.close()
if __name__ == "__main__":
print("Playwright 테스트 시작...\n")
asyncio.run(test_herb_info_page())
print("\n테스트 완료!")

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
개선된 Excel 입고 처리 테스트
"""
import sys
sys.path.append('/root/kdrug')
from excel_processor import ExcelProcessor
import pandas as pd
def test_excel_processing():
"""Excel 처리 테스트"""
processor = ExcelProcessor()
# 한의정보 샘플 파일 테스트
print("=== 한의정보 샘플 파일 처리 테스트 ===\n")
if processor.read_excel('sample/한의정보.xlsx'):
print(f"✓ 파일 읽기 성공")
print(f"✓ 형식 감지: {processor.format_type}")
# 처리
df = processor.process()
print(f"✓ 데이터 처리 완료: {len(df)}")
# 보험코드 확인
if 'insurance_code' in df.columns:
print("\n보험코드 샘플 (처리 후):")
for idx, code in enumerate(df['insurance_code'].head(5)):
herb_name = df.iloc[idx]['herb_name']
print(f" {herb_name}: {code} (길이: {len(str(code))})")
print("\n=== 한의사랑 샘플 파일 처리 테스트 ===\n")
processor2 = ExcelProcessor()
if processor2.read_excel('sample/한의사랑.xlsx'):
print(f"✓ 파일 읽기 성공")
print(f"✓ 형식 감지: {processor2.format_type}")
# 처리
df2 = processor2.process()
print(f"✓ 데이터 처리 완료: {len(df2)}")
# 보험코드 확인
if 'insurance_code' in df2.columns:
print("\n보험코드 샘플 (처리 후):")
for idx, code in enumerate(df2['insurance_code'].head(5)):
herb_name = df2.iloc[idx]['herb_name']
print(f" {herb_name}: {code} (길이: {len(str(code))})")
if __name__ == "__main__":
test_excel_processing()

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""JavaScript 디버깅을 위한 Playwright 테스트"""
import asyncio
from playwright.async_api import async_playwright
async def debug_herb_info():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# 콘솔 메시지 캡처
console_messages = []
page.on("console", lambda msg: console_messages.append({
"type": msg.type,
"text": msg.text,
"args": msg.args
}))
# 네트워크 요청 캡처
network_requests = []
page.on("request", lambda req: network_requests.append({
"url": req.url,
"method": req.method
}))
# 네트워크 응답 캡처
network_responses = []
async def log_response(response):
if "/api/" in response.url:
try:
body = await response.text()
network_responses.append({
"url": response.url,
"status": response.status,
"body": body[:200] if body else None
})
except:
pass
page.on("response", log_response)
try:
# 페이지 접속
print("페이지 접속 중...")
await page.goto("http://localhost:5001")
await page.wait_for_load_state("networkidle")
# JavaScript 실행하여 직접 함수 호출
print("\n직접 JavaScript 함수 테스트...")
# loadHerbInfo 함수 존재 확인
has_function = await page.evaluate("typeof loadHerbInfo === 'function'")
print(f"1. loadHerbInfo 함수 존재: {has_function}")
# loadAllHerbsInfo 함수 존재 확인
has_all_herbs = await page.evaluate("typeof loadAllHerbsInfo === 'function'")
print(f"2. loadAllHerbsInfo 함수 존재: {has_all_herbs}")
# displayHerbCards 함수 존재 확인
has_display = await page.evaluate("typeof displayHerbCards === 'function'")
print(f"3. displayHerbCards 함수 존재: {has_display}")
# 약재 정보 페이지로 이동
await page.click('a[data-page="herb-info"]')
await page.wait_for_timeout(2000)
# herbInfoGrid 요소 확인
grid_exists = await page.evaluate("document.getElementById('herbInfoGrid') !== null")
print(f"4. herbInfoGrid 요소 존재: {grid_exists}")
# herbInfoGrid 내용 확인
grid_html = await page.evaluate("document.getElementById('herbInfoGrid')?.innerHTML || 'EMPTY'")
print(f"5. herbInfoGrid 내용 길이: {len(grid_html)} 문자")
if grid_html and grid_html != 'EMPTY':
print(f" 처음 100자: {grid_html[:100]}...")
# API 호출 직접 테스트
print("\n\nAPI 응답 직접 테스트...")
api_response = await page.evaluate("""
fetch('/api/herbs/masters')
.then(res => res.json())
.then(data => ({
success: data.success,
dataLength: data.data ? data.data.length : 0,
firstItem: data.data ? data.data[0] : null
}))
.catch(err => ({ error: err.toString() }))
""")
print(f"API 응답: {api_response}")
# displayHerbCards 직접 호출 테스트
if api_response.get('dataLength', 0) > 0:
print("\n\ndisplayHerbCards 직접 호출...")
await page.evaluate("""
fetch('/api/herbs/masters')
.then(res => res.json())
.then(data => {
if (typeof displayHerbCards === 'function') {
displayHerbCards(data.data);
} else {
console.error('displayHerbCards 함수가 없습니다');
}
})
""")
await page.wait_for_timeout(1000)
# 다시 확인
grid_html_after = await page.evaluate("document.getElementById('herbInfoGrid')?.innerHTML || 'EMPTY'")
print(f"displayHerbCards 호출 후 내용 길이: {len(grid_html_after)} 문자")
card_count = await page.evaluate("document.querySelectorAll('.herb-card').length")
print(f"herb-card 요소 개수: {card_count}")
# 콘솔 메시지 출력
print("\n\n=== 콘솔 메시지 ===")
for msg in console_messages:
if 'error' in msg['type'].lower():
print(f"{msg['type']}: {msg['text']}")
else:
print(f"📝 {msg['type']}: {msg['text']}")
# API 응답 상태 확인
print("\n\n=== API 응답 ===")
for resp in network_responses:
if '/api/herbs/masters' in resp['url']:
print(f"URL: {resp['url']}")
print(f"상태: {resp['status']}")
print(f"응답: {resp['body'][:100] if resp['body'] else 'No body'}")
finally:
await browser.close()
if __name__ == "__main__":
asyncio.run(debug_herb_info())

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
로트 배분 검증 테스트 - 재고 부족 및 잘못된 배분 테스트
"""
import json
import requests
BASE_URL = "http://localhost:5001"
def test_insufficient_stock():
print("=== 로트 배분 검증 테스트 ===\n")
# 1. 배분 합계가 맞지 않는 경우
print("1. 배분 합계가 필요량과 맞지 않는 경우")
compound_data = {
"patient_id": 1,
"formula_id": None,
"je_count": 1,
"cheop_total": 1,
"pouch_total": 1,
"ingredients": [
{
"herb_item_id": 63,
"grams_per_cheop": 100.0,
"total_grams": 100.0,
"origin": "manual",
"lot_assignments": [
{"lot_id": 208, "quantity": 50.0}, # 50g
{"lot_id": 219, "quantity": 30.0} # 30g = 총 80g (100g 필요)
]
}
]
}
response = requests.post(f"{BASE_URL}/api/compounds", json=compound_data, headers={"Content-Type": "application/json"})
if response.status_code != 200:
result = response.json()
print(f" ✅ 예상된 오류 발생: {result.get('error')}")
else:
print(f" ❌ 오류가 발생해야 하는데 성공함")
# 2. 로트 재고가 부족한 경우
print("\n2. 로트 재고가 부족한 경우")
compound_data = {
"patient_id": 1,
"formula_id": None,
"je_count": 1,
"cheop_total": 1,
"pouch_total": 1,
"ingredients": [
{
"herb_item_id": 63,
"grams_per_cheop": 5000.0, # 5000g 요청
"total_grams": 5000.0,
"origin": "manual",
"lot_assignments": [
{"lot_id": 208, "quantity": 5000.0} # 로트 208에 5000g 요청 (실제로는 4784g만 있음)
]
}
]
}
response = requests.post(f"{BASE_URL}/api/compounds", json=compound_data, headers={"Content-Type": "application/json"})
if response.status_code != 200:
result = response.json()
print(f" ✅ 예상된 오류 발생: {result.get('error')}")
else:
print(f" ❌ 오류가 발생해야 하는데 성공함")
# 3. 존재하지 않는 로트
print("\n3. 존재하지 않는 로트 ID 사용")
compound_data = {
"patient_id": 1,
"formula_id": None,
"je_count": 1,
"cheop_total": 1,
"pouch_total": 1,
"ingredients": [
{
"herb_item_id": 63,
"grams_per_cheop": 10.0,
"total_grams": 10.0,
"origin": "manual",
"lot_assignments": [
{"lot_id": 99999, "quantity": 10.0} # 존재하지 않는 로트
]
}
]
}
response = requests.post(f"{BASE_URL}/api/compounds", json=compound_data, headers={"Content-Type": "application/json"})
if response.status_code != 200:
result = response.json()
print(f" ✅ 예상된 오류 발생: {result.get('error')}")
else:
print(f" ❌ 오류가 발생해야 하는데 성공함")
print("\n✅ 모든 검증 테스트 완료 - 잘못된 요청을 올바르게 거부함")
if __name__ == "__main__":
test_insufficient_stock()

View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
복합 로트 사용 E2E 테스트
- 당귀 2개 로트를 수동 배분하여 커스텀 조제 테스트
"""
import json
import requests
from datetime import datetime
BASE_URL = "http://localhost:5001"
def test_multi_lot_compound():
print("=== 복합 로트 사용 E2E 테스트 시작 ===\n")
# 1. 당귀 재고 현황 확인
print("1. 당귀(휴먼일당귀) 재고 현황 확인")
response = requests.get(f"{BASE_URL}/api/herbs/63/available-lots")
if response.status_code == 200:
data = response.json()['data']
print(f" - 약재명: {data['herb_name']}")
print(f" - 총 재고: {data['total_quantity']}g")
for origin in data['origins']:
print(f"\n [{origin['origin_country']}] 로트 {origin['lot_count']}개, 총 {origin['total_quantity']}g")
for lot in origin['lots']:
print(f" - 로트 #{lot['lot_id']}: {lot['quantity_onhand']}g @ {lot['unit_price_per_g']}원/g")
else:
print(f" ❌ 오류: {response.status_code}")
return
# 2. 커스텀 조제 생성 (당귀 100g 필요)
print("\n2. 커스텀 조제 생성 - 당귀 100g를 2개 로트로 수동 배분")
compound_data = {
"patient_id": 1, # 테스트 환자
"formula_id": None, # 커스텀 조제
"je_count": 1,
"cheop_total": 1,
"pouch_total": 1,
"ingredients": [
{
"herb_item_id": 63, # 휴먼일당귀
"grams_per_cheop": 100.0,
"total_grams": 100.0, # total_grams 추가
"origin": "manual", # 수동 배분
"lot_assignments": [
{"lot_id": 208, "quantity": 60.0}, # 중국산 60g
{"lot_id": 219, "quantity": 40.0} # 한국산 40g
]
}
]
}
print(" - 로트 배분:")
print(" * 로트 #208 (중국산): 60g")
print(" * 로트 #219 (한국산): 40g")
response = requests.post(
f"{BASE_URL}/api/compounds",
json=compound_data,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
result = response.json()
if result.get('success'):
compound_id = result.get('compound_id')
total_cost = result.get('total_cost')
print(f"\n ✅ 조제 성공!")
print(f" - 조제 ID: {compound_id}")
print(f" - 총 원가: {total_cost}")
# 3. 조제 상세 확인
print("\n3. 조제 상세 정보 확인")
response = requests.get(f"{BASE_URL}/api/compounds/{compound_id}")
if response.status_code == 200:
detail = response.json()['data']
print(" - 소비 내역:")
for con in detail.get('consumptions', []):
print(f" * 로트 #{con['lot_id']}: {con['quantity_used']}g @ {con['unit_cost_per_g']}원/g = {con['cost_amount']}")
# 4. 재고 변동 확인
print("\n4. 재고 변동 확인")
response = requests.get(f"{BASE_URL}/api/herbs/63/available-lots")
if response.status_code == 200:
after_data = response.json()['data']
print(" - 조제 후 재고:")
for origin in after_data['origins']:
for lot in origin['lots']:
if lot['lot_id'] in [208, 219]:
print(f" * 로트 #{lot['lot_id']} ({origin['origin_country']}): {lot['quantity_onhand']}g")
print("\n✅ 복합 로트 사용 테스트 성공!")
print(" - 2개의 로트를 수동으로 배분하여 조제")
print(" - 각 로트별 재고가 정확히 차감됨")
print(" - 소비 내역이 올바르게 기록됨")
else:
print(f" ❌ 상세 조회 실패: {response.status_code}")
else:
print(f" ❌ 조제 실패: {result.get('error')}")
else:
print(f" ❌ API 호출 실패: {response.status_code}")
print(f" 응답: {response.text}")
if __name__ == "__main__":
try:
test_multi_lot_compound()
except Exception as e:
print(f"\n❌ 테스트 중 오류 발생: {e}")

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API를 통한 Excel 입고 처리 테스트
"""
import requests
import json
# API 베이스 URL
BASE_URL = "http://localhost:5001"
def test_upload_excel():
"""Excel 업로드 테스트"""
# 1. 도매상 목록 확인
print("=== 도매상 목록 확인 ===")
response = requests.get(f"{BASE_URL}/api/suppliers")
suppliers = response.json()
if suppliers['success'] and suppliers['data']:
print(f"✓ 도매상 {len(suppliers['data'])}개 조회")
supplier_id = suppliers['data'][0]['supplier_id']
supplier_name = suppliers['data'][0]['name']
print(f"✓ 선택된 도매상: {supplier_name} (ID: {supplier_id})")
else:
print("도매상이 없습니다. 새로 생성합니다.")
# 도매상 생성
supplier_data = {
'name': '한의정보',
'business_no': '123-45-67890',
'contact_person': '담당자',
'phone': '02-1234-5678'
}
response = requests.post(f"{BASE_URL}/api/suppliers", json=supplier_data)
result = response.json()
if result['success']:
supplier_id = result['supplier_id']
print(f"✓ 도매상 생성 완료 (ID: {supplier_id})")
else:
print(f"✗ 도매상 생성 실패: {result.get('error')}")
return
# 2. Excel 파일 업로드
print("\n=== Excel 파일 업로드 ===")
# 파일 열기
file_path = 'sample/한의정보.xlsx'
with open(file_path, 'rb') as f:
files = {'file': ('한의정보.xlsx', f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
data = {'supplier_id': supplier_id}
# 업로드
response = requests.post(f"{BASE_URL}/api/upload/purchase", files=files, data=data)
# 결과 확인
result = response.json()
if result['success']:
print(f"✓ 업로드 성공!")
print(f" - 형식: {result['summary']['format']}")
print(f" - 처리된 행: {result['summary']['processed_rows']}")
if 'processed_items' in result['summary']:
print(f" - 처리된 품목: {result['summary']['processed_items']}")
if 'total_amount' in result['summary']:
total = result['summary']['total_amount']
if isinstance(total, (int, float)):
print(f" - 총액: {total:,.0f}")
else:
print(f" - 총액: {total}")
else:
print(f"✗ 업로드 실패: {result.get('error')}")
# 3. 입고된 herb_items 확인
print("\n=== 입고된 herb_items 확인 ===")
response = requests.get(f"{BASE_URL}/api/herbs")
herbs = response.json()
if herbs['success']:
print(f"✓ 총 {len(herbs['data'])}개 herb_items")
# 샘플 출력
for herb in herbs['data'][:5]:
print(f" - {herb['herb_name']}: 보험코드={herb.get('insurance_code', 'N/A')}, 재고={herb.get('stock_quantity', 0):,.0f}g")
# 4. 재고 현황 확인
print("\n=== 재고 현황 확인 ===")
response = requests.get(f"{BASE_URL}/api/inventory/summary")
inventory = response.json()
if inventory['success']:
summary = inventory['data']
print(f"✓ 재고 요약:")
print(f" - 총 품목: {summary['total_items']}")
print(f" - 재고 있는 품목: {summary['items_with_stock']}")
print(f" - 총 재고 가치: {summary['total_value']:,.0f}")
if __name__ == "__main__":
test_upload_excel()

157
direct_mapping.py Normal file
View File

@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
휴먼허브 약재와 한의사랑 제품명 직접 매핑
"""
import sqlite3
def get_connection():
"""데이터베이스 연결"""
return sqlite3.connect('database/kdrug.db')
def create_direct_mapping():
"""약재명 기준으로 직접 매핑"""
# 매핑 테이블 (약재명 순서대로)
mapping = {
'신흥숙지황': '숙지황(9증)(신흥.1kg)[완]',
'휴먼갈근': '갈근.각',
'휴먼감초': '감초.1호[야생](1kg)',
'휴먼건강': { # 가격으로 구분
12.4: '건강', # 페루산 저가
51.4: '건강.土' # 한국산 고가
},
'휴먼계지': '계지',
'휴먼구기자': '구기자(영하)(1kg)',
'휴먼길경': '길경.片[특]',
'휴먼대추': '대추(절편)(1kg)',
'휴먼마황': '마황(1kg)',
'휴먼반하생강백반제': '반하생강백반제(1kg)',
'휴먼백출': '백출.당[1kg]',
'휴먼복령': '복령(1kg)',
'휴먼석고': '석고[통포장](kg)',
'휴먼세신': '세신.中',
'휴먼오미자': '오미자<토매지>(1kg)',
'휴먼용안육': '용안육.名品(1kg)',
'휴먼육계': '육계.YB',
'휴먼일당귀': '일당귀.中(1kg)',
'휴먼자소엽': '자소엽.土',
'휴먼작약': '작약(1kg)',
'휴먼작약주자': '작약주자.土[酒炙]',
'휴먼전호': '전호[재배]',
'휴먼지각': '지각',
'휴먼지황': '지황.건[회](1kg)',
'휴먼진피(陳皮)': '진피.비열[非熱](1kg)',
'휴먼창출': '창출[북창출.재배](1kg)',
'휴먼천궁': '천궁.일<토매지>(1kg)',
'휴먼황기': '황기(직절.小)(1kg)'
}
return mapping
def apply_mapping():
"""매핑 적용"""
conn = get_connection()
cursor = conn.cursor()
print("\n" + "="*80)
print("휴먼허브 → 한의사랑 제품명 직접 매핑")
print("="*80)
mapping = create_direct_mapping()
# 모든 inventory_lots 조회
cursor.execute("""
SELECT
l.lot_id,
h.herb_name,
l.unit_price_per_g,
l.origin_country
FROM inventory_lots l
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
ORDER BY h.herb_name
""")
lots = cursor.fetchall()
success_count = 0
for lot in lots:
lot_id, herb_name, unit_price, origin = lot
display_name = None
# 매핑 찾기
if herb_name in mapping:
mapped = mapping[herb_name]
# 건강처럼 가격으로 구분하는 경우
if isinstance(mapped, dict):
# 가장 가까운 가격 찾기
closest_price = min(mapped.keys(), key=lambda x: abs(x - unit_price))
if abs(closest_price - unit_price) < 5: # 5원 이내 차이만 허용
display_name = mapped[closest_price]
else:
display_name = mapped
if display_name:
# display_name 업데이트
cursor.execute("""
UPDATE inventory_lots
SET display_name = ?
WHERE lot_id = ?
""", (display_name, lot_id))
# lot_variants 추가/업데이트
try:
cursor.execute("""
INSERT INTO lot_variants
(lot_id, raw_name, parsed_at, parsed_method)
VALUES (?, ?, datetime('now'), 'direct_mapping')
""", (lot_id, display_name))
except sqlite3.IntegrityError:
cursor.execute("""
UPDATE lot_variants
SET raw_name = ?, parsed_at = datetime('now'), parsed_method = 'direct_mapping'
WHERE lot_id = ?
""", (display_name, lot_id))
print(f"✓ Lot #{lot_id:3d}: {herb_name:20s}{display_name}")
success_count += 1
else:
print(f"✗ Lot #{lot_id:3d}: {herb_name:20s} - 매핑 실패")
conn.commit()
print("\n" + "="*80)
print(f"매핑 완료: {success_count}/{len(lots)}")
print("="*80)
# 결과 확인
cursor.execute("""
SELECT
h.herb_name,
l.display_name,
l.unit_price_per_g,
l.origin_country
FROM inventory_lots l
JOIN herb_items h ON l.herb_item_id = h.herb_item_id
WHERE l.display_name IS NOT NULL
ORDER BY h.herb_name
""")
results = cursor.fetchall()
print("\n설정된 Display Names:")
print("-" * 80)
for res in results:
print(f"{res[0]:20s}{res[1]:30s} ({res[2]:.1f}원/g, {res[3]})")
conn.close()
def main():
"""메인 실행"""
apply_mapping()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
현재 스키마 흐름
herb_masters (성분코드 마스터)
ingredient_code: '3400H1AHM' → 인삼
├──── official_formula_ingredients (100처방 원방 구성)
│ ingredient_code + grams_per_cheop
│ → "가미패독산 원방: 인삼 3g/첩"
└──── formula_ingredients (운영 처방 구성)
ingredient_code + grams_per_cheop
→ "내 쌍화탕: 인삼 4g/첩 (가감)"
둘 다 ingredient_code 기반으로 통일되어 있어서:
- 100처방 마스터에 원방 구성을 채워두면
- 운영 처방 만들 때 원방을 복사 → 가감하는 워크플로우가 가능합니다
- 특정 제품(휴먼인삼 vs 한퓨어인삼)에 종속되지 않고, 조제 시점에 재고 기반으로 제품이 자동 선택됩니다
1. 100처방 목록에서 가미패독산 클릭 → 상세 모달
2. "내 처방으로 등록" 버튼 클릭
3. 처방 등록 모달이 열리면서 자동 프리필:
- 처방명: 어울림 가미패독산
- 구성 약재 13종 + 첩당 용량 전부 채워짐
- 각 약재의 역할(notes)도 포함
4. 여기서 자유롭게 약재 추가/제거/용량 변경 후 저장
원방을 기반으로 가감방을 만드는 워크플로우가 완성된 겁니다.
app.py - create_formula 함수:
1. formula_ingredients INSERT 컬럼을 herb_item_id → ingredient_code로 변경 (line 572)
2. official_formula_id 저장 추가 — 100처방 기반으로 생성한 처방은 원방 연결 추적 가능
static/app.js:
1. 100처방에서 "내 처방으로 등록" 시 official_formula_id를 모달 데이터에 저장
2. 처방 저장 시 official_formula_id를 API로 전달
3. 저장 완료 후 official-formula-id 데이터 초기화
이제 가미패독산 등 100처방에서 "내 처방으로 등록" → 저장이 정상 동작할 것입니다. 재고가 없는 약재도 ingredient_code 기반으로 저장되므로 문제없습니다.
1. 내 처방 목록 가감 표시 기능
처방명 "어울림" 스타일링:
- "어울림 가미패독산" → 어울림(초록색 볼드) + 가미패독산
가감 뱃지 (100처방 원방 대비 변경사항):
- 용량 변경: 생강 3g→4g — 파란색 뱃지
- 약재 추가: +건강 2g — 초록색 뱃지
- 약재 제거: -생강 — 빨간색 뱃지
- 원방 그대로인 경우: 원방 그대로 — 회색 뱃지
조제 시 가감 감지 코드(checkCustomPrescription)와 동일한 패턴을 처방 목록에도 적용했습니다.

View File

@@ -0,0 +1,393 @@
# 100처방 마스터 데이터 등록 가이드
> AI Agent가 `official_formulas` / `official_formula_ingredients` 테이블을 채울 때 따라야 할 절차와 규칙.
---
## 1. DB 구조
### 1-1. `official_formulas` (처방 기본 정보)
| 컬럼 | 타입 | 설명 | 채워야 하는 값 |
|------|------|------|---------------|
| `official_formula_id` | INTEGER PK | 자동생성 | (이미 존재) |
| `formula_number` | INTEGER | 연번 1~100 | (이미 존재) |
| `formula_name` | TEXT | 처방명 (한글) | (이미 존재) |
| `formula_name_hanja` | TEXT | **한자명** | ✅ 채워야 함 |
| `source_text` | TEXT | 출전 | (이미 존재) |
| `description` | TEXT | **효능 요약** | ✅ 채워야 함 |
| `reference_notes` | TEXT | **상담 참고자료** | ✅ 채워야 함 |
### 1-2. `official_formula_ingredients` (구성 약재)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `ingredient_id` | INTEGER PK | 자동생성 |
| `official_formula_id` | INTEGER FK | 처방 ID |
| `ingredient_code` | TEXT | **herb_masters.ingredient_code** (예: `3400H1AHM`) |
| `grams_per_cheop` | REAL | **1첩당 그램수** |
| `notes` | TEXT | **역할** (군약/신약/좌약/사약 등) |
| `sort_order` | INTEGER | 정렬 순서 (1부터) |
---
## 2. 등록 절차 (Step-by-Step)
### Step 1: 대상 처방 확인
```sql
-- 미등록 처방 확인 (formula_name_hanja가 NULL이면 미등록)
SELECT official_formula_id, formula_number, formula_name, source_text
FROM official_formulas
WHERE formula_name_hanja IS NULL
ORDER BY formula_number;
```
### Step 2: 원방 조사
출전 서적 기반으로 아래 정보를 조사한다:
1. **한자명** — 예: 四君子湯
2. **효능 요약** — 한의학 용어 + 한자 병기. 예: `보기건비 (補氣健脾)`
3. **구성 약재** — 약재명, 1첩당 그램수, 군신좌사 역할
4. **상담 참고자료** — 출전, 주치, OTC 대비 장점 (1~2문장)
#### 용량 기준 원칙
- **출전(원방)의 현대 환산 용량**을 우선 적용
- 1전(錢) = 3.75g, 1냥(兩) = 37.5g 기준 환산
- 감초는 대부분 사약으로 2~3g (다른 약재보다 적은 것이 일반적)
- 동량(等分) 처방이라도 현대 임상 표준이 차등이면 표준을 따름
#### description 작성 규칙
```
한글효능 (한자효능)
```
예시:
- `발한해기, 생진서근 (發汗解肌, 生津舒筋)`
- `온보기혈 (溫補氣血)`
- `익기해표, 이기화담 (益氣解表, 理氣化痰)`
#### reference_notes 작성 규칙
1문장: 출전 + 주치 증상
나머지: OTC 대비 차별점 또는 첩제만의 장점
```
예: "상한론 원방. 외감풍한으로 인한 두통, 발열, 오한, 항강 증상에 사용. OTC 갈근탕 대비 생약량 2.16배, 생강→건강 대체 없이 원물 사용으로 발한 효과 우수."
```
#### notes(역할) 작성 규칙
| 값 | 의미 |
|----|------|
| `군약` | 주약 (主藥) — 처방의 핵심 |
| `신약` | 보조약 (臣藥) — 군약을 도움 |
| `좌약` | 보좌약 (佐藥) — 부작용 완화 또는 보조 |
| `사약` | 조화약 (使藥) — 제약 조화 (대부분 감초) |
복합 역할인 경우 괄호로 추가 설명:
- `군약(보기)` — 보기 역할의 군약
- `신약(보혈)` — 보혈 역할의 신약
### Step 3: ingredient_code 조회
```sql
-- 약재명으로 성분코드 조회 (정확 매칭 우선)
SELECT ingredient_code, herb_name
FROM herb_masters
WHERE herb_name = '인삼';
-- 정확 매칭 없으면 LIKE 검색
SELECT ingredient_code, herb_name
FROM herb_masters
WHERE herb_name LIKE '%복령%';
```
#### 주의: 동명이약 구분
| 약재 | 올바른 코드 | 주의 |
|------|------------|------|
| 진피(陳皮) | `3466H1AHM` | 진피(秦皮) `3467H1AHM`과 구분 |
| 백출 | `3204H1AHM` | 백출초 `3611H1AHM`, 백출미감침 `3610H1AHM`과 구분 |
| 감초 | `3007H1AHM` | 감초초 `3010H1AHM`, 감초밀자 `3009H1AHM`과 구분 |
| 반하 | `3182H1AHM` | 반하생강백반제 등 포제품과 구분 |
| 마황 | `3147H1AHM` | 마황탕포 `3606H1AHM`과 구분 |
### Step 4: SQL 실행
```sql
-- (1) 기본 정보 업데이트
UPDATE official_formulas SET
formula_name_hanja = '四君子湯',
description = '보기건비 (補氣健脾)',
reference_notes = '화제국방 원방. 비기허로 인한 ...',
updated_at = CURRENT_TIMESTAMP
WHERE official_formula_id = 38;
-- (2) 구성 약재 INSERT (sort_order는 1부터 순서대로)
INSERT INTO official_formula_ingredients
(official_formula_id, ingredient_code, grams_per_cheop, notes, sort_order)
VALUES
(38, '3400H1AHM', 4.0, '군약', 1), -- 인삼
(38, '3204H1AHM', 4.0, '신약', 2), -- 백출
(38, '3215H1AHM', 4.0, '좌약', 3), -- 복령
(38, '3007H1AHM', 2.0, '사약', 4); -- 감초
```
### Step 5: 검증
```sql
-- 등록 결과 확인
SELECT hm.herb_name, ofi.ingredient_code, ofi.grams_per_cheop, ofi.notes
FROM official_formula_ingredients ofi
JOIN herb_masters hm ON ofi.ingredient_code = hm.ingredient_code
WHERE ofi.official_formula_id = ?
ORDER BY ofi.sort_order;
```
검증 체크리스트:
- [ ] herb_masters에 없는 ingredient_code가 없는지
- [ ] grams_per_cheop이 0 이하인 것이 없는지
- [ ] 중복 ingredient_code가 없는지
- [ ] 1첩 총량이 상식적 범위(10~50g)인지
- [ ] 군약이 최소 1개 이상인지
---
## 3. 등록 완료 예시
### 사군자탕 (四君子湯) — ID:38
| 순서 | 약재 | 성분코드 | 1첩량 | 역할 |
|------|------|----------|-------|------|
| 1 | 인삼 | 3400H1AHM | 4.0g | 군약 |
| 2 | 백출 | 3204H1AHM | 4.0g | 신약 |
| 3 | 복령 | 3215H1AHM | 4.0g | 좌약 |
| 4 | 감초 | 3007H1AHM | 2.0g | 사약 |
- 한자명: 四君子湯
- 설명: `보기건비 (補氣健脾)`
- 1첩 총량: 14.0g
### 갈근탕 (葛根湯) — ID:3
| 순서 | 약재 | 성분코드 | 1첩량 | 역할 |
|------|------|----------|-------|------|
| 1 | 갈근 | 3002H1AHM | 8.0g | 군약 |
| 2 | 마황 | 3147H1AHM | 4.0g | 신약 |
| 3 | 계지 | 3033H1AHM | 3.0g | 좌약 |
| 4 | 작약 | 3419H1AHM | 3.0g | 좌약 |
| 5 | 생강 | 3260H1AHM | 3.0g | 좌약 |
| 6 | 대추 | 3115H1AHM | 4.0g | 좌약 |
| 7 | 감초 | 3007H1AHM | 2.0g | 사약 |
- 한자명: 葛根湯
- 설명: `발한해기, 생진서근 (發汗解肌, 生津舒筋)`
- 1첩 총량: 27.0g
---
## 4. 자주 사용되는 성분코드 (Quick Reference)
| 약재 | ingredient_code | 비고 |
|------|----------------|------|
| 감초 | 3007H1AHM | 사약 역할 빈출 |
| 갈근 | 3002H1AHM | |
| 건강 | 3017H1AHM | 생강 포제품 |
| 계지 | 3033H1AHM | |
| 길경 | 3077H1AHM | |
| 당귀 | 3105H1AHM | |
| 대추 | 3115H1AHM | |
| 마황 | 3147H1AHM | |
| 반하 | 3182H1AHM | |
| 백출 | 3204H1AHM | |
| 복령 | 3215H1AHM | |
| 생강 | 3260H1AHM | |
| 석고 | 3265H1AHM | |
| 세신 | 3285H1AHM | |
| 숙지황 | 3299H1AHM | |
| 오미자 | 3342H1AHM | |
| 인삼 | 3400H1AHM | |
| 자소엽 | 3411H1AHM | |
| 작약 | 3419H1AHM | |
| 전호 | 3433H1AHM | |
| 지각 | 3454H1AHM | |
| 진피(陳皮) | 3466H1AHM | ⚠️ 秦皮와 구분 |
| 천궁 | 3475H1AHM | |
| 황기 | 3583H1AHM | |
| 육계 | 3384H1AHM | |
---
## 5. 미등록 처방 목록 (93개)
| # | 처방명 | 출전 |
|---|--------|------|
| 1 | 가미온담탕 | 의종금감 |
| 4 | 강활유풍탕 | 의학발명 |
| 5 | 계지가용골모려탕 | 금궤요략 |
| 6 | 계지작약지모탕 | 금궤요략 |
| 7 | 곽향정기산 | 화제국방 |
| 8 | 구미강활탕 | 차사난지 |
| 9 | 궁귀교애탕 | 금궤요략 |
| 10 | 귀비탕 | 제생방 |
| 11 | 귀출파징탕 | 동의보감 |
| 12 | 금수육군전 | 경악전서 |
| 13 | 녹용대보탕 | 갑병원류서촉 |
| 14 | 당귀사역가오수유생강탕 | 상한론 |
| 15 | 당귀수산 | 의학입문 |
| 16 | 당귀육황탕 | 난실비장 |
| 17 | 당귀작약산 | 금궤요략 |
| 18 | 대강활탕 | 위생보감 |
| 19 | 대건중탕 | 금궤요략 |
| 20 | 대금음자 | 화제국방 |
| 21 | 대방풍탕 | 화제국방 |
| 22 | 대청룡탕 | 상한론 |
| 23 | 대황목단피탕 | 금궤요략 |
| 24 | 독활기생탕 | 천금방 |
| 25 | 마행의감탕 | 금궤요략 |
| 26 | 마황부자세신탕 | 상한론 |
| 27 | 반하백출천마탕 | 의학심오 |
| 28 | 반하사심탕 | 상한론 |
| 29 | 반하후박탕 | 금궤요략 |
| 30 | 방기황기탕 | 금궤요략 |
| 31 | 방풍통성산 | 선명논방 |
| 32 | 배농산급탕 | 춘림헌방함 |
| 33 | 백출산 | 외대비요 |
| 34 | 보생탕 | 부인양방 |
| 35 | 보중익기탕 | 비위론 |
| 36 | 복령음 | 외대비요 |
| 37 | 분심기음 | 직지방 |
| 39 | 사물탕 | 화제국방 |
| 40 | 삼령백출산 | 화제국방 |
| 42 | 삼출건비탕 | 동의보감 |
| 43 | 삼환사심탕 | 금궤요략 |
| 44 | 생혈윤부탕 | 의학정전 |
| 45 | 세간명목탕 | 중보만병회춘 |
| 46 | 소건중탕 | 상한론 |
| 47 | 소시호탕 | 상한론 |
| 48 | 소요산 | 화제국방 |
| 49 | 소자강기탕 | 화제국방 |
| 50 | 소적정원산 | 의학입문 |
| 52 | 소풍산 | 외과정종 |
| 53 | 소풍활혈탕 | 심씨존생서 |
| 54 | 속명탕 | 금궤요략 |
| 55 | 승마갈근탕 | 염씨소아방론 |
| 56 | 시함탕 | 중정통속상한론 |
| 57 | 시호계강탕 | 상한론 |
| 58 | 시호억간탕 | 의학입문 |
| 59 | 시호청간탕 | 구치유요 |
| 62 | 안중산 | 화제국방 |
| 63 | 양격산 | 화제국방 |
| 64 | 연령고본단 | 만병회춘 |
| 65 | 영감강미신하인탕 | 금궤요략 |
| 66 | 영계출감탕 | 상한론 |
| 67 | 오약순기산 | 화제국방 |
| 68 | 오적산 | 화제국방 |
| 69 | 온경탕 | 금궤요략 |
| 70 | 온백원 | 화제금궤 |
| 71 | 용담사간탕 | 의종금감 |
| 73 | 위령탕 | 만병회춘 |
| 74 | 육군자탕 | 부인양방 |
| 75 | 육미지황환 | 소아약증직결 |
| 76 | 육울탕 | 단계심법 |
| 77 | 이기거풍산 | 고금의감 |
| 78 | 이중환 | 상한론 |
| 79 | 이진탕 | 화제국방 |
| 80 | 인삼양영탕 | 화제국방 |
| 81 | 인삼양위탕 | 화제국방 |
| 82 | 인삼패독산 | 소아약증질결 |
| 83 | 인진오령산 | 금궤요략 |
| 84 | 자감초탕 | 상한론 |
| 85 | 자음강화탕 | 만병회춘 |
| 86 | 자음건비탕 | 만병회푼 |
| 87 | 저령탕 | 상한론 |
| 88 | 조경종옥탕 | 고금의감 |
| 89 | 지황음자 | 선명논방 |
| 90 | 진무탕 | 상한론 |
| 91 | 청간해올탕 | 증치준승 |
| 92 | 청금강화탕 | 고금의감 |
| 93 | 청상방풍탕 | 만병회춘 |
| 94 | 청서익기탕 | 비위론 |
| 95 | 청심연자음 | 화제국방 |
| 96 | 평위산 | 화제국방 |
| 97 | 형계연교탕 | 일관당 |
| 98 | 형방패독산 | 섭생중묘방 |
| 99 | 황련아교탕 | 상한론 |
| 100 | 황련해독탕 | 외대비요 |
---
## 6. Agent 실행 시 Python 코드 템플릿
```python
import sqlite3
DB_PATH = '/root/kdrug/database/kdrug.db'
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# === 대상 처방 ===
FORMULA_ID = 38 # official_formula_id
HANJA = '四君子湯'
DESCRIPTION = '보기건비 (補氣健脾)'
REFERENCE = '화제국방 원방. 비기허로 인한 ...'
# === Step 1: 기본 정보 업데이트 ===
c.execute('''UPDATE official_formulas SET
formula_name_hanja = ?,
description = ?,
reference_notes = ?,
updated_at = CURRENT_TIMESTAMP
WHERE official_formula_id = ?''', (HANJA, DESCRIPTION, REFERENCE, FORMULA_ID))
# === Step 2: 구성 약재 등록 ===
# (official_formula_id, ingredient_code, grams_per_cheop, notes, sort_order)
ingredients = [
(FORMULA_ID, '3400H1AHM', 4.0, '군약', 1), # 인삼
(FORMULA_ID, '3204H1AHM', 4.0, '신약', 2), # 백출
(FORMULA_ID, '3215H1AHM', 4.0, '좌약', 3), # 복령
(FORMULA_ID, '3007H1AHM', 2.0, '사약', 4), # 감초
]
for ing in ingredients:
c.execute('''INSERT INTO official_formula_ingredients
(official_formula_id, ingredient_code, grams_per_cheop, notes, sort_order)
VALUES (?, ?, ?, ?, ?)''', ing)
conn.commit()
# === Step 3: 검증 ===
c.execute('''SELECT hm.herb_name, ofi.ingredient_code, ofi.grams_per_cheop, ofi.notes
FROM official_formula_ingredients ofi
JOIN herb_masters hm ON ofi.ingredient_code = hm.ingredient_code
WHERE ofi.official_formula_id = ?
ORDER BY ofi.sort_order''', (FORMULA_ID,))
total = 0
for r in c.fetchall():
total += r[2]
print(f' {r[0]} ({r[1]}): {r[2]}g ({r[3]})')
print(f' 1첩 총량: {total}g')
conn.close()
```
---
## 7. 주의사항
1. **ingredient_code는 반드시 herb_masters 테이블에 존재해야 한다** — JOIN이 실패하면 UI에 표시 안 됨
2. **한 처방에 같은 ingredient_code 중복 불가** — UNIQUE 제약
3. **기존 데이터 확인 후 INSERT** — 이미 등록된 처방에 중복 INSERT하면 에러
4. **출전(source_text)은 이미 seed 데이터로 들어가 있음** — UPDATE 불필요
5. **grams_per_cheop은 소수점 1자리까지** — 예: 4.0, 3.5, 2.0
6. **DB 경로**: `/root/kdrug/database/kdrug.db`
---
*이 문서는 kdrug 시스템의 100처방 마스터 데이터 일괄 등록을 위해 작성되었습니다.*
*최종 수정: 2026-02-18*

View File

@@ -0,0 +1,124 @@
# DB 매핑 구조: 의약품 마스터 ↔ 한약재 코드 테이블
> 의약품 마스터(medicine_master.db)와 기존 한약재 테이블(kdrug.db)은
> **`representative_code`(대표코드)** 로 연결된다.
---
## 1. 테이블 관계도
```
herb_masters (454개 약재, 공통 성분 마스터)
│ ingredient_code (PK) 예: 3033H1AHM = 계지
├─→ herb_products (53,769건, 제품별 코드)
│ ingredient_code (FK)
│ representative_code ← 매핑 키
│ standard_code
│ product_code (보험코드 9자리)
└─→ herb_items (29건, 우리 약국 보유 약재)
ingredient_code (FK)
insurance_code
herb_name 예: "휴먼계지"
medicine_master.db (305,522건, 의약품표준코드 마스터)
│ representative_code ← 매핑 키
│ standard_code
│ item_std_code (품목기준코드)
│ product_name, company_name, spec, category, ...
```
---
## 2. 매핑 키: `representative_code` (대표코드)
| 구분 | herb_products | medicine_master |
|------|:---:|:---:|
| 대표코드 수 | 17,356 | 13,771 (한약재) |
| **매칭 수** | **13,188 (95.8%)** | |
| 표준코드 수 | 53,769 | - |
| 표준코드 매칭 | **40,364 (75.1%)** | |
- 풀체인 매칭 약재: **447 / 454개 (98.5%)**
- medicine_master에만 있는 한약재 대표코드: 6개 (무시 가능)
- herb_products에만 있는 대표코드: 4,168개 (취소/폐지 제품 등)
---
## 3. 매핑 예시: 계지
```sql
-- herb_masters: 공통명
ingredient_code = '3033H1AHM', herb_name = '계지'
-- herb_products: 제품들 (57개 제조사)
rep: 8800624003904 std: 8800624003911 (500g)
rep: 8800628002200 std: 8800628002224 (500g)
rep: 8800640000505 std: 8800640000512 (500g)
...
-- medicine_master: 동일 대표코드로 매칭
rep: 8800624003904 item_std: 200406389 category:
rep: 8800628002200 item_std: 200406525 category:
...
```
---
## 4. 매핑 SQL
### 특정 약재의 전체 제품 + 의약품 마스터 정보 조회
```sql
SELECT hm.herb_name AS ,
hp.product_name, hp.company_name,
hp.representative_code, hp.standard_code,
mm.spec, mm.form_type, mm.package_type,
mm.item_std_code, mm.category
FROM herb_masters hm
JOIN herb_products hp ON hp.ingredient_code = hm.ingredient_code
LEFT JOIN med_master.medicine_master mm
ON mm.representative_code = hp.representative_code
AND (mm.cancel_date IS NULL OR mm.cancel_date = '')
WHERE hm.herb_name = '계지';
```
### herb_items(보유 약재) → 의약품 마스터 연결
```sql
SELECT hi.herb_name, hi.ingredient_code,
mm.item_std_code, mm.representative_code, mm.standard_code,
mm.spec, mm.package_type
FROM herb_items hi
JOIN herb_products hp ON hp.ingredient_code = hi.ingredient_code
JOIN med_master.medicine_master mm
ON mm.representative_code = hp.representative_code
AND mm.product_name = hi.herb_name
WHERE mm.category = '한약재'
AND (mm.cancel_date IS NULL OR mm.cancel_date = '');
```
---
## 5. 코드 체계 정리
| 코드 | 출처 | 예시 | 설명 |
|------|------|------|------|
| `ingredient_code` | herb_masters | 3033H1AHM | 성분코드 (약재 공통) |
| `product_code` | herb_products | 062400390 | 보험 제품코드 9자리 |
| `representative_code` | 양쪽 | 8800624003904 | **대표 바코드 (매핑 키)** |
| `standard_code` | 양쪽 | 8800624003911 | 규격별 바코드 |
| `item_std_code` | medicine_master | 200406389 | 식약처 품목기준코드 |
| `insurance_code` | herb_items | 062400390 | = product_code와 동일 체계 |
---
## 6. 활용 가능 시나리오
1. **바코드 스캔 입고**: 제품 바코드(standard_code) → herb_products → ingredient_code → herb_items 자동 매칭
2. **제품 비교**: 같은 ingredient_code의 다른 업체 제품 가격/규격 비교
3. **마스터 검색 → 입고 연동**: 의약품 마스터에서 검색 → representative_code로 herb_products 조회 → 입고 등록
4. **유효성 검증**: 취소(cancel_date) 제품 식별, 현행 유통 제품만 필터링
---
*최종 수정: 2026-02-19*

553
docs/api_documentation.md Normal file
View File

@@ -0,0 +1,553 @@
# 한의원 약재 관리 시스템 API 문서
## 목차
1. [약재 관리 API](#약재-관리-api)
2. [처방 관리 API](#처방-관리-api)
3. [조제 관리 API](#조제-관리-api)
4. [재고 관리 API](#재고-관리-api)
5. [환자 관리 API](#환자-관리-api)
6. [구매/입고 API](#구매입고-api)
7. [재고 조정 API](#재고-조정-api)
## 약재 관리 API
### GET /api/herbs
약재 제품 목록 조회
**Response:**
```json
{
"success": true,
"data": [
{
"herb_item_id": 1,
"insurance_code": "A001300",
"herb_name": "인삼",
"stock_quantity": 1500.0,
"efficacy_tags": ["보기", "생진"]
}
]
}
```
### GET /api/herbs/masters
마스터 약재 목록 조회 (454개 표준 약재)
**Response:**
```json
{
"success": true,
"data": [
{
"ingredient_code": "3400H1AHM",
"herb_name": "인삼",
"herb_name_hanja": "人蔘",
"herb_name_latin": "Ginseng Radix",
"stock_quantity": 7000.0,
"has_stock": 1,
"lot_count": 3,
"product_count": 2,
"company_count": 2,
"efficacy_tags": ["보기", "생진"]
}
]
}
```
### GET /api/herbs/by-ingredient/{ingredient_code}
특정 성분코드의 제품 목록 조회
**Parameters:**
- `ingredient_code`: 성분코드 (예: 3400H1AHM)
**Response:**
```json
{
"success": true,
"data": [
{
"herb_item_id": 38,
"insurance_code": "060600420",
"herb_name": "인삼",
"product_name": "신흥인삼",
"company_name": "신흥",
"specification": "(주)신흥제약",
"stock_quantity": 7000.0,
"lot_count": 2,
"avg_price": 17.5
}
]
}
```
### GET /api/herbs/{herb_item_id}/available-lots
조제용 가용 로트 목록 (원산지별 그룹화)
**Parameters:**
- `herb_item_id`: 제품 ID
**Response:**
```json
{
"success": true,
"data": {
"herb_name": "인삼",
"insurance_code": "060600420",
"origins": [
{
"origin_country": "한국",
"total_quantity": 3000.0,
"min_price": 20.0,
"max_price": 25.0,
"lot_count": 2,
"lots": [
{
"lot_id": 1,
"quantity_onhand": 1500.0,
"unit_price_per_g": 20.0,
"received_date": "2024-01-15"
}
]
}
],
"total_quantity": 7000.0
}
}
```
## 처방 관리 API
### GET /api/formulas
처방 목록 조회
**Response:**
```json
{
"success": true,
"data": [
{
"formula_id": 1,
"formula_code": "F001",
"formula_name": "쌍화탕",
"formula_type": "탕제",
"base_cheop": 20,
"base_pouches": 60
}
]
}
```
### POST /api/formulas
처방 등록
**Request Body:**
```json
{
"formula_code": "F002",
"formula_name": "십전대보탕",
"formula_type": "탕제",
"base_cheop": 20,
"base_pouches": 60,
"description": "기혈 보충",
"ingredients": [
{
"ingredient_code": "3400H1AHM",
"grams_per_cheop": 6.0,
"notes": ""
}
]
}
```
### GET /api/formulas/{formula_id}/ingredients
처방 구성 약재 조회 (ingredient_code 기반)
**Response:**
```json
{
"success": true,
"data": [
{
"ingredient_code": "3400H1AHM",
"herb_name": "인삼",
"grams_per_cheop": 6.0,
"total_available_stock": 7000.0,
"available_products": [
{
"herb_item_id": 38,
"herb_name": "신흥인삼",
"specification": "(주)신흥제약",
"stock": 7000.0
}
]
}
]
}
```
## 조제 관리 API
### POST /api/compounds
조제 실행
**Request Body:**
```json
{
"patient_id": 1,
"formula_id": 1,
"je_count": 1,
"cheop_total": 20,
"pouch_total": 60,
"ingredients": [
{
"herb_item_id": 38,
"grams_per_cheop": 6.0,
"origin": "auto"
}
]
}
```
**Response:**
```json
{
"success": true,
"compound_id": 123,
"message": "조제가 완료되었습니다",
"summary": {
"total_cost": 15000,
"consumptions": [
{
"herb_name": "인삼",
"total_used": 120.0,
"lots_used": [
{
"lot_id": 1,
"origin": "한국",
"quantity": 120.0,
"unit_price": 20.0
}
]
}
]
}
}
```
### GET /api/compounds/recent
최근 조제 내역
**Query Parameters:**
- `limit`: 조회 건수 (기본값: 10)
- `patient_id`: 환자 ID (선택)
**Response:**
```json
{
"success": true,
"data": [
{
"compound_id": 123,
"compound_date": "2024-01-20",
"patient_name": "홍길동",
"formula_name": "쌍화탕",
"cheop_total": 20,
"pouch_total": 60,
"cost_total": 15000,
"status": "completed"
}
]
}
```
## 재고 관리 API
### GET /api/inventory/summary
재고 현황 요약
**Response:**
```json
{
"success": true,
"data": {
"total_items": 87,
"items_with_stock": 75,
"total_value": 2500000,
"by_origin": {
"한국": {
"item_count": 30,
"total_quantity": 15000,
"total_value": 1200000
},
"중국": {
"item_count": 45,
"total_quantity": 25000,
"total_value": 1300000
}
}
}
}
```
### GET /api/inventory/low-stock
재고 부족 약재 조회
**Query Parameters:**
- `threshold`: 재고 기준량 (기본값: 100g)
**Response:**
```json
{
"success": true,
"data": [
{
"herb_item_id": 5,
"herb_name": "당귀",
"current_stock": 50.0,
"threshold": 100.0,
"last_purchase_date": "2024-01-01"
}
]
}
```
### GET /api/stock-ledger
재고 원장 조회
**Query Parameters:**
- `herb_item_id`: 제품 ID (선택)
- `start_date`: 시작일 (선택)
- `end_date`: 종료일 (선택)
- `event_type`: IN/OUT/ADJUST (선택)
**Response:**
```json
{
"success": true,
"data": [
{
"ledger_id": 1,
"event_time": "2024-01-20 14:30:00",
"event_type": "OUT",
"herb_name": "인삼",
"lot_id": 1,
"quantity_delta": -120.0,
"reference_table": "compound_consumptions",
"reference_id": 456
}
]
}
```
## 환자 관리 API
### GET /api/patients
환자 목록 조회
**Query Parameters:**
- `search`: 검색어 (이름, 전화번호)
- `is_active`: 활성 여부
**Response:**
```json
{
"success": true,
"data": [
{
"patient_id": 1,
"name": "홍길동",
"phone": "010-1234-5678",
"gender": "M",
"birth_date": "1980-01-01",
"last_visit": "2024-01-20"
}
]
}
```
### POST /api/patients
환자 등록
**Request Body:**
```json
{
"name": "홍길동",
"phone": "010-1234-5678",
"jumin_no": "800101-1******",
"gender": "M",
"birth_date": "1980-01-01",
"address": "서울시 강남구",
"notes": ""
}
```
### GET /api/patients/{patient_id}/prescriptions
환자 처방 이력
**Response:**
```json
{
"success": true,
"data": [
{
"compound_id": 123,
"compound_date": "2024-01-20",
"formula_name": "쌍화탕",
"cheop_total": 20,
"pouch_total": 60,
"cost_total": 15000,
"ingredients": [
{
"herb_name": "인삼",
"product_name": "신흥인삼",
"grams_per_cheop": 6.0,
"total_grams": 120.0,
"origin_country": "한국"
}
]
}
]
}
```
## 구매/입고 API
### POST /api/purchases/upload
거래명세표 업로드
**Form Data:**
- `file`: 엑셀 파일
- `supplier_id`: 공급처 ID
**Response:**
```json
{
"success": true,
"receipt_id": 789,
"summary": {
"total_items": 25,
"total_amount": 500000,
"items_processed": 25,
"items_failed": 0
}
}
```
### POST /api/purchases/receipts
구매 영수증 등록
**Request Body:**
```json
{
"supplier_id": 1,
"receipt_date": "2024-01-20",
"receipt_no": "R2024012001",
"vat_included": true,
"vat_rate": 10.0,
"total_amount": 550000,
"lines": [
{
"herb_item_id": 38,
"origin_country": "한국",
"quantity_g": 1000,
"unit_price_per_g": 20.0,
"line_total": 20000
}
]
}
```
## 재고 조정 API
### POST /api/stock-adjustments
재고 보정
**Request Body:**
```json
{
"adjustment_date": "2024-01-20",
"adjustment_type": "correction",
"notes": "실사 보정",
"details": [
{
"herb_item_id": 38,
"lot_id": 1,
"quantity_after": 1480.0,
"reason": "실사 차이"
}
]
}
```
### GET /api/stock-adjustments
재고 보정 내역 조회
**Query Parameters:**
- `start_date`: 시작일
- `end_date`: 종료일
- `herb_item_id`: 제품 ID (선택)
**Response:**
```json
{
"success": true,
"data": [
{
"adjustment_id": 1,
"adjustment_date": "2024-01-20",
"adjustment_no": "ADJ20240120001",
"adjustment_type": "correction",
"total_items": 5,
"created_by": "admin"
}
]
}
```
## 통계 API
### GET /api/stats/herb-usage
약재 사용 통계
**Query Parameters:**
- `period`: daily/weekly/monthly
- `start_date`: 시작일
- `end_date`: 종료일
**Response:**
```json
{
"success": true,
"data": {
"period": "2024-01",
"top_used": [
{
"herb_name": "인삼",
"total_quantity": 5000.0,
"usage_count": 25,
"total_cost": 100000
}
],
"total_compounds": 150,
"total_cost": 3000000
}
}
```
## 에러 응답
모든 API는 다음과 같은 에러 응답 형식을 따릅니다:
```json
{
"success": false,
"error": "에러 메시지",
"code": "ERROR_CODE"
}
```
### HTTP 상태 코드
- `200 OK`: 성공
- `400 Bad Request`: 잘못된 요청
- `404 Not Found`: 리소스를 찾을 수 없음
- `500 Internal Server Error`: 서버 오류

252
docs/database_erd.md Normal file
View File

@@ -0,0 +1,252 @@
# 한의원 약재 관리 시스템 ER 다이어그램
## 핵심 엔티티 관계도
```mermaid
erDiagram
HERB_MASTERS ||--o{ HERB_ITEMS : "has"
HERB_MASTERS ||--o{ FORMULA_INGREDIENTS : "used_in"
HERB_ITEMS ||--o{ INVENTORY_LOTS : "has"
HERB_ITEMS ||--o{ COMPOUND_INGREDIENTS : "used_in"
HERB_ITEMS ||--o{ HERB_ITEM_TAGS : "has"
HERB_EFFICACY_TAGS ||--o{ HERB_ITEM_TAGS : "assigned_to"
FORMULAS ||--o{ FORMULA_INGREDIENTS : "contains"
FORMULAS ||--o{ COMPOUNDS : "used_for"
PATIENTS ||--o{ COMPOUNDS : "receives"
PATIENTS ||--o{ PATIENT_SURVEYS : "completes"
COMPOUNDS ||--o{ COMPOUND_INGREDIENTS : "contains"
COMPOUND_INGREDIENTS ||--o{ COMPOUND_CONSUMPTIONS : "consumes"
INVENTORY_LOTS ||--o{ COMPOUND_CONSUMPTIONS : "consumed_by"
INVENTORY_LOTS }o--|| SUPPLIERS : "supplied_by"
INVENTORY_LOTS }o--|| PURCHASE_RECEIPT_LINES : "received_from"
SUPPLIERS ||--o{ PURCHASE_RECEIPTS : "provides"
PURCHASE_RECEIPTS ||--o{ PURCHASE_RECEIPT_LINES : "contains"
STOCK_ADJUSTMENTS ||--o{ STOCK_ADJUSTMENT_DETAILS : "contains"
STOCK_LEDGER }o--|| HERB_ITEMS : "tracks"
STOCK_LEDGER }o--|| INVENTORY_LOTS : "tracks"
HERB_MASTERS {
string ingredient_code PK
string herb_name
string herb_name_hanja
string herb_name_latin
text description
boolean is_active
}
HERB_ITEMS {
int herb_item_id PK
string ingredient_code FK
string insurance_code
string herb_name
string specification
boolean is_active
}
INVENTORY_LOTS {
int lot_id PK
int herb_item_id FK
int supplier_id FK
date received_date
string origin_country
decimal unit_price_per_g
decimal quantity_onhand
date expiry_date
boolean is_depleted
}
FORMULAS {
int formula_id PK
string formula_code
string formula_name
string formula_type
int base_cheop
int base_pouches
boolean is_active
}
FORMULA_INGREDIENTS {
int ingredient_id PK
int formula_id FK
string ingredient_code FK
decimal grams_per_cheop
int sort_order
}
COMPOUNDS {
int compound_id PK
int patient_id FK
int formula_id FK
date compound_date
decimal cheop_total
decimal pouch_total
decimal cost_total
string status
}
PATIENTS {
int patient_id PK
string name
string phone
string gender
date birth_date
boolean is_active
}
```
## 재고 흐름도
```mermaid
flowchart TB
subgraph 입고
S[공급처/Suppliers]
PR[구매영수증/Purchase Receipts]
PRL[구매영수증라인/Purchase Receipt Lines]
S --> PR
PR --> PRL
end
subgraph 재고
HM[약재마스터/Herb Masters]
HI[약재제품/Herb Items]
IL[재고로트/Inventory Lots]
SL[재고원장/Stock Ledger]
HM --> HI
HI --> IL
PRL --> IL
IL -.-> SL
end
subgraph 조제/출고
F[처방/Formulas]
FI[처방구성/Formula Ingredients]
C[조제/Compounds]
CI[조제구성/Compound Ingredients]
CC[조제소비/Compound Consumptions]
F --> FI
FI -.->|ingredient_code| HM
F --> C
C --> CI
CI --> CC
CC --> IL
end
subgraph 재고조정
SA[재고조정/Stock Adjustments]
SAD[조정상세/Adjustment Details]
SA --> SAD
SAD --> IL
end
IL --> SL
CC --> SL
SAD --> SL
classDef master fill:#e1f5fe
classDef transaction fill:#fff3e0
classDef inventory fill:#f3e5f5
classDef ledger fill:#e8f5e9
class HM,HI,F master
class PR,PRL,C,CI,CC transaction
class IL inventory
class SL,SA,SAD ledger
```
## 데이터 플로우
### 1. 약재 마스터 데이터 흐름
```
herb_masters (성분코드)
herb_items (제품별 재고 단위)
inventory_lots (로트별 실재고)
```
### 2. 처방 → 조제 흐름
```
formulas (처방 마스터)
formula_ingredients (ingredient_code 기반)
compounds (실제 조제)
compound_ingredients (제품 선택)
compound_consumptions (로트별 소비)
```
### 3. 구매 → 재고 흐름
```
suppliers (공급처)
purchase_receipts (영수증)
purchase_receipt_lines (라인)
inventory_lots (재고 생성)
stock_ledger (원장 기록)
```
## 주요 비즈니스 규칙
### 재고 관리
1. **FIFO (선입선출)**: 오래된 로트부터 자동 소비
2. **로트 추적**: 모든 재고는 lot_id로 추적
3. **원산지 관리**: 로트별 origin_country 관리
4. **가격 추적**: 로트별 unit_price_per_g 관리
### 처방 관리
1. **성분코드 기반**: formula_ingredients는 ingredient_code 사용
2. **유연한 제품 선택**: 동일 성분의 다른 제품 선택 가능
3. **2단계 선택**: 약재(마스터) → 제품 → 로트
### 조제 관리
1. **처방 조제**: 등록된 처방 사용
2. **직접 조제**: 처방 없이 직접 구성
3. **가감 가능**: 처방 기본 구성에서 추가/제거 가능
### 원가 계산
```
조제 원가 = Σ(사용량 × 로트별 단가)
```
## 인덱스 전략
### 주요 인덱스
- `herb_items.ingredient_code`: 성분코드 조회
- `inventory_lots.herb_item_id, is_depleted`: 가용 재고 조회
- `inventory_lots.origin_country`: 원산지별 재고 조회
- `formula_ingredients.formula_id`: 처방 구성 조회
- `compounds.patient_id, compound_date`: 환자 조제 이력
- `stock_ledger.herb_item_id, event_time`: 재고 변동 이력
## 데이터 무결성
### 외래키 제약
- `herb_items.ingredient_code``herb_masters.ingredient_code`
- `formula_ingredients.ingredient_code``herb_masters.ingredient_code`
- `inventory_lots.herb_item_id``herb_items.herb_item_id`
- `compound_ingredients.herb_item_id``herb_items.herb_item_id`
- `compound_consumptions.lot_id``inventory_lots.lot_id`
### 체크 제약
- `inventory_lots.quantity_onhand >= 0`
- `compound_consumptions.quantity_used > 0`
- `purchase_receipt_lines.quantity_g > 0`
- `formula_ingredients.grams_per_cheop > 0`
### 트리거
- 재고 변동 시 `stock_ledger` 자동 기록
- `inventory_lots.quantity_onhand = 0``is_depleted = 1` 자동 설정

398
docs/database_schema.md Normal file
View File

@@ -0,0 +1,398 @@
# 한의원 약재 관리 시스템 데이터베이스 스키마
## 목차
1. [핵심 마스터 테이블](#핵심-마스터-테이블)
2. [재고 관리 테이블](#재고-관리-테이블)
3. [조제 관리 테이블](#조제-관리-테이블)
4. [구매/입고 관리 테이블](#구매입고-관리-테이블)
5. [환자 관리 테이블](#환자-관리-테이블)
6. [재고 조정 테이블](#재고-조정-테이블)
7. [설문 관리 테이블](#설문-관리-테이블)
## 핵심 마스터 테이블
### herb_masters (약재 마스터)
통합 약재 마스터 정보. 성분코드(ingredient_code) 기준 관리.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| ingredient_code | VARCHAR(10) PK | 성분코드 (예: 3400H1AHM) |
| herb_name | VARCHAR(100) | 한글명 (예: 인삼) |
| herb_name_hanja | VARCHAR(100) | 한자명 (예: 人蔘) |
| herb_name_latin | VARCHAR(200) | 라틴명 |
| description | TEXT | 설명 |
| is_active | BOOLEAN | 사용 여부 |
| created_at | TIMESTAMP | 생성일시 |
| updated_at | TIMESTAMP | 수정일시 |
### herb_items (약재 제품)
실제 재고 관리 단위. 제조사별 개별 제품.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| herb_item_id | INTEGER PK | 제품 ID |
| ingredient_code | VARCHAR(10) FK | 성분코드 (herb_masters 참조) |
| insurance_code | TEXT | 보험코드 |
| herb_name | TEXT | 제품명 (예: 신흥인삼) |
| specification | TEXT | 규격/제조사 (예: (주)신흥제약) |
| default_unit | TEXT | 기본 단위 |
| is_active | INTEGER | 사용 여부 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### herb_products (표준 약재 제품 목록)
건강보험 표준 약재 제품 목록.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| product_id | INTEGER PK | 제품 ID |
| ingredient_code | VARCHAR(10) FK | 성분코드 |
| product_code | VARCHAR(9) | 제품코드 |
| company_name | VARCHAR(200) | 제조사명 |
| product_name | VARCHAR(200) | 제품명 |
| standard_code | VARCHAR(20) | 표준코드 |
| representative_code | VARCHAR(20) | 대표코드 |
| package_size | VARCHAR(20) | 포장 크기 |
| package_unit | VARCHAR(20) | 포장 단위 |
| valid_from | DATE | 유효 시작일 |
| valid_to | DATE | 유효 종료일 |
| is_active | BOOLEAN | 사용 여부 |
### formulas (처방 마스터)
등록된 처방 정보.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| formula_id | INTEGER PK | 처방 ID |
| formula_code | TEXT | 처방코드 |
| formula_name | TEXT | 처방명 (예: 쌍화탕) |
| formula_type | TEXT | 처방 유형 |
| base_cheop | INTEGER | 기본 첩수 |
| base_pouches | INTEGER | 기본 포수 |
| description | TEXT | 설명 |
| is_active | INTEGER | 사용 여부 |
| created_by | TEXT | 생성자 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### formula_ingredients (처방 구성)
처방별 구성 약재. **ingredient_code 기반**.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| ingredient_id | INTEGER PK | 구성 ID |
| formula_id | INTEGER FK | 처방 ID |
| ingredient_code | TEXT | 성분코드 (herb_masters 참조) |
| grams_per_cheop | REAL | 1첩당 용량(g) |
| notes | TEXT | 비고 |
| sort_order | INTEGER | 정렬 순서 |
| created_at | DATETIME | 생성일시 |
## 재고 관리 테이블
### inventory_lots (재고 로트)
로트별 재고 관리.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| lot_id | INTEGER PK | 로트 ID |
| herb_item_id | INTEGER FK | 제품 ID |
| supplier_id | INTEGER FK | 공급처 ID |
| receipt_line_id | INTEGER FK | 입고 라인 ID |
| received_date | DATE | 입고일 |
| origin_country | TEXT | 원산지 (예: 중국, 한국) |
| unit_price_per_g | REAL | g당 단가 |
| quantity_received | REAL | 입고량(g) |
| quantity_onhand | REAL | 현재고량(g) |
| expiry_date | DATE | 유효기간 |
| lot_number | TEXT | 로트번호 |
| is_depleted | INTEGER | 소진 여부 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### stock_ledger (재고 원장)
모든 재고 변동 이력 관리.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| ledger_id | INTEGER PK | 원장 ID |
| event_time | DATETIME | 이벤트 시간 |
| event_type | TEXT | 이벤트 유형 (IN/OUT/ADJUST) |
| herb_item_id | INTEGER FK | 제품 ID |
| lot_id | INTEGER FK | 로트 ID |
| quantity_delta | REAL | 수량 변동 (+/-) |
| unit_cost_per_g | REAL | g당 단가 |
| reference_table | TEXT | 참조 테이블명 |
| reference_id | INTEGER | 참조 ID |
| notes | TEXT | 비고 |
| created_by | TEXT | 생성자 |
| created_at | DATETIME | 생성일시 |
## 조제 관리 테이블
### compounds (조제 내역)
환자별 조제 내역.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| compound_id | INTEGER PK | 조제 ID |
| patient_id | INTEGER FK | 환자 ID |
| formula_id | INTEGER FK | 처방 ID (NULL 가능 - 직접조제) |
| compound_date | DATE | 조제일 |
| je_count | REAL | 제수 |
| cheop_total | REAL | 총 첩수 |
| pouch_total | REAL | 총 포수 |
| cost_total | REAL | 원가 합계 |
| sell_price_total | REAL | 판매가 합계 |
| prescription_no | TEXT | 처방전 번호 |
| status | TEXT | 상태 |
| notes | TEXT | 비고 |
| created_by | TEXT | 생성자 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### compound_ingredients (조제 구성 약재)
조제별 사용 약재 구성.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| compound_ingredient_id | INTEGER PK | 구성 ID |
| compound_id | INTEGER FK | 조제 ID |
| herb_item_id | INTEGER FK | 제품 ID |
| grams_per_cheop | REAL | 1첩당 용량(g) |
| total_grams | REAL | 총 용량(g) |
| notes | TEXT | 비고 |
| created_at | DATETIME | 생성일시 |
### compound_consumptions (조제 소비 내역)
조제 시 실제 소비한 재고 로트별 내역.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| consumption_id | INTEGER PK | 소비 ID |
| compound_id | INTEGER FK | 조제 ID |
| herb_item_id | INTEGER FK | 제품 ID |
| lot_id | INTEGER FK | 로트 ID |
| quantity_used | REAL | 사용량(g) |
| unit_cost_per_g | REAL | g당 단가 |
| cost_amount | REAL | 원가 금액 |
| created_at | DATETIME | 생성일시 |
## 구매/입고 관리 테이블
### suppliers (공급처)
약재 공급처 정보.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| supplier_id | INTEGER PK | 공급처 ID |
| name | TEXT | 공급처명 |
| business_no | TEXT | 사업자번호 |
| contact_person | TEXT | 담당자 |
| phone | TEXT | 전화번호 |
| address | TEXT | 주소 |
| is_active | INTEGER | 사용 여부 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### purchase_receipts (구매 영수증)
구매/입고 영수증 헤더.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| receipt_id | INTEGER PK | 영수증 ID |
| supplier_id | INTEGER FK | 공급처 ID |
| receipt_date | DATE | 거래일 |
| receipt_no | TEXT | 영수증 번호 |
| vat_included | INTEGER | VAT 포함 여부 |
| vat_rate | REAL | VAT 비율 |
| total_amount | REAL | 총 금액 |
| source_file | TEXT | 원본 파일 |
| notes | TEXT | 비고 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### purchase_receipt_lines (구매 영수증 라인)
구매/입고 영수증 상세 라인.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| line_id | INTEGER PK | 라인 ID |
| receipt_id | INTEGER FK | 영수증 ID |
| herb_item_id | INTEGER FK | 제품 ID |
| origin_country | TEXT | 원산지 |
| quantity_g | REAL | 수량(g) |
| unit_price_per_g | REAL | g당 단가 |
| line_total | REAL | 라인 합계 |
| expiry_date | DATE | 유효기간 |
| lot_number | TEXT | 로트번호 |
| created_at | DATETIME | 생성일시 |
## 환자 관리 테이블
### patients (환자)
환자 기본 정보.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| patient_id | INTEGER PK | 환자 ID |
| name | TEXT | 환자명 |
| phone | TEXT | 전화번호 |
| jumin_no | TEXT | 주민번호 |
| gender | TEXT | 성별 |
| birth_date | DATE | 생년월일 |
| address | TEXT | 주소 |
| notes | TEXT | 비고 |
| is_active | INTEGER | 사용 여부 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
## 재고 조정 테이블
### stock_adjustments (재고 조정)
재고 보정 헤더.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| adjustment_id | INTEGER PK | 조정 ID |
| adjustment_date | DATE | 조정일 |
| adjustment_no | TEXT | 조정 번호 |
| adjustment_type | TEXT | 조정 유형 |
| notes | TEXT | 비고 |
| created_by | TEXT | 생성자 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### stock_adjustment_details (재고 조정 상세)
재고 보정 상세 내역.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| detail_id | INTEGER PK | 상세 ID |
| adjustment_id | INTEGER FK | 조정 ID |
| herb_item_id | INTEGER FK | 제품 ID |
| lot_id | INTEGER FK | 로트 ID |
| quantity_before | REAL | 조정 전 수량 |
| quantity_after | REAL | 조정 후 수량 |
| quantity_delta | REAL | 조정량 |
| reason | TEXT | 사유 |
| created_at | DATETIME | 생성일시 |
## 효능 태그 테이블
### herb_efficacy_tags (효능 태그)
약재 효능 마스터.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| tag_id | INTEGER PK | 태그 ID |
| tag_name | VARCHAR(50) | 태그명 (예: 보혈, 활혈) |
| tag_category | VARCHAR(50) | 카테고리 |
| description | TEXT | 설명 |
| created_at | TIMESTAMP | 생성일시 |
### herb_item_tags (약재-태그 연결)
약재와 효능 태그 다대다 관계.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| herb_item_id | INTEGER FK | 제품 ID |
| tag_id | INTEGER FK | 태그 ID |
## 설문 관리 테이블
### survey_templates (설문 템플릿)
설문 질문 템플릿.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| template_id | INTEGER PK | 템플릿 ID |
| category | TEXT | 카테고리 |
| category_name | TEXT | 카테고리명 |
| question_code | TEXT | 질문 코드 |
| question_text | TEXT | 질문 내용 |
| question_subtext | TEXT | 보조 설명 |
| input_type | TEXT | 입력 타입 |
| options | TEXT | 선택 옵션 |
| is_required | INTEGER | 필수 여부 |
| sort_order | INTEGER | 정렬 순서 |
| is_active | INTEGER | 사용 여부 |
| created_at | DATETIME | 생성일시 |
### patient_surveys (환자 설문)
환자별 설문 내역.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| survey_id | INTEGER PK | 설문 ID |
| patient_id | INTEGER FK | 환자 ID |
| survey_token | TEXT | 설문 토큰 |
| survey_date | DATE | 설문일 |
| status | TEXT | 상태 |
| created_at | DATETIME | 생성일시 |
| completed_at | DATETIME | 완료일시 |
| reviewed_at | DATETIME | 검토일시 |
| reviewed_by | TEXT | 검토자 |
| notes | TEXT | 비고 |
### survey_responses (설문 응답)
설문 응답 내역.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| response_id | INTEGER PK | 응답 ID |
| survey_id | INTEGER FK | 설문 ID |
| category | TEXT | 카테고리 |
| question_code | TEXT | 질문 코드 |
| question_text | TEXT | 질문 내용 |
| answer_value | TEXT | 응답 값 |
| answer_type | TEXT | 응답 타입 |
| created_at | DATETIME | 생성일시 |
| updated_at | DATETIME | 수정일시 |
### survey_progress (설문 진행 상태)
설문 카테고리별 진행 상태.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| progress_id | INTEGER PK | 진행 ID |
| survey_id | INTEGER FK | 설문 ID |
| category | TEXT | 카테고리 |
| total_questions | INTEGER | 전체 질문 수 |
| answered_questions | INTEGER | 응답 질문 수 |
| is_completed | INTEGER | 완료 여부 |
| last_updated | DATETIME | 최종 수정일시 |
## 주요 관계
1. **약재 계층 구조**
- `herb_masters` (마스터) ← `herb_items` (제품) ← `inventory_lots` (로트)
- ingredient_code로 연결
2. **처방-조제 관계**
- `formulas``formula_ingredients` (ingredient_code 기반)
- `compounds``compound_ingredients``compound_consumptions`
3. **재고 추적**
- 입고: `purchase_receipts``purchase_receipt_lines``inventory_lots`
- 출고: `compound_consumptions``inventory_lots`
- 이력: 모든 변동은 `stock_ledger`에 기록
4. **가격 정책**
- FIFO (선입선출) 기준
- lot별 unit_price_per_g 관리
## 마이그레이션 이력
### 2024년 주요 변경사항
1. `formula_ingredients` 테이블: `herb_item_id``ingredient_code` 변경
- 특정 제품이 아닌 성분코드 기준 처방 구성
- 조제 시 동일 성분의 다른 제품 선택 가능
2. `herb_masters` 테이블 추가
- 454개 표준 약재 마스터 데이터
- ingredient_code 기준 통합 관리
3. `herb_efficacy_tags` 시스템 추가
- 18개 기본 효능 태그
- 약재별 효능 분류 체계

View File

@@ -0,0 +1,9 @@
경악전서의 가미패독산과 일반 패독산은 구성 성분과 효능에서 차이가 나는 다른 처방입니다. 패독산은 감기·몸살에 쓰는 해열·발한제인 반면, 경악전서의 가미패독산은 강활, 독활, 인삼, 대황, 창출 등을 포함하여 습열독(濕熱毒)을 해소하는 益氣解表(익기해표), 散風祛濕(산풍거습) 약재들로 구성되어 있습니다.
약학정보원
약학정보원
+1
가미패독산 (경악전서 기준): 강활, 독활, 전호, 시호, 길경, 인삼, 복령, 지각, 감초, 천궁, 대황, 창출로 구성되어 있으며 생강을 더해 달입니다.
차이점: 일반 패독산(형방패독산 등)이 감기 초기 발한 위주라면, 경악전서 가미패독산은 이와 더불어 내열이나 습한 기운을 제거하는 대황과 창출 등이 추가되어 있어, 족이양경(태양방광경, 소양담경)의 열독 유주 등에 더 특화된 처방입니다.
약학정보원
약학정보원
따라서, 단순 감기 증상에는 패독산이, 열독이 심하거나 습(濕)이 겹친 경우에는 경악전서의 가미패독산이 사용될 수 있어 구성과 적응증에서 분명한 차이가 있습니

View File

@@ -0,0 +1,72 @@
# 경방신약 주문 매핑 결과 (2026-02-19)
## 조건
- 제약사: 경방신약(주)
- 규격: 500그램
- 포장: 병/통
- DB: medicine_master (의약품 표준코드)
## 매핑 결과: 38/38건 전체 매칭 완료 (한방 OTC 37건 + 일반 OTC 1건)
### 한방 OTC (37건) — 소계 1,531,300원
| # | 주문 약품명 | 수량 | 단가(원) | 매칭 제품명 | 표준코드 | 비고 |
|---|-----------|------|---------|-----------|---------|------|
| 1 | 가미귀비탕 | 1 | 53,400 | 진경안신엑스과립(가미귀비탕엑스과립) | 8806613031439 | 500g 통 (수동매핑) |
| 2 | 가미소요산 | 1 | 35,000 | 경방가미소요산엑스과립 | 8806613001012 | |
| 3 | 가미온담탕 | 1 | 69,600 | 자양심간탕엑스과립(가미온담탕) | 8806613030616 | |
| 4 | 갈근탕 | 1 | 27,700 | 치감엑스과립(갈근탕엑스과립) | 8806613032337 | |
| 5 | 계지복령환 | 1 | 35,000 | 모시나엑스과립(계지복령환) | 8806613023816 | |
| 6 | 곽향정기산 | 1 | 31,800 | 씨즌쿨엑스과립(곽향정기산) | 8806613027531 | |
| 7 | 당귀작약산 | 1 | 36,200 | 경방당귀작약산엑스과립 | 8806613004716 | |
| 8 | 대시호탕 | 1 | 37,300 | 경방대시호탕엑스과립 | 8806613004914 | |
| 9 | 마행감석탕 | 1 | 28,500 | 사브엑스과립(마행감석탕) | 8806613025438 | |
| 10 | 맥문동탕 | 1 | 35,800 | 윤폐탕엑스과립(맥문동탕엑스과립) | 8806613030234 | |
| 11 | 반하백출천마탕 | 1 | 57,200 | 경방반하백출천마탕엑스과립 | 8806613007717 | |
| 12 | 반하사심탕 | 1 | 49,000 | 스토마큐(반하사심탕엑스과립) | 8806613026541 | |
| 13 | 방풍통성산 | 1 | 33,600 | 경방방풍통성산엑스과립 | 8806613008219 | |
| 14 | 배농산급탕 | 1 | 26,700 | 오메가엑스과립(배농산급탕) | 8806613028439 | |
| 15 | 보중익기탕 | 1 | 58,800 | 경방보중익기탕엑스과립 | 8806613008912 | |
| 16 | 사물탕 | 1 | 35,000 | 경방사물탕엑스과립 | 8806613009612 | |
| 17 | 소건중탕 | 1 | 25,000 | 포키드엑스과립(소건중탕) | 8806613033617 | |
| 18 | 소경활혈탕 | 1 | 40,300 | 경방소경활혈탕엑스과립 | 8806613011110 | |
| 19 | 소시호탕 | 1 | 55,000 | 정해탕(소시호탕엑스과립) | 8806613030937 | |
| 20 | 소청룡탕 | 1 | 41,400 | 소폐탕엑스과립(소청룡탕) | 8806613026138 | 500g 통 (수동매핑) |
| 21 | 시함탕 | 1 | 49,800 | 폐활탕엑스과립(시함탕) | 8806613033433 | |
| 22 | 시호가용골모려탕 | 1 | 46,200 | 브아피엑스과립(시호가용골모려탕엑스) | 8806613024738 | |
| 23 | 십미패독산 | 1 | 48,700 | 해스킨엑스과립(십미패독탕) | 8806613034430 | 산→탕 명칭차이 (수동매핑) |
| 24 | 오령산 | 1 | 37,200 | 자리투엑스과립(오령산엑스과립) | 8806613030531 | |
| 25 | 오적산 | 1 | 50,400 | 경방오적산엑스과립 | 8806613014814 | |
| 26 | 용담사간탕 | 1 | 37,100 | 용패탕(용담사간탕엑스과립) | 8806613029436 | |
| 27 | 월비가출탕 | 1 | 40,400 | 마이진탕엑스과립(월비가출탕) | 8806613023618 | |
| 28 | 육미지황탕 | 1 | 40,300 | 경방육미지황탕엑스과립 | 8806613015613 | |
| 29 | 인삼패독산 | 1 | 52,300 | 가쁘레엑스과립(인삼패독산) | 8806613000138 | 500g 통 (수동매핑) |
| 30 | 자음강화탕 | 1 | 41,500 | 자활탕(자음강화탕엑스과립) | 8806613030715 | |
| 31 | 저령탕 | 1 | 36,700 | 바디스엑스과립(저령탕엑스) | 8806613024332 | |
| 32 | 청화보음탕 | 1 | 34,200 | 디휘버엑스과립(청화보음탕) | 8806613023038 | |
| 33 | 팔미지황환 | 1 | 41,700 | 경방팔미지황환엑스과립 | 8806613019710 | |
| 34 | 평위산 | 1 | 28,000 | 위제나과립(평위산엑스과립) | 8806613029818 | |
| 35 | 향사육군자탕 | 1 | 58,400 | 스토비엑스과립(향사육군자탕) | 8806613026817 | |
| 36 | 형개연교탕 | 1 | 35,600 | 노넥스엑스과립(형개연교탕) | 8806613022314 | |
| 37 | 황련해독탕 | 1 | 40,500 | 오브스과립(황련해독탕엑스과립) | 8806613028613 | |
### 일반 OTC (1건) — 소계 30,500원
| # | 주문 약품명 | 수량 | 단가(원) | 매칭 제품명 | 표준코드 | 비고 |
|---|-----------|------|---------|-----------|---------|------|
| 1 | 미소그린에스과립 | 1 | 30,500 | 미소그린에스과립 | 8806613066615 | 420g 병. 한방 OTC 아님 |
### 총액: 1,561,800원 (한방 OTC 1,531,300 + 일반 OTC 30,500)
---
### 수동 매핑 처리 완료 (4건)
초기 자동매칭 시 누락되었으나, 수동 확인으로 전부 매칭 완료.
| # | 주문 약품명 | 매칭 제품명 | 표준코드 | 사유 |
|---|-----------|-----------|---------|------|
| 1 | 가미귀비탕 | 진경안신엑스과립(가미귀비탕엑스과립) | 8806613031439 | 500g 통. 브랜드명이 달라 자동매칭 실패 |
| 2 | 소청룡탕 | 소폐탕엑스과립(소청룡탕) | 8806613026138 | 500g 통. 포장이 "통"이라 "병" 필터에서 누락 |
| 3 | 십미패독산 | 해스킨엑스과립(십미패독탕) | 8806613034430 | 500g 병. DB에 "산"이 아닌 "탕"으로 등록 |
| 4 | 인삼패독산 | 가쁘레엑스과립(인삼패독산) | 8806613000138 | 500g 통. 포장이 "통"이라 "병" 필터에서 누락 |

View File

@@ -0,0 +1,367 @@
# 한약 재고관리 시스템 데이터 구조 및 흐름
> 최종 수정: 2026-02-17
> 작성자: 시스템 개발팀
## 📊 1. 전체 시스템 개요
### 1.1 시스템 목적
- 한의원의 한약재 재고 관리
- 처방 조제 및 소비 추적
- 보험 청구를 위한 코드 관리
- 효능 기반 약재 정보 관리
### 1.2 핵심 개념
```
┌─────────────────────────────────────────────┐
│ 성분코드 (ingredient_code) │
│ - 한약재의 본질적 정체성 │
│ - 예: "3400H1AHM" = 인삼 │
│ - 총 454개 표준 약재 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 보험코드 (insurance_code) │
│ - 실제 청구/재고 관리 단위 │
│ - 9자리 제품 코드 │
│ - 예: "062400740" = 휴먼감초 │
└─────────────────────────────────────────────┘
```
## 🗂️ 2. 테이블 구조 상세
### 2.1 마스터 데이터 (Master Data)
#### **herb_masters** - 성분코드 마스터
```sql
CREATE TABLE herb_masters (
ingredient_code VARCHAR(10) PRIMARY KEY, -- "3400H1AHM"
herb_name VARCHAR(100), -- "인삼"
herb_name_hanja VARCHAR(100), -- "人蔘"
herb_name_latin VARCHAR(200) -- "Ginseng Radix"
)
-- 역할: 454개 표준 한약재 정의
-- 특징: 보험급여 약재 목록
```
#### **herb_master_extended** - 확장 정보
```sql
CREATE TABLE herb_master_extended (
herb_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 단순 인덱스
ingredient_code VARCHAR(10) UNIQUE, -- herb_masters와 1:1
property VARCHAR(50), -- 성(性): 온/한/평
taste VARCHAR(100), -- 미(味): 감/고/신
meridian_tropism TEXT, -- 귀경: 비,폐,심
main_effects TEXT, -- 주요 효능
dosage_range VARCHAR(50) -- 상용량: "3-9g"
)
-- 역할: 한의학적 속성 정보 저장
-- 관계: ingredient_code로 herb_masters와 연결
```
### 2.2 제품 및 재고 (Products & Inventory)
#### **herb_products** - 제품 카탈로그
```sql
CREATE TABLE herb_products (
product_id INTEGER PRIMARY KEY,
ingredient_code VARCHAR(10), -- 성분코드 (FK)
product_code VARCHAR(9), -- 보험코드 9자리
company_name VARCHAR(200), -- "휴먼허브"
product_name VARCHAR(200) -- "휴먼감초"
)
-- 역할: 성분코드 ↔ 보험코드 매핑
-- 특징: 여러 회사가 같은 성분을 다른 코드로 판매
```
#### **herb_items** - 재고 관리 단위
```sql
CREATE TABLE herb_items (
herb_item_id INTEGER PRIMARY KEY,
insurance_code VARCHAR(20), -- 보험코드 (9자리)
herb_name VARCHAR(100),
ingredient_code VARCHAR(10) -- 일부만 보유 (28/31)
)
-- 역할: 우리가 실제 보유한 약재 목록
-- 현황: 총 31개 약재 보유
```
#### **inventory_lots** - 로트별 재고
```sql
CREATE TABLE inventory_lots (
lot_id INTEGER PRIMARY KEY,
herb_item_id INTEGER, -- FK to herb_items
quantity_onhand REAL, -- 현재 재고량(g)
unit_price_per_g REAL, -- g당 단가
origin_country TEXT, -- 원산지
expiry_date DATE -- 유효기간
)
-- 역할: 실제 재고 수량 관리
-- 특징: FIFO 소비, 로트별 추적
```
### 2.3 효능 관리 (Efficacy System)
#### **herb_efficacy_tags** - 효능 태그 마스터
```sql
CREATE TABLE herb_efficacy_tags (
tag_id INTEGER PRIMARY KEY,
tag_name VARCHAR(50) UNIQUE, -- "보혈", "활혈", "청열"
tag_category VARCHAR(30), -- "보익", "거사", "조리"
description TEXT
)
-- 역할: 18개 표준 효능 태그 정의
```
#### **herb_item_tags** - 약재-태그 매핑 ⭐ 개선됨!
```sql
CREATE TABLE herb_item_tags (
item_tag_id INTEGER PRIMARY KEY,
ingredient_code VARCHAR(10), -- 성분코드 직접 사용! (개선)
tag_id INTEGER,
strength INTEGER DEFAULT 3, -- 효능 강도 (1-5)
UNIQUE(ingredient_code, tag_id)
)
-- 이전: herb_id 사용 (복잡한 JOIN 필요)
-- 현재: ingredient_code 직접 사용 (간단!)
```
### 2.4 처방 및 조제 (Prescriptions & Compounding)
#### **formulas** - 처방 마스터
```sql
CREATE TABLE formulas (
formula_id INTEGER PRIMARY KEY,
formula_name VARCHAR(100), -- "십전대보탕"
formula_name_hanja VARCHAR(100), -- "十全大補湯"
je_count INTEGER -- 기준 제수
)
```
#### **formula_ingredients** - 처방 구성
```sql
CREATE TABLE formula_ingredients (
formula_id INTEGER,
ingredient_code VARCHAR(10), -- 성분코드 사용
grams_per_cheop REAL -- 첩당 용량
)
```
#### **compounds** - 조제 기록
```sql
CREATE TABLE compounds (
compound_id INTEGER PRIMARY KEY,
patient_id INTEGER,
formula_id INTEGER,
is_custom BOOLEAN, -- 가감방 여부
custom_details TEXT, -- 가감 내용
total_cost REAL,
compound_date DATETIME
)
```
#### **compound_consumptions** - 소비 내역
```sql
CREATE TABLE compound_consumptions (
compound_id INTEGER,
herb_item_id INTEGER,
lot_id INTEGER,
quantity_used REAL, -- 사용량(g)
unit_cost_per_g REAL,
cost_amount REAL
)
-- 특징: 복합 로트 지원 (한 약재에 여러 로트 사용 가능)
```
## 🔄 3. 데이터 흐름
### 3.1 입고 프로세스
```
1. Excel 업로드 (한의사랑 카탈로그)
2. herb_products 매칭 (보험코드 기준)
3. purchase_receipts 생성 (입고 헤더)
4. purchase_receipt_lines 생성 (입고 상세)
5. inventory_lots 생성 (로트별 재고)
6. stock_ledger 기록 (재고 원장)
```
### 3.2 조제 프로세스
```
1. 처방 선택 (formulas)
2. 구성 약재 로드 (formula_ingredients)
3. 재고 매핑 (herb_items + inventory_lots)
4. 가감 여부 확인 (원방 vs 현재 구성 비교)
5. 로트 선택 (자동 FIFO 또는 수동 배분)
6. compounds 생성 (조제 기록)
7. compound_consumptions 생성 (소비 내역)
8. inventory_lots 차감 (재고 감소)
9. stock_ledger 기록 (원장 업데이트)
```
### 3.3 효능 태그 조회 (개선된 JOIN)
#### Before (복잡했던 구조):
```sql
-- 5단계 JOIN 필요
FROM herb_items h
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_masters hm ON hp.ingredient_code = hm.ingredient_code
LEFT JOIN herb_master_extended hme ON hm.ingredient_code = hme.ingredient_code
LEFT JOIN herb_item_tags hit ON hme.herb_id = hit.herb_id -- herb_id 찾기
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
```
#### After (개선된 구조):
```sql
-- 3단계 JOIN으로 단순화!
FROM herb_items h
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
LEFT JOIN herb_efficacy_tags het ON hit.tag_id = het.tag_id
```
## 🎯 4. 핵심 매핑 관계
### 4.1 코드 체계 매핑
```
보험코드(9자리) → 성분코드(10자리)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
062400740 → 3007H1AHM (감초)
062403450 → 3105H1AHM (당귀)
A001300 → 3400H1AHM (인삼)
* herb_products 테이블이 중개 역할
```
### 4.2 재고 관계
```
herb_items (31개)
↓ 1:N
inventory_lots (여러 로트)
↓ 1:N
compound_consumptions (소비 기록)
```
### 4.3 처방 관계
```
formulas (처방)
↓ 1:N
formula_ingredients (구성 약재)
↓ ingredient_code
herb_masters (약재 마스터)
```
## 📈 5. 주요 통계 쿼리
### 5.1 재고 현황 요약
```sql
-- 주성분코드 기준 보유율
SELECT
COUNT(DISTINCT m.ingredient_code) as _약재,
COUNT(DISTINCT CASE WHEN inv.total > 0 THEN m.ingredient_code END) as _약재,
ROUND(COUNT(DISTINCT CASE WHEN inv.total > 0 THEN m.ingredient_code END) * 100.0 /
COUNT(DISTINCT m.ingredient_code), 1) as
FROM herb_masters m
LEFT JOIN ( ) inv ON m.ingredient_code = inv.ingredient_code
```
### 5.2 효능별 약재 검색
```sql
-- 간단해진 쿼리!
SELECT DISTINCT h.*, GROUP_CONCAT(et.tag_name)
FROM herb_items h
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
WHERE et.tag_name IN ('보혈', '활혈')
GROUP BY h.herb_item_id
```
## 🚀 6. 최근 개선사항 (2026-02-17)
### 6.1 효능 태그 시스템 리팩토링
- **문제**: `herb_id`를 통한 복잡한 JOIN
- **해결**: `ingredient_code` 직접 사용
- **효과**: JOIN 단계 5개 → 3개로 감소
### 6.2 가감방 감지 시스템
- **구현**: 실시간 처방 변경 감지
- **방식**: `ingredient_code` 기준 비교
- **UI**: 가감방 배지 자동 표시
### 6.3 복합 로트 시스템
- **기능**: 한 약재에 여러 로트 사용 가능
- **UI**: 수동 로트 배분 모달
- **검증**: 재고량 실시간 체크
## 📝 7. 주의사항
### 7.1 ID 체계 혼동 주의
```
⚠️ herb_item_id ≠ herb_id
- herb_item_id: herb_items의 PK (재고 관리)
- herb_id: herb_master_extended의 PK (단순 인덱스)
- 실제 KEY: ingredient_code (성분코드)
```
### 7.2 코드 매핑 순서
```
1. 보험코드로 입력받음 (9자리)
2. herb_products에서 ingredient_code 찾기
3. ingredient_code로 모든 정보 연결
```
### 7.3 재고 없는 약재 처리
```
- herb_items에 없어도 herb_masters에는 존재
- 효능 정보는 ingredient_code 기준
- UI에서 재고 0으로 표시
```
## 🔧 8. 개발 가이드
### 8.1 새 약재 추가 시
```python
# 1. herb_masters에 성분코드 확인
# 2. herb_products에 보험코드 매핑 추가
# 3. herb_items에 재고 단위 생성
# 4. 입고 처리로 inventory_lots 생성
```
### 8.2 효능 태그 추가 시
```python
# 간단해진 방식!
INSERT INTO herb_item_tags (ingredient_code, tag_id, strength)
VALUES ('3400H1AHM', 1, 5) -- 인삼에 보기(5) 추가
```
### 8.3 API 개발 시
```python
# 항상 ingredient_code 중심으로 JOIN
# herb_products 테이블 활용
# COALESCE로 안전하게 처리
```
## 📚 9. 관련 문서
- [조제 프로세스 및 커스텀 처방](./조제_프로세스_및_커스텀_처방.md)
- [복합 로트 사용 분석](./복합_로트_사용_분석.md)
- [한약재 정보 관리 시스템 설계](./한약재_정보_관리_시스템_설계.md)
---
*이 문서는 시스템의 핵심 데이터 구조와 흐름을 설명합니다.*
*질문이나 수정사항은 개발팀에 문의해주세요.*

View File

@@ -0,0 +1,128 @@
# Excel 입고 시 보험코드 매핑 문제 분석
## 현상
Excel 파일에서 입고 처리 시, 보험코드가 제대로 매핑되지 않는 문제 발생
## 문제 원인
### 1. 보험코드 형식
- **표준 보험코드**: 9자리 문자열 (예: `060600420`, `062401050`)
- 일부 코드는 0으로 시작함
### 2. Excel 읽기 문제
```python
# 현재 상황
Excel 파일의 제품코드 컬럼 pandas가 int64로 자동 인식
060600420 60600420 (앞의 0 사라짐)
062401050 62401050 (앞의 0 사라짐)
```
### 3. DB 매핑 실패
- DB의 `herb_items.insurance_code`: `"060600420"` (9자리 문자열)
- Excel에서 읽은 값: `60600420` (8자리 숫자)
- **결과**: 매칭 실패 → 새로운 herb_item 생성
## 현재 코드 분석
### excel_processor.py (19번 줄)
```python
HANISARANG_MAPPING = {
'품목명': 'herb_name',
'제품코드': 'insurance_code', # 여기서 매핑은 되지만 타입 처리 안함
...
}
```
### app.py (577-589번 줄)
```python
# 약재 확인/생성
cursor.execute("""
SELECT herb_item_id FROM herb_items
WHERE insurance_code = ? OR herb_name = ?
""", (row.get('insurance_code'), row['herb_name'])) # insurance_code가 숫자로 들어옴
if not herb:
# 매칭 실패 시 새로 생성 (잘못된 코드로)
cursor.execute("""
INSERT INTO herb_items (insurance_code, herb_name)
VALUES (?, ?)
""", (row.get('insurance_code'), row['herb_name']))
```
## 해결 방안
### 방안 1: Excel 읽을 때 문자열로 처리 (권장)
```python
# excel_processor.py 수정
def read_excel(self, file_path):
try:
# 제품코드를 문자열로 읽도록 dtype 지정
self.df_original = pd.read_excel(
file_path,
dtype={'제품코드': str, 'insurance_code': str}
)
# 또는 converters 사용
# converters={'제품코드': lambda x: str(x).zfill(9)}
```
### 방안 2: 처리 시 9자리로 패딩
```python
# excel_processor.py의 process_hanisarang/process_haninfo 메소드에서
if 'insurance_code' in df_mapped.columns:
# 숫자로 읽힌 경우 9자리로 패딩
df_mapped['insurance_code'] = df_mapped['insurance_code'].apply(
lambda x: str(int(x)).zfill(9) if pd.notna(x) and str(x).isdigit() else x
)
```
### 방안 3: app.py에서 매핑 시 보정
```python
# app.py의 upload_purchase_excel 함수에서
insurance_code = row.get('insurance_code')
if insurance_code and str(insurance_code).isdigit():
# 숫자인 경우 9자리로 패딩
insurance_code = str(int(insurance_code)).zfill(9)
cursor.execute("""
SELECT herb_item_id FROM herb_items
WHERE insurance_code = ? OR herb_name = ?
""", (insurance_code, row['herb_name']))
```
## 영향 범위
### 이미 잘못 등록된 데이터
```sql
-- 잘못된 보험코드로 등록된 herb_items 확인
SELECT herb_item_id, herb_name, insurance_code
FROM herb_items
WHERE length(insurance_code) = 8
AND insurance_code NOT LIKE 'A%';
```
### 수정 필요 테이블
1. `herb_items` - insurance_code 수정
2. `purchase_receipt_lines` - 잘못된 herb_item_id 참조
3. `inventory_lots` - 잘못된 herb_item_id 참조
## 권장 해결 순서
1. **즉시 조치**: `excel_processor.py` 수정하여 제품코드를 문자열로 읽도록 처리
2. **데이터 정리**: 기존 잘못 등록된 herb_items 정리
3. **검증 로직 추가**: 보험코드 형식 검증 (9자리 또는 특정 패턴)
4. **테스트**: 샘플 파일로 입고 처리 테스트
## 추가 고려사항
1. **보험코드 형식 표준화**
- 9자리 숫자: `060600420`
- 영문+숫자: `A001100`
- 기타 형식 확인 필요
2. **Excel 파일 형식 가이드**
- 제품코드를 텍스트 형식으로 저장하도록 안내
- 또는 '060600420 형태로 입력 (앞에 ' 추가)
3. **중복 방지**
- 같은 약재가 다른 insurance_code로 중복 등록되는 것 방지
- 약재명 + 제조사로 추가 검증

View File

@@ -0,0 +1,167 @@
# 복합 로트 사용 기능 구현 분석
## 1. 현재 시스템 구조
### 1.1 좋은 소식 - 이미 지원 가능한 구조
현재 `compound_consumptions` 테이블은 **이미 복합 로트를 지원할 수 있는 구조**입니다:
```sql
compound_consumptions (
consumption_id INTEGER PRIMARY KEY,
compound_id INTEGER,
herb_item_id INTEGER,
lot_id INTEGER,
quantity_used REAL,
unit_cost_per_g REAL,
cost_amount REAL
)
```
**핵심 포인트:**
- 한 조제(`compound_id`)에서 같은 약재(`herb_item_id`)에 대해 여러 레코드 생성 가능
- 각 레코드는 다른 `lot_id`를 가질 수 있음
- 즉, **DB 구조 변경 없이** 복합 로트 사용 가능
### 1.2 현재 백엔드 로직
`app.py`의 조제 생성 로직을 보면:
```python
# 이미 FIFO 방식으로 여러 로트를 순차 소비하는 로직이 구현되어 있음
for lot in available_lots:
lot_id = lot[0]
available = lot[1]
unit_price = lot[2]
used = min(remaining_qty, available)
# 각 로트별로 별도의 소비 레코드 생성
cursor.execute("""
INSERT INTO compound_consumptions (compound_id, herb_item_id, lot_id,
quantity_used, unit_cost_per_g, cost_amount)
VALUES (?, ?, ?, ?, ?, ?)
""", (compound_id, herb_item_id, lot_id, used, unit_price, cost))
```
**즉, 백엔드는 이미 복합 로트를 지원하고 있습니다!**
## 2. 필요한 개선 사항
### 2.1 프론트엔드 UI/UX 개선
현재 문제는 **프론트엔드에서 복합 로트 선택을 지원하지 않는 것**입니다.
#### 현재 상태:
- 약재별로 "자동 선택" 또는 단일 원산지/로트만 선택 가능
- 수동으로 여러 로트를 조합할 수 없음
#### 개선 방안:
1. **자동 모드 (현재 구현됨)**
- FIFO 방식으로 자동 할당
- 재고가 부족하면 다음 로트에서 자동 보충
2. **수동 모드 (구현 필요)**
- 약재별로 "로트 배분" 버튼 추가
- 모달 창에서 사용 가능한 로트 목록 표시
- 각 로트별 사용량 수동 입력
- 예: 로트A 40g + 로트B 60g = 총 100g
### 2.2 API 개선
현재 API 구조:
```javascript
{
"herb_item_id": 52,
"grams_per_cheop": 4.8,
"origin": "auto", // 또는 특정 원산지
"lot_assignments": [] // 현재 미사용
}
```
개선된 API 구조:
```javascript
{
"herb_item_id": 52,
"grams_per_cheop": 4.8,
"origin": "auto" | "manual",
"lot_assignments": [ // manual일 때 사용
{"lot_id": 123, "quantity": 40},
{"lot_id": 124, "quantity": 60}
]
}
```
## 3. 구현 방안
### 3.1 최소 변경 방안 (권장)
**DB 스키마 변경 없이** 프론트엔드와 백엔드 로직만 개선:
1. **백엔드 (app.py)**
- `origin: "manual"`일 때 `lot_assignments` 배열 처리
- 지정된 로트별로 소비 처리
- 검증: 총량 일치 확인, 재고 충분 확인
2. **프론트엔드 (app.js)**
- 로트 배분 모달 추가
- 사용 가능 로트 목록 표시 (재고, 단가, 원산지 정보)
- 로트별 사용량 입력 UI
- 실시간 합계 및 검증
### 3.2 영향도 분석
#### 영향 없는 부분:
- ✅ DB 스키마 (변경 불필요)
- ✅ 재고 관리 로직
- ✅ 원가 계산 로직
- ✅ 입출고 원장
- ✅ 조제 내역 조회
#### 수정 필요한 부분:
- ⚠️ 조제 생성 API (`/api/compounds` POST)
- ⚠️ 프론트엔드 조제 화면
- ⚠️ 로트 가용성 확인 API (수동 모드 지원)
## 4. 구현 우선순위
### Phase 1: 백엔드 지원 (필수)
1. API에서 `lot_assignments` 처리 로직 추가
2. 수동 로트 배분 검증 로직
3. 트랜잭션 안전성 확보
### Phase 2: 프론트엔드 기본 (필수)
1. 로트 배분 모달 UI
2. 수동 입력 폼
3. 실시간 검증 및 피드백
### Phase 3: UX 개선 (선택)
1. 드래그 앤 드롭으로 로트 배분
2. 자동 최적화 제안 (단가 최소화, FIFO 등)
3. 로트 배분 히스토리 저장 및 재사용
## 5. 예상 시나리오
### 시나리오 1: 재고 부족으로 인한 복합 사용
- 감초 100g 필요
- 로트A(한국산): 40g 재고, 20원/g
- 로트B(중국산): 70g 재고, 15원/g
- 수동 배분: 로트A 40g + 로트B 60g
### 시나리오 2: 원가 최적화
- 당귀 100g 필요
- 로트A(구재고): 80g, 10원/g
- 로트B(신재고): 50g, 15원/g
- 원가 최적화: 로트A 80g + 로트B 20g
### 시나리오 3: 품질 균일성
- 인삼 100g 필요
- 같은 원산지의 다른 로트들 조합
- 품질 일관성 유지
## 6. 결론
**좋은 소식: 현재 시스템은 이미 복합 로트를 지원할 수 있는 구조입니다!**
- DB 스키마 변경 불필요
- 백엔드는 일부 로직 추가만 필요
- 주로 프론트엔드 UI/UX 개선이 필요
**구현 난이도: 중간**
- 기존 시스템에 미치는 영향 최소
- 점진적 구현 가능 (자동 모드는 이미 작동 중)
- 수동 모드는 선택적 기능으로 추가 가능

113
docs/셀프체크.md Normal file
View File

@@ -0,0 +1,113 @@
우리가 상담을 하기전에 사전조사가 필요해
카카오 알림톡이나 , 약국에 QR코드를 인쇄해놔서
환자 개인상태를 받고싶어
즉 모바일에서 1,2,3,4,5
도는 각각에 예시 값들이 있어서 정보를 받아서 DB에 저장하고싶어
일단은 먼저 예시 를 만들고싶어
일단 우리 페이지에 목업으로 이것을 화면을 만들어줘
아래를 읽고 불편한 곳에 표시하여 주십시오. 불편한점○ 극심한곳◎ 약간□ 가끔△ 자주M 불쾌× 전증상
추위
더위
심하게 탄다, 타는 편,약간 탄다 ,안탄다 , 겨울이 싫다.
심하게 탄다, 타는 편, 약간 탄다, 안탄다,선풍기에어컨 바람 ·
건조하다없다약간보통많다아주많다, , , , ,
얼굴몸전체손발하체머리이마겨드랑이, , , , , , ,
잘 때식사할 때긴장할 때여름에일할 때수시로, , , , ,
윗 배
아랫배
몸전체
매우차다,약간차다,보통,따뜻하다,뜨겁다,화끈거림,저림,쥐
매우차다,약간차다,보통,따뜻하다,뜨겁다,화끈거림,저림쥐,
매우차다,약간차다,보통,따뜻하다,뜨겁다 시림
매우차다,약간차다,보통,따뜻하다,뜨겁다복부비만,
매우차다,약간차다,보통,따뜻하다,뜨겁다,무겁다,아프다,부종
식성
기호
식 성
식사량
소화력
소화불량
찬 것시원한 것보통따뜻한 것뜨거운 것모두, , , , ,
신 것단 것매운 것짠 것쓴 것담백한 것모두, , , , , ,
된장채식육류해물커피술 주일 , , , , , 1 회담배 일 , 1 갑
많이 마심자주보통거의 안마심, , ,
식욕없다별로보통좋다왕성일 , , , , , 1 끼 먹음아침 생략,
적다보통많다공기이하이상일정치않다일정함저녁많이, , , 1 , , , ∙ ∙
잘된다보통약하다잘 안됨잘 체한다, , , , .
막힌듯함답답함걸린듯함더부룩함그득함, , , ,
속쓰림헛배부름가스참느글거림트림구토헛구역, , , , , ,
방귀꾸룩소리남명치아픔복통딸꾹질하품, , , , ,
대 변
대변상태
소변상태
소변빈도
소변색
소변이상
일 회매일아침불규칙음주, , , 다음날설사 회
변비,된편,굵다,토끼똥,설사,물변,보통,가늘다,퍼진다,냄새심
잘나옴잘안나옴시원치 않다오래봄힘들게나옴조금나옴, , , , ,
남아있는 듯 함조금씩 자주지, , 림못 참음,
자다가 회낮 , 회, 시간마다 회1
거의 안봄가끔보통자주매우자주밤에 오줌싼다, , , , ,
붉 다노랗다보 통탁하다맑 다커피색, , , , ,
거품이 남기름이 뜸단내남뿌옇다정액이 나옴, , , ,
수면상태
하루 시간, 시잠부족잠충분, ,
잘잠잘못잠거의못잠가끔못잠뒤척임, , , ,
곧잔다잠들기 어렵다깊이 잠옅은 잠잠귀밝음잘깸깨면 안옴, , , , , ,
밤새꿈자주꾼다가끔꿈거의없다안꾼다잠꼬대, , , , ,
무서운꿈죽은사람 꿈쫓기는 꿈개꿈기억안남기억남, , , , ,
심장
기울증상
전 신
가슴뜀가슴답답가슴뻐근한숨쉼호흡곤란숨참뒷목뻐근, , , , , ,
열달아오름 자주: ( 일 회가끔), ( 주 회)
잘놀람,불안,초조,우울,비관,신경질,짜증,매사귀찮다,손떨림,졸도
가슴막힌 듯조이는 듯기억력격감건망증현기증눈피로감, , , , ,
피로기운이 없다아침에 잘 못일어난다의욕이 없다무겁다, , , ,
결 혼
유 산
생리주기
생리량
생리통
냉대하
미혼, 결 혼 년, 출 산 회,불 임 년
유 산자연 ( 회인공 , 회제 왕 ), 회
자궁근종적 출 , 년전초산시난산순산, ( , )
일 간격정상부정확건넘중단폐경계속나옴, , , , , ,
일간 일 많고 일 적다늦어짐빨라짐, ,
너무많다, 많다 , 보통 , 약간적다 , 아주적다 , 줄어듬
검붉다검다일부덩어리찌꺼기묽다, , , ,
없다약간심하다극심생리전중, , , , ( ) ∙ 일부터 일간
아랫배허리허벅지가슴머리전신몸살과민, , , , , ,
없다약간많다심하다투명누렇다, , , , ,
희다묽다냄새악취가렵다, , , ,
피 부
성 품
보통약간황색약간검다희다창백누렇다약간붉다, , , , , ,
보통섬세얇다약간두텁다두텁다지성중성건성, , , , , , ,
○ 저돌적이다기세가 강하다남의 말을 잘 듣지 않는다, ,
거침이 없다독불장군이다안하무인이다뚜렷하다, , ,
○ 말과 행동 빠름, 음식을 빨리먹음 , 눈매예리, 날카롭다
부지런함적극적활동적소변자주본다일을 안미룬다 , , , ,
나 다니기를 좋아함분명하다나서기 잘함질투가 심하다 , , ,
○ 눕기 좋아함엉덩이 무겁다느긋함땀이많다과묵하다, , , ,
사람좋다무던하다부드럽다가정적이다씻기를 싫어함, , , ,
원만정중은근꾸준함우유부단된장쓴 것 좋아함, , , , , ,
○ 약해보인다겁이많음세심하다소심차분함자상함, , , , ,
연약영민잘미룬다깐깐치밀궁리는 많으나 실행은 적

View File

@@ -0,0 +1,66 @@
약재명 원산지 표준량(g) 조제량(g) g당 단가 단가 변경/삭제
숙지황(9증/1kg) 중국 4.8 g
4.8
g 37.8 원
181.4
원 변경 삭제
작약(백작약/의성) 경북(의성) 4.8 g
4.8
g 35 원
168
원 변경 삭제
인삼(한국/4년30편/백삼) 충남(금산) 4.8 g
4.8
g 189.96 원
911.8
원 변경 삭제
백출(특) 중국 4.8 g
4.8
g 37.38 원
179.4
원 변경 삭제
황기(유피/토) 국산 4 g
4
g 56 원
224
원 변경 삭제
대추(대조/상/토/600g) 경북(경산) 4 g
4
g 27.45 원
109.8
원 변경 삭제
당귀(토) 경북(봉화) 4.8 g
4.8
g 37.38 원
179.4
원 변경 삭제
계피(육계/특/YB-2/거피) 베트남 4 g
4
g 17.92 원
71.7
원 변경 삭제
복령(백복령/설복) 중국 4.8 g
4.8
g 19.88 원
95.4
원 변경 삭제
감초(신강1호) 중국 4.8 g
4.8
g 30.38 원
145.8
원 변경 삭제
천궁(일) 경북(영양) 4.8 g
4.8
g 40.88 원
196.2
원 변경 삭제
생강(토/1KG) 충남(서산) 3 g
3
g 25.5 원
76.5
원 변경 삭제
소계
53.4
g
2539.548

View File

@@ -0,0 +1,180 @@
# 입고 프로세스 개선 방안
## 현재 구조 이해
### 3단계 데이터 계층
```
1. herb_masters (454개)
- 성분코드(ingredient_code) 기준 마스터
- 예: 3400H1AHM = 인삼
2. herb_products (53,769개)
- 성분코드별 보험코드 매핑 (참조 테이블)
- 예: 060600420 = 신흥인삼 → 성분코드 3400H1AHM
- 예: 060801010 = 세화인삼 → 성분코드 3400H1AHM
3. herb_items (40개)
- 실제 사용/재고 관리 단위
- ingredient_code + insurance_code 모두 보유
```
## 현재 문제점
### 1. Excel 입고 시 보험코드 처리 문제
- Excel에서 보험코드를 숫자로 읽음: `060600420``60600420`
- DB 매핑 실패 → 새로운 herb_item 생성 (중복/잘못된 데이터)
### 2. 성분코드 연결 누락
- 입고 시 보험코드만으로 herb_item 생성
- ingredient_code 연결 안 됨
- 성분코드 기준 재고 집계 불가
## 개선된 입고 프로세스
### 1단계: Excel 읽기 개선
```python
# excel_processor.py 수정
def read_excel(self, file_path):
# 제품코드를 문자열로 읽기
self.df_original = pd.read_excel(
file_path,
dtype={'제품코드': str}
)
def process_hanisarang/haninfo(self):
# 보험코드 9자리 패딩 처리
if 'insurance_code' in df_mapped.columns:
df_mapped['insurance_code'] = df_mapped['insurance_code'].apply(
lambda x: str(x).zfill(9) if pd.notna(x) and str(x).isdigit() else x
)
```
### 2단계: 보험코드 → 성분코드 매핑
```python
# app.py의 upload_purchase_excel 함수 수정
# 1. 보험코드로 herb_products에서 성분코드 찾기
insurance_code = str(row.get('insurance_code')).zfill(9) # 9자리 패딩
cursor.execute("""
SELECT DISTINCT ingredient_code, product_name, company_name
FROM herb_products
WHERE product_code = ?
""", (insurance_code,))
product_info = cursor.fetchone()
if product_info:
ingredient_code = product_info[0]
# 2. herb_items에서 해당 보험코드 제품 확인
cursor.execute("""
SELECT herb_item_id
FROM herb_items
WHERE insurance_code = ?
""", (insurance_code,))
herb_item = cursor.fetchone()
if not herb_item:
# 3. 새 제품 생성 (ingredient_code 포함!)
cursor.execute("""
INSERT INTO herb_items (
ingredient_code,
insurance_code,
herb_name,
specification
) VALUES (?, ?, ?, ?)
""", (
ingredient_code,
insurance_code,
product_info[1], # product_name
product_info[2] # company_name
))
herb_item_id = cursor.lastrowid
else:
herb_item_id = herb_item[0]
else:
# herb_products에 없는 경우 (비보험 약재 등)
# 기존 로직 유지 또는 경고
pass
```
## 성분코드 기준 재고 관리
### 재고 조회 쿼리
```sql
-- 성분코드별 통합 재고 조회
SELECT
hm.ingredient_code,
hm.herb_name as master_name,
hm.herb_name_hanja,
COUNT(DISTINCT hi.herb_item_id) as product_count,
COUNT(DISTINCT hi.insurance_code) as insurance_code_count,
COUNT(DISTINCT il.lot_id) as lot_count,
COALESCE(SUM(il.quantity_onhand), 0) as total_stock,
GROUP_CONCAT(DISTINCT hi.specification) as companies
FROM herb_masters hm
LEFT JOIN herb_items hi ON hm.ingredient_code = hi.ingredient_code
LEFT JOIN inventory_lots il ON hi.herb_item_id = il.herb_item_id
WHERE hm.is_active = 1
GROUP BY hm.ingredient_code
ORDER BY hm.herb_name;
```
### 조제 시 제품 선택
```sql
-- 성분코드로 가용 제품 조회
SELECT
hi.herb_item_id,
hi.insurance_code,
hi.herb_name as product_name,
hi.specification as company,
SUM(il.quantity_onhand) as available_stock
FROM herb_items hi
JOIN inventory_lots il ON hi.herb_item_id = il.herb_item_id
WHERE hi.ingredient_code = ?
AND il.quantity_onhand > 0
GROUP BY hi.herb_item_id
ORDER BY available_stock DESC;
```
## 구현 우선순위
### 1. 즉시 수정 (보험코드 문제 해결)
- [ ] excel_processor.py: 제품코드 문자열 처리
- [ ] app.py: 9자리 패딩 로직 추가
### 2. 데이터 정리
- [ ] 기존 잘못된 herb_items 정리
- [ ] ingredient_code 누락된 항목 업데이트
### 3. 프로세스 개선
- [ ] 입고 시 herb_products 참조하여 성분코드 자동 연결
- [ ] 성분코드 기준 재고 조회 API 추가
### 4. UI 개선
- [ ] 재고 현황을 성분코드 기준으로 표시
- [ ] 제품별 상세 보기 추가
## 기대 효과
1. **정확한 매핑**: 보험코드 → 성분코드 자동 연결
2. **통합 재고 관리**: 성분코드 기준으로 여러 제품의 재고 통합 관리
3. **유연한 조제**: 같은 성분의 다른 제품 선택 가능
4. **데이터 일관성**: 표준 코드 체계 준수
## 추가 고려사항
### 비보험 약재 처리
- herb_products에 없는 약재 입고 시
- 수동으로 성분코드 매핑 또는
- 별도 비보험 약재 테이블 관리
### 검증 로직
- 보험코드 형식 검증 (9자리 숫자)
- 중복 제품 생성 방지
- 성분코드 매핑 실패 시 경고
### 마스터 데이터 관리
- herb_products 정기 업데이트
- 신규 보험코드 추가 프로세스
- 성분코드 변경 이력 관리

View File

@@ -0,0 +1,164 @@
│ Plan to implement │
│ │
│ 입고장 수정 기능 구현 계획 │
│ │
│ Context │
│ │
│ Excel로 입고 시 유효기한, 로트번호 등을 입력할 수 없음. 기존 입고장을 수동입고 모달에 불러와 부족한 정보를 채워넣는 수정 기능이 필요함. │
│ │
│ 수정 파일 (2개) │
│ │
│ 1. /root/kdrug/app.py — 백엔드 (기존 PUT API 확장) │
│ │
│ 기존 PUT /api/purchase-receipts/<receipt_id>/lines/<line_id> 확장 (line 1171) │
│ - lot_number, expiry_date 필드를 purchase_receipt_lines UPDATE에 추가 │
│ - inventory_lots에도 lot_number, expiry_date UPDATE 추가 │
│ - 수량/단가 변경 없이 유효기한/로트번호만 추가하는 경우도 처리 가능하게 │
│ │
│ 새 엔드포인트: PUT /api/purchase-receipts/<receipt_id> │
│ - 입고장 헤더(receipt_date, supplier_id, notes) 수정 │
│ - 전체 라인을 일괄 수정하는 것은 복잡하므로, 헤더만 처리 │
│ - 라인 수정은 기존 라인별 PUT API를 활용 │
│ │
│ → 대안: 전체 일괄 수정 엔드포인트 PUT /api/purchase-receipts/<receipt_id>/bulk │
│ - 모달에서 전체 라인을 한번에 보내서 일괄 업데이트 │
│ - 각 라인의 line_id + 수정 필드를 배열로 받음 │
│ - 헤더(notes) + 라인(origin_country, lot_number, expiry_date, quantity_g, unit_price_per_g) 일괄 처리 │
│ - 이 방식 채택 — 모달에서 "저장" 한 번으로 끝나므로 UX가 좋음 │
│ │
│ 2. /root/kdrug/static/app.js — 프론트엔드 │
│ │
│ A. 입고장 목록에 "수정" 버튼 추가 (loadPurchaseReceipts 함수 내) │
│ - 기존 상세 버튼 옆에 수정 버튼 추가 │
│ - 클릭 시 editReceipt(receiptId) 호출 │
│ │
│ B. editReceipt(receiptId) 함수 추가 │
│ - GET /api/purchase-receipts/<receipt_id> 로 기존 데이터 로드 │
│ - 수동입고 모달을 "수정 모드"로 열기: │
│ - 모달 제목: "입고장 수정" │
│ - 입고일, 도매상: 기존 값 세팅 (도매상은 변경 불가 — 재고 참조 때문) │
│ - 비고: 기존 값 세팅 │
│ - 품목 테이블: 기존 라인 데이터로 행 채우기 (약재 select에 기존 값 세팅, 수량/단가/원산지/로트번호/유효기한 채우기) │
│ - 약재 select: 수정 모드에서는 disabled (약재 변경은 위험) │
│ - "품목 추가" 버튼, "삭제" 버튼: 수정 모드에서는 숨김 (라인 추가/삭제는 불가) │
│ - 저장 버튼 텍스트: "수정 저장" │
│ │
│ C. 저장 로직 분기 │
│ - 모달에 data-edit-mode, data-receipt-id 속성 관리 │
│ - 저장 시: edit mode이면 PUT /api/purchase-receipts/<receipt_id>/bulk 호출 │
│ - 성공 시 모달 닫기 + 입고장 목록 새로고침 │
│ │
│ 데이터 흐름 │
│ │
│ [입고장 목록 "수정" 클릭] → GET /api/purchase-receipts/:id │
│ → 수동입고 모달에 기존 데이터 로드 (수정 모드) │
│ → 유효기한/로트번호/원산지 등 수정 │
│ → [수정 저장] → PUT /api/purchase-receipts/:id/bulk │
│ → purchase_receipt_lines UPDATE (각 라인) │
│ → inventory_lots UPDATE (lot_number, expiry_date) │
│ → purchase_receipts UPDATE (notes, total_amount) │
│ │
│ 주의사항 │
│ │
│ - 약재 변경 불가 (재고 추적 무결성) │
│ - 이미 사용된 재고의 수량 변경은 기존 validation 유지 │
│ - 라인 추가/삭제는 지원하지 않음 (수정만) │
│ │
│ 검증 │
│ │
│ 1. Excel로 입고한 입고장에서 "수정" 클릭 │
│ 2. 모달에 기존 데이터가 채워져 있는지 확인 │
│ 3. 유효기한, 로트번호 입력 후 저장 │
│ 4. 상세보기에서 변경 확인 │
│ 5. 재고 상세에서 lot_number, expiry_date 표시 확인
데이터 흐름상 이게 어디 어디 테이블에 업데이트가 되는거야?
● bulk 수정 API의 데이터 흐름을 정리하면:
PUT /api/purchase-receipts/<receipt_id>/bulk
업데이트되는 테이블 3개 (+ 조건부 1개)
┌──────┬────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────┐
│ 순서 │ 테이블 │ 업데이트 필드 │ 조건 │
├──────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 1 │ purchase_receipt_lines │ quantity_g, unit_price_per_g, origin_country, lot_number, expiry_date, line_total │ 항상 (변경된 필드만) │
├──────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 2 │ inventory_lots │ lot_number, expiry_date, origin_country, unit_price_per_g, quantity_received, quantity_onhand │ lot_id가 있을 때 │
├──────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 3 │ stock_ledger │ INSERT (ADJUST 이벤트) │ 수량 변경 시에만 │
├──────┼────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┤
│ 4 │ purchase_receipts │ notes, total_amount, updated_at │ 항상 (헤더) │
└──────┴────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────┘
흐름도
모달에서 "수정 저장" 클릭
├─ 각 라인 반복 ──┐
│ ├─① purchase_receipt_lines UPDATE (수량/단가/원산지/로트/유효기한)
│ ├─② inventory_lots UPDATE (로트번호/유효기한/원산지/단가/수량)
│ └─③ stock_ledger INSERT (수량 변경 시만)
└─④ purchase_receipts UPDATE (비고, 총액 재계산)
즉 purchase_receipt_lines와 inventory_lots 양쪽 다 같은 값을 저장합니다. 이전에 유효기한이 안 보였던 이유가 inventory_lots에만 저장하고 purchase_receipt_lines에는 안 넣어서 조회 시 충돌이 났던 거였기 때문에, 이제는 양쪽 모두 업데이트합니다.
정리하면:
purchase_receipt_lines — 입고 전표 (장부용)
"이 입고장에 뭘 얼마나 샀는가"를 기록하는 구매 기록 테이블
┌─────────────────────┬──────────────────────────────────────────────────────────┐
│ 용도 │ 설명 │
├─────────────────────┼──────────────────────────────────────────────────────────┤
│ 입고장 상세 조회 │ 입고장 클릭 시 "무슨 약재를 몇g, g당 얼마에 샀는지" 표시 │
├─────────────────────┼──────────────────────────────────────────────────────────┤
│ 입고장 삭제 시 참조 │ 삭제 가능 여부 판단의 기준 │
├─────────────────────┼──────────────────────────────────────────────────────────┤
│ 금액 집계 │ line_total 합산 → 입고장 total_amount │
└─────────────────────┴──────────────────────────────────────────────────────────┘
→ 입고 이후에는 거의 안 씀. 입고장 조회/출력용 원본 기록.
---
inventory_lots — 재고 로트 (실물 추적용)
"실제 재고가 지금 얼마나 남아있는가"를 추적하는 핵심 재고 테이블
┌──────────────────────────┬─────────────────────────────────────────────────────┐
│ 용도 │ 설명 │
├──────────────────────────┼─────────────────────────────────────────────────────┤
│ 조제 시 차감 │ quantity_onhand 에서 사용량 차감, FIFO 순서로 소진 │
├──────────────────────────┼─────────────────────────────────────────────────────┤
│ 재고 현황 조회 │ 약재별 현재 재고량, 로트별 잔량 표시 │
├──────────────────────────┼─────────────────────────────────────────────────────┤
│ 재고 조정 │ 실사 후 수량 보정 │
├──────────────────────────┼─────────────────────────────────────────────────────┤
│ 재고 원장 (stock_ledger) │ 입고/출고/조정 이력의 참조 대상 │
├──────────────────────────┼─────────────────────────────────────────────────────┤
│ 로트 추적 │ lot_number, expiry_date, origin_country로 품질 관리 │
├──────────────────────────┼─────────────────────────────────────────────────────┤
│ 원가 계산 │ unit_price_per_g로 조제 원가 산출 │
└──────────────────────────┴─────────────────────────────────────────────────────┘
→ 입고 이후 모든 비즈니스 로직이 이 테이블을 참조.
---
한 줄 요약
purchase_receipt_lines = "이걸 샀다" (구매 기록, 장부)
inventory_lots = "이게 남아있다" (재고 실물, 운영)
둘 다 같은 입고 건에서 1:1로 생성되지만, 이후에 실제로 조제/재고조정/원장에서 사용하는 건 inventory_lots만입니다. 그래서 lot_number나 expiry_date도 inventory_lots에 저장된 값이 중요하고, bulk 수정 시 양쪽 다 업데이트하도록 해둔 겁니다.
✻ Churned for 1m 0s
──────────────────────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,190 @@
# 제품 3단계 분류 체계
> 한약재는 **성분 → 제품 → 로트** 3단계로 식별된다.
> 같은 성분코드의 약재도 제조사, 형태, 산지, 등급에 따라 품질과 가격이 크게 다르다.
---
## 1. 3단계 구조
```
[1단계] 성분 (herb_masters) — 약재의 본질
└─ ingredient_code: 3002H1AHM = "갈근"
[2단계] 제품 (herb_items) — 도매상별 상품
└─ insurance_code: 062401050 = "휴먼갈근" (주식회사휴먼허브)
[3단계] 로트 (inventory_lots) — 입고 건별 실물
└─ lot_id: 190 = "갈근.각" (한국산, ₩17/g)
```
### 테이블 매핑
| 단계 | 테이블 | PK | 식별키 | 행수 | 예시 |
|------|--------|-----|-------|------|------|
| 성분 | `herb_masters` | herb_id | `ingredient_code` | 454 | 갈근 (3002H1AHM) |
| 제품 | `herb_items` | herb_item_id | `insurance_code` | ~30 | 휴먼갈근 (062401050) |
| 로트 | `inventory_lots` | lot_id | receipt_line_id | ~30 | 갈근.각 (한국, ₩17) |
### herb_items 컬럼 역할
| 컬럼 | 실제 내용 | 예시 |
|------|----------|------|
| `herb_name` | 제품명 (품명) | "휴먼갈근" |
| `insurance_code` | 보험코드 (=product_code) | "062401050" |
| `ingredient_code` | 성분코드 (herb_masters FK) | "3002H1AHM" |
| `specification` | 제조사명 | "주식회사휴먼허브" |
---
## 2. 로트 세부 분류 (display_name + lot_variants)
### 2-1. `inventory_lots.display_name`
**엑셀 입고 시 NULL로 들어감**. AI가 쇼핑몰/카탈로그 정보를 참고하여 사후에 채워넣는 값.
도매상 카탈로그의 실제 품명으로, 같은 제품(herb_items)이라도 로트마다 다를 수 있다:
| herb_name (제품) | display_name (로트) | 차이점 |
|-----------------|-------------------|--------|
| 휴먼건강 | 건강 | 페루산, ₩12/g |
| 휴먼건강 | 건강.土 | 한국산(토종), ₩51/g |
| 휴먼일당귀 | 일당귀(한국산) | 한국산, ₩19/g |
| 휴먼일당귀 | 일당귀.中(1kg) | 중국산 중품, ₩13/g |
### 2-2. `lot_variants` 테이블 (파싱 결과)
`display_name`을 구조화된 필드로 파싱한 결과를 저장:
| 컬럼 | 용도 | 파싱 예시 |
|------|------|----------|
| `raw_name` | 원본 품명 | "건강.土" |
| `form` | 형태 | 각(角), 片(편), 절편, 통 |
| `processing` | 포제/가공 | 초(炒), 자(炙), 酒炙, 9증 |
| `selection_state` | 선별/원산 | 土(토종), 正, 中, 재배, 야생 |
| `grade` | 등급 | 1호, 특, 名品, 소(小) |
| `age_years` | 연근 | 4, 6 (년근) |
---
## 3. display_name 명명 패턴 (도매상 기준)
### 기본 구조
```
약재명.형태[등급](포장단위)
약재명.선별<유통경로>(포장단위)[비고]
```
### 실제 패턴 분석 (30개 로트)
| display_name | 약재 | form | processing | selection | grade |
|-------------|------|------|-----------|-----------|-------|
| `갈근.각` | 갈근 | 각(角) | - | - | - |
| `감초.1호[야생](1kg)` | 감초 | - | - | 야생 | 1호 |
| `건강` | 건강 | - | - | - | - |
| `건강.土` | 건강 | - | - | 土(토종) | - |
| `길경.片[특]` | 길경 | 片(편) | - | - | 특 |
| `세신.中` | 세신 | - | - | 中(중품) | - |
| `백출.당[1kg]` | 백출 | - | - | 당(當) | - |
| `작약주자.土[酒炙]` | 작약주자 | - | 酒炙 | 土(토종) | - |
| `숙지황(9증)(신흥.1kg)[완]` | 숙지황 | - | 9증 | - | 완 |
| `육계.YB` | 육계 | - | - | YB | - |
| `진피.비열[非熱](1kg)` | 진피 | - | 非熱(비열) | - | - |
| `창출[북창술.재배](1kg)` | 창출 | - | - | 재배 | - |
| `천궁.일<토매지>(1kg)` | 천궁 | - | - | 일(日) | - |
| `황기(직절.小)(1kg)` | 황기 | 직절 | - | - | 小 |
| `용안육.名品(1kg)` | 용안육 | - | - | - | 名品 |
| `오미자<토매지>(1kg)` | 오미자 | - | - | - | - |
| `전호[재배]` | 전호 | - | - | 재배 | - |
| `지황.건[회](1kg)` | 지황 | - | 건(乾) | 회(灰) | - |
### 구분자 규칙
| 구분자 | 의미 | 예시 |
|--------|------|------|
| `.` | 주 속성 구분 | `건강.土` → 토종 |
| `[...]` | 부가 정보/등급 | `길경.片[특]` → 특등 |
| `(...)` | 포장/가공/산지 | `대추(절편)(1kg)` |
| `<...>` | 유통경로 | `오미자<토매지>(1kg)` |
---
## 4. AI가 display_name / lot_variants를 채우는 절차
### 언제 실행하는가
1. 엑셀 입고 완료 후 (`inventory_lots.display_name = NULL`)
2. 도매상 쇼핑몰에서 해당 품목 정보를 AI에게 제공
3. AI가 정보를 파싱하여 `display_name` + `lot_variants` 업데이트
### Step 1: NULL인 로트 확인
```sql
SELECT il.lot_id, h.herb_name, h.insurance_code, il.origin_country,
il.unit_price_per_g, il.quantity_received
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE il.display_name IS NULL AND il.is_depleted = 0;
```
### Step 2: 쇼핑몰/카탈로그 정보 참고
사용자가 도매상 쇼핑몰에서 해당 제품의 상세 품명을 제공하면,
AI가 **가격, 단가, 원산지, 포장단위** 등을 교차 참고하여 올바른 로트에 매칭.
참고 가능한 매칭 단서:
- **보험코드** (insurance_code) — 제품 특정
- **원산지** (origin_country) — 같은 제품의 로트 구분
- **단가** (unit_price_per_g) — 등급/선별 구분 (土 > 일반, 한국산 > 중국산)
- **수량** (quantity_received) — 포장 단위 매칭
### Step 3: display_name 업데이트
```sql
UPDATE inventory_lots
SET display_name = '건강.土'
WHERE lot_id = 193;
```
### Step 4: lot_variants 파싱 결과 저장
```sql
INSERT INTO lot_variants (lot_id, raw_name, form, processing, selection_state, grade, parsed_method)
VALUES (193, '건강.土', NULL, NULL, '', NULL, 'ai_parsing');
```
### Step 5: 검증
```sql
SELECT il.lot_id, il.display_name, h.herb_name, il.origin_country,
lv.form, lv.processing, lv.selection_state, lv.grade
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
LEFT JOIN lot_variants lv ON il.lot_id = lv.lot_id
WHERE il.lot_id = 193;
```
---
## 5. 활용처
| 화면 | 사용 값 | 표시 예시 |
|------|---------|----------|
| 입고장 상세 | `display_name` | 갈근.각, 건강.土 |
| 재고 상세 모달 | `display_name` + `origin_country` | 건강.土 (한국) |
| 조제 시 로트 선택 | `display_name` + `unit_price` | 건강.土 ₩51/g vs 건강 ₩12/g |
| 재고 원장 | `display_name` | 입출고 이력에 로트 구분 |
---
## 6. 주의사항
1. **엑셀 입고 로직은 수정하지 않는다**`display_name`은 항상 NULL로 입고
2. **AI가 사후에 채운다** — 쇼핑몰 정보 기반, `parsed_method = 'ai_parsing'`
3. **같은 herb_item_id에 여러 display_name 가능** — 로트마다 다른 실물이므로 정상
4. **lot_variants 파싱이 안 되어도 display_name만으로 구분 가능** — 파싱은 선택적
---
*이 문서는 kdrug 시스템의 약재 3단계 분류 체계와 AI 기반 로트 분류 절차를 정의합니다.*
*최종 수정: 2026-02-18*

333
docs/제품_세부분류.md Normal file
View File

@@ -0,0 +1,333 @@
우리는
성분코드아래
보험코드로 묶여있고
지금 "건강' 제품 같은경우에는
우리가 조제 할때 선택을 , 국산과 페루산 등으로 원산지를 구분해서
선택할수 잇게 되어있어,
이게 우리가 입고를 할때 엑셀에서 , 구분값으로
우리가 보험코드를 사용하지만
국산과 수입품이 모두 같은 보험코드를 사용하기때문에,
우리는 성분을 바라보고 , "쌍화탕" 을 만들때
성분코드 기준으로 , 첩재를 만들게 되게 설계되어있을꺼고
성분코드에 따른 재고를 선택하지만, 성분 코드 아래 , 우리가
국산과, 수입품을 구분해서 롯트를 잡아뒀기때문에 이게 가능한거같아 맞는지 확인해
우리가 입고장 엑셀에 국산./해외품 구분밖에없지만,
우리는 추가적으로,
자 내가 주문한 데이터야
갈근.각
5 배송중 42,000 400 0
롯데택배
256733159384
배송조회
감초.1호[야생](1kg)
5 배송중 110,500 0
건강
10 배송중 62,000 600
건강.土
3 배송중 77,100 750
계지
5 배송중 14,500 100
구기자(영하)(1kg)
3 배송중 53,700 510
길경.片[특]
3 배송중 15,900 0
대추(절편)(1kg)
5 배송중 100,000 1,000
마황(1kg)
5 배송중 48,000 0
반하생강백반제(1kg)
3 배송중 101,100 990
백출.당[1kg]
2 배송중 23,600 0
복령(1kg)
5 배송중 57,500 550
석고[통포장](kg)
4 배송중 18,800 160
세신.中
3 배송중 193,500 0
숙지황(9증)(신흥.1kg)[완]
5 배송중 100,000 1,000
오미자<토매지>(1kg)
2 배송중 35,000 340
용안육.名品(1kg)
3 배송중 62,100 600
육계.YB2
5 배송중 36,500 350
일당귀.中(1kg)
5 배송중 64,500 600
자소엽.土
3 배송중 20,700 180
작약(1kg)
3 배송중 56,100 540
작약주자.土[酒炙]
3 배송중 36,900 360
전호[재배]
3 배송중 21,000 210
지각
3 배송중 15,000 150
지황.건[](1kg)
1 배송중 11,500 110
진피.비열[非熱](1kg)
5 배송중 68,500 0
창출[북창출.재배](1kg)
3 배송중 40,500 0
천궁.일<토매지>(1kg)
3 배송중 35,700 330
황기(직절.小)(1kg)
3 배송중 29,700 270
우리가 variant 값으로
원산지
형태
가공
선별상태
등급
년생 ← 추가 (중요)
을 넣는다고했을때 , 제품에따라 제공해주기도하고 제공해주지 않기도해
위에 내용처럼, 따라서 db에서 모두 값을 넣을 필요가없고 있는것만 구분함녀되
어차피 성품코드는 고정적여 즉 년생이나 형태가 달라도 성분코드는 동일하고 그아래 구분값으로 달리는거야 내가 제공해준것들 기준으로 목업을 만들어보자
컬럼
타입
설명
variant_id
PK
내부키
herb_id
FK
herb_master 참조
raw_name
TEXT
쇼핑몰 표기 그대로(예: “진피.비열非熱”)
origin
TEXT NULL
원산지(있으면)
form
TEXT NULL
형태(토/편/각/절편/직절 등)
processing
TEXT NULL
가공(비열/주자/9증/회 등)
selection_state
TEXT NULL
선별상태(정선/토매지/재배/야생 등)
grade
TEXT NULL
등급(특/명품/중/소/1호/YB2/당 등)
age_years
INT NULL
년생(있으면)
unit
TEXT NULL
포장단위(1kg 등)
B. herb_variant (옵션/변형값)
• 원산지/형태/가공/선별/등급/년생은 NULL 허용
• raw_name 저장 필수
로요약하자면
우리가 엑셀로 받아오는 정보에는
성분코드까지만 잇어, 즉 세부적으로
갈라지는 부분을 커버할수가없어
하지만 일단 엑셀 입고장에서, 도매상이
일단은 열을 분리해줄꺼기 때문에 우리가 입고처리 이후에 세부구분을 수동혹은 ai를 통해하거나 enum값을 이용해서 로트에서 처리해주고싶어
즉 지금 처럼 입고 처리를 하고 그뒤에 그것에 더분을 더붙이고
우리가 나중에, 조제할때 제품을 선택잘할수있게 보여주이싶어
raw_name
herb_name_std
form
processing
selection_state
grade
unit
갈근.각
갈근
NULL
NULL
NULL
NULL
감초.1호야생
감초
NULL
NULL
야생
1호
1kg
건강
건강
NULL
NULL
NULL
NULL
NULL
건강.土
건강
NULL
NULL
NULL
NULL
계지
계지
NULL
NULL
NULL
NULL
NULL
구기자(영하)(1kg)
구기자
NULL
NULL
NULL
영하
1kg
길경.片[특]
길경
NULL
NULL
NULL
대추(절편)(1kg)
대추
절편
NULL
NULL
NULL
1kg
마황(1kg)
마황
NULL
NULL
NULL
NULL
1kg
반하생강백반제(1kg)
반하생강백반제
NULL
생강백반제
NULL
NULL
1kg
백출.당[1kg]
백출
NULL
NULL
NULL
1kg
복령(1kg)
복령
NULL
NULL
NULL
NULL
1kg
석고통포장
석고
NULL
통포장
NULL
NULL
kg
세신.中
세신
NULL
NULL
NULL
NULL
숙지황(9증)(신흥.1kg)[완]
숙지황
NULL
9증
NULL
1kg
오미자<토매지>(1kg)
오미자
NULL
NULL
토매지
NULL
1kg
용안육.名品(1kg)
용안육
NULL
NULL
NULL
名品
1kg
육계.YB2
육계
NULL
NULL
NULL
YB2
NULL
일당귀.中(1kg)
일당귀
NULL
NULL
NULL
1kg
자소엽.土
자소엽
NULL
NULL
NULL
NULL
작약(1kg)
작약
NULL

View File

@@ -0,0 +1,122 @@
# 조제 용도 구분 (usage_type)
> 조제된 약은 반드시 판매 목적이 아닐 수 있다.
> 자가소비, 샘플 제공, 폐기 등 **재고는 차감되지만 매출이 아닌 경우**를 구분한다.
---
## 1. 기존 status와의 차이
| 구분 | `status` | `usage_type` |
|------|----------|-------------|
| 역할 | 판매 진행 **흐름 상태** | 조제의 **용도 분류** |
| 변화 | 시간에 따라 전이 (PREPARED → PAID → COMPLETED) | 조제 시점에 결정, 사후 변경 가능 |
| 예시 | "이 조제는 결제 완료 상태" | "이 조제는 자가소비 용도" |
**핵심**: `status``usage_type`은 서로 다른 차원. 자가소비도 `PREPARED → COMPLETED` 상태 흐름을 탈 수 있다.
---
## 2. usage_type 값 정의
| 값 | 한글명 | 설명 | 매출 포함 | 판매가 |
|----|--------|------|:---------:|--------|
| `SALE` | 판매 | 환자에게 판매 (기본값) | O | 설정 가격 |
| `SELF_USE` | 자가소비 | 약국 자체 사용 | X | 0원 |
| `SAMPLE` | 샘플 | 시음/샘플 제공 | X | 0원 |
| `DISPOSAL` | 폐기 | 유통기한 초과 등 폐기 처리 | X | 0원 |
---
## 3. DB 스키마
```sql
-- compounds 테이블에 추가된 컬럼
ALTER TABLE compounds ADD COLUMN usage_type TEXT DEFAULT 'SALE';
```
- 기본값 `'SALE'` — 기존 데이터 호환
- `COALESCE(usage_type, 'SALE')` 패턴으로 NULL 안전 처리
---
## 4. 동작 규칙
### 4-1. 조제 생성 시
- 조제 실행 폼에서 **용도** 드롭다운 선택 (기본: 판매)
- `SALE`이 아닌 용도 → `sell_price_total = 0` 자동 설정
- 재고 차감은 용도에 관계없이 **항상 발생**
### 4-2. 사후 변경
- 조제 목록의 **용도 뱃지 클릭** → 번호 입력으로 변경
- API: `PUT /api/compounds/:id/usage-type`
- 판매 → 자가소비 변경 시 `sell_price_total = 0`으로 자동 변경
- 변경 이력이 `sales_status_history`에 기록됨
### 4-3. 매출 통계 제외
- 대시보드 월매출: `usage_type = 'SALE'`만 집계
- 판매 통계 API: `COALESCE(usage_type, 'SALE') = 'SALE'` 조건
- 자가소비/샘플/폐기는 매출에서 완전 제외
### 4-4. UI 표시
| 용도 | 뱃지 색상 | 판매 버튼 | 판매가 표시 |
|------|----------|:---------:|:----------:|
| 판매 | 초록 | O | 금액 표시 |
| 자가소비 | 노랑 | X | `-` |
| 샘플 | 파랑 | X | `-` |
| 폐기 | 회색 | X | `-` |
---
## 5. API 명세
### 용도 변경
```
PUT /api/compounds/:compound_id/usage-type
Content-Type: application/json
{
"usage_type": "SELF_USE" // SALE, SELF_USE, SAMPLE, DISPOSAL
}
Response:
{
"success": true,
"message": "용도가 변경되었습니다: SELF_USE"
}
```
### 조제 생성 시 용도 지정
```
POST /api/compounds
{
"patient_id": 1,
"formula_id": 5,
"je_count": 1,
"cheop_total": 30,
"pouch_total": 30,
"usage_type": "SELF_USE", // 생략 시 기본 SALE
"ingredients": [...]
}
```
---
## 6. 활용 시나리오
### 자가소비 조제
1. 약사가 본인/가족용으로 쌍화탕 1제 조제
2. 조제 실행 시 용도 → "자가소비" 선택
3. 재고 차감됨, 판매가 = 0, 매출 통계 제외
4. 원가만 기록 → 비용 관리 목적
### 기존 조제를 자가소비로 변경
1. 실수로 판매로 조제했는데 실제로는 자가소비
2. 조제 내역 목록에서 "판매" 뱃지 클릭
3. "2" (자가소비) 입력
4. 즉시 변경 → 매출에서 제외
---
*최종 수정: 2026-02-19*

View File

@@ -0,0 +1,310 @@
# 조제 프로세스 및 커스텀 처방 관리
## 목차
1. [조제 프로세스 흐름](#1-조제-프로세스-흐름)
2. [데이터베이스 구조](#2-데이터베이스-구조)
3. [커스텀 처방 처리](#3-커스텀-처방-처리)
4. [구현 제안사항](#4-구현-제안사항)
---
## 1. 조제 프로세스 흐름
### 1.1 전체 흐름도
```
[처방 선택] → [구성 약재 자동 로드] → [약재 커스터마이징] → [재고 확인] → [조제 실행] → [기록 저장]
↓ ↓ ↓ ↓ ↓ ↓
십전대보탕 formula_ingredients 약재 추가/삭제/수정 inventory_lots 재고 차감 compounds
확인 stock_ledger compound_ingredients
```
### 1.2 단계별 상세 프로세스
#### Step 1: 처방 선택
- **테이블**: `formulas`
- **주요 필드**:
- `formula_id`: 처방 ID
- `formula_name`: 처방명 (예: "십전대보탕")
- `base_cheop_per_je`: 1제당 기본 첩수 (보통 20첩)
#### Step 2: 구성 약재 자동 로드
- **테이블**: `formula_ingredients`
- **동작**: 선택한 처방의 기본 구성 약재를 자동으로 불러옴
- **예시**: 십전대보탕 선택 시 인삼, 백출, 복령, 감초 등 10가지 약재 자동 로드
#### Step 3: 약재 커스터마이징
- **가능한 작업**:
- ✅ 약재 추가 (예: 구기자 3g 추가)
- ✅ 약재 삭제 (특정 약재 제외)
- ✅ 용량 수정 (기본 5g → 7g으로 변경)
#### Step 4: 재고 확인 및 선택
- **테이블**: `inventory_lots`
- **display_name 표시**: 각 약재의 정확한 variant 확인
- 예: "건강" → "건강.土[한국산]" vs "건강[페루산]"
- **원산지 선택**: 자동(FIFO) 또는 수동 선택
#### Step 5: 조제 실행 및 재고 차감
- **FIFO 방식**: 오래된 로트부터 우선 소비
- **재고 부족 체크**: 부족 시 경고 표시
- **로트별 차감**: `compound_consumptions`에 상세 기록
#### Step 6: 조제 기록 저장
- **compounds 테이블**: 조제 마스터 정보
- **compound_ingredients 테이블**: 실제 사용된 약재 구성
- **compound_consumptions 테이블**: 로트별 차감 내역
---
## 2. 데이터베이스 구조
### 2.1 처방 관련 테이블
```sql
-- 처방 마스터 (기본 처방)
formulas
formula_id (PK)
formula_name -- "십전대보탕"
base_cheop_per_je -- 20첩
-- 처방 기본 구성
formula_ingredients
formula_id (FK)
herb_item_id (FK)
grams_per_cheop -- 1첩당 용량
-- 실제 조제 기록
compounds
compound_id (PK)
patient_id (FK) -- 환자
formula_id (FK) -- 원 처방 참조
compound_date -- 조제일
cheop_total -- 총 첩수
notes -- "구기자 3g 추가" 등 커스텀 내역
-- 실제 사용 약재 (커스텀 포함)
compound_ingredients
compound_id (FK)
herb_item_id (FK)
grams_per_cheop -- 실제 사용 용량
```
### 2.2 재고 관련 테이블
```sql
-- 재고 로트
inventory_lots
lot_id (PK)
herb_item_id (FK)
display_name -- "갈근.각", "건강.土" 등
quantity_onhand -- 현재 재고량
unit_price_per_g -- g당 단가
-- 로트 변형 정보
lot_variants
lot_id (FK)
raw_name -- 상세 제품명
form -- 형태 (각, 片, 土)
processing -- 가공법 (9증, 酒炙)
grade -- 등급 (特, 中, 小)
```
---
## 3. 커스텀 처방 처리
### 3.1 현재 시스템의 처리 방식
현재 시스템은 이미 커스텀 처방을 처리할 수 있는 구조를 가지고 있습니다:
1. **formula_ingredients**: 처방의 기본 구성 (변경되지 않음)
2. **compound_ingredients**: 실제 조제 시 사용된 구성 (커스텀 반영)
### 3.2 커스텀 처방 식별 방법
#### 방법 1: 비교를 통한 자동 감지
```python
def is_custom_prescription(compound_id):
"""조제가 원 처방과 다른지 확인"""
# 1. compound의 formula_id 확인
original_formula = get_formula_ingredients(formula_id)
# 2. 실제 사용된 약재 확인
actual_ingredients = get_compound_ingredients(compound_id)
# 3. 비교
if original_formula != actual_ingredients:
return True, get_differences()
return False, None
```
#### 방법 2: 플래그 추가 (권장)
```sql
-- compounds 테이블에 컬럼 추가
ALTER TABLE compounds ADD COLUMN is_custom BOOLEAN DEFAULT 0;
ALTER TABLE compounds ADD COLUMN custom_notes TEXT;
```
### 3.3 화면 표시 제안
#### 조제 내역 표시 예시
**원 처방 그대로 조제한 경우:**
```
조제일: 2024-02-17
처방: 십전대보탕
첩수: 20첩
```
**커스텀 조제한 경우:**
```
조제일: 2024-02-17
처방: 십전대보탕 (가감방) ⚠️
첩수: 20첩
추가: 구기자 3g
제외: 감초
변경: 인삼 5g → 7g
```
---
## 4. 구현 제안사항
### 4.1 데이터베이스 개선
```sql
-- 1. compounds 테이블에 커스텀 플래그 추가
ALTER TABLE compounds ADD COLUMN is_custom BOOLEAN DEFAULT 0;
ALTER TABLE compounds ADD COLUMN custom_type TEXT; -- 'added', 'removed', 'modified', 'mixed'
ALTER TABLE compounds ADD COLUMN custom_summary TEXT; -- "구기자 3g 추가"
-- 2. compound_ingredients에 변경 타입 추가
ALTER TABLE compound_ingredients ADD COLUMN modification_type TEXT; -- 'original', 'added', 'modified'
ALTER TABLE compound_ingredients ADD COLUMN original_grams REAL; -- 원래 용량 (수정된 경우)
```
### 4.2 API 개선 제안
```python
@app.route('/api/compounds', methods=['POST'])
def create_compound():
"""조제 실행 - 커스텀 처방 감지 포함"""
data = request.json
formula_id = data.get('formula_id')
ingredients = data.get('ingredients')
# 원 처방과 비교
original = get_formula_ingredients(formula_id)
is_custom, differences = compare_ingredients(original, ingredients)
if is_custom:
# 커스텀 정보 저장
custom_summary = generate_custom_summary(differences)
# compounds 테이블에 is_custom=1, custom_summary 저장
```
### 4.3 UI 개선 제안
#### 조제 화면
```javascript
// 커스텀 여부 실시간 표시
function checkCustomization() {
const original = getOriginalFormula();
const current = getCurrentIngredients();
if (hasChanges(original, current)) {
$('#customBadge').show().html('가감방');
$('#customDetails').html(getChangesSummary());
}
}
```
#### 환자 처방 내역 화면
```javascript
// 커스텀 처방 구분 표시
function displayPrescriptionHistory(patient_id) {
// 처방 내역 표시 시
if (compound.is_custom) {
html += `<span class="badge bg-warning">가감</span>`;
html += `<small class="text-muted">${compound.custom_summary}</small>`;
}
}
```
### 4.4 보고서 개선
환자 처방 내역서에 커스텀 정보 포함:
```
===========================================
환자명: 홍길동
기간: 2024-01-01 ~ 2024-02-17
===========================================
1. 2024-01-15: 십전대보탕 (20첩)
- 표준 처방
2. 2024-02-01: 십전대보탕 가감방 (20첩)
- 추가: 구기자 3g/첩
- 제외: 감초
- 용량변경: 인삼 5g → 7g/첩
3. 2024-02-17: 쌍화탕 (15첩)
- 표준 처방
```
---
## 5. 현재 시스템 활용 방안
현재 구조에서도 충분히 커스텀 처방을 관리할 수 있습니다:
1. **조제 시**: `compound_ingredients`에 실제 사용 약재 저장
2. **조회 시**: `formula_ingredients`와 비교하여 커스텀 여부 판단
3. **표시**: 차이점을 계산하여 화면에 표시
### 예시 쿼리
```sql
-- 커스텀 처방 찾기
SELECT
c.compound_id,
c.formula_id,
f.formula_name,
CASE
WHEN ci_count != fi_count THEN '가감방'
ELSE '표준방'
END as prescription_type
FROM compounds c
JOIN formulas f ON c.formula_id = f.formula_id
JOIN (
-- 실제 사용 약재 수
SELECT compound_id, COUNT(*) as ci_count
FROM compound_ingredients
GROUP BY compound_id
) ci ON c.compound_id = ci.compound_id
LEFT JOIN (
-- 원 처방 약재 수
SELECT formula_id, COUNT(*) as fi_count
FROM formula_ingredients
GROUP BY formula_id
) fi ON c.formula_id = fi.formula_id;
```
---
## 6. 결론
현재 시스템은 이미 커스텀 처방을 저장할 수 있는 구조를 갖추고 있습니다:
- `formula_ingredients`: 원 처방 (불변)
- `compound_ingredients`: 실제 조제 (커스텀 가능)
추가 개선사항:
1. `compounds` 테이블에 `is_custom` 플래그 추가
2. 커스텀 내역을 요약하여 `custom_summary`에 저장
3. UI에서 가감방 표시 및 상세 내역 표시
4. 환자 처방 내역에 커스텀 정보 포함
이렇게 하면 "십전대보탕 + 구기자 3g"을 정확히 기록하고 추적할 수 있습니다.

View File

@@ -0,0 +1,59 @@
약재명 보험코드 원산지 수량 단가 금액 현재고
휴먼갈근
갈근.각 062401050 한국 2500g ₩17 ₩42,000 2500g
휴먼감초
감초.1호[야생](1kg) 062400740 중국 5000g ₩22 ₩110,500 4610g
휴먼건강
건강 062400730 페루 5000g ₩12 ₩62,000 4880g
휴먼건강
건강.土 062400730 한국 1500g ₩51 ₩77,100 1390g
휴먼계지
계지 062400390 베트남 2500g ₩6 ₩14,500 2500g
휴먼구기자
구기자(영하)(1kg) 062400090 중국 3000g ₩18 ₩53,700 3000g
휴먼길경
길경.片[특] 062400980 중국 1500g ₩11 ₩15,900 1500g
휴먼대추
대추(절편)(1kg) 062401120 한국 5000g ₩20 ₩100,000 4650g
휴먼마황
마황(1kg) 062401400 중국 5000g ₩10 ₩48,000 4970g
휴먼반하생강백반제
반하생강백반제(1kg) 062401790 중국 3000g ₩34 ₩101,100 2850g
휴먼백출
백출.당[1kg] 062402150 중국 2000g ₩12 ₩23,600 1560g
휴먼복령
복령(1kg) 062402310 중국 5000g ₩12 ₩57,500 4760g
휴먼석고
석고[통포장](kg) 062402650 중국 4000g ₩5 ₩18,800 4000g
휴먼세신
세신.中 062402860 중국 1500g ₩129 ₩193,500 1500g
신흥숙지황
숙지황(9증)(신흥.1kg)[완] 060600050 중국 5000g ₩20 ₩100,000 4360g
휴먼오미자
오미자<토매지>(1kg) 062402800 중국 2000g ₩18 ₩35,000 2000g
휴먼용안육
용안육.名品(1kg) 062403120 태국 3000g ₩21 ₩62,100 3000g
휴먼육계
육계.YB 062403210 베트남 2500g ₩15 ₩36,500 2400g
휴먼일당귀
일당귀.中(1kg) 062403450 중국 5000g ₩13 ₩64,500 4640g
휴먼자소엽
자소엽.土 062403350 한국 1500g ₩14 ₩20,700 1410g
휴먼작약
작약(1kg) 062403380 한국 3000g ₩19 ₩56,100 2460g
휴먼작약주자
작약주자.土[酒炙] 062403470 한국 1500g ₩25 ₩36,900 1500g
휴먼전호
전호[재배] 062403490 중국 1500g ₩14 ₩21,000 1500g
휴먼지각
지각 062402220 중국 1500g ₩10 ₩15,000 1500g
휴먼지황
지황.건[](1kg) 062402030 중국 1000g ₩12 ₩11,500 1000g
휴먼진피(陳皮)
진피.비열[非熱](1kg) 062401950 한국 5000g ₩14 ₩68,500 5000g
휴먼창출
창출[북창출.재배](1kg) 062401830 중국 3000g ₩14 ₩40,500 3000g
휴먼천궁
천궁.일<토매지>(1kg) 062401810 중국 3000g ₩12 ₩35,700 2560g
휴먼황기
황기(직절.小)(1kg) 062400040 중국 3000g ₩10 ₩29,700 2500g

View File

@@ -0,0 +1,317 @@
# 직접조제(Custom Compound) 고도화 기획 문서
## 1. 현황 분석
### 1.1 현재 직접조제 데이터 구조
#### 실제 데이터 사례
```
Compound ID 10: 휴먼건강 3.0g (단품)
Compound ID 8: 휴먼건강 2.0g (단품)
Compound ID 7: 휴먼일당귀 100.0g (단품)
Compound ID 6: 휴먼일당귀 100.0g (단품)
```
#### 현재 저장 방식
- formula_id: NULL
- is_custom: 0 (잘못된 설정)
- custom_type: 'standard' (잘못된 설정)
- custom_summary: NULL
- 약재 정보: compound_ingredients에 단일 약재로 저장
### 1.2 문제점
1. **분류 체계 미비**
- 직접조제임에도 is_custom=0으로 저장
- custom_type이 'standard'로 잘못 설정
- 표준처방과 직접조제 구분 불명확
2. **데이터 무결성**
- formula_id NULL이지만 custom 플래그 미설정
- 직접조제 사유/목적 미기록
- 처방명 없음 (custom_summary 미사용)
3. **관리 기능 부족**
- 직접조제 이력 추적 어려움
- 가격 책정 기준 불명확
- 재조제시 참조 데이터 부재
## 2. 직접조제 유형 분류
### 2.1 단품 판매 (Single Item)
- **특징**: 단일 약재 판매
- **사례**: 녹용 100g, 홍삼 50g
- **용도**: 환자 요청에 의한 약재 구매
### 2.2 맞춤 조제 (Custom Formula)
- **특징**: 여러 약재 조합하여 맞춤 처방
- **사례**: 기존 처방 가감방, 한약사 임의 조제
- **용도**: 환자 체질/증상에 맞춘 개별화 처방
### 2.3 OTC 조제 (Over The Counter)
- **특징**: 처방전 없이 판매 가능한 제품
- **사례**: 쌍화탕 파우치, 공진단
- **용도**: 일반 판매용 제품
### 2.4 테스트/샘플 (Test/Sample)
- **특징**: 시음용, 테스트용 소량 조제
- **사례**: 처방 샘플 1일분
- **용도**: 환자 시음, 품질 테스트
## 3. 개선 방안
### 3.1 데이터 구조 개선
#### 3.1.1 compounds 테이블 수정
```sql
-- 직접조제 분류 강화
ALTER TABLE compounds ADD COLUMN custom_category TEXT;
-- 'SINGLE_ITEM', 'CUSTOM_FORMULA', 'OTC', 'SAMPLE'
ALTER TABLE compounds ADD COLUMN custom_name TEXT;
-- 직접조제명 (예: '감기 맞춤처방', '홍삼 단품')
ALTER TABLE compounds ADD COLUMN custom_purpose TEXT;
-- 조제 목적/사유
ALTER TABLE compounds ADD COLUMN reference_compound_id INTEGER;
-- 재조제시 참조할 이전 조제 ID
```
#### 3.1.2 custom_formulas 테이블 신규
```sql
CREATE TABLE custom_formulas (
custom_formula_id INTEGER PRIMARY KEY AUTOINCREMENT,
compound_id INTEGER REFERENCES compounds(compound_id),
formula_name TEXT NOT NULL,
formula_description TEXT,
base_formula_id INTEGER REFERENCES formulas(formula_id),
-- 기본이 된 표준처방 (가감방인 경우)
modification_summary TEXT,
-- 가감 내용 요약
symptoms TEXT,
-- 대상 증상
contraindications TEXT,
-- 금기사항
created_by TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_reusable BOOLEAN DEFAULT 0,
-- 재사용 가능 여부 (다른 환자에게도 적용 가능)
reuse_count INTEGER DEFAULT 0
-- 재사용 횟수
);
```
### 3.2 비즈니스 로직 개선
#### 3.2.1 직접조제 생성 프로세스
```
1. 조제 유형 선택
└─ 단품/맞춤/OTC/샘플
2. 유형별 정보 입력
├─ 단품: 약재, 용량, 판매사유
├─ 맞춤: 처방명, 약재구성, 증상, 기반처방
├─ OTC: 제품명, 수량
└─ 샘플: 목적, 대상처방, 용량
3. 가격 책정
├─ 자동계산: 약재원가 + 조제료
└─ 수동입력: 특별가 적용
4. 데이터 저장
├─ compounds: is_custom=1, custom_category 설정
├─ compound_ingredients: 약재 구성
└─ custom_formulas: 맞춤처방 상세정보
```
#### 3.2.2 직접조제 관리 기능
1. **템플릿 관리**
- 자주 사용하는 직접조제 템플릿 저장
- 템플릿에서 빠른 조제 생성
2. **이력 관리**
- 환자별 직접조제 이력 조회
- 동일 처방 재조제 기능
3. **가격 정책**
- 직접조제 유형별 조제료 설정
- 단품 판매 마진율 관리
### 3.3 UI/UX 개선
#### 3.3.1 직접조제 입력 화면
```
┌─────────────── 직접조제 등록 ──────────────┐
│ │
│ 조제 유형: [단품 판매 ▼] │
│ │
│ ─────── 기본 정보 ───────── │
│ 조제명: [________________] │
│ 환자: [환자선택 ▼] 또는 [비회원] │
│ │
│ ─────── 약재 구성 ───────── │
│ [+ 약재 추가] │
│ ┌────────┬──────┬──────┬────┐ │
│ │약재명 │용량 │단가 │삭제 │ │
│ ├────────┼──────┼──────┼────┤ │
│ │홍삼 │100g │500원/g│ X │ │
│ └────────┴──────┴──────┴────┘ │
│ │
│ ─────── 가격 정보 ───────── │
│ 약재비: 50,000원 │
│ 조제료: [10,000원] │
│ 판매가: [60,000원] │
│ │
│ 조제 목적: [________________] │
│ 비고: [____________________] │
│ │
│ [템플릿 저장] [취소] [조제 등록] │
└────────────────────────────────────────────┘
```
#### 3.3.2 직접조제 목록 화면
```
[직접조제 관리]
필터: [전체 ▼] [2024-02-01] ~ [2024-02-29] [검색]
┌────┬──────┬────────┬──────┬──────┬────────┬──────┐
│번호│유형 │조제명 │환자 │약재수 │판매가 │작업 │
├────┼──────┼────────┼──────┼──────┼────────┼──────┤
│ 1 │단품 │홍삼100g │홍길동 │ 1 │ 60,000 │[상세] │
│ 2 │맞춤 │감기처방 │김철수 │ 8 │120,000 │[재조제]│
│ 3 │OTC │공진단3환 │비회원 │ 3 │ 90,000 │[상세] │
│ 4 │샘플 │시음1일분 │이영희 │ 5 │ 0 │[상세] │
└────┴──────┴────────┴──────┴──────┴────────┴──────┘
```
### 3.4 데이터 마이그레이션
#### 3.4.1 기존 데이터 정리
```sql
-- formula_id가 NULL인 조제 데이터 업데이트
UPDATE compounds
SET is_custom = 1,
custom_type = 'custom',
custom_category = 'SINGLE_ITEM',
custom_name = (
SELECT h.herb_name || ' ' || ci.total_grams || 'g'
FROM compound_ingredients ci
JOIN herb_items h ON ci.herb_item_id = h.herb_item_id
WHERE ci.compound_id = compounds.compound_id
LIMIT 1
)
WHERE formula_id IS NULL;
```
#### 3.4.2 데이터 검증
- 모든 직접조제 데이터의 무결성 확인
- is_custom 플래그와 formula_id NULL 일치 여부 검증
- custom_category 분류 정확성 확인
## 4. API 설계
### 4.1 직접조제 API
```python
# 직접조제 생성
POST /api/compounds/custom
{
"patient_id": 1, # nullable for OTC
"custom_category": "SINGLE_ITEM",
"custom_name": "홍삼 100g",
"custom_purpose": "면역력 증진",
"ingredients": [
{
"herb_item_id": 123,
"grams": 100,
"unit_price": 500
}
],
"dispensing_fee": 10000,
"sell_price": 60000
}
# 직접조제 템플릿 저장
POST /api/custom-templates
{
"template_name": "감기 기본방",
"category": "CUSTOM_FORMULA",
"ingredients": [...],
"default_price": 80000
}
# 직접조제 이력 조회
GET /api/compounds/custom?patient_id=1&category=SINGLE_ITEM
# 재조제
POST /api/compounds/{compound_id}/reorder
{
"patient_id": 1,
"quantity_multiplier": 1.5 # 1.5배 용량
}
```
## 5. 구현 로드맵
### Phase 1: 기반 구축 (1주)
- [ ] DB 스키마 수정 (custom_category, custom_name 등 추가)
- [ ] 기존 데이터 마이그레이션
- [ ] 직접조제 분류 체계 구현
### Phase 2: 핵심 기능 (2주)
- [ ] 직접조제 생성 UI/API
- [ ] 유형별 입력 폼 구현
- [ ] 가격 자동계산 로직
### Phase 3: 관리 기능 (1주)
- [ ] 직접조제 목록/검색
- [ ] 템플릿 관리
- [ ] 재조제 기능
### Phase 4: 고도화 (2주)
- [ ] 통계 및 리포트
- [ ] 환자별 구매 패턴 분석
- [ ] 인기 직접조제 랭킹
## 6. 주의사항
### 6.1 규제 준수
- 의약품 판매 관련 법규 확인
- 처방전 필요 여부 명확히 구분
- OTC 판매 가능 품목 관리
### 6.2 데이터 정합성
- formula_id NULL과 is_custom=1 일치 유지
- 직접조제는 반드시 custom_category 설정
- 가격 정보 필수 입력
### 6.3 사용자 교육
- 직접조제 유형별 사용 가이드
- 가격 책정 기준 안내
- 템플릿 활용 방법
## 7. 기대 효과
1. **체계적 관리**
- 직접조제 데이터 일관성 확보
- 판매 이력 추적 가능
2. **업무 효율성**
- 템플릿으로 빠른 조제
- 재조제 간소화
3. **매출 증대**
- 단품 판매 활성화
- 맞춤 처방 서비스 확대
4. **고객 만족**
- 개인 맞춤 서비스
- 투명한 가격 정책

View File

@@ -0,0 +1,293 @@
# 처방 데이터 추가 가이드
## 개요
이 문서는 K-Drug 시스템에 새로운 한방 처방을 추가하는 방법을 설명합니다.
## 데이터베이스 구조
### 1. formulas 테이블
처방의 기본 정보를 저장하는 테이블입니다.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| formula_id | INTEGER | 처방 고유 ID (자동생성) |
| formula_code | TEXT | 처방 코드 (예: SCR001) |
| formula_name | TEXT | 처방 이름 (예: 소청룡탕) |
| formula_type | TEXT | 처방 타입 (STANDARD/CUSTOM) |
| base_cheop | INTEGER | 기본 첩수 |
| base_pouches | INTEGER | 기본 포수 |
| description | TEXT | 처방 설명 |
| is_active | INTEGER | 활성 상태 (1: 활성, 0: 비활성) |
### 2. formula_ingredients 테이블
처방을 구성하는 약재 정보를 저장하는 테이블입니다.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| ingredient_id | INTEGER | 구성 약재 ID (자동생성) |
| formula_id | INTEGER | 처방 ID (formulas 테이블 참조) |
| ingredient_code | TEXT | 약재 성분 코드 (herb_masters 테이블 참조) |
| grams_per_cheop | REAL | 1첩당 용량(g) |
| notes | TEXT | 약재 역할/효능 설명 |
| sort_order | INTEGER | 정렬 순서 |
### 3. herb_masters 테이블
약재 마스터 정보를 저장하는 테이블입니다.
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| ingredient_code | VARCHAR(10) | 약재 성분 코드 (PK) |
| herb_name | VARCHAR(100) | 약재 이름 |
| herb_name_hanja | VARCHAR(100) | 약재 한자명 |
| herb_name_latin | VARCHAR(200) | 약재 학명 |
## 처방 추가 절차
### 1단계: 약재 성분 코드 확인
처방에 사용할 약재들의 성분 코드를 먼저 확인해야 합니다.
```python
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# 약재 이름으로 성분 코드 검색
herb_name = "마황"
cursor.execute("""
SELECT ingredient_code, herb_name
FROM herb_masters
WHERE herb_name LIKE ?
""", (f'%{herb_name}%',))
results = cursor.fetchall()
for code, name in results:
print(f"{name}: {code}")
conn.close()
```
### 2단계: 처방 데이터 준비
처방 정보와 구성 약재 정보를 준비합니다.
```python
prescription_data = {
'formula_code': 'SCR001', # 고유한 처방 코드
'formula_name': '소청룡탕',
'formula_type': 'STANDARD', # STANDARD 또는 CUSTOM
'base_cheop': 1, # 기본 첩수
'base_pouches': 1, # 기본 포수
'description': '처방 설명',
'ingredients': [
{
'code': '3147H1AHM', # 약재 성분 코드
'amount': 6.0, # 1첩당 용량(g)
'notes': '발한해표' # 약재 역할
},
# ... 추가 약재들
]
}
```
### 3단계: 데이터베이스에 추가
준비한 데이터를 데이터베이스에 저장합니다.
```python
import sqlite3
def add_prescription(prescription_data):
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 1. formulas 테이블에 처방 추가
cursor.execute("""
INSERT INTO formulas (
formula_code, formula_name, formula_type,
base_cheop, base_pouches, description,
is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (
prescription_data['formula_code'],
prescription_data['formula_name'],
prescription_data['formula_type'],
prescription_data['base_cheop'],
prescription_data['base_pouches'],
prescription_data['description']
))
formula_id = cursor.lastrowid
# 2. formula_ingredients 테이블에 약재 추가
for ingredient in prescription_data['ingredients']:
cursor.execute("""
INSERT INTO formula_ingredients (
formula_id, ingredient_code,
grams_per_cheop, notes,
sort_order, created_at
) VALUES (?, ?, ?, ?, 0, CURRENT_TIMESTAMP)
""", (
formula_id,
ingredient['code'],
ingredient['amount'],
ingredient['notes']
))
conn.commit()
print(f"처방 '{prescription_data['formula_name']}' 추가 완료!")
except Exception as e:
conn.rollback()
print(f"오류 발생: {e}")
finally:
conn.close()
```
## 자동화 스크립트 사용법
### add_prescription_data.py 스크립트
프로젝트에 포함된 `add_prescription_data.py` 스크립트를 사용하여 처방을 쉽게 추가할 수 있습니다.
1. 스크립트 실행:
```bash
python3 add_prescription_data.py
```
2. 스크립트 수정하여 새 처방 추가:
```python
prescriptions = [
{
'formula_code': '새처방코드',
'formula_name': '새처방이름',
'formula_type': 'STANDARD',
'base_cheop': 1,
'base_pouches': 1,
'description': '처방 설명',
'ingredients': [
# 구성 약재 목록
]
}
]
```
## 주의사항
### 1. 성분 코드 확인
- 반드시 herb_masters 테이블에 존재하는 ingredient_code를 사용해야 합니다
- 보험 코드(insurance_code)가 아닌 성분 코드(ingredient_code)를 사용합니다
### 2. 중복 확인
- formula_code는 고유해야 합니다
- 동일한 처방 코드로 중복 추가하지 않도록 주의합니다
### 3. 약재 용량
- grams_per_cheop은 1첩 기준 용량입니다
- 소수점 사용 가능 (예: 1.5, 0.5)
### 4. 처방 타입
- STANDARD: 표준 처방
- CUSTOM: 사용자 정의 처방
## 데이터 검증
### 추가된 처방 확인
```python
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# 처방 목록 조회
cursor.execute("""
SELECT formula_code, formula_name, formula_type
FROM formulas
WHERE is_active = 1
""")
for row in cursor.fetchall():
print(f"{row[0]}: {row[1]} ({row[2]})")
conn.close()
```
### 처방 상세 정보 조회
```python
# 특정 처방의 구성 약재 확인
formula_code = 'SCR001'
cursor.execute("""
SELECT hm.herb_name, fi.grams_per_cheop, fi.notes
FROM formulas f
JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
WHERE f.formula_code = ?
ORDER BY fi.grams_per_cheop DESC
""", (formula_code,))
for herb_name, amount, notes in cursor.fetchall():
print(f"- {herb_name}: {amount}g ({notes})")
```
## 문제 해결
### 1. 약재를 찾을 수 없는 경우
- herb_masters 테이블에서 정확한 약재명 확인
- 대체 이름 검색 (예: 대조 → 대추, 백작약 → 작약)
### 2. 외래 키 제약 오류
- ingredient_code가 herb_masters 테이블에 존재하는지 확인
- formula_id가 올바른지 확인
### 3. 중복 키 오류
- formula_code가 이미 존재하는지 확인
- 필요시 기존 처방 삭제 또는 코드 변경
## 예제: 실제 처방 추가
### 소청룡탕 추가 예제
```python
{
'formula_code': 'SCR001',
'formula_name': '소청룡탕',
'formula_type': 'STANDARD',
'base_cheop': 1,
'base_pouches': 1,
'description': '외감풍한, 내정수음으로 인한 기침, 천식을 치료하는 처방',
'ingredients': [
{'code': '3147H1AHM', 'amount': 6.0, 'notes': '발한해표'}, # 마황
{'code': '3419H1AHM', 'amount': 6.0, 'notes': '화영지통'}, # 백작약
{'code': '3342H1AHM', 'amount': 6.0, 'notes': '렴폐지해'}, # 오미자
{'code': '3182H1AHM', 'amount': 6.0, 'notes': '화담지구'}, # 반하
{'code': '3285H1AHM', 'amount': 4.0, 'notes': '온폐산한'}, # 세신
{'code': '3017H1AHM', 'amount': 4.0, 'notes': '온중산한'}, # 건강
{'code': '3033H1AHM', 'amount': 4.0, 'notes': '해표발한'}, # 계지
{'code': '3007H1AHM', 'amount': 4.0, 'notes': '조화제약'}, # 감초
]
}
```
### 갈근탕 추가 예제
```python
{
'formula_code': 'GGT001',
'formula_name': '갈근탕',
'formula_type': 'STANDARD',
'base_cheop': 1,
'base_pouches': 1,
'description': '외감풍한으로 인한 두통, 발열, 오한, 항강을 치료하는 처방',
'ingredients': [
{'code': '3002H1AHM', 'amount': 8.0, 'notes': '승진해기'}, # 갈근
{'code': '3147H1AHM', 'amount': 6.0, 'notes': '발한해표'}, # 마황
{'code': '3115H1AHM', 'amount': 6.0, 'notes': '보중익기'}, # 대조
{'code': '3033H1AHM', 'amount': 4.0, 'notes': '해표발한'}, # 계지
{'code': '3419H1AHM', 'amount': 4.0, 'notes': '화영지통'}, # 작약
{'code': '3007H1AHM', 'amount': 4.0, 'notes': '조화제약'}, # 감초
{'code': '3017H1AHM', 'amount': 2.0, 'notes': '온중산한'}, # 건강
]
}
```
## 참고 자료
- 데이터베이스 스키마: `database/kdrug.db`
- 자동화 스크립트: `add_prescription_data.py`
- 약재 마스터 데이터: `herb_masters` 테이블

View File

@@ -0,0 +1,298 @@
# 한약 판매 관리 시스템 기획 문서
## 1. 개요
### 1.1 배경
- 한약사가 조제한 한약은 재고 차감과 별개로 판매 프로세스를 거쳐야 함
- 조제 완료 후 판매 상태 관리 및 가격 책정이 필요
- 환자 관리 및 마케팅을 위한 카카오 채널 연동 계획
### 1.2 목적
- 조제된 한약의 판매 상태 관리
- 유연한 가격 정책 적용
- 판매 이력 추적 및 매출 관리
- 환자 정보 연계 강화
## 2. 현재 시스템 분석
### 2.1 기존 구조
```
compounds 테이블:
- compound_id: 조제 고유번호
- status: 현재 'PREPARED'로 고정
- sell_price_total: 판매가격 필드 존재하나 미활용
- cost_total: 원가 필드 존재
```
### 2.2 현재 문제점
- 조제 후 판매 상태 추적 불가
- 가격 정책 유연성 부족
- 판매 완료/취소/반품 등 프로세스 미비
- 매출 통계 기능 없음
## 3. 기능 요구사항
### 3.1 판매 상태 관리
#### 3.1.1 상태 유형
```
PREPARED (조제완료) → 현재 기본값
PENDING_PAYMENT (결제대기)
PAID (결제완료)
PENDING_DELIVERY (배송대기)
DELIVERED (배송완료)
COMPLETED (판매완료)
OTC_CONVERTED (OTC전환)
CANCELLED (취소)
REFUNDED (환불)
```
#### 3.1.2 상태 전이 규칙
```
조제완료 → 결제대기 → 결제완료 → 배송대기 → 배송완료 → 판매완료
↓ ↓
OTC전환 취소/환불
```
### 3.2 가격 관리
#### 3.2.1 가격 정책
1. **기본가격 산정**
- 표준처방: 처방별 기본가격 테이블 참조
- 직접조제: 약재별 단가 × 용량 × 첩수 자동 계산
2. **가격 조정**
- 가감방 적용시 자동 재계산
- 수동 가격 조정 가능
- 할인율 적용 기능
3. **가격 구성**
```
약재원가 + 조제료 + 배송비 = 기본가격
기본가격 × (1 - 할인율) = 최종판매가
```
### 3.3 판매 처리 기능
#### 3.3.1 판매 정보 입력
- 결제 방법 (현금/카드/계좌이체/카카오페이)
- 결제 일시
- 실제 판매가격
- 할인 사유 및 금액
- 배송 정보 (택배/직접수령/퀵서비스)
- 영수증 발행 여부
#### 3.3.2 OTC 전환
- 처방전 없이 판매 가능한 경우
- 환자 정보 없이도 판매 처리
- 별도 재고 관리 필요
## 4. 데이터베이스 설계
### 4.1 기존 테이블 수정
#### compounds 테이블 수정사항
```sql
ALTER TABLE compounds ADD COLUMN payment_method TEXT;
ALTER TABLE compounds ADD COLUMN payment_date DATETIME;
ALTER TABLE compounds ADD COLUMN discount_rate REAL DEFAULT 0;
ALTER TABLE compounds ADD COLUMN discount_reason TEXT;
ALTER TABLE compounds ADD COLUMN delivery_method TEXT;
ALTER TABLE compounds ADD COLUMN delivery_date DATETIME;
ALTER TABLE compounds ADD COLUMN invoice_number TEXT;
```
### 4.2 신규 테이블
#### sales_transactions (판매 거래)
```sql
CREATE TABLE sales_transactions (
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
compound_id INTEGER REFERENCES compounds(compound_id),
transaction_date DATETIME NOT NULL,
transaction_type TEXT NOT NULL, -- SALE, REFUND, CANCEL
amount REAL NOT NULL,
payment_method TEXT,
payment_status TEXT, -- PENDING, COMPLETED, FAILED
notes TEXT,
created_by TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
#### price_policies (가격 정책)
```sql
CREATE TABLE price_policies (
policy_id INTEGER PRIMARY KEY AUTOINCREMENT,
formula_id INTEGER REFERENCES formulas(formula_id),
base_price REAL NOT NULL,
dispensing_fee REAL DEFAULT 0,
is_active BOOLEAN DEFAULT 1,
effective_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
#### sales_status_history (판매 상태 이력)
```sql
CREATE TABLE sales_status_history (
history_id INTEGER PRIMARY KEY AUTOINCREMENT,
compound_id INTEGER REFERENCES compounds(compound_id),
old_status TEXT,
new_status TEXT NOT NULL,
changed_by TEXT,
change_reason TEXT,
changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
## 5. 사용자 인터페이스
### 5.1 판매 관리 화면
#### 5.1.1 조제 목록 화면 개선
```
[조제 목록]
┌────┬──────┬──────┬────────┬────────┬────────┬──────────┐
│번호│환자명│처방명│조제일자│ 상태 │판매가격│ 작업 │
├────┼──────┼──────┼────────┼────────┼────────┼──────────┤
│ 1 │홍길동│갈근탕│02-18 │조제완료│ 80,000 │[판매처리]│
│ 2 │김철수│쌍화탕│02-18 │결제대기│ 60,000 │[결제확인]│
│ 3 │이영희│십전대│02-17 │배송대기│120,000 │[배송처리]│
└────┴──────┴──────┴────────┴────────┴────────┴──────────┘
```
#### 5.1.2 판매 처리 모달
```
┌─────────────── 판매 처리 ──────────────┐
│ │
│ 처방명: 갈근탕 │
│ 환자명: 홍길동 │
│ 조제일: 2026-02-18 │
│ │
│ ─────── 가격 정보 ───────── │
│ 약재원가: 45,000원 │
│ 조제료: 20,000원 │
│ 기본가격: 65,000원 │
│ │
│ 할인율: [10%▼] │
│ 할인사유: [_______________] │
│ 최종가격: 58,500원 │
│ │
│ ─────── 결제 정보 ───────── │
│ 결제방법: [카드▼] │
│ 결제일시: [2026-02-18 14:30] │
│ │
│ ─────── 배송 정보 ───────── │
│ 배송방법: [택배▼] │
│ 배송예정: [2026-02-19] │
│ │
│ [취소] [판매확정] │
└─────────────────────────────────────────┘
```
### 5.2 매출 통계 화면
```
[매출 현황]
기간: [2026-02-01] ~ [2026-02-29]
┌─────────────────────────────────┐
│ 총 매출: 3,540,000원 │
│ 총 건수: 42건 │
│ 평균 단가: 84,286원 │
└─────────────────────────────────┘
[일별 매출 추이 그래프]
[처방별 매출 비중 차트]
[결제 방법별 통계]
```
## 6. API 설계
### 6.1 판매 관련 API
```python
# 판매 상태 업데이트
POST /api/compounds/{compound_id}/status
{
"status": "PAID",
"payment_method": "CARD",
"payment_date": "2026-02-18T14:30:00",
"amount": 58500
}
# 가격 조정
PUT /api/compounds/{compound_id}/price
{
"sell_price_total": 58500,
"discount_rate": 10,
"discount_reason": "단골 할인"
}
# 판매 통계 조회
GET /api/sales/statistics?start_date=2026-02-01&end_date=2026-02-29
# OTC 전환
POST /api/compounds/{compound_id}/convert-to-otc
{
"reason": "처방전 미제출",
"notes": "환자 요청"
}
```
## 7. 구현 우선순위
### Phase 1 (1주차)
1. compounds 테이블 칼럼 추가
2. 판매 상태 변경 기능
3. 기본 가격 입력/수정 기능
4. 판매 처리 UI 구현
### Phase 2 (2주차)
1. sales_transactions 테이블 생성
2. 판매 이력 관리 기능
3. 매출 통계 API
4. 통계 화면 구현
### Phase 3 (3주차)
1. 가격 정책 테이블 구현
2. 자동 가격 계산 로직
3. OTC 전환 기능
4. 영수증 발행 기능
## 8. 추후 확장 계획
### 8.1 카카오 채널 연동
- QR 코드 생성 및 출력
- 카카오 회원가입 유도
- 알림톡 발송 (조제완료, 배송안내)
### 8.2 고객 관리
- 구매 이력 관리
- 재구매 주기 분석
- 맞춤 처방 추천
### 8.3 재고 연계
- OTC 전환시 별도 재고 관리
- 유통기한 관리
- 재고 부족 알림
## 9. 기대 효과
1. **업무 효율성 향상**
- 조제부터 판매까지 일원화된 관리
- 자동 가격 계산으로 실수 방지
2. **매출 관리 개선**
- 실시간 매출 현황 파악
- 처방별/기간별 분석 가능
3. **고객 서비스 향상**
- 체계적인 배송 관리
- 투명한 가격 정책
4. **의사결정 지원**
- 데이터 기반 가격 정책 수립
- 인기 처방 파악 및 재고 관리 최적화

View File

@@ -0,0 +1,460 @@
# kdrug 프로젝트 전체 분석 보고서
## 목차
1. [프로젝트 개요](#프로젝트-개요)
2. [시스템 아키텍처](#시스템-아키텍처)
3. [디렉토리 구조](#디렉토리-구조)
4. [데이터베이스 설계](#데이터베이스-설계)
5. [백엔드 구조](#백엔드-구조)
6. [프론트엔드 구조](#프론트엔드-구조)
7. [주요 기능 분석](#주요-기능-분석)
8. [비즈니스 로직](#비즈니스-로직)
9. [기술 스택](#기술-스택)
10. [개선 권장사항](#개선-권장사항)
---
## 프로젝트 개요
### 프로젝트명
**kdrug - 한의원 약재 관리 시스템**
### 목적
한의원 및 한약방을 위한 웹 기반 통합 관리 시스템으로, 약재 재고 관리, 처방 관리, 조제 관리, 환자 관리 등을 효율적으로 처리하는 것을 목표로 합니다.
### 주요 특징
- 건강보험 표준 약재 코드 기반 체계적 관리
- Excel 입고장 자동 처리 (한의사랑, 한의정보 형식)
- FIFO(선입선출) 기반 로트별 재고 관리
- 2단계 약재 체계 (마스터 약재 → 제품)
- 정확한 원가 계산 및 추적
- 모바일 친화적 환자 문진표 시스템
### 현재 상태 (2026-02-16 기준)
- 데이터베이스: 16MB
- 약재 마스터: 454개
- 표준 제품: 53,769개
- 실사용 제품: 40개
- 등록 처방: 2개
- 조제 내역: 3건
- 제조사: 128개
---
## 시스템 아키텍처
### 전체 구조
```
┌─────────────────────────────────────────┐
│ 웹 브라우저 (클라이언트) │
│ - Single Page Application (SPA) │
│ - Bootstrap 5.1.3 + jQuery 3.6.0 │
│ - RESTful API 통신 │
└──────────────────┬──────────────────────┘
│ HTTP/AJAX
│ Port 5001
┌──────────────────▼──────────────────────┐
│ Flask 웹 서버 (백엔드) │
│ - app.py: REST API 엔드포인트 │
│ - excel_processor.py: Excel 처리 │
│ - 트랜잭션 관리, 비즈니스 로직 │
└──────────────────┬──────────────────────┘
│ SQL
┌──────────────────▼──────────────────────┐
│ SQLite 데이터베이스 │
│ - database/kdrug.db (16MB) │
│ - 26개 테이블 │
│ - 정규화된 관계형 설계 │
└─────────────────────────────────────────┘
```
### 데이터 플로우
```
입고 → 로트 생성 → 재고 증가 → 조제 시 소비 → 재고 감소 → 원가 계산
↓ ↓ ↓ ↓ ↓ ↓
Excel lot_id inventory_lots FIFO 차감 stock_ledger 원가추적
```
---
## 디렉토리 구조
### 주요 디렉토리
```
/root/kdrug/
├── 📄 Core Files
│ ├── app.py (1,916줄) # Flask 애플리케이션
│ ├── excel_processor.py (285줄) # Excel 처리 모듈
│ └── run_server.sh # 서버 실행 스크립트
├── 📁 templates/ # HTML 템플릿
│ ├── index.html (1,233줄) # 메인 관리 화면
│ └── survey.html (881줄) # 환자 문진표
├── 📁 static/ # 정적 파일
│ └── app.js (2,386줄) # 프론트엔드 JavaScript
├── 📁 database/ # 데이터베이스
│ ├── kdrug.db (16MB) # 메인 DB ⭐
│ ├── schema.sql (229줄) # 스키마 정의
│ └── [기타 SQL 파일들]
├── 📁 docs/ # 프로젝트 문서
│ ├── api_documentation.md # API 명세
│ ├── database_schema.md # DB 스키마
│ ├── database_erd.md # ER 다이어그램
│ └── [기타 문서들]
├── 📁 refactoring/ # 리팩토링 스크립트
├── 📁 sample/ # 샘플 데이터
├── 📁 uploads/ # 업로드 파일
├── 📁 backups/ # 백업 파일
└── 📁 .claude/ # Claude AI 설정
```
---
## 데이터베이스 설계
### 테이블 구조 (26개 테이블)
#### 1. 핵심 마스터 테이블
| 테이블명 | 레코드수 | 설명 |
|---------|---------|------|
| herb_masters | 454 | 주성분코드 기반 약재 마스터 |
| herb_items | 40 | 제조사별 실제 제품 |
| herb_products | 53,769 | 건강보험 표준 제품 목록 |
| product_companies | 128 | 제조/유통 업체 |
| formulas | 2 | 처방 마스터 |
| patients | - | 환자 정보 |
#### 2. 재고 관리 테이블
| 테이블명 | 설명 |
|---------|------|
| inventory_lots | 로트별 재고 (FIFO 관리) |
| stock_ledger | 모든 재고 변동 이력 |
| stock_adjustments | 재고 조정 헤더 |
| stock_adjustment_details | 재고 조정 상세 |
#### 3. 조제 관리 테이블
| 테이블명 | 설명 |
|---------|------|
| compounds | 조제 내역 |
| compound_ingredients | 조제별 약재 구성 |
| compound_consumptions | 로트별 실제 소비 내역 |
#### 4. 처방 관리 테이블
| 테이블명 | 설명 |
|---------|------|
| formula_ingredients | 처방 구성 (ingredient_code 기반) |
#### 5. 입고 관리 테이블
| 테이블명 | 설명 |
|---------|------|
| suppliers | 도매상 정보 |
| purchase_receipts | 입고장 헤더 |
| purchase_receipt_lines | 입고장 상세 |
#### 6. 기타 테이블
| 테이블명 | 설명 |
|---------|------|
| herb_efficacy_tags | 효능 태그 (18개) |
| herb_item_tags | 약재-태그 연결 |
| survey_templates | 문진표 템플릿 |
| patient_surveys | 환자별 문진표 |
| survey_responses | 문진표 응답 |
| survey_progress | 문진표 진행 상태 |
### 주요 관계
1. **약재 계층**: herb_masters → herb_items → inventory_lots
2. **처방-조제**: formulas → formula_ingredients → compounds → compound_ingredients
3. **재고 추적**: purchase_receipts → inventory_lots → compound_consumptions
4. **원가 관리**: lot별 unit_price_per_g → FIFO 기반 원가 계산
---
## 백엔드 구조
### Flask 애플리케이션 (app.py)
#### API 엔드포인트 (7개 카테고리)
##### 1. 약재 관리 API
- `GET /api/herbs` - 약재 제품 목록
- `GET /api/herbs/masters` - 마스터 약재 목록
- `GET /api/herbs/by-ingredient/{code}` - 성분코드별 제품
- `GET /api/herbs/{id}/available-lots` - 가용 로트 조회
##### 2. 처방 관리 API
- `GET /api/formulas` - 처방 목록
- `POST /api/formulas` - 처방 등록
- `GET /api/formulas/{id}/ingredients` - 처방 구성 조회
##### 3. 조제 관리 API
- `POST /api/compounds` - 조제 실행
- `GET /api/compounds/recent` - 최근 조제 내역
- `GET /api/compounds/{id}` - 조제 상세
##### 4. 재고 관리 API
- `GET /api/inventory/summary` - 재고 현황 요약
- `GET /api/inventory/low-stock` - 재고 부족 약재
- `GET /api/stock-ledger` - 재고 원장
##### 5. 환자 관리 API
- `GET /api/patients` - 환자 목록
- `POST /api/patients` - 환자 등록
- `GET /api/patients/{id}/prescriptions` - 환자 처방 이력
##### 6. 구매/입고 API
- `POST /api/purchases/upload` - Excel 업로드
- `POST /api/purchases/receipts` - 입고장 등록
- `GET /api/purchases/receipts` - 입고장 조회
##### 7. 재고 조정 API
- `POST /api/stock-adjustments` - 재고 보정
- `GET /api/stock-adjustments` - 보정 내역 조회
### Excel 처리 모듈 (excel_processor.py)
#### 주요 기능
1. **형식 자동 감지**: 한의사랑, 한의정보 형식 자동 인식
2. **컬럼 매핑**: 다양한 컬럼명 유연한 처리
3. **데이터 변환**: 숫자, 날짜, 텍스트 자동 변환
4. **검증 및 요약**: 데이터 검증 및 처리 결과 리포트
---
## 프론트엔드 구조
### 메인 관리 화면 (index.html)
#### 7개 주요 섹션
1. **대시보드**: 통계 요약, 최근 조제 내역
2. **환자 관리**: 환자 등록, 조회, 처방 내역
3. **입고 관리**: Excel 업로드, 입고장 관리
4. **처방 관리**: 처방 등록, 구성 관리
5. **조제 관리**: 조제 실행, 내역 조회
6. **재고 현황**: 재고 조회, 보정, 원장
7. **약재 관리**: 마스터 약재 관리
### JavaScript 애플리케이션 (app.js)
#### 주요 기능 모듈
1. **페이지 네비게이션**: SPA 방식 클라이언트 라우팅
2. **API 통신**: jQuery 기반 RESTful API 호출
3. **동적 UI 생성**: 테이블, 폼, 모달 동적 생성
4. **데이터 로딩**: 20+ 데이터 로딩 함수
5. **이벤트 처리**: 30+ 이벤트 핸들러
6. **유틸리티**: 포맷팅, 계산, 검증
### 환자 문진표 (survey.html)
#### 모바일 최적화 설계
- **반응형 레이아웃**: 모바일 친화적 UI
- **진행률 추적**: 실시간 진행 상태 표시
- **카테고리 네비게이션**: 9개 건강 카테고리
- **자동 저장**: 로컬스토리지 + 서버 동기화
- **오프라인 지원**: 로컬 백업 및 복원
---
## 주요 기능 분석
### 1. 재고 관리 시스템
#### 2단계 약재 체계
```
1단계: herb_masters (성분코드 기준)
2단계: herb_items (제조사별 제품)
로트: inventory_lots (입고 단위)
```
#### FIFO 재고 관리
- 선입선출 원칙으로 자동 차감
- 로트별 추적 가능
- 정확한 원가 계산
### 2. Excel 입고 자동화
#### 지원 형식
- 한의사랑 거래명세표
- 한의정보 거래명세표
- 자동 형식 감지 및 처리
#### 처리 프로세스
```
Excel 업로드 → 형식 감지 → 데이터 추출 → 검증 → DB 저장 → 로트 생성
```
### 3. 처방-조제 시스템
#### 처방 구성
- ingredient_code 기반 (유연한 제품 선택)
- 기본 첩수/파우치 설정
- 1첩당 용량 관리
#### 조제 프로세스
```
처방 선택 → 제품 선택 → 원산지/로트 선택 → 조제 실행 → 재고 차감
```
### 4. 원가 관리
#### 원가 계산 방식
- 로트별 단가 기준
- FIFO 차감 시 실제 원가 추적
- 조제별 정확한 원가 집계
### 5. 환자 문진표 시스템
#### 특징
- 모바일 최적화
- 9개 건강 카테고리
- 진행률 추적
- 자동 저장/복원
---
## 비즈니스 로직
### 1. 단위 환산 체계
```
1제 = 20첩 (기본값, 조정 가능)
1제 = 30파우치 (기본값, 조정 가능)
```
### 2. 재고 차감 로직
```python
def consume_inventory(herb_item_id, quantity_needed):
# 1. 가용 로트 조회 (FIFO 순서)
lots = get_available_lots(herb_item_id, order='received_date')
# 2. 순차적 차감
consumptions = []
for lot in lots:
if quantity_needed <= 0:
break
consumed = min(lot.quantity_onhand, quantity_needed)
lot.quantity_onhand -= consumed
quantity_needed -= consumed
consumptions.append({
'lot_id': lot.id,
'quantity': consumed,
'unit_price': lot.unit_price_per_g
})
# 3. stock_ledger 기록
# 4. compound_consumptions 기록
return consumptions
```
### 3. 원가 계산
```python
def calculate_cost(consumptions):
total_cost = 0
for consumption in consumptions:
cost = consumption['quantity'] * consumption['unit_price']
total_cost += cost
return total_cost
```
### 4. 재고 보정 유형
- **감모**: 자연 감소, 손실
- **발견**: 추가 발견된 재고
- **재고조사**: 실사 보정
- **반품**: 반품 처리
- **기타**: 기타 사유
---
## 기술 스택
### 백엔드
- **언어**: Python 3.12
- **프레임워크**: Flask
- **데이터베이스**: SQLite 3
- **Excel 처리**: pandas, openpyxl
- **CORS**: flask-cors
### 프론트엔드
- **HTML5/CSS3**: 시맨틱 마크업
- **JavaScript**: ES6+
- **라이브러리**:
- jQuery 3.6.0 (DOM, AJAX)
- Bootstrap 5.1.3 (UI Framework)
- Bootstrap Icons 1.8.1
### 개발/운영 환경
- **OS**: Linux (6.8.4-3-pve)
- **가상환경**: Python venv
- **포트**: 5001
- **프로세스 관리**: bash script
---
## 개선 권장사항
### 1. 성능 최적화
- [ ] 데이터베이스 인덱스 최적화
- [ ] API 페이지네이션 구현
- [ ] 캐싱 전략 도입
- [ ] 대용량 데이터 처리 개선
### 2. 보안 강화
- [ ] 사용자 인증/인가 시스템
- [ ] API 접근 제어
- [ ] SQL Injection 방어 강화
- [ ] XSS 방어 강화
### 3. 코드 품질
- [ ] 코드 모듈화 (app.py 분리)
- [ ] 에러 핸들링 개선
- [ ] 단위 테스트 추가
- [ ] API 문서 자동화 (Swagger)
### 4. 사용자 경험
- [ ] 실시간 알림 시스템
- [ ] 대시보드 커스터마이징
- [ ] 고급 검색 기능
- [ ] 다크 모드 지원
### 5. 기능 확장
- [ ] 바코드/QR 코드 지원
- [ ] 보고서 생성 기능
- [ ] 다중 사업장 지원
- [ ] 모바일 앱 개발
### 6. 데이터 관리
- [ ] 자동 백업 시스템
- [ ] 데이터 마이그레이션 도구
- [ ] 감사 로그 시스템
- [ ] 데이터 분석 대시보드
---
## 결론
kdrug 프로젝트는 한의원의 실무 요구사항을 충실히 반영한 **실용적이고 체계적인 관리 시스템**입니다.
### 강점
1. **표준화**: 건강보험 표준 코드 기반
2. **자동화**: Excel 입고 자동 처리
3. **정확성**: FIFO 기반 정확한 원가 추적
4. **사용성**: 직관적인 UI/UX
5. **확장성**: 모듈화된 구조
### 핵심 가치
- 업무 효율성 향상
- 정확한 재고 관리
- 체계적인 원가 관리
- 환자 서비스 품질 향상
프로젝트는 지속적인 개선과 확장을 통해 한의원 통합 관리 솔루션으로 발전할 수 있는 견고한 기반을 갖추고 있습니다.
---
**작성일**: 2026-02-16
**작성자**: Claude AI Assistant
**버전**: 1.0

View File

@@ -0,0 +1,283 @@
1. 원방 갈근탕 vs OTC 갈근탕 성분 비교
(1) 상한론 원방 갈근탕 (기준 처방, 상대비)
전통적인 갈근탕 구성 (비율 기준):
약재 원방 용량 비율 (%)
갈근 8 29.6%
마황 4 14.8%
계지 3 11.1%
작약 3 11.1%
생강 3 11.1%
대추 4 14.8%
감초 2 7.4%
총합 27 100%
(2) 현재 OTC 갈근탕 (사진 제품 기준)
표기된 생약 환산량 총합:
작약 1 g
감초 0.67 g
마황 1.33 g
건강 0.33 g
계지 1 g
갈근 2.67 g
대추 1.33 g
총합 = 8.33 g
비율 계산:
약재 함량 비율 (%)
갈근 2.67 g 32.0%
마황 1.33 g 16.0%
계지 1.00 g 12.0%
작약 1.00 g 12.0%
건강 0.33 g 4.0%
대추 1.33 g 16.0%
감초 0.67 g 8.0%
총합 8.33 g 100%
2. 핵심 비교 결과
✔ 거의 동일한 처방 구조
특히 주요 약재:
약재 원방 OTC 평가
갈근 29.6% 32% 거의 동일
마황 14.8% 16% 동일
계지 11.1% 12% 동일
작약 11.1% 12% 동일
대추 14.8% 16% 동일
감초 7.4% 8% 동일
➡ 즉, 처방 철학은 그대로 유지됨
⚠ 차이점 1 — 생강 감소
원방 생강: 11.1%
OTC 건강: 4%
→ 이유:
보존성
안정성
위장 자극 감소
제품 표준화 목적
⚠ 차이점 2 — 총량 감소 (중요)
원방 1첩 기준 (현대 환산):
보통 30~40 g 생약
OTC:
8.33 g
약 1/4 수준
하지만
연조엑스 2.23 g로 농축됨
추출수율:
2.23 / 8.33 = 약 27%
정상적인 수율입니다.
3. 실제 임상 효과 차이
✔ 효과 방향성은 동일
발한
근육이완
해열
항염
동일
⚠ 그러나 potency 차이 존재
탕약 > OTC
이유:
탕약은
더 많은 총량
휘발성 성분 보존
생강 성분 풍부
OTC는
편의성
일정한 품질
경증에 적합
4. 실제 임상 equivalent 환산
OTC 1포 = 생약 8.33 g
탕약 1첩 = 약 30 g
OTC 3포 ≈ 탕약 1첩 약효
5. 약사 관점 핵심 요약
✔ 처방 구성 → 동일
✔ 비율 → 거의 동일
✔ 총량 → OTC가 적음
✔ 약효 강도 → 탕약 > OTC
✔ 경증 감기 → OTC 충분
✔ 근육통 강하거나 오한 심하면 → 탕약 유리
6. 결론 (핵심)
OTC 갈근탕은
✔ 원방 처방 비율을 거의 그대로 유지한 농축 extract이며
✔ 처방 자체가 바뀐 것이 아니라
✔ 총량만 줄인 표준화된 형태입니다.
“1제 20첩 → 30파우치” 기준으로 OTC 갈근탕과 정확히 potency 비교를 계산해보겠습니다.
1. 먼저 기준 정리
한약국 탕전 기준
1제 = 20첩 → 30파우치
1파우치 = 20첩 / 30
= 0.667첩
OTC 갈근탕 기준 (사진 제품)
1포 = 생약 환산량 8.33 g
2. 갈근탕 1첩의 평균 생약량
실제 한약국 갈근탕 1첩 구성:
약재 평균
갈근 8 g
마황 4 g
계지 3 g
작약 3 g
생강 3 g
대추 4 g
감초 2 g
총합 약 27 g
1첩 = 27 g 생약
3. 한약국 1파우치의 생약량
1파우치 = 0.667첩
= 27 × 0.667
= 18 g
✔ 결론:
한약국 갈근탕 1파우치 = 생약 약 18 g
4. OTC 1포 vs 한약국 1파우치 비교
구분 생약량
OTC 갈근탕 1포 8.33 g
한약국 갈근탕 1파우치 18 g
5. 비율 계산
18 / 8.33 = 2.16
✔ 한약국 1파우치 = OTC 2.16포
6. 실전 복용 기준 비교
OTC:
1회 1포
한약국:
1회 1파우치
한약국 1회 용량 = OTC 약 2.2배 potency
7. 하루 기준 비교
OTC:
1일 3포
= 8.33 × 3
= 25 g 생약
한약국:
1일 3파우치
= 18 × 3
= 54 g 생약
8. 하루 potency 비교
54 / 25 = 2.16배
✔ 동일하게 약 2.2배 차이
9. 임상적으로 매우 중요한 의미
다시 말하면
구분 potency
OTC 갈근탕 1
한약국 갈근탕 2.2
✔ 한약국 갈근탕이 약 2배 이상 강함
10. 약사 실전 해석
경증 감기
→ OTC 충분
다음 경우
→ 한약국 탕약이 훨씬 효과적
오한 심함
무한
항강 심함
근육통 심함
초기 인플루엔자
체격 큰 성인
11. 매우 중요한 추가 포인트 (추출 효율 차이)
탕약은
휘발성 성분 유지
생강 정유 유지
마황 알칼로이드 extraction optimal
OTC는
제조 안정성 위해 일부 감소
즉 실제 체감 potency는
2.2배 × (추출차이 포함)
≈ 2.5배 정도
최종 결론 (약국 기준 정확 비교)
한약국 갈근탕 1파우치 ≈ OTC 갈근탕 약 2.2포
또는
한약국 갈근탕 1일 복용량 ≈ OTC 약 6~7포 수준
원하시면
갈근탕뿐 아니라
소청룡탕
마황탕
소시호탕
도 동일 기준으로 정확 potency 비교표 만들어 드리겠습니다

View File

@@ -0,0 +1,250 @@
# 한약국 첩제(탕전) vs OTC 상담 가이드
> 본 문서는 한약국에서 직접 달여 만드는 첩제(탕약)가 시중 OTC 액제/연조엑스 대비 어떤 차별점을 갖는지 체계적으로 정리한 기반 문서입니다.
> AI 상담, 알림톡, 웹 URL 설명 자료, 100처방 마스터 reference_notes 작성 시 참고합니다.
---
## 1. 용어 정리
| 용어 | 설명 |
|------|------|
| **첩제(탕약)** | 한약국에서 생약을 직접 달여 파우치로 포장한 탕전약 |
| **OTC 액제** | 약국에서 처방 없이 판매하는 한약 액상 추출물 (갈근탕, 쌍화탕 등) |
| **연조엑스** | 생약에서 추출·농축한 반고체 또는 액상 엑스제 |
| **원방** | 상한론, 금궤요략 등 원전에 기재된 원래 처방 구성 |
| **1제** | 한약국 조제 단위 = 20첩 = 30파우치 (표준) |
---
## 2. 핵심 차별점 요약 (5대 포인트)
### 2-1. 생약 총량 차이 (Potency)
한약국 첩제는 OTC 대비 **약 2~2.5배** 높은 생약량을 함유합니다.
| 구분 | 갈근탕 기준 | 비고 |
|------|-----------|------|
| 원방 1첩 생약량 | 약 27g | 전통 용량 |
| 한약국 1파우치 | 약 18g | 1제=20첩→30파우치, 0.667첩분 |
| OTC 1포 | 약 8.33g | 제품 표기 생약 환산량 |
| **파우치 vs OTC** | **2.16배** | 18g ÷ 8.33g |
- 1일 기준: 한약국 3파우치(54g) vs OTC 3포(25g) → **2.16배**
- 추출 효율 차이 감안 시 실제 체감 **약 2.5배**
> **상담 포인트:** "시중 갈근탕 1포는 저희 한약 1파우치의 절반도 안 되는 양입니다. 증상이 심하실 때는 첩제가 훨씬 효과적입니다."
### 2-2. 휘발성 성분 보존
| 구분 | 첩제 | OTC |
|------|------|-----|
| 생강 정유(gingerol) | 생강 그대로 달여 보존 | 건강(乾薑) 대체, 정유 감소 |
| 마황 알칼로이드 | 최적 추출 온도로 보존 | 공장 공정 중 일부 손실 |
| 계피 정유(cinnamaldehyde) | 탕전 직후 밀봉 보존 | 장기 보관 과정에서 감소 |
| 박하·형개 등 방향성 약재 | 후하(後下) 기법으로 보존 | 일괄 추출로 손실 |
> **상담 포인트:** "감기에 중요한 발한 성분은 휘발성이라 달인 직후가 가장 강합니다. 공장에서 만들어 유통하는 과정에서 상당 부분 날아갑니다."
### 2-3. 맞춤 처방 (가감방)
| 구분 | 첩제 | OTC |
|------|------|-----|
| 처방 조정 | 원방 기준 가감 가능 | 고정 처방, 변경 불가 |
| 용량 조절 | 환자 체질·증상에 맞춤 | 표준 1회 용량 고정 |
| 약재 추가/제거 | 가능 (가미, 거방) | 불가 |
| 첩수/파우치 조절 | 1제 기준 자유 조절 | 고정 포수 |
> **상담 포인트:** "저희는 선생님 증상에 맞춰 약재를 추가하거나 빼서 조절할 수 있습니다. 기성 제품은 모든 사람에게 같은 처방입니다."
### 2-4. 신선도와 품질 관리
| 구분 | 첩제 | OTC |
|------|------|-----|
| 조제 시점 | 주문 후 당일 탕전 | 제조일로부터 수개월~수년 |
| 유통기한 | 냉장 2주, 냉동 3개월 | 1~3년 |
| 첨가물 | 없음 (순수 탕전액) | 보존제, 감미제, 착향료 가능 |
| 추출 용매 | 정제수 | 정제수 + 에탄올(일부) |
> **상담 포인트:** "저희 한약은 오늘 달여서 바로 드리는 신선한 약입니다. 첨가물도 전혀 없습니다."
### 2-5. 복합 처방의 시너지
| 구분 | 첩제 | OTC |
|------|------|-----|
| 약재간 상호작용 | 함께 달여 시너지 극대화 | 개별 추출 후 혼합 가능 |
| 군신좌사(君臣佐使) | 전통 배합 원리 그대로 구현 | 추출 효율 중심 공정 |
| 약재 품질 확인 | 한약사가 직접 확인 | 제조사 QC에 의존 |
> **상담 포인트:** "한약은 여러 약재를 함께 달일 때 약효가 높아지는 상승 작용이 있습니다. 공장에서는 각각 추출해서 섞는 방식이라 이 시너지를 온전히 살리기 어렵습니다."
---
## 3. 가격 대비 가치 분석 (업셀링 근거)
### 3-1. 실질 복용 단가 비교
| 구분 | 단가(예시) | 1회 생약량 | g당 단가 |
|------|-----------|-----------|---------|
| OTC 갈근탕 1포 | ~1,500원 | 8.33g | ~180원/g |
| 한약국 1파우치 | ~3,000~5,000원 | 18g | ~167~278원/g |
**g당 단가는 비슷하거나 오히려 저렴** (생약량 기준)
### 3-2. 효과 대비 비용
| 시나리오 | OTC | 한약국 첩제 |
|----------|-----|-----------|
| 경증 감기 | OTC 3일분 ~13,500원 | 첩제 불필요 |
| 중등도 감기·몸살 | OTC 5일분 ~22,500원 (효과 제한적) | 첩제 3일분 ~27,000~45,000원 (빠른 회복) |
| 중증 오한·근육통 | OTC 효과 미흡 → 추가 진료비 | 첩제 단독 대응 가능 |
> **업셀링 포인트:** "OTC로 5일 드셔도 안 나으면 결국 더 쓰시게 됩니다. 첩제는 2~3일이면 확실한 차이를 느끼실 수 있어요."
---
## 4. 증상별 첩제 추천 기준
### OTC로 충분한 경우
- 경미한 감기 초기 (콧물, 미열)
- 가벼운 소화불량
- 일시적 피로감
- 예방 목적 복용
### 첩제가 확실히 유리한 경우
- 심한 오한, 고열, 무한(無汗)
- 근육통·관절통이 동반된 감기
- 만성 피로, 기혈 허약
- 수술 후 회복, 산후 보양
- 만성 소화기 질환
- 기침·천식이 심한 경우
- 체질적으로 허약한 환자
- OTC 복용 후에도 증상 지속
---
## 5. 100처방별 reference_notes 작성 가이드
각 처방의 `official_formulas.reference_notes`에 아래 구조로 작성하면 일관된 상담 자료가 됩니다.
### 작성 템플릿
```
[처방명] 상담 참고자료
■ 처방 개요
- 출전: (출전서적)
- 원방 구성: (주요 약재 나열)
- 주치: (어떤 증상에 사용하는지)
■ OTC 대비 첩제 장점
- (해당 처방 특유의 차별점)
- (OTC에서 빠지거나 줄어드는 약재)
- (potency 차이)
■ 적응 환자
- (어떤 환자에게 추천하면 좋은지)
- (OTC로는 부족한 케이스)
■ 상담 화법
- "(환자에게 직접 쓸 수 있는 멘트)"
■ 주의사항
- (금기, 주의할 체질 등)
```
### 작성 예시: 갈근탕
```
[갈근탕] 상담 참고자료
■ 처방 개요
- 출전: 상한론
- 원방 구성: 갈근 8g, 마황 4g, 계지 3g, 작약 3g, 생강 3g, 대추 4g, 감초 2g
- 주치: 외감풍한, 두통, 발열, 오한, 항강(뒷목 뻣뻣), 무한
■ OTC 대비 첩제 장점
- 생약 총량 2.16배 (파우치 18g vs OTC 8.33g)
- 생강 → OTC는 건강으로 대체, 발한 효과 감소
- 마황 알칼로이드 최적 추출 (탕전 > 공장 추출)
- 추출 효율 포함 시 실질 potency 약 2.5배
■ 적응 환자
- 심한 오한과 뒷목 뻣뻣함이 있는 감기
- 체격이 큰 성인 (OTC 용량으로는 부족)
- 근육통이 심한 초기 인플루엔자
- OTC 갈근탕 3일 복용에도 증상 지속 시
■ 상담 화법
- "시중 갈근탕은 저희 약의 절반도 안 되는 양이에요.
오한이 심하시면 저희 약이 확실히 빠릅니다."
- "뒷목이 뻣뻣하고 땀이 안 나시죠?
이런 증상에는 농도 높은 탕약이 훨씬 효과적입니다."
■ 주의사항
- 마황 함유 → 고혈압, 심장질환 환자 주의
- 자한(自汗, 저절로 땀 나는 경우) 환자에게는 부적합
- 허약 체질에는 용량 조절 필요
```
---
## 6. AI 활용 시나리오
### 6-1. 알림톡/문자 상담
```
[환자명]님, 요즘 감기가 유행이네요.
시중 감기약으로 안 낫는 심한 오한·근육통에는
저희 한약국의 갈근탕이 2배 이상 강력합니다.
▶ 자세히 보기: [웹URL]
```
### 6-2. 웹 설명 페이지 구성
1. 처방 소개 (원방 유래, 출전)
2. OTC와 비교표 (생약량, 성분 차이)
3. 이런 분께 추천 (적응 환자)
4. 가격 안내 및 주문
### 6-3. AI 챗봇 상담 데이터
- `official_formulas.reference_notes` → RAG 소스
- 환자 증상 → 100처방 중 매칭 → 첩제 장점 설명
- OTC 대비 차별점을 자동으로 안내
---
## 7. 핵심 수치 요약 (전 처방 공통)
| 지표 | 수치 | 근거 |
|------|------|------|
| 첩제 vs OTC 생약량 | 약 2~2.5배 | 1파우치 ÷ OTC 1포 |
| 휘발성 성분 보존율 | 첩제 >> OTC | 탕전 직후 밀봉 vs 공장 유통 |
| 맞춤 처방 가능 여부 | 첩제 O / OTC X | 가감방 |
| 첨가물 | 첩제 0 / OTC 有 | 보존제, 감미제 |
| 복용 편의성 | 첩제 = OTC | 파우치 포장 동일 |
---
## 부록: 처방별 OTC 존재 여부 참고
100처방 중 OTC 제품이 시판되는 주요 처방 (비교 대상):
| 처방 | OTC 시판 | 비교 포인트 |
|------|---------|-----------|
| 갈근탕 | O | 생약량 2.16배, 생강→건강 |
| 쌍화탕 | O | 녹용·당귀 품질 차이, 맞춤 가감 |
| 소청룡탕 | O | 세신·마황 용량 차이 |
| 반하사심탕 | O | 반하 품질·용량 차이 |
| 소시호탕 | O | 시호·인삼 용량 차이 |
| 보중익기탕 | O | 황기·인삼 용량 차이 |
| 십전대보탕 | O | 전체 약재 용량 차이 |
| 방풍통성산 | O | 다제 처방, 용량 차이 큼 |
| 오적산 | O | 구성 약재 다수, 총량 차이 |
| 귀비탕 | O | 용안육·산조인 품질 차이 |
> 위 처방들은 OTC와 직접 비교가 가능하므로 업셀링 효과가 가장 큰 처방군입니다.
> 나머지 처방은 OTC가 없으므로 "한약국에서만 받을 수 있는 처방"으로 포지셔닝합니다.
---
*본 문서는 한약국 운영 시스템(kdrug)의 AI 상담 기반 데이터로 활용됩니다.*
*100처방 마스터 테이블(`official_formulas.reference_notes`)과 연동하여 사용하세요.*

View File

@@ -0,0 +1,462 @@
# 한약재 정보 관리 시스템 (K-Drug Information System)
## 1. 개요
### 1.1 목표
- 양방약 DUR(Drug Utilization Review) 시스템처럼 한약재 정보를 체계적으로 관리
- AI/API를 통한 지속적인 정보 업데이트
- 근거 기반 한의학(Evidence-Based Korean Medicine) 데이터베이스 구축
### 1.2 벤치마킹
- **건강보험심사평가원 의약품안전사용서비스(DUR)**
- **KIMS (대한민국의약정보센터)**
- **Micromedex (미국)**
- **한국한의학연구원 전통의학정보포털**
## 2. 데이터베이스 설계
### 2.1 핵심 테이블 구조
```sql
-- 1. 약재 기본 정보 (확장)
CREATE TABLE herb_master_extended (
herb_id INTEGER PRIMARY KEY,
ingredient_code VARCHAR(10) UNIQUE,
-- 기본 명칭
name_korean VARCHAR(100) NOT NULL,
name_hanja VARCHAR(100),
name_latin VARCHAR(200),
name_english VARCHAR(200),
name_pharmaceutical VARCHAR(200), -- 약전명
-- 분류 정보
family_latin VARCHAR(100), -- 과명
genus_species VARCHAR(200), -- 학명
origin_plant TEXT, -- 기원식물
medicinal_part VARCHAR(100), -- 약용부위
-- 성미귀경
property VARCHAR(50), -- 성(性): 한/열/온/량/평
taste VARCHAR(100), -- 미(味): 고/감/산/신/함/담
meridian_tropism TEXT, -- 귀경: 입경 경락
-- 효능 효과
main_effects TEXT, -- 주요 효능
indications TEXT, -- 적응증
contraindications TEXT, -- 금기증
precautions TEXT, -- 주의사항
-- 용법 용량
dosage_range VARCHAR(50), -- 상용량 (예: "3-12g")
dosage_max VARCHAR(50), -- 극량
preparation_method TEXT, -- 포제법
-- 성분 정보
active_compounds TEXT, -- 주요 성분
chemical_constituents JSON, -- 화학 성분 상세 (JSON)
-- 약리 작용
pharmacological_effects TEXT, -- 약리작용
clinical_applications TEXT, -- 임상응용
-- 상호작용
drug_interactions JSON, -- 약물 상호작용 (JSON)
food_interactions JSON, -- 음식 상호작용 (JSON)
-- 품질 기준
quality_standards TEXT, -- 품질 기준
identification_method TEXT, -- 감별법
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
data_source VARCHAR(100), -- 데이터 출처
reliability_score INTEGER, -- 신뢰도 점수 (1-10)
review_status VARCHAR(20) -- 검토 상태
);
-- 2. 약재 연구 문헌
CREATE TABLE herb_research_papers (
paper_id INTEGER PRIMARY KEY,
ingredient_code VARCHAR(10), -- 성분코드 직접 사용 (개선)
title TEXT NOT NULL,
authors TEXT,
journal VARCHAR(200),
publication_year INTEGER,
volume VARCHAR(50),
pages VARCHAR(50),
doi VARCHAR(100),
pubmed_id VARCHAR(20),
abstract TEXT,
keywords TEXT,
study_type VARCHAR(50), -- RCT, 관찰연구, 리뷰 등
evidence_level INTEGER, -- 근거수준 (1-5)
findings TEXT, -- 주요 발견
clinical_relevance TEXT, -- 임상적 의미
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
indexed_at TIMESTAMP
);
-- 3. 약재 안전성 정보
CREATE TABLE herb_safety_info (
safety_id INTEGER PRIMARY KEY,
ingredient_code VARCHAR(10), -- 성분코드 직접 사용 (개선)
-- 독성 정보
toxicity_level VARCHAR(20), -- 독성 등급
ld50_value VARCHAR(50), -- 반수치사량
toxic_compounds TEXT, -- 독성 성분
-- 부작용
common_side_effects TEXT, -- 흔한 부작용
rare_side_effects TEXT, -- 드문 부작용
serious_adverse_events TEXT, -- 중대 이상반응
-- 특수 집단
pregnancy_category VARCHAR(10), -- 임신 등급
pregnancy_safety TEXT, -- 임신 안전성
lactation_safety TEXT, -- 수유 안전성
pediatric_use TEXT, -- 소아 사용
geriatric_use TEXT, -- 노인 사용
-- 모니터링
monitoring_parameters TEXT, -- 모니터링 항목
laboratory_tests TEXT, -- 필요 검사
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 처방 구성 규칙
CREATE TABLE prescription_rules (
rule_id INTEGER PRIMARY KEY,
-- 배합 규칙 (성분코드 사용으로 개선)
ingredient_code_1 VARCHAR(10),
ingredient_code_2 VARCHAR(10),
relationship_type VARCHAR(50), -- 상수/상사/상외/상오/상쇄/상반/상살
description TEXT,
clinical_significance TEXT,
evidence_source TEXT,
severity_level INTEGER, -- 심각도 (1-5)
action_required VARCHAR(50), -- 조치사항
is_absolute BOOLEAN, -- 절대 금기 여부
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. 질병-약재 매핑
CREATE TABLE disease_herb_mapping (
mapping_id INTEGER PRIMARY KEY,
disease_code VARCHAR(20), -- KCD 코드
disease_name VARCHAR(200),
herb_id INTEGER REFERENCES herb_master_extended(herb_id),
indication_type VARCHAR(50), -- 주적응증/부적응증
evidence_level INTEGER, -- 근거수준
recommendation_grade VARCHAR(10), -- 권고등급
clinical_notes TEXT,
references TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6. AI/API 업데이트 로그
CREATE TABLE data_update_logs (
log_id INTEGER PRIMARY KEY,
update_type VARCHAR(50), -- AI/API/MANUAL
source VARCHAR(100), -- 데이터 소스
target_table VARCHAR(50),
target_id INTEGER,
before_data JSON, -- 변경 전 데이터
after_data JSON, -- 변경 후 데이터
update_reason TEXT,
confidence_score FLOAT, -- AI 신뢰도
is_reviewed BOOLEAN DEFAULT FALSE,
reviewed_by VARCHAR(50),
review_notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## 3. API/AI 연동 방안
### 3.1 외부 데이터 소스
#### 국내 소스
- **한국한의학연구원 API**
- 한약재 데이터베이스
- 처방 데이터베이스
- 임상 연구 자료
- **건강보험심사평가원**
- 한약제제 급여 정보
- 안전성 정보
- **식품의약품안전처**
- 한약재 품질 기준
- 안전성 정보
#### 국제 소스
- **PubMed API**
- 한약재 연구 논문
- 임상시험 결과
- **WHO Traditional Medicine**
- 국제 표준 정보
- 안전성 데이터
- **ClinicalTrials.gov**
- 진행 중인 임상시험
### 3.2 AI 활용 방안
```python
# AI 기반 정보 추출 및 업데이트 예시
class HerbInfoAIUpdater:
def __init__(self):
self.nlp_model = load_korean_medical_nlp()
self.ocr_model = load_medical_ocr()
def extract_from_literature(self, pdf_path):
"""의학 문헌에서 약재 정보 추출"""
text = self.ocr_model.extract_text(pdf_path)
entities = self.nlp_model.extract_entities(text, types=[
'HERB_NAME',
'DOSAGE',
'INDICATION',
'CONTRAINDICATION',
'SIDE_EFFECT',
'INTERACTION'
])
return self.validate_and_structure(entities)
def update_from_clinical_data(self, clinical_records):
"""임상 데이터에서 패턴 분석"""
# 처방 패턴 분석
prescription_patterns = self.analyze_prescription_patterns(clinical_records)
# 효능 검증
efficacy_data = self.validate_efficacy(clinical_records)
# 안전성 모니터링
safety_signals = self.detect_safety_signals(clinical_records)
return {
'patterns': prescription_patterns,
'efficacy': efficacy_data,
'safety': safety_signals
}
def cross_reference_validation(self, herb_info):
"""교차 검증"""
sources = [
self.query_kmri_api(herb_info['name']),
self.query_pubmed(herb_info['latin_name']),
self.query_who_database(herb_info['code'])
]
return self.reconcile_information(sources)
```
## 4. 기능 구현
### 4.1 약재 정보 조회 API
```python
@app.route('/api/herbs/<int:herb_id>/full-info', methods=['GET'])
def get_herb_full_info(herb_id):
"""약재 종합 정보 조회"""
return {
'basic_info': get_basic_info(herb_id),
'pharmacology': get_pharmacology(herb_id),
'safety': get_safety_info(herb_id),
'interactions': get_interactions(herb_id),
'research': get_research_papers(herb_id),
'clinical_use': get_clinical_applications(herb_id)
}
@app.route('/api/herbs/search', methods=['POST'])
def search_herbs_advanced():
"""고급 검색"""
criteria = request.json
# 증상으로 검색
if criteria.get('symptoms'):
return search_by_symptoms(criteria['symptoms'])
# 성분으로 검색
if criteria.get('compounds'):
return search_by_compounds(criteria['compounds'])
# 처방 호환성 검색
if criteria.get('compatibility'):
return check_prescription_compatibility(criteria['herbs'])
```
### 4.2 안전성 검증 시스템
```python
class HerbSafetyChecker:
def check_prescription_safety(self, herbs, patient_info):
"""처방 안전성 종합 검증"""
results = {
'is_safe': True,
'warnings': [],
'contraindications': [],
'interactions': [],
'dosage_alerts': []
}
# 1. 약재 간 상호작용 확인
for herb1, herb2 in combinations(herbs, 2):
interaction = self.check_herb_interaction(herb1, herb2)
if interaction:
results['interactions'].append(interaction)
# 2. 환자 특성별 금기 확인
if patient_info.get('pregnancy'):
self.check_pregnancy_safety(herbs, results)
if patient_info.get('allergies'):
self.check_allergy_risk(herbs, patient_info['allergies'], results)
# 3. 용량 검증
self.validate_dosages(herbs, results)
# 4. 질병-약물 상호작용
if patient_info.get('conditions'):
self.check_disease_interactions(herbs, patient_info['conditions'], results)
return results
```
### 4.3 데이터 품질 관리
```python
class DataQualityManager:
def validate_herb_data(self, herb_data):
"""데이터 품질 검증"""
scores = {
'completeness': self.check_completeness(herb_data),
'accuracy': self.verify_accuracy(herb_data),
'consistency': self.check_consistency(herb_data),
'timeliness': self.check_timeliness(herb_data)
}
herb_data['quality_score'] = sum(scores.values()) / len(scores)
herb_data['quality_details'] = scores
return herb_data
def reconcile_conflicts(self, data_sources):
"""데이터 충돌 해결"""
# 신뢰도 기반 가중 평균
weighted_data = {}
for source in data_sources:
weight = source['reliability_score']
for field, value in source['data'].items():
if field not in weighted_data:
weighted_data[field] = []
weighted_data[field].append((value, weight))
# 최종 값 결정
final_data = {}
for field, values in weighted_data.items():
final_data[field] = self.select_best_value(values)
return final_data
```
## 5. 사용자 인터페이스
### 5.1 약재 정보 대시보드
- 약재 상세 정보 카드
- 효능/효과 시각화
- 안전성 정보 알림
- 연구 논문 목록
- 처방 활용 통계
### 5.2 처방 안전성 검증
- 실시간 DUR 체크
- 약재 조합 검증
- 용량 적정성 평가
- 환자별 맞춤 알림
### 5.3 지식 관리 도구
- 새로운 연구 결과 알림
- 데이터 품질 모니터링
- AI 제안 검토
- 전문가 협업 도구
## 6. 구현 로드맵
### Phase 1: 기반 구축 (1-2개월)
- [ ] 확장 데이터베이스 스키마 구현
- [ ] 기본 CRUD API 개발
- [ ] 데이터 마이그레이션
### Phase 2: 외부 연동 (2-3개월)
- [ ] 한의학연구원 API 연동
- [ ] PubMed API 연동
- [ ] 자동 업데이트 스케줄러
### Phase 3: AI 통합 (3-4개월)
- [ ] NLP 모델 훈련
- [ ] 문헌 자동 분석
- [ ] 패턴 인식 시스템
### Phase 4: 안전성 시스템 (2개월)
- [ ] DUR 체크 시스템
- [ ] 실시간 경고 시스템
- [ ] 보고서 생성
### Phase 5: 고도화 (지속)
- [ ] 사용자 피드백 수집
- [ ] 모델 개선
- [ ] 새로운 데이터 소스 추가
## 7. 기대 효과
1. **근거 기반 처방**
- 최신 연구 결과 반영
- 객관적 데이터 기반 의사결정
2. **환자 안전성 향상**
- 실시간 안전성 검증
- 부작용 예방
3. **업무 효율성**
- 자동화된 정보 관리
- 빠른 정보 검색
4. **지식 축적**
- 체계적인 데이터베이스
- 지속적인 학습 시스템
5. **표준화**
- 한약재 정보 표준화
- 품질 관리 체계화

View File

@@ -36,6 +36,18 @@ class ExcelProcessor:
'비고': 'notes'
}
# 한퓨어 형식 컬럼 매핑
HANPURE_MAPPING = {
'상품명': 'herb_name',
'제조사코드': 'insurance_code',
'주성분코드': 'ingredient_code',
'제조사명': 'supplier_name',
'원산지': 'origin_country',
'소계': 'total_amount',
'옵션항목': 'option_detail',
'주문번호': 'order_number',
}
def __init__(self):
self.format_type = None
self.df_original = None
@@ -45,6 +57,11 @@ class ExcelProcessor:
"""Excel 형식 자동 감지"""
columns = df.columns.tolist()
# 한퓨어 형식 체크 (주문번호, 주성분코드, 제조사코드, 옵션항목이 특징)
hanpure_cols = ['주문번호', '주성분코드', '제조사코드', '상품명', '옵션항목']
if all(col in columns for col in hanpure_cols):
return 'hanpure'
# 한의사랑 형식 체크
hanisarang_cols = ['품목명', '제품코드', '일그램당단가', '총구입량', '총구입단가']
if all(col in columns for col in hanisarang_cols):
@@ -64,7 +81,10 @@ class ExcelProcessor:
def read_excel(self, file_path):
"""Excel 파일 읽기"""
try:
self.df_original = pd.read_excel(file_path)
# 코드 컬럼을 문자열로 읽기 위한 dtype 설정
self.df_original = pd.read_excel(file_path, dtype={
'제품코드': str, '제조사코드': str, '주성분코드': str, '대표코드': str
})
self.format_type = self.detect_format(self.df_original)
return True
except Exception as e:
@@ -82,6 +102,12 @@ class ExcelProcessor:
if old_col in df.columns:
df_mapped[new_col] = df[old_col]
# 보험코드 9자리 패딩 처리
if 'insurance_code' in df_mapped.columns:
df_mapped['insurance_code'] = df_mapped['insurance_code'].apply(
lambda x: str(x).zfill(9) if pd.notna(x) and str(x).isdigit() else str(x) if pd.notna(x) else None
)
# 업체명 추가 (기본값)
df_mapped['supplier_name'] = '한의사랑'
@@ -112,6 +138,12 @@ class ExcelProcessor:
if old_col in df.columns:
df_mapped[new_col] = df[old_col]
# 보험코드 9자리 패딩 처리
if 'insurance_code' in df_mapped.columns:
df_mapped['insurance_code'] = df_mapped['insurance_code'].apply(
lambda x: str(x).zfill(9) if pd.notna(x) and str(x).isdigit() else str(x) if pd.notna(x) else None
)
# 날짜 처리 (YYYYMMDD 형식)
if 'receipt_date' in df_mapped.columns:
df_mapped['receipt_date'] = df_mapped['receipt_date'].astype(str)
@@ -124,12 +156,96 @@ class ExcelProcessor:
self.df_processed = df_mapped
return df_mapped
@staticmethod
def parse_option_quantity_g(option_text):
"""옵션항목에서 총 중량(g) 파싱
예: '인삼 특A (4~5년근) 600g*5개' → 3000
'감초 1kg' → 1000
'복령 500g' → 500
'백출 300g*3개' → 900
"""
if not option_text or pd.isna(option_text):
return None
text = str(option_text)
# 패턴1: NNNg*N개 또는 NNNg×N개
m = re.search(r'(\d+(?:\.\d+)?)\s*g\s*[*×x]\s*(\d+)', text, re.IGNORECASE)
if m:
return float(m.group(1)) * int(m.group(2))
# 패턴2: N kg*N개
m = re.search(r'(\d+(?:\.\d+)?)\s*kg\s*[*×x]\s*(\d+)', text, re.IGNORECASE)
if m:
return float(m.group(1)) * 1000 * int(m.group(2))
# 패턴3: NNNg (단독)
m = re.search(r'(\d+(?:\.\d+)?)\s*g(?!\w)', text, re.IGNORECASE)
if m:
return float(m.group(1))
# 패턴4: Nkg (단독)
m = re.search(r'(\d+(?:\.\d+)?)\s*kg(?!\w)', text, re.IGNORECASE)
if m:
return float(m.group(1)) * 1000
return None
def process_hanpure(self):
"""한퓨어 형식 처리"""
df = self.df_original.copy()
df_mapped = pd.DataFrame()
for old_col, new_col in self.HANPURE_MAPPING.items():
if old_col in df.columns:
df_mapped[new_col] = df[old_col]
# 보험코드 9자리 패딩 처리
if 'insurance_code' in df_mapped.columns:
df_mapped['insurance_code'] = df_mapped['insurance_code'].apply(
lambda x: str(x).zfill(9) if pd.notna(x) and str(x).strip().isdigit() else str(x).strip() if pd.notna(x) else None
)
# 주문번호에서 날짜 추출 (20260211-22511888 → 20260211)
if 'order_number' in df_mapped.columns:
df_mapped['receipt_date'] = df_mapped['order_number'].apply(
lambda x: str(x).split('-')[0] if pd.notna(x) else None
)
# 옵션항목에서 중량(g) 파싱
if 'option_detail' in df_mapped.columns:
df_mapped['quantity'] = df_mapped['option_detail'].apply(self.parse_option_quantity_g)
# 업체명 기본값
if 'supplier_name' not in df_mapped.columns or df_mapped['supplier_name'].isnull().all():
df_mapped['supplier_name'] = '한퓨어'
# 단가 계산 (소계 / 중량g)
if 'total_amount' in df_mapped.columns and 'quantity' in df_mapped.columns:
df_mapped['unit_price'] = df_mapped.apply(
lambda row: round(row['total_amount'] / row['quantity'], 2)
if pd.notna(row.get('quantity')) and row.get('quantity', 0) > 0
else None, axis=1
)
# 비고에 옵션항목 원문 저장
df_mapped['notes'] = df_mapped.get('option_detail', '')
# 임시 컬럼 제거
df_mapped.drop(columns=['order_number', 'option_detail'], errors='ignore', inplace=True)
self.df_processed = df_mapped
return df_mapped
def process(self):
"""형식에 따라 자동 처리"""
if self.format_type == 'hanisarang':
return self.process_hanisarang()
elif self.format_type == 'haninfo':
return self.process_haninfo()
elif self.format_type == 'hanpure':
return self.process_hanpure()
else:
raise ValueError(f"지원하지 않는 형식: {self.format_type}")
@@ -208,7 +324,8 @@ class ExcelProcessor:
standard_columns = [
'insurance_code', 'supplier_name', 'herb_name',
'receipt_date', 'quantity', 'total_amount',
'unit_price', 'origin_country', 'notes'
'unit_price', 'origin_country', 'notes',
'ingredient_code'
]
# 있는 컬럼만 선택

192
find_duplicate_issue.py Normal file
View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
JOIN으로 인한 중복 문제 찾기
"""
import sqlite3
def find_duplicate_issue():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=" * 80)
print("JOIN으로 인한 중복 문제 분석")
print("=" * 80)
print()
# 1. 효능 태그 JOIN 없이 계산
print("1. 효능 태그 JOIN 없이 계산")
print("-" * 60)
cursor.execute("""
SELECT
h.herb_item_id,
h.herb_name,
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
GROUP BY h.herb_item_id, h.herb_name
HAVING total_value > 0
ORDER BY total_value DESC
LIMIT 5
""")
simple_results = cursor.fetchall()
simple_total = 0
for item in simple_results:
simple_total += item['total_value']
print(f" {item['herb_name']:15}{item['total_value']:10,.0f}")
# 전체 합계
cursor.execute("""
SELECT SUM(total_value) as grand_total
FROM (
SELECT
h.herb_item_id,
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
GROUP BY h.herb_item_id
HAVING total_value > 0
)
""")
simple_grand_total = cursor.fetchone()['grand_total'] or 0
print(f"\n 총합: ₩{simple_grand_total:,.0f}")
# 2. 효능 태그 JOIN 포함 계산 (API와 동일)
print("\n2. 효능 태그 JOIN 포함 계산 (API 쿼리)")
print("-" * 60)
cursor.execute("""
SELECT
h.herb_item_id,
h.herb_name,
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value,
COUNT(*) as row_count
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
GROUP BY h.herb_item_id, h.herb_name
HAVING total_value > 0
ORDER BY total_value DESC
LIMIT 5
""")
api_results = cursor.fetchall()
for item in api_results:
print(f" {item['herb_name']:15}{item['total_value']:10,.0f} (행수: {item['row_count']})")
# 전체 합계 (API 방식)
cursor.execute("""
SELECT SUM(total_value) as grand_total
FROM (
SELECT
h.herb_item_id,
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
GROUP BY h.herb_item_id
HAVING total_value > 0
)
""")
api_grand_total = cursor.fetchone()['grand_total'] or 0
print(f"\n 총합: ₩{api_grand_total:,.0f}")
# 3. 중복 원인 분석
print("\n3. 중복 원인 분석")
print("-" * 60)
print(f" ✅ 정상 계산: ₩{simple_grand_total:,.0f}")
print(f" ❌ API 계산: ₩{api_grand_total:,.0f}")
print(f" 차이: ₩{api_grand_total - simple_grand_total:,.0f}")
if api_grand_total > simple_grand_total:
ratio = api_grand_total / simple_grand_total if simple_grand_total > 0 else 0
print(f" 배율: {ratio:.2f}")
# 4. 효능 태그 중복 확인
print("\n4. 효능 태그로 인한 중복 확인")
print("-" * 60)
cursor.execute("""
SELECT
h.herb_name,
h.ingredient_code,
COUNT(DISTINCT hit.tag_id) as tag_count
FROM herb_items h
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
WHERE h.herb_item_id IN (
SELECT herb_item_id FROM inventory_lots
WHERE is_depleted = 0 AND quantity_onhand > 0
)
GROUP BY h.herb_item_id
HAVING tag_count > 1
ORDER BY tag_count DESC
LIMIT 5
""")
multi_tags = cursor.fetchall()
if multi_tags:
print(" 여러 효능 태그를 가진 약재:")
for herb in multi_tags:
print(f" - {herb['herb_name']}: {herb['tag_count']}개 태그")
# 5. 특정 약재 상세 분석 (휴먼감초)
print("\n5. 휴먼감초 상세 분석")
print("-" * 60)
# 정상 계산
cursor.execute("""
SELECT
il.lot_id,
il.quantity_onhand,
il.unit_price_per_g,
il.quantity_onhand * il.unit_price_per_g as value
FROM inventory_lots il
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
WHERE h.herb_name = '휴먼감초' AND il.is_depleted = 0
""")
gamcho_lots = cursor.fetchall()
actual_total = sum(lot['value'] for lot in gamcho_lots)
print(f" 실제 LOT 수: {len(gamcho_lots)}")
for lot in gamcho_lots:
print(f" LOT {lot['lot_id']}: {lot['quantity_onhand']}g ×{lot['unit_price_per_g']} = ₩{lot['value']:,.0f}")
print(f" 실제 합계: ₩{actual_total:,.0f}")
# JOIN 포함 계산
cursor.execute("""
SELECT COUNT(*) as join_rows
FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
LEFT JOIN herb_products hp ON h.insurance_code = hp.product_code
LEFT JOIN herb_item_tags hit ON COALESCE(h.ingredient_code, hp.ingredient_code) = hit.ingredient_code
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
WHERE h.herb_name = '휴먼감초' AND il.lot_id IS NOT NULL
""")
join_rows = cursor.fetchone()['join_rows']
print(f"\n JOIN 후 행 수: {join_rows}")
if join_rows > len(gamcho_lots):
print(f" ⚠️ 중복 발생! {join_rows / len(gamcho_lots):.1f}배로 뻥튀기됨")
conn.close()
if __name__ == "__main__":
find_duplicate_issue()

29
find_jihwang.py Normal file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""
지황 관련 약재 찾기
"""
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# 지황 관련 약재 검색
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name LIKE '%지황%'
OR herb_name LIKE '%생지%'
OR herb_name LIKE '%건지%'
OR herb_name LIKE '%숙지%'
ORDER BY herb_name
""")
results = cursor.fetchall()
print("🌿 지황 관련 약재 검색 결과:")
print("="*60)
for code, name, hanja in results:
print(f"{code}: {name} ({hanja})")
conn.close()

42
find_jinpi.py Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
진피 관련 약재 찾기
"""
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
# 진피 관련 약재 검색
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name LIKE '%진피%'
OR herb_name = '진피'
OR herb_name = '陳皮'
ORDER BY herb_name
""")
results = cursor.fetchall()
print("🌿 진피 관련 약재 검색 결과:")
print("="*60)
for code, name, hanja in results:
print(f"{code}: {name} ({hanja})")
# 정확히 '진피'만 찾기
print("\n정확히 '진피' 검색:")
cursor.execute("""
SELECT ingredient_code, herb_name, herb_name_hanja
FROM herb_masters
WHERE herb_name = '진피'
""")
result = cursor.fetchone()
if result:
print(f"✅ 찾음: {result[0]}: {result[1]} ({result[2]})")
else:
print("❌ 정확한 '진피'를 찾을 수 없음")
conn.close()

43
get_ingredient_codes.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""데이터베이스에서 실제 ingredient_code 확인"""
import sqlite3
conn = sqlite3.connect('database/kdrug.db')
cur = conn.cursor()
# herb_items와 herb_products를 조인하여 ingredient_code 확인
cur.execute("""
SELECT DISTINCT
hi.herb_name,
COALESCE(hi.ingredient_code, hp.ingredient_code) as ingredient_code,
hi.insurance_code
FROM herb_items hi
LEFT JOIN herb_products hp ON hi.insurance_code = hp.product_code
WHERE COALESCE(hi.ingredient_code, hp.ingredient_code) IS NOT NULL
ORDER BY hi.herb_name
""")
print("=== 실제 약재 ingredient_code 목록 ===")
herbs = cur.fetchall()
for herb in herbs:
print(f"{herb[0]:10s} -> {herb[1]} (보험코드: {herb[2]})")
# 십전대보탕 구성 약재들 확인
target_herbs = ['인삼', '백출', '복령', '감초', '숙지황', '작약', '천궁', '당귀', '황기', '육계']
print(f"\n=== 십전대보탕 구성 약재 ({len(target_herbs)}개) ===")
for target in target_herbs:
cur.execute("""
SELECT hi.herb_name,
COALESCE(hi.ingredient_code, hp.ingredient_code) as code
FROM herb_items hi
LEFT JOIN herb_products hp ON hi.insurance_code = hp.product_code
WHERE hi.herb_name = ?
""", (target,))
result = cur.fetchone()
if result and result[1]:
print(f"{result[0]:6s} -> {result[1]}")
else:
print(f"{target:6s} -> ingredient_code 없음")
conn.close()

Some files were not shown because too many files have changed in this diff Show More