Compare commits
No commits in common. "ad9ac396e202eaafac4ffc3f0cdededcd91f994f" and "bfc5c992dea67a5d1ea571cf06825c920b8a49aa" have entirely different histories.
ad9ac396e2
...
bfc5c992de
@ -1,123 +0,0 @@
|
||||
# 한약 재고관리 시스템 - 개발 가이드라인
|
||||
|
||||
## 📋 테스트 데이터 입력 규칙
|
||||
|
||||
### 🔴 중요: 입고 테스트 시 필수 준수사항
|
||||
|
||||
**모든 재고 입고 테스트는 반드시 다음 프로세스를 따라야 합니다:**
|
||||
|
||||
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
1
.gitignore
vendored
@ -66,4 +66,3 @@ Thumbs.db
|
||||
# Excel temporary files
|
||||
~$*.xlsx
|
||||
~$*.xls
|
||||
uploads/
|
||||
|
||||
13
README.md
13
README.md
@ -53,33 +53,24 @@
|
||||
## 데이터베이스 구조
|
||||
|
||||
### 핵심 테이블
|
||||
- `herb_masters` - 약재 마스터 (성분코드 기준, 454개 표준 약재)
|
||||
- `herb_items` - 약재 제품 (제조사별 개별 제품)
|
||||
- `patients` - 환자 정보
|
||||
- `herb_items` - 약재 마스터 (보험코드 기준)
|
||||
- `suppliers` - 도매상 정보
|
||||
- `purchase_receipts` - 입고장 헤더
|
||||
- `purchase_receipt_lines` - 입고장 상세
|
||||
- `inventory_lots` - 로트별 재고
|
||||
- `formulas` - 처방 마스터
|
||||
- `formula_ingredients` - 처방 구성 약재 (ingredient_code 기반)
|
||||
- `formula_ingredients` - 처방 구성 약재
|
||||
- `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. 필수 요구사항
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
#!/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❌ 작업 중 오류가 발생했습니다.")
|
||||
@ -1,168 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,317 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,163 +0,0 @@
|
||||
#!/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❌ 처방 추가 중 오류가 발생했습니다.")
|
||||
@ -1,81 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,213 +0,0 @@
|
||||
#!/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❌ 처방 추가 중 오류가 발생했습니다.")
|
||||
@ -1,180 +0,0 @@
|
||||
#!/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❌ 처방 추가 중 오류가 발생했습니다.")
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,428 +0,0 @@
|
||||
#!/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()
|
||||
241
claude.md
241
claude.md
@ -1,241 +0,0 @@
|
||||
# 한약 재고관리 시스템 (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*
|
||||
@ -1,120 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,244 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,315 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,186 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,224 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,121 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,45 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,143 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,179 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,51 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,35 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,109 +0,0 @@
|
||||
#!/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}",')
|
||||
@ -1,119 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,81 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,19 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,78 +0,0 @@
|
||||
#!/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}",')
|
||||
@ -1,115 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,193 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,183 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,145 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,200 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,92 +0,0 @@
|
||||
#!/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. 드롭다운에 약재 목록이 나타나는지 확인")
|
||||
@ -1,84 +0,0 @@
|
||||
#!/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}")
|
||||
@ -1,200 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,192 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,162 +0,0 @@
|
||||
#!/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테스트 완료!")
|
||||
@ -1,54 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,134 +0,0 @@
|
||||
#!/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())
|
||||
@ -1,108 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,112 +0,0 @@
|
||||
#!/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}")
|
||||
@ -1,97 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,157 +0,0 @@
|
||||
#!/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()
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
@ -1,553 +0,0 @@
|
||||
# 한의원 약재 관리 시스템 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`: 서버 오류
|
||||
@ -1,252 +0,0 @@
|
||||
# 한의원 약재 관리 시스템 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` 자동 설정
|
||||
@ -1,398 +0,0 @@
|
||||
# 한의원 약재 관리 시스템 데이터베이스 스키마
|
||||
|
||||
## 목차
|
||||
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개 기본 효능 태그
|
||||
- 약재별 효능 분류 체계
|
||||
@ -1,367 +0,0 @@
|
||||
# 한약 재고관리 시스템 데이터 구조 및 흐름
|
||||
|
||||
> 최종 수정: 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)
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 시스템의 핵심 데이터 구조와 흐름을 설명합니다.*
|
||||
*질문이나 수정사항은 개발팀에 문의해주세요.*
|
||||
@ -1,128 +0,0 @@
|
||||
# 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로 중복 등록되는 것 방지
|
||||
- 약재명 + 제조사로 추가 검증
|
||||
@ -1,167 +0,0 @@
|
||||
# 복합 로트 사용 기능 구현 분석
|
||||
|
||||
## 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 개선이 필요
|
||||
|
||||
**구현 난이도: 중간**
|
||||
- 기존 시스템에 미치는 영향 최소
|
||||
- 점진적 구현 가능 (자동 모드는 이미 작동 중)
|
||||
- 수동 모드는 선택적 기능으로 추가 가능
|
||||
@ -1,180 +0,0 @@
|
||||
# 입고 프로세스 개선 방안
|
||||
|
||||
## 현재 구조 이해
|
||||
|
||||
### 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 정기 업데이트
|
||||
- 신규 보험코드 추가 프로세스
|
||||
- 성분코드 변경 이력 관리
|
||||
333
docs/제품_세부분류.md
333
docs/제품_세부분류.md
@ -1,333 +0,0 @@
|
||||
우리는
|
||||
|
||||
성분코드아래
|
||||
|
||||
보험코드로 묶여있고
|
||||
|
||||
|
||||
지금 "건강' 제품 같은경우에는
|
||||
|
||||
우리가 조제 할때 선택을 , 국산과 페루산 등으로 원산지를 구분해서
|
||||
선택할수 잇게 되어있어,
|
||||
|
||||
|
||||
이게 우리가 입고를 할때 엑셀에서 , 구분값으로
|
||||
|
||||
우리가 보험코드를 사용하지만
|
||||
|
||||
|
||||
국산과 수입품이 모두 같은 보험코드를 사용하기때문에,
|
||||
우리는 성분을 바라보고 , "쌍화탕" 을 만들때
|
||||
|
||||
성분코드 기준으로 , 첩재를 만들게 되게 설계되어있을꺼고
|
||||
|
||||
성분코드에 따른 재고를 선택하지만, 성분 코드 아래 , 우리가
|
||||
|
||||
국산과, 수입품을 구분해서 롯트를 잡아뒀기때문에 이게 가능한거같아 맞는지 확인해
|
||||
|
||||
|
||||
|
||||
우리가 입고장 엑셀에 국산./해외품 구분밖에없지만,
|
||||
|
||||
|
||||
우리는 추가적으로,
|
||||
|
||||
자 내가 주문한 데이터야
|
||||
|
||||
갈근.각
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,310 +0,0 @@
|
||||
# 조제 프로세스 및 커스텀 처방 관리
|
||||
|
||||
## 목차
|
||||
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"을 정확히 기록하고 추적할 수 있습니다.
|
||||
@ -1,293 +0,0 @@
|
||||
# 처방 데이터 추가 가이드
|
||||
|
||||
## 개요
|
||||
이 문서는 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` 테이블
|
||||
@ -1,460 +0,0 @@
|
||||
# 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
|
||||
@ -1,462 +0,0 @@
|
||||
# 한약재 정보 관리 시스템 (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. **표준화**
|
||||
- 한약재 정보 표준화
|
||||
- 품질 관리 체계화
|
||||
@ -64,8 +64,7 @@ class ExcelProcessor:
|
||||
def read_excel(self, file_path):
|
||||
"""Excel 파일 읽기"""
|
||||
try:
|
||||
# 제품코드를 문자열로 읽기 위한 dtype 설정
|
||||
self.df_original = pd.read_excel(file_path, dtype={'제품코드': str})
|
||||
self.df_original = pd.read_excel(file_path)
|
||||
self.format_type = self.detect_format(self.df_original)
|
||||
return True
|
||||
except Exception as e:
|
||||
@ -83,12 +82,6 @@ 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'] = '한의사랑'
|
||||
|
||||
@ -119,12 +112,6 @@ 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)
|
||||
|
||||
@ -1,192 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,29 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,42 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,43 +0,0 @@
|
||||
#!/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()
|
||||
@ -1,275 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
한의사랑 카탈로그 데이터 import 및 가격 매칭
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import re
|
||||
|
||||
def get_connection():
|
||||
"""데이터베이스 연결"""
|
||||
return sqlite3.connect('database/kdrug.db')
|
||||
|
||||
def parse_catalog_data():
|
||||
"""제공된 카탈로그 데이터 파싱"""
|
||||
raw_data = """갈근.각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
|
||||
육계.YB25 배송중 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"""
|
||||
|
||||
items = []
|
||||
for line in raw_data.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
# 택배 추적번호 제거
|
||||
line = re.sub(r'롯데택배\d+배송조회', '', line)
|
||||
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 4:
|
||||
# 약재명 추출 (뒤의 수량 숫자 제거)
|
||||
raw_name = re.sub(r'\d+$', '', parts[0])
|
||||
|
||||
# 가격 파싱 (콤마 제거)
|
||||
total_price = int(parts[2].replace(',', ''))
|
||||
|
||||
# g당 단가
|
||||
if len(parts) >= 5 and parts[4] != '0':
|
||||
unit_price = int(parts[4].replace(',', ''))
|
||||
else:
|
||||
# g당 단가가 0이면 총액에서 계산 (1kg 기준)
|
||||
if '1kg' in raw_name or 'kg' in raw_name:
|
||||
unit_price = total_price / 1000
|
||||
else:
|
||||
unit_price = total_price / 1000 # 기본적으로 1kg로 가정
|
||||
|
||||
items.append({
|
||||
'raw_name': raw_name.strip(),
|
||||
'total_price': total_price,
|
||||
'unit_price': unit_price,
|
||||
'status': parts[1] if len(parts) > 1 else '배송중'
|
||||
})
|
||||
|
||||
return items
|
||||
|
||||
def import_to_catalog():
|
||||
"""카탈로그 데이터를 DB에 저장"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("한의사랑 카탈로그 데이터 Import")
|
||||
print("="*80)
|
||||
|
||||
# 한의사랑 supplier_id 조회
|
||||
cursor.execute("SELECT supplier_id FROM suppliers WHERE name = '한의사랑'")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
# 한의사랑 공급처 생성
|
||||
cursor.execute("""
|
||||
INSERT INTO suppliers (name, is_active)
|
||||
VALUES ('한의사랑', 1)
|
||||
""")
|
||||
supplier_id = cursor.lastrowid
|
||||
print(f"한의사랑 공급처 생성 (ID: {supplier_id})")
|
||||
else:
|
||||
supplier_id = result[0]
|
||||
print(f"한의사랑 공급처 확인 (ID: {supplier_id})")
|
||||
|
||||
# 기존 데이터 삭제
|
||||
cursor.execute("DELETE FROM supplier_product_catalog WHERE supplier_id = ?", (supplier_id,))
|
||||
|
||||
# 카탈로그 데이터 파싱
|
||||
items = parse_catalog_data()
|
||||
|
||||
print(f"\n총 {len(items)}개 항목을 파싱했습니다.")
|
||||
print("-" * 60)
|
||||
|
||||
# 데이터 삽입
|
||||
for item in items:
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO supplier_product_catalog
|
||||
(supplier_id, raw_name, unit_price, package_unit, stock_status, last_updated)
|
||||
VALUES (?, ?, ?, '1kg', ?, date('now'))
|
||||
""", (supplier_id, item['raw_name'], item['unit_price'], item['status']))
|
||||
|
||||
print(f"추가: {item['raw_name']:30s} | {item['unit_price']:8.1f}원/g | {item['status']}")
|
||||
except sqlite3.IntegrityError:
|
||||
print(f"중복: {item['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()
|
||||
return items
|
||||
|
||||
def match_with_inventory():
|
||||
"""현재 inventory_lots와 가격 매칭"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("Inventory Lots와 가격 매칭")
|
||||
print("="*80)
|
||||
|
||||
# 휴먼허브 inventory lots 조회
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
l.lot_id,
|
||||
h.herb_name,
|
||||
l.unit_price_per_g,
|
||||
l.origin_country,
|
||||
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 l.display_name IS NULL
|
||||
ORDER BY h.herb_name, l.unit_price_per_g
|
||||
""")
|
||||
|
||||
lots = cursor.fetchall()
|
||||
print(f"\ndisplay_name이 없는 로트: {len(lots)}개\n")
|
||||
|
||||
matched_count = 0
|
||||
no_match = []
|
||||
|
||||
for lot in lots:
|
||||
lot_id, herb_name, unit_price, origin, supplier = lot
|
||||
|
||||
# 한의사랑 카탈로그에서 비슷한 가격 찾기 (±10% 허용)
|
||||
cursor.execute("""
|
||||
SELECT raw_name, unit_price
|
||||
FROM supplier_product_catalog spc
|
||||
JOIN suppliers s ON spc.supplier_id = s.supplier_id
|
||||
WHERE s.name = '한의사랑'
|
||||
AND ABS(spc.unit_price - ?) / ? < 0.1
|
||||
ORDER BY ABS(spc.unit_price - ?)
|
||||
LIMIT 5
|
||||
""", (unit_price, unit_price, unit_price))
|
||||
|
||||
matches = cursor.fetchall()
|
||||
|
||||
if matches:
|
||||
print(f"\nLot #{lot_id}: {herb_name} ({unit_price:.1f}원/g, {origin})")
|
||||
print(" 매칭 후보:")
|
||||
|
||||
best_match = None
|
||||
for match in matches:
|
||||
match_name, match_price = match
|
||||
diff_percent = abs(match_price - unit_price) / unit_price * 100
|
||||
print(f" - {match_name:30s} | {match_price:8.1f}원/g | 차이: {diff_percent:.1f}%")
|
||||
|
||||
# 약재명에서 핵심 단어 추출하여 매칭
|
||||
herb_core = herb_name.replace('휴먼', '').replace('신흥', '')
|
||||
if herb_core in match_name or any(keyword in match_name for keyword in [herb_core[:2], herb_core[-2:]]):
|
||||
if not best_match or abs(match_price - unit_price) < abs(best_match[1] - unit_price):
|
||||
best_match = match
|
||||
|
||||
if best_match:
|
||||
# display_name 업데이트
|
||||
cursor.execute("""
|
||||
UPDATE inventory_lots
|
||||
SET display_name = ?
|
||||
WHERE lot_id = ?
|
||||
""", (best_match[0], lot_id))
|
||||
|
||||
# lot_variants 추가/업데이트
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO lot_variants
|
||||
(lot_id, raw_name, parsed_at, parsed_method)
|
||||
VALUES (?, ?, datetime('now'), 'catalog_price_match')
|
||||
""", (lot_id, best_match[0]))
|
||||
except sqlite3.IntegrityError:
|
||||
cursor.execute("""
|
||||
UPDATE lot_variants
|
||||
SET raw_name = ?, parsed_at = datetime('now'), parsed_method = 'catalog_price_match'
|
||||
WHERE lot_id = ?
|
||||
""", (best_match[0], lot_id))
|
||||
|
||||
print(f" ✓ 매칭: {best_match[0]}")
|
||||
matched_count += 1
|
||||
else:
|
||||
print(" ✗ 적합한 매칭 없음")
|
||||
no_match.append((lot_id, herb_name, unit_price, origin))
|
||||
else:
|
||||
no_match.append((lot_id, herb_name, unit_price, origin))
|
||||
|
||||
conn.commit()
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("매칭 결과")
|
||||
print("="*80)
|
||||
print(f"✓ 매칭 성공: {matched_count}개")
|
||||
print(f"✗ 매칭 실패: {len(no_match)}개")
|
||||
|
||||
if no_match:
|
||||
print("\n매칭 실패한 로트:")
|
||||
for lot in no_match:
|
||||
print(f" Lot #{lot[0]}: {lot[1]:20s} | {lot[2]:8.1f}원/g | {lot[3]}")
|
||||
|
||||
# 최종 결과 확인
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as total,
|
||||
COUNT(display_name) as with_display
|
||||
FROM inventory_lots
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
|
||||
print(f"\n전체 로트: {result[0]}개")
|
||||
print(f"display_name 설정됨: {result[1]}개")
|
||||
|
||||
conn.close()
|
||||
|
||||
def main():
|
||||
"""메인 실행"""
|
||||
print("\n한의사랑 카탈로그 데이터 Import 및 매칭")
|
||||
print("="*80)
|
||||
|
||||
# 1. 카탈로그 데이터 import
|
||||
items = import_to_catalog()
|
||||
|
||||
# 2. inventory lots와 매칭
|
||||
match_with_inventory()
|
||||
|
||||
print("\n완료!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
커스텀 처방 관리를 위한 데이터베이스 스키마 업데이트
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
def get_connection():
|
||||
"""데이터베이스 연결"""
|
||||
return sqlite3.connect('database/kdrug.db')
|
||||
|
||||
def add_custom_fields():
|
||||
"""커스텀 처방 관련 필드 추가"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("커스텀 처방 관리를 위한 DB 스키마 업데이트")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# 1. compounds 테이블에 커스텀 관련 필드 추가
|
||||
print("\n1. compounds 테이블 업데이트...")
|
||||
|
||||
# is_custom 컬럼 추가
|
||||
cursor.execute("""
|
||||
ALTER TABLE compounds
|
||||
ADD COLUMN is_custom BOOLEAN DEFAULT 0
|
||||
""")
|
||||
print(" ✓ is_custom 컬럼 추가")
|
||||
|
||||
# custom_summary 컬럼 추가
|
||||
cursor.execute("""
|
||||
ALTER TABLE compounds
|
||||
ADD COLUMN custom_summary TEXT
|
||||
""")
|
||||
print(" ✓ custom_summary 컬럼 추가")
|
||||
|
||||
# custom_type 컬럼 추가
|
||||
cursor.execute("""
|
||||
ALTER TABLE compounds
|
||||
ADD COLUMN custom_type TEXT
|
||||
""")
|
||||
print(" ✓ custom_type 컬럼 추가")
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column" in str(e):
|
||||
print(" ⚠ 이미 컬럼이 존재합니다.")
|
||||
else:
|
||||
raise e
|
||||
|
||||
try:
|
||||
# 2. compound_ingredients 테이블에 modification 관련 필드 추가
|
||||
print("\n2. compound_ingredients 테이블 업데이트...")
|
||||
|
||||
# modification_type 컬럼 추가
|
||||
cursor.execute("""
|
||||
ALTER TABLE compound_ingredients
|
||||
ADD COLUMN modification_type TEXT DEFAULT 'original'
|
||||
""")
|
||||
print(" ✓ modification_type 컬럼 추가")
|
||||
|
||||
# original_grams 컬럼 추가 (원래 용량 저장)
|
||||
cursor.execute("""
|
||||
ALTER TABLE compound_ingredients
|
||||
ADD COLUMN original_grams REAL
|
||||
""")
|
||||
print(" ✓ original_grams 컬럼 추가")
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column" in str(e):
|
||||
print(" ⚠ 이미 컬럼이 존재합니다.")
|
||||
else:
|
||||
raise e
|
||||
|
||||
# 3. 인덱스 추가
|
||||
try:
|
||||
print("\n3. 인덱스 생성...")
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_compounds_is_custom
|
||||
ON compounds(is_custom)
|
||||
""")
|
||||
print(" ✓ is_custom 인덱스 생성")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_compounds_patient_custom
|
||||
ON compounds(patient_id, is_custom)
|
||||
""")
|
||||
print(" ✓ patient_id + is_custom 복합 인덱스 생성")
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
print(f" ⚠ 인덱스 생성 중 오류: {e}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 4. 스키마 확인
|
||||
print("\n4. 업데이트된 스키마 확인...")
|
||||
|
||||
cursor.execute("PRAGMA table_info(compounds)")
|
||||
columns = cursor.fetchall()
|
||||
print("\n compounds 테이블 컬럼:")
|
||||
for col in columns:
|
||||
if col[1] in ['is_custom', 'custom_summary', 'custom_type']:
|
||||
print(f" ✓ {col[1]:20s} {col[2]}")
|
||||
|
||||
cursor.execute("PRAGMA table_info(compound_ingredients)")
|
||||
columns = cursor.fetchall()
|
||||
print("\n compound_ingredients 테이블 컬럼:")
|
||||
for col in columns:
|
||||
if col[1] in ['modification_type', 'original_grams']:
|
||||
print(f" ✓ {col[1]:20s} {col[2]}")
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ DB 스키마 업데이트 완료!")
|
||||
print("="*60)
|
||||
|
||||
def test_custom_fields():
|
||||
"""업데이트된 필드 테스트"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("\n테스트: 커스텀 필드 동작 확인...")
|
||||
|
||||
try:
|
||||
# 테스트 쿼리
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
compound_id,
|
||||
is_custom,
|
||||
custom_summary,
|
||||
custom_type
|
||||
FROM compounds
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
print(" ✓ compounds 테이블 커스텀 필드 정상")
|
||||
else:
|
||||
print(" ℹ compounds 테이블이 비어있습니다.")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
compound_ingredient_id,
|
||||
modification_type,
|
||||
original_grams
|
||||
FROM compound_ingredients
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
print(" ✓ compound_ingredients 테이블 커스텀 필드 정상")
|
||||
else:
|
||||
print(" ℹ compound_ingredients 테이블이 비어있습니다.")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ 테스트 실패: {e}")
|
||||
|
||||
conn.close()
|
||||
|
||||
def main():
|
||||
"""메인 실행"""
|
||||
add_custom_fields()
|
||||
test_custom_fields()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,448 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
한약재 확장 정보 테이블 추가
|
||||
- herb_master_extended: 약재 상세 정보
|
||||
- herb_research_papers: 연구 문헌
|
||||
- herb_safety_info: 안전성 정보
|
||||
- prescription_rules: 처방 구성 규칙
|
||||
- disease_herb_mapping: 질병-약재 매핑
|
||||
- data_update_logs: AI/API 업데이트 로그
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
def get_connection():
|
||||
"""데이터베이스 연결"""
|
||||
return sqlite3.connect('../database/kdrug.db')
|
||||
|
||||
def create_herb_master_extended():
|
||||
"""약재 확장 정보 테이블 생성"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 기존 테이블 확인
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='herb_master_extended'
|
||||
""")
|
||||
|
||||
if cursor.fetchone():
|
||||
print("herb_master_extended 테이블이 이미 존재합니다.")
|
||||
else:
|
||||
cursor.execute("""
|
||||
CREATE TABLE herb_master_extended (
|
||||
herb_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
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 TEXT, -- 화학 성분 상세 (JSON)
|
||||
|
||||
-- 약리 작용
|
||||
pharmacological_effects TEXT, -- 약리작용
|
||||
clinical_applications TEXT, -- 임상응용
|
||||
|
||||
-- 상호작용
|
||||
drug_interactions TEXT, -- 약물 상호작용 (JSON)
|
||||
food_interactions TEXT, -- 음식 상호작용 (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) -- 검토 상태
|
||||
)
|
||||
""")
|
||||
print("✅ herb_master_extended 테이블이 생성되었습니다.")
|
||||
|
||||
# 기존 herb_masters 데이터 마이그레이션
|
||||
cursor.execute("""
|
||||
INSERT INTO herb_master_extended (
|
||||
ingredient_code, name_korean, name_hanja, name_latin
|
||||
)
|
||||
SELECT
|
||||
ingredient_code,
|
||||
herb_name AS name_korean,
|
||||
herb_name_hanja AS name_hanja,
|
||||
herb_name_latin AS name_latin
|
||||
FROM herb_masters
|
||||
""")
|
||||
|
||||
print(f" - {cursor.rowcount}개의 기존 데이터가 마이그레이션되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def create_herb_research_papers():
|
||||
"""약재 연구 문헌 테이블 생성"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS herb_research_papers (
|
||||
paper_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
herb_id INTEGER,
|
||||
|
||||
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
|
||||
)
|
||||
""")
|
||||
|
||||
print("✅ herb_research_papers 테이블이 생성되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def create_herb_safety_info():
|
||||
"""약재 안전성 정보 테이블 생성"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS herb_safety_info (
|
||||
safety_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
herb_id INTEGER,
|
||||
|
||||
-- 독성 정보
|
||||
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
|
||||
)
|
||||
""")
|
||||
|
||||
print("✅ herb_safety_info 테이블이 생성되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def create_prescription_rules():
|
||||
"""처방 구성 규칙 테이블 생성"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS prescription_rules (
|
||||
rule_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
-- 배합 규칙
|
||||
herb1_id INTEGER,
|
||||
herb2_id INTEGER,
|
||||
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
|
||||
)
|
||||
""")
|
||||
|
||||
# 인덱스 추가
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_prescription_rules_herbs
|
||||
ON prescription_rules(herb1_id, herb2_id)
|
||||
""")
|
||||
|
||||
print("✅ prescription_rules 테이블이 생성되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def create_disease_herb_mapping():
|
||||
"""질병-약재 매핑 테이블 생성"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS disease_herb_mapping (
|
||||
mapping_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
disease_code VARCHAR(20), -- KCD 코드
|
||||
disease_name VARCHAR(200),
|
||||
herb_id INTEGER,
|
||||
|
||||
indication_type VARCHAR(50), -- 주적응증/부적응증
|
||||
evidence_level INTEGER, -- 근거수준
|
||||
recommendation_grade VARCHAR(10), -- 권고등급
|
||||
|
||||
clinical_notes TEXT,
|
||||
reference_sources TEXT,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 인덱스 추가
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_disease_herb_mapping
|
||||
ON disease_herb_mapping(disease_code, herb_id)
|
||||
""")
|
||||
|
||||
print("✅ disease_herb_mapping 테이블이 생성되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def create_data_update_logs():
|
||||
"""AI/API 업데이트 로그 테이블 생성"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS data_update_logs (
|
||||
log_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
update_type VARCHAR(50), -- AI/API/MANUAL
|
||||
source VARCHAR(100), -- 데이터 소스
|
||||
target_table VARCHAR(50),
|
||||
target_id INTEGER,
|
||||
|
||||
before_data TEXT, -- 변경 전 데이터 (JSON)
|
||||
after_data TEXT, -- 변경 후 데이터 (JSON)
|
||||
|
||||
update_reason TEXT,
|
||||
confidence_score REAL, -- AI 신뢰도
|
||||
|
||||
is_reviewed BOOLEAN DEFAULT 0,
|
||||
reviewed_by VARCHAR(50),
|
||||
review_notes TEXT,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
print("✅ data_update_logs 테이블이 생성되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def create_herb_efficacy_tags():
|
||||
"""약재 효능 태그 시스템 테이블 생성"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 효능 태그 마스터 테이블
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS herb_efficacy_tags (
|
||||
tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tag_name VARCHAR(50) UNIQUE NOT NULL,
|
||||
tag_category VARCHAR(30), -- 보익/거사/조리/기타
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 약재-태그 매핑 테이블
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS herb_item_tags (
|
||||
item_tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
herb_id INTEGER,
|
||||
tag_id INTEGER,
|
||||
strength INTEGER DEFAULT 3, -- 효능 강도 (1-5)
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(herb_id, tag_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# 기본 효능 태그 삽입
|
||||
basic_tags = [
|
||||
('보혈', '보익', '혈을 보하는 효능'),
|
||||
('보기', '보익', '기를 보하는 효능'),
|
||||
('보양', '보익', '양기를 보하는 효능'),
|
||||
('보음', '보익', '음액을 보하는 효능'),
|
||||
('활혈', '거사', '혈액순환을 개선하는 효능'),
|
||||
('거담', '거사', '담을 제거하는 효능'),
|
||||
('이수', '거사', '수분대사를 개선하는 효능'),
|
||||
('해표', '거사', '표증을 해소하는 효능'),
|
||||
('청열', '거사', '열을 내리는 효능'),
|
||||
('해독', '거사', '독을 해소하는 효능'),
|
||||
('이기', '조리', '기의 순환을 조절하는 효능'),
|
||||
('소화', '조리', '소화를 돕는 효능'),
|
||||
('안신', '조리', '정신을 안정시키는 효능'),
|
||||
('평간', '조리', '간기능을 조절하는 효능'),
|
||||
('지혈', '기타', '출혈을 멈추는 효능'),
|
||||
('진통', '기타', '통증을 완화하는 효능'),
|
||||
('항염', '기타', '염증을 억제하는 효능'),
|
||||
('항균', '기타', '균을 억제하는 효능')
|
||||
]
|
||||
|
||||
for tag_name, tag_category, description in basic_tags:
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO herb_efficacy_tags (tag_name, tag_category, description)
|
||||
VALUES (?, ?, ?)
|
||||
""", (tag_name, tag_category, description))
|
||||
|
||||
print("✅ herb_efficacy_tags 테이블이 생성되었습니다.")
|
||||
print(f" - {len(basic_tags)}개의 기본 효능 태그가 등록되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def add_sample_data():
|
||||
"""샘플 데이터 추가"""
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 인삼 상세 정보 업데이트
|
||||
cursor.execute("""
|
||||
UPDATE herb_master_extended
|
||||
SET
|
||||
property = '온',
|
||||
taste = '감,미고',
|
||||
meridian_tropism = '비,폐,심',
|
||||
main_effects = '대보원기, 보비익폐, 생진지갈, 안신증지',
|
||||
indications = '기허증, 비허증, 폐허증, 심기허증, 진액부족',
|
||||
contraindications = '실증, 열증',
|
||||
precautions = '복용 중 무 섭취 금지',
|
||||
dosage_range = '3-9g',
|
||||
dosage_max = '30g',
|
||||
active_compounds = '인삼사포닌(ginsenoside), 다당체, 아미노산',
|
||||
pharmacological_effects = '면역증강, 항피로, 항산화, 혈당조절',
|
||||
clinical_applications = '만성피로, 면역력저하, 당뇨병 보조치료'
|
||||
WHERE ingredient_code = '3400H1AHM'
|
||||
""")
|
||||
|
||||
# 감초 상세 정보 업데이트
|
||||
cursor.execute("""
|
||||
UPDATE herb_master_extended
|
||||
SET
|
||||
property = '평',
|
||||
taste = '감',
|
||||
meridian_tropism = '비,위,폐,심',
|
||||
main_effects = '보비익기, 청열해독, 거담지해, 완급지통, 조화제약',
|
||||
indications = '비허증, 해수, 인후통, 소화성궤양',
|
||||
contraindications = '습증, 수종',
|
||||
precautions = '장기복용 시 부종 주의',
|
||||
dosage_range = '2-10g',
|
||||
dosage_max = '30g',
|
||||
active_compounds = 'glycyrrhizin, flavonoid, triterpenoid',
|
||||
pharmacological_effects = '항염증, 항궤양, 간보호, 진해거담',
|
||||
clinical_applications = '위염, 위궤양, 기관지염, 약물조화'
|
||||
WHERE ingredient_code = '3400H1ADL'
|
||||
""")
|
||||
|
||||
print("✅ 샘플 데이터가 추가되었습니다.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
print("\n" + "="*80)
|
||||
print("한약재 확장 정보 시스템 테이블 생성")
|
||||
print("="*80 + "\n")
|
||||
|
||||
try:
|
||||
# 1. 확장 정보 테이블 생성
|
||||
create_herb_master_extended()
|
||||
|
||||
# 2. 연구 문헌 테이블 생성
|
||||
create_herb_research_papers()
|
||||
|
||||
# 3. 안전성 정보 테이블 생성
|
||||
create_herb_safety_info()
|
||||
|
||||
# 4. 처방 규칙 테이블 생성
|
||||
create_prescription_rules()
|
||||
|
||||
# 5. 질병-약재 매핑 테이블 생성
|
||||
create_disease_herb_mapping()
|
||||
|
||||
# 6. 업데이트 로그 테이블 생성
|
||||
create_data_update_logs()
|
||||
|
||||
# 7. 효능 태그 시스템 생성
|
||||
create_herb_efficacy_tags()
|
||||
|
||||
# 8. 샘플 데이터 추가
|
||||
add_sample_data()
|
||||
|
||||
print("\n✨ 모든 테이블이 성공적으로 생성되었습니다!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 오류 발생: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
입고 관련 데이터 전체 초기화 스크립트
|
||||
- 입고장, 재고, 로트, 조제, 재고 조정 등 모두 초기화
|
||||
- herb_items는 기본 31개만 유지
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
def reset_purchase_data():
|
||||
"""입고 및 관련 데이터 전체 초기화"""
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
print("=== 입고 및 관련 데이터 초기화 시작 ===\n")
|
||||
|
||||
# 1. 조제 관련 초기화 (재고 소비 기록)
|
||||
cursor.execute("DELETE FROM compound_consumptions")
|
||||
print(f"✓ compound_consumptions 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
cursor.execute("DELETE FROM compound_ingredients")
|
||||
print(f"✓ compound_ingredients 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
cursor.execute("DELETE FROM compounds")
|
||||
print(f"✓ compounds 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
# 2. 재고 원장 전체 초기화
|
||||
cursor.execute("DELETE FROM stock_ledger")
|
||||
print(f"✓ stock_ledger 전체 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
# 3. 재고 로트 초기화
|
||||
cursor.execute("DELETE FROM inventory_lots")
|
||||
print(f"✓ inventory_lots 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
# 4. 입고장 라인 초기화
|
||||
cursor.execute("DELETE FROM purchase_receipt_lines")
|
||||
print(f"✓ purchase_receipt_lines 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
# 5. 입고장 헤더 초기화
|
||||
cursor.execute("DELETE FROM purchase_receipts")
|
||||
print(f"✓ purchase_receipts 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
# 6. 재고 조정 초기화
|
||||
cursor.execute("DELETE FROM stock_adjustment_details")
|
||||
print(f"✓ stock_adjustment_details 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
cursor.execute("DELETE FROM stock_adjustments")
|
||||
print(f"✓ stock_adjustments 초기화: {cursor.rowcount}개 삭제")
|
||||
|
||||
# 7. herb_items 중 보험코드가 8자리인 잘못된 데이터 삭제
|
||||
cursor.execute("""
|
||||
DELETE FROM herb_items
|
||||
WHERE LENGTH(insurance_code) = 8
|
||||
AND insurance_code NOT LIKE 'A%'
|
||||
""")
|
||||
print(f"✓ 잘못된 herb_items 삭제 (8자리 보험코드): {cursor.rowcount}개")
|
||||
|
||||
# 8. 테스트용으로 추가된 herb_items 삭제 (ID 32 이후)
|
||||
cursor.execute("""
|
||||
DELETE FROM herb_items
|
||||
WHERE herb_item_id > 31
|
||||
""")
|
||||
print(f"✓ 테스트 herb_items 삭제 (ID > 31): {cursor.rowcount}개")
|
||||
|
||||
# 9. 기존 herb_items의 ingredient_code, specification 초기화
|
||||
cursor.execute("""
|
||||
UPDATE herb_items
|
||||
SET ingredient_code = NULL,
|
||||
specification = NULL
|
||||
WHERE herb_item_id <= 31
|
||||
""")
|
||||
print(f"✓ herb_items ingredient_code/specification 초기화: {cursor.rowcount}개")
|
||||
|
||||
# 커밋
|
||||
conn.commit()
|
||||
|
||||
print("\n=== 현재 데이터 상태 ===")
|
||||
|
||||
# 현재 상태 확인
|
||||
cursor.execute("SELECT COUNT(*) FROM herb_items")
|
||||
herb_count = cursor.fetchone()[0]
|
||||
print(f"herb_items: {herb_count}개")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM purchase_receipts")
|
||||
receipt_count = cursor.fetchone()[0]
|
||||
print(f"purchase_receipts: {receipt_count}개")
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM inventory_lots")
|
||||
lot_count = cursor.fetchone()[0]
|
||||
print(f"inventory_lots: {lot_count}개")
|
||||
|
||||
print("\n✓ 입고 데이터 초기화 완료!")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"✗ 오류 발생: {str(e)}", file=sys.stderr)
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 확인
|
||||
print("입고 관련 데이터를 초기화합니다.")
|
||||
print("계속하시겠습니까? (y/n): ", end="")
|
||||
|
||||
confirm = input().strip().lower()
|
||||
if confirm == 'y':
|
||||
if reset_purchase_data():
|
||||
print("\n초기화가 완료되었습니다.")
|
||||
else:
|
||||
print("\n초기화 실패!")
|
||||
else:
|
||||
print("취소되었습니다.")
|
||||
@ -1,55 +0,0 @@
|
||||
Traceback (most recent call last):
|
||||
File "/root/kdrug/analyze_inventory_full.py", line 315, in <module>
|
||||
analyze_inventory_discrepancy()
|
||||
File "/root/kdrug/analyze_inventory_full.py", line 261, in analyze_inventory_discrepancy
|
||||
cursor.execute("""
|
||||
sqlite3.OperationalError: no such column: quantity
|
||||
|
||||
================================================================================
|
||||
재고 자산 금액 불일치 상세 분석
|
||||
분석 시간: 2026-02-18 01:23:14
|
||||
================================================================================
|
||||
|
||||
1. 현재 시스템 재고 자산 (inventory_lots 테이블)
|
||||
------------------------------------------------------------
|
||||
💰 총 재고 자산: ₩1,529,434
|
||||
📦 활성 LOT 수: 30개
|
||||
⚖️ 총 재고량: 86,420.0g
|
||||
🌿 약재 종류: 28종
|
||||
|
||||
2. 입고장 데이터 분석 (purchase_receipts + purchase_receipt_lines)
|
||||
------------------------------------------------------------
|
||||
📋 총 입고 금액: ₩1,551,900
|
||||
📑 입고장 수: 1건
|
||||
📝 입고 라인 수: 29개
|
||||
⚖️ 총 입고량: 88,000.0g
|
||||
|
||||
최근 입고장 5건:
|
||||
- PR-20260211-0001 (20260211): ₩1,551,900
|
||||
|
||||
3. LOT-입고장 매칭 분석
|
||||
------------------------------------------------------------
|
||||
✅ 입고장과 연결된 LOT: 30개 (₩1,529,434)
|
||||
❌ 입고장 없는 LOT: 0개 (₩0)
|
||||
|
||||
4. 입고장 라인별 LOT 생성 확인
|
||||
------------------------------------------------------------
|
||||
📝 전체 입고 라인: 30개
|
||||
✅ LOT 생성된 라인: 30개
|
||||
❌ LOT 없는 라인: 0개
|
||||
|
||||
5. 재고 자산 차이 분석
|
||||
------------------------------------------------------------
|
||||
💰 현재 LOT 재고 가치: ₩1,529,434
|
||||
📋 원본 입고 금액: ₩1,616,400
|
||||
📊 차이: ₩-86,966
|
||||
|
||||
6. 출고 및 소비 내역
|
||||
------------------------------------------------------------
|
||||
처방전 테이블이 없습니다.
|
||||
🏭 복합제 소비 금액: ₩77,966
|
||||
⚖️ 복합제 소비량: 4,580.0g
|
||||
📦 복합제 수: 8개
|
||||
|
||||
7. 재고 보정 내역
|
||||
------------------------------------------------------------
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
1246
static/app.js
1246
static/app.js
File diff suppressed because it is too large
Load Diff
3172
static/app.js.backup
3172
static/app.js.backup
File diff suppressed because it is too large
Load Diff
@ -122,11 +122,6 @@
|
||||
<i class="bi bi-flower1"></i> 약재 관리
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#" data-page="herb-info">
|
||||
<i class="bi bi-book"></i> 약재 정보
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -156,14 +151,8 @@
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<h5>
|
||||
<i class="bi bi-cash-stack"></i> 재고 자산
|
||||
<button class="btn btn-sm btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#inventorySettingsModal" title="계산 설정">
|
||||
<i class="bi bi-gear"></i>
|
||||
</button>
|
||||
</h5>
|
||||
<h5><i class="bi bi-cash-stack"></i> 재고 자산</h5>
|
||||
<div class="value" id="inventoryValue">0</div>
|
||||
<small class="text-muted" id="inventoryMode">전체 재고</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -394,7 +383,7 @@
|
||||
<th>약재명</th>
|
||||
<th>1첩당 용량(g)</th>
|
||||
<th>총 용량(g)</th>
|
||||
<th>제품/로트 선택</th>
|
||||
<th>원산지 선택</th>
|
||||
<th>재고</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
@ -980,239 +969,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Herb Information Page -->
|
||||
<div id="herb-info" class="main-content">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3><i class="bi bi-book"></i> 한약재 정보 시스템</h3>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-primary active" data-view="search">
|
||||
<i class="bi bi-search"></i> 검색
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" data-view="efficacy">
|
||||
<i class="bi bi-tags"></i> 효능별
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" data-view="category">
|
||||
<i class="bi bi-grid-3x3"></i> 분류별
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Section -->
|
||||
<div id="herb-search-section" class="mb-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control" id="herbSearchInput"
|
||||
placeholder="약재명, 학명, 효능으로 검색...">
|
||||
<button class="btn btn-primary" id="herbSearchBtn">검색</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="herbInfoEfficacyFilter">
|
||||
<option value="">모든 효능</option>
|
||||
<option value="보혈">보혈</option>
|
||||
<option value="보기">보기</option>
|
||||
<option value="활혈">활혈</option>
|
||||
<option value="청열">청열</option>
|
||||
<option value="해독">해독</option>
|
||||
<option value="거담">거담</option>
|
||||
<option value="이수">이수</option>
|
||||
<option value="안신">안신</option>
|
||||
</select>
|
||||
<select class="form-select" id="herbInfoPropertyFilter">
|
||||
<option value="">모든 성미</option>
|
||||
<option value="한">한(寒)</option>
|
||||
<option value="열">열(熱)</option>
|
||||
<option value="온">온(溫)</option>
|
||||
<option value="량">량(涼)</option>
|
||||
<option value="평">평(平)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Efficacy Tags Section (Hidden by default) -->
|
||||
<div id="herb-efficacy-section" class="mb-4" style="display: none;">
|
||||
<div class="row g-3" id="efficacyTagsContainer">
|
||||
<!-- Dynamic efficacy tag buttons will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Grid -->
|
||||
<div class="row g-3" id="herbInfoGrid">
|
||||
<!-- Herb cards will be dynamically added here -->
|
||||
</div>
|
||||
|
||||
<!-- Herb Detail Modal -->
|
||||
<div class="modal fade" id="herbDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-success text-white">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-flower1"></i>
|
||||
<span id="herbDetailName">약재명</span>
|
||||
<span id="herbDetailHanja" class="ms-2"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<!-- 기본 정보 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-info-circle"></i> 기본 정보</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">성분코드</dt>
|
||||
<dd class="col-sm-8" id="detailIngredientCode">-</dd>
|
||||
|
||||
<dt class="col-sm-4">학명</dt>
|
||||
<dd class="col-sm-8" id="detailLatinName">-</dd>
|
||||
|
||||
<dt class="col-sm-4">약용부위</dt>
|
||||
<dd class="col-sm-8" id="detailMedicinalPart">-</dd>
|
||||
|
||||
<dt class="col-sm-4">기원식물</dt>
|
||||
<dd class="col-sm-8" id="detailOriginPlant">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-thermometer"></i> 성미귀경</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">성(性)</dt>
|
||||
<dd class="col-sm-8">
|
||||
<span class="badge bg-info" id="detailProperty">-</span>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4">미(味)</dt>
|
||||
<dd class="col-sm-8" id="detailTaste">-</dd>
|
||||
|
||||
<dt class="col-sm-4">귀경</dt>
|
||||
<dd class="col-sm-8" id="detailMeridian">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 효능 정보 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-heart-pulse"></i> 효능효과</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<strong>주요 효능:</strong>
|
||||
<div id="detailMainEffects" class="mt-2">-</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>적응증:</strong>
|
||||
<div id="detailIndications" class="mt-2">-</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>효능 태그:</strong>
|
||||
<div id="detailEfficacyTags" class="mt-2">
|
||||
<!-- Dynamic tags -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-capsule"></i> 용법용량</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">상용량</dt>
|
||||
<dd class="col-sm-8" id="detailDosageRange">-</dd>
|
||||
|
||||
<dt class="col-sm-4">극량</dt>
|
||||
<dd class="col-sm-8" id="detailDosageMax">-</dd>
|
||||
|
||||
<dt class="col-sm-4">포제법</dt>
|
||||
<dd class="col-sm-8" id="detailPreparation">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 추가 정보 탭 -->
|
||||
<div class="mt-4">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#tabSafety">
|
||||
<i class="bi bi-shield-check"></i> 안전성
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tabComponents">
|
||||
<i class="bi bi-diagram-3"></i> 성분정보
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#tabClinical">
|
||||
<i class="bi bi-clipboard2-pulse"></i> 임상응용
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content p-3 border border-top-0">
|
||||
<div class="tab-pane fade show active" id="tabSafety">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>금기사항</h6>
|
||||
<div id="detailContraindications" class="text-danger">-</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>주의사항</h6>
|
||||
<div id="detailPrecautions" class="text-warning">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tabComponents">
|
||||
<h6>주요 성분</h6>
|
||||
<div id="detailActiveCompounds">-</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tabClinical">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>약리작용</h6>
|
||||
<div id="detailPharmacological">-</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>임상응용</h6>
|
||||
<div id="detailClinical">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
<button type="button" class="btn btn-primary" id="editHerbInfoBtn">
|
||||
<i class="bi bi-pencil"></i> 정보 수정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Patient Modal -->
|
||||
<div class="modal fade" id="patientModal" tabindex="-1">
|
||||
@ -1398,10 +1157,6 @@
|
||||
<label class="form-label">설명</label>
|
||||
<textarea class="form-control" id="formulaDescription" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="form-label">주요 효능</label>
|
||||
<textarea class="form-control" id="formulaEfficacy" rows="2" placeholder="예: 기혈양허 치료, 병후 회복, 만성 피로 개선"></textarea>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h6>구성 약재</h6>
|
||||
<table class="table table-sm">
|
||||
@ -1431,140 +1186,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formula Detail Modal (처방 상세 모달) -->
|
||||
<div class="modal fade" id="formulaDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-journal-medical"></i>
|
||||
<span id="formulaDetailName">처방명</span> 상세 정보
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 처방 기본 정보 카드 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-info-circle"></i> 기본 정보</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">처방 코드:</dt>
|
||||
<dd class="col-sm-8" id="detailFormulaCode">-</dd>
|
||||
|
||||
<dt class="col-sm-4">처방명:</dt>
|
||||
<dd class="col-sm-8" id="detailFormulaName">-</dd>
|
||||
|
||||
<dt class="col-sm-4">처방 유형:</dt>
|
||||
<dd class="col-sm-8" id="detailFormulaType">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">기본 첩수:</dt>
|
||||
<dd class="col-sm-8" id="detailBaseCheop">-</dd>
|
||||
|
||||
<dt class="col-sm-4">기본 파우치:</dt>
|
||||
<dd class="col-sm-8" id="detailBasePouches">-</dd>
|
||||
|
||||
<dt class="col-sm-4">등록일:</dt>
|
||||
<dd class="col-sm-8" id="detailCreatedAt">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<dt>설명:</dt>
|
||||
<dd id="detailDescription" class="text-muted">-</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 구성 약재 정보 카드 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="bi bi-list-ul"></i> 구성 약재</h6>
|
||||
<div>
|
||||
<span class="badge bg-primary" id="totalIngredientsCount">0개</span>
|
||||
<span class="badge bg-success" id="totalGramsPerCheop">0g</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th width="50">#</th>
|
||||
<th width="200">약재명</th>
|
||||
<th width="100">1첩당 용량</th>
|
||||
<th width="150" style="white-space: nowrap;">1제 기준 <small style="font-size: 0.85em;" class="text-muted">(20첩/30파우치)</small></th>
|
||||
<th style="padding-left: 15px;">효능/역할</th>
|
||||
<th width="150">재고 상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="formulaDetailIngredients">
|
||||
<!-- 동적으로 추가 -->
|
||||
</tbody>
|
||||
<tfoot class="table-secondary">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>합계</th>
|
||||
<th class="text-end" id="totalGrams1Cheop">0g</th>
|
||||
<th class="text-end" id="totalGrams1Je">0g</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 효능 및 주의사항 카드 -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-heart-pulse"></i> 주요 효능</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="formulaEffects">
|
||||
<p class="text-muted">처방의 주요 효능 정보가 여기에 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-exclamation-triangle"></i> 사용 시 주의사항</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="formulaPrecautions">
|
||||
<p class="text-muted">처방 사용 시 주의사항이 여기에 표시됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
<button type="button" class="btn btn-warning" id="editFormulaDetailBtn">
|
||||
<i class="bi bi-pencil"></i> 수정
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" id="deleteFormulaBtn">
|
||||
<i class="bi bi-trash"></i> 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Supplier Modal -->
|
||||
<div class="modal fade" id="supplierModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
@ -1605,112 +1226,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 로트 배분 모달 -->
|
||||
<div class="modal fade" id="lotAllocationModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-shuffle"></i> 로트 배분
|
||||
<span id="lotAllocationHerbName" class="text-primary"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>필요량: <span id="lotAllocationRequired">0</span>g</strong>
|
||||
<span class="float-end">배분 합계: <span id="lotAllocationTotal" class="fw-bold">0</span>g</span>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>로트 번호</th>
|
||||
<th>원산지</th>
|
||||
<th>재고량</th>
|
||||
<th>단가</th>
|
||||
<th>사용량</th>
|
||||
<th>소계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="lotAllocationList">
|
||||
</tbody>
|
||||
<tfoot class="table-secondary">
|
||||
<tr>
|
||||
<th colspan="4" class="text-end">합계:</th>
|
||||
<th id="lotAllocationSumQty">0g</th>
|
||||
<th id="lotAllocationSumCost">0원</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div id="lotAllocationError" class="alert alert-danger d-none"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-warning" id="lotAllocationAutoBtn">
|
||||
<i class="bi bi-magic"></i> 자동 배분
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="lotAllocationConfirmBtn">
|
||||
<i class="bi bi-check"></i> 확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/app.js?v=20260217"></script>
|
||||
<!-- 재고 자산 계산 설정 모달 -->
|
||||
<div class="modal fade" id="inventorySettingsModal" tabindex="-1" aria-labelledby="inventorySettingsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="inventorySettingsModalLabel">
|
||||
<i class="bi bi-calculator"></i> 재고 자산 계산 설정
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">계산 방식 선택</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="inventoryMode" id="modeAll" value="all" checked>
|
||||
<label class="form-check-label" for="modeAll">
|
||||
<strong>전체 재고</strong>
|
||||
<div class="text-muted small">모든 LOT의 재고를 포함하여 계산</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="radio" name="inventoryMode" id="modeReceiptOnly" value="receipt_only">
|
||||
<label class="form-check-label" for="modeReceiptOnly">
|
||||
<strong>입고장 기준</strong>
|
||||
<div class="text-muted small">입고장과 연결된 LOT만 계산</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="radio" name="inventoryMode" id="modeVerified" value="verified">
|
||||
<label class="form-check-label" for="modeVerified">
|
||||
<strong>검증된 재고</strong>
|
||||
<div class="text-muted small">검증 확인된 LOT만 계산</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info" id="modeInfo" style="display: none;">
|
||||
<h6 class="alert-heading"><i class="bi bi-info-circle"></i> 현재 상태</h6>
|
||||
<div id="modeInfoContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveInventorySettings()">적용</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js?v=20260215"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
165
test_compound_e2e.py
Normal file
165
test_compound_e2e.py
Normal file
@ -0,0 +1,165 @@
|
||||
#!/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("✓ 조제관리 화면 진입")
|
||||
|
||||
# 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] 처방 선택...")
|
||||
|
||||
# 처방 드롭다운 찾기 (유연하게)
|
||||
formula_select = page.locator('select').first
|
||||
|
||||
if formula_select.count() > 0:
|
||||
# 옵션 확인
|
||||
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로 시도
|
||||
formula_select.select_option(index=1)
|
||||
time.sleep(3)
|
||||
print("✓ 첫 번째 처방 선택 완료")
|
||||
else:
|
||||
print("❌ 처방 드롭다운을 찾을 수 없음")
|
||||
|
||||
# 4. 약재 목록 확인
|
||||
print("\n[4] 약재 목록 확인...")
|
||||
|
||||
# 약재 테이블이나 목록이 나타날 때까지 대기
|
||||
page.wait_for_selector('table, .ingredient-list', timeout=10000)
|
||||
|
||||
# 페이지 스크린샷
|
||||
page.screenshot(path='/tmp/compound_screen_1.png')
|
||||
print("✓ 스크린샷 저장: /tmp/compound_screen_1.png")
|
||||
|
||||
# 5. 인삼 항목 찾기
|
||||
print("\n[5] 인삼 항목 찾기...")
|
||||
|
||||
# 인삼을 포함하는 행 찾기
|
||||
ginseng_row = page.locator('tr:has-text("인삼"), div:has-text("인삼")').first
|
||||
|
||||
if ginseng_row.count() > 0:
|
||||
print("✓ 인삼 항목 발견")
|
||||
|
||||
# 6. 제품 선택 드롭다운 확인
|
||||
print("\n[6] 제품 선택 드롭다운 확인...")
|
||||
|
||||
# 인삼 행에서 select 요소 찾기
|
||||
product_select = ginseng_row.locator('select').first
|
||||
|
||||
if product_select.count() > 0:
|
||||
print("✓ 제품 선택 드롭다운 발견")
|
||||
|
||||
# 옵션 개수 확인
|
||||
options = product_select.locator('option').all()
|
||||
print(f"✓ 사용 가능한 제품: {len(options)}개")
|
||||
|
||||
# 각 옵션 출력
|
||||
for idx, option in enumerate(options):
|
||||
text = option.text_content()
|
||||
value = option.get_attribute('value')
|
||||
print(f" [{idx}] {text} (value: {value})")
|
||||
|
||||
# 신흥인삼 또는 세화인삼 선택 가능한지 확인
|
||||
has_shinheung = any('신흥인삼' in opt.text_content() for opt in options)
|
||||
has_sehwa = any('세화인삼' in opt.text_content() for opt in options)
|
||||
|
||||
if has_shinheung or has_sehwa:
|
||||
print("\n✅ 인삼 제품 선택 가능!")
|
||||
|
||||
# 첫 번째 제품 선택 시도
|
||||
if len(options) > 0:
|
||||
product_select.select_option(index=0)
|
||||
print(f"✓ '{options[0].text_content()}' 선택 완료")
|
||||
else:
|
||||
print("\n❌ 인삼 대체 제품이 드롭다운에 없음")
|
||||
else:
|
||||
print("❌ 제품 선택 드롭다운을 찾을 수 없음")
|
||||
print("페이지 HTML 일부:")
|
||||
print(ginseng_row.inner_html()[:500])
|
||||
else:
|
||||
print("❌ 인삼 항목을 찾을 수 없음")
|
||||
print("\n페이지 내용:")
|
||||
print(page.content()[:2000])
|
||||
|
||||
# 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()
|
||||
@ -1,128 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>커스텀 처방 실시간 감지 테스트</title>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: auto; }
|
||||
.badge { padding: 2px 8px; border-radius: 4px; color: white; }
|
||||
.bg-warning { background-color: #ffc107; color: #333; }
|
||||
.alert { padding: 10px; margin-top: 10px; border-radius: 4px; }
|
||||
.alert-warning { background-color: #fff3cd; border: 1px solid #ffeaa7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>커스텀 처방 감지 테스트</h1>
|
||||
|
||||
<div style="margin: 20px 0;">
|
||||
<h3>시나리오:</h3>
|
||||
<p>십전대보탕을 선택한 후, 약재를 추가/삭제/변경하면 "가감방" 표시가 나타납니다.</p>
|
||||
</div>
|
||||
|
||||
<div id="testResult" style="margin-top: 20px; padding: 20px; border: 1px solid #ddd;">
|
||||
<h3>테스트 결과:</h3>
|
||||
<div id="resultContent">테스트를 시작하려면 페이지를 새로고침하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 원래 처방 구성 저장용 변수
|
||||
let originalFormulaIngredients = {};
|
||||
|
||||
// 테스트 시나리오
|
||||
function runTest() {
|
||||
const results = [];
|
||||
|
||||
// 1. 십전대보탕 원래 구성 저장
|
||||
originalFormulaIngredients = {
|
||||
'3052A12AM': { herb_name: '감초', grams_per_cheop: 1.5 },
|
||||
'3047A10AM': { herb_name: '당귀', grams_per_cheop: 3.0 },
|
||||
'3065A10AM': { herb_name: '백출', grams_per_cheop: 3.0 },
|
||||
'3064B19AM': { herb_name: '백작약', grams_per_cheop: 3.0 },
|
||||
'3073B11AM': { herb_name: '숙지황', grams_per_cheop: 3.0 },
|
||||
'3054A14AM': { herb_name: '인삼', grams_per_cheop: 3.0 },
|
||||
'3072A17AM': { herb_name: '천궁', grams_per_cheop: 3.0 },
|
||||
'3056A18AM': { herb_name: '황기', grams_per_cheop: 3.0 },
|
||||
'3063A18AM': { herb_name: '백복령', grams_per_cheop: 3.0 },
|
||||
'3055A13AM': { herb_name: '육계', grams_per_cheop: 1.5 }
|
||||
};
|
||||
|
||||
// 2. 현재 약재 구성 (구기자 추가)
|
||||
const currentIngredients = {
|
||||
'3052A12AM': 1.5, // 감초
|
||||
'3047A10AM': 3.0, // 당귀
|
||||
'3065A10AM': 3.0, // 백출
|
||||
'3064B19AM': 3.0, // 백작약
|
||||
'3073B11AM': 3.0, // 숙지황
|
||||
'3054A14AM': 3.0, // 인삼
|
||||
'3072A17AM': 3.0, // 천궁
|
||||
'3056A18AM': 3.0, // 황기
|
||||
'3063A18AM': 3.0, // 백복령
|
||||
'3055A13AM': 1.5, // 육계
|
||||
'3147H1AHM': 3.0 // 구기자 (추가)
|
||||
};
|
||||
|
||||
// 3. 커스텀 감지 로직
|
||||
const customDetails = [];
|
||||
let isCustom = false;
|
||||
|
||||
// 추가된 약재 확인
|
||||
for (const code in currentIngredients) {
|
||||
if (!originalFormulaIngredients[code]) {
|
||||
customDetails.push(`구기자 ${currentIngredients[code]}g 추가`);
|
||||
isCustom = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 삭제된 약재 확인
|
||||
for (const code in originalFormulaIngredients) {
|
||||
if (!currentIngredients[code]) {
|
||||
customDetails.push(`${originalFormulaIngredients[code].herb_name} 제거`);
|
||||
isCustom = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 용량 변경된 약재 확인
|
||||
for (const code in currentIngredients) {
|
||||
if (originalFormulaIngredients[code]) {
|
||||
const originalGrams = originalFormulaIngredients[code].grams_per_cheop;
|
||||
const currentGrams = currentIngredients[code];
|
||||
|
||||
if (Math.abs(originalGrams - currentGrams) > 0.01) {
|
||||
const herbName = originalFormulaIngredients[code].herb_name;
|
||||
customDetails.push(`${herbName} ${originalGrams}g→${currentGrams}g`);
|
||||
isCustom = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 결과 표시
|
||||
if (isCustom) {
|
||||
const badgeHtml = `
|
||||
<div class="alert alert-warning">
|
||||
<span class="badge bg-warning">가감방</span>
|
||||
<small style="margin-left: 10px;">${customDetails.join(' | ')}</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#resultContent').html(`
|
||||
<p><strong>✅ 테스트 성공!</strong></p>
|
||||
<p>십전대보탕에 구기자 3g를 추가한 경우:</p>
|
||||
${badgeHtml}
|
||||
<p style="margin-top: 10px;">커스텀 처방이 정상적으로 감지되었습니다.</p>
|
||||
`);
|
||||
} else {
|
||||
$('#resultContent').html('<p>❌ 커스텀 처방이 감지되지 않았습니다.</p>');
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 테스트 실행
|
||||
$(document).ready(function() {
|
||||
runTest();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>약재 선택 테스트</title>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>약재 선택 드롭다운 테스트</h1>
|
||||
|
||||
<div id="test-area"></div>
|
||||
|
||||
<button id="add-row">행 추가</button>
|
||||
|
||||
<script>
|
||||
$('#add-row').click(function() {
|
||||
// API 호출하여 약재 목록 가져오기
|
||||
$.get('http://localhost:5001/api/herbs/masters', function(response) {
|
||||
console.log('API Response:', response);
|
||||
|
||||
if (response.success) {
|
||||
const select = $('<select></select>');
|
||||
select.append('<option value="">약재 선택</option>');
|
||||
|
||||
// 재고가 있는 약재만 필터링
|
||||
const herbsWithStock = response.data.filter(herb => herb.has_stock === 1);
|
||||
console.log('Herbs with stock:', herbsWithStock.length);
|
||||
|
||||
herbsWithStock.forEach(herb => {
|
||||
let displayName = herb.herb_name;
|
||||
if (herb.herb_name_hanja) {
|
||||
displayName += ` (${herb.herb_name_hanja})`;
|
||||
}
|
||||
select.append(`<option value="${herb.ingredient_code}">${displayName}</option>`);
|
||||
});
|
||||
|
||||
$('#test-area').append(select);
|
||||
$('#test-area').append('<br><br>');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,72 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>로트 배분 모달 테스트</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h2>로트 배분 필요량 테스트</h2>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">시나리오 테스트</h5>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-3">
|
||||
<label>첩당 용량 (g)</label>
|
||||
<input type="number" class="form-control" id="testGramsPerCheop" value="2.2" step="0.1">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>총 첩수</label>
|
||||
<input type="number" class="form-control" id="testCheopTotal" value="10" step="1">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label>계산된 필요량</label>
|
||||
<input type="text" class="form-control" id="calculatedQty" readonly>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-primary mt-4" onclick="calculateAndShow()">계산 및 확인</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function calculateAndShow() {
|
||||
const gramsPerCheop = parseFloat($('#testGramsPerCheop').val()) || 0;
|
||||
const cheopTotal = parseFloat($('#testCheopTotal').val()) || 0;
|
||||
const requiredQty = gramsPerCheop * cheopTotal;
|
||||
|
||||
$('#calculatedQty').val(requiredQty.toFixed(1) + 'g');
|
||||
|
||||
$('#result').html(`
|
||||
<div class="alert alert-info">
|
||||
<h6>계산 결과:</h6>
|
||||
<ul>
|
||||
<li>첩당 용량: ${gramsPerCheop}g</li>
|
||||
<li>총 첩수: ${cheopTotal}첩</li>
|
||||
<li><strong>필요량: ${requiredQty.toFixed(1)}g</strong></li>
|
||||
</ul>
|
||||
<p class="mb-0 mt-2">
|
||||
<strong>테스트:</strong> 2.2g × 10첩 = 22g가 모달에 정상적으로 전달되어야 합니다.
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// 초기 계산
|
||||
$(document).ready(function() {
|
||||
calculateAndShow();
|
||||
|
||||
$('#testGramsPerCheop, #testCheopTotal').on('input', calculateAndShow);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
기존 조제 데이터의 커스텀 처방 여부를 재검사하여 업데이트
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
def get_connection():
|
||||
return sqlite3.connect('database/kdrug.db')
|
||||
|
||||
def update_custom_prescriptions():
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# formula_id가 있는 모든 조제 조회
|
||||
cursor.execute("""
|
||||
SELECT compound_id, formula_id
|
||||
FROM compounds
|
||||
WHERE formula_id IS NOT NULL
|
||||
""")
|
||||
compounds = cursor.fetchall()
|
||||
|
||||
print(f"검사할 조제: {len(compounds)}개")
|
||||
|
||||
updated_count = 0
|
||||
for compound_id, formula_id in compounds:
|
||||
# 원 처방 구성 조회 (ingredient_code 기준)
|
||||
cursor.execute("""
|
||||
SELECT fi.ingredient_code, hm.herb_name, fi.grams_per_cheop
|
||||
FROM formula_ingredients fi
|
||||
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
|
||||
WHERE fi.formula_id = ?
|
||||
""", (formula_id,))
|
||||
|
||||
original_by_code = {}
|
||||
for row in cursor.fetchall():
|
||||
ingredient_code = row[0]
|
||||
herb_name = row[1]
|
||||
grams = row[2]
|
||||
original_by_code[ingredient_code] = {
|
||||
'herb_name': herb_name,
|
||||
'grams': grams
|
||||
}
|
||||
|
||||
# 실제 조제 구성 조회 (ingredient_code 기준)
|
||||
cursor.execute("""
|
||||
SELECT hi.ingredient_code, hi.herb_name, ci.grams_per_cheop
|
||||
FROM compound_ingredients ci
|
||||
JOIN herb_items hi ON ci.herb_item_id = hi.herb_item_id
|
||||
WHERE ci.compound_id = ?
|
||||
""", (compound_id,))
|
||||
|
||||
actual_by_code = {}
|
||||
for row in cursor.fetchall():
|
||||
ingredient_code = row[0]
|
||||
herb_name = row[1]
|
||||
grams = row[2]
|
||||
if ingredient_code not in actual_by_code:
|
||||
actual_by_code[ingredient_code] = {
|
||||
'herb_name': herb_name,
|
||||
'grams': grams
|
||||
}
|
||||
|
||||
# 커스텀 여부 판단
|
||||
is_custom = False
|
||||
custom_details = []
|
||||
|
||||
# 추가된 약재 확인
|
||||
for code, info in actual_by_code.items():
|
||||
if code not in original_by_code:
|
||||
custom_details.append(f"{info['herb_name']} {info['grams']}g 추가")
|
||||
is_custom = True
|
||||
|
||||
# 제거된 약재 확인
|
||||
for code, info in original_by_code.items():
|
||||
if code not in actual_by_code:
|
||||
custom_details.append(f"{info['herb_name']} 제거")
|
||||
is_custom = True
|
||||
|
||||
# 용량 변경된 약재 확인
|
||||
for code in original_by_code:
|
||||
if code in actual_by_code:
|
||||
original_grams = original_by_code[code]['grams']
|
||||
actual_grams = actual_by_code[code]['grams']
|
||||
if abs(original_grams - actual_grams) > 0.01:
|
||||
herb_name = original_by_code[code]['herb_name']
|
||||
custom_details.append(f"{herb_name} {original_grams}g→{actual_grams}g")
|
||||
is_custom = True
|
||||
|
||||
# 커스텀인 경우 업데이트
|
||||
if is_custom:
|
||||
custom_summary = " | ".join(custom_details)
|
||||
cursor.execute("""
|
||||
UPDATE compounds
|
||||
SET is_custom = 1,
|
||||
custom_summary = ?,
|
||||
custom_type = 'custom'
|
||||
WHERE compound_id = ?
|
||||
""", (custom_summary, compound_id))
|
||||
|
||||
# 처방명 조회
|
||||
cursor.execute("""
|
||||
SELECT f.formula_name
|
||||
FROM compounds c
|
||||
JOIN formulas f ON c.formula_id = f.formula_id
|
||||
WHERE c.compound_id = ?
|
||||
""", (compound_id,))
|
||||
formula_name = cursor.fetchone()[0]
|
||||
|
||||
print(f" - Compound #{compound_id} ({formula_name}): 가감방으로 업데이트")
|
||||
print(f" 변경사항: {custom_summary}")
|
||||
updated_count += 1
|
||||
|
||||
conn.commit()
|
||||
print(f"\n완료! {updated_count}개의 조제가 가감방으로 업데이트되었습니다.")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"오류 발생: {e}")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_custom_prescriptions()
|
||||
@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
십전대보탕 약재별 효능 설명 추가
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def update_sipjeondaebotang():
|
||||
"""십전대보탕 약재별 효능 설명 업데이트"""
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 십전대보탕 ID 확인
|
||||
cursor.execute("""
|
||||
SELECT formula_id, formula_name
|
||||
FROM formulas
|
||||
WHERE formula_code = 'SJDB01'
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
print("❌ 십전대보탕을 찾을 수 없습니다.")
|
||||
return False
|
||||
|
||||
formula_id, formula_name = result
|
||||
print(f"📋 {formula_name} (ID: {formula_id}) 효능 설명 추가")
|
||||
print("="*60)
|
||||
|
||||
# 각 약재별 효능 설명 업데이트
|
||||
herb_notes = {
|
||||
"숙지황": "보음보혈",
|
||||
"작약": "보혈지통",
|
||||
"인삼": "대보원기",
|
||||
"백출": "보기건비",
|
||||
"황기": "보기승양",
|
||||
"대추": "보중익기",
|
||||
"일당귀": "보혈활혈",
|
||||
"복령": "건비이수",
|
||||
"감초": "조화제약",
|
||||
"천궁": "활혈행기",
|
||||
"반하생강백반제": "화담지구"
|
||||
}
|
||||
|
||||
print("\n약재별 효능 설명 추가:")
|
||||
print("-"*60)
|
||||
|
||||
for herb_name, notes in herb_notes.items():
|
||||
# 약재 코드 찾기
|
||||
cursor.execute("""
|
||||
SELECT fi.ingredient_id, hm.herb_name, fi.notes
|
||||
FROM formula_ingredients fi
|
||||
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
|
||||
WHERE fi.formula_id = ? AND hm.herb_name = ?
|
||||
""", (formula_id, herb_name))
|
||||
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
ingredient_id, actual_name, current_notes = result
|
||||
|
||||
# 효능 설명 업데이트
|
||||
cursor.execute("""
|
||||
UPDATE formula_ingredients
|
||||
SET notes = ?
|
||||
WHERE ingredient_id = ?
|
||||
""", (notes, ingredient_id))
|
||||
|
||||
if current_notes:
|
||||
print(f" {actual_name}: '{current_notes}' → '{notes}'")
|
||||
else:
|
||||
print(f" {actual_name}: 효능 설명 추가 → '{notes}'")
|
||||
else:
|
||||
print(f" ⚠️ {herb_name}: 약재를 찾을 수 없음")
|
||||
|
||||
conn.commit()
|
||||
print(f"\n✅ 효능 설명 추가 완료!")
|
||||
|
||||
# 업데이트 후 확인
|
||||
print(f"\n📊 업데이트된 십전대보탕 구성:")
|
||||
print("-"*60)
|
||||
|
||||
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
|
||||
""", (formula_id,))
|
||||
|
||||
results = cursor.fetchall()
|
||||
for herb, amount, notes in results:
|
||||
check = "✅" if notes else "❌"
|
||||
print(f" {check} {herb:15s}: {amount:5.1f}g - {notes if notes else '효능 설명 없음'}")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ 데이터베이스 오류: {e}")
|
||||
conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🌿 십전대보탕 효능 설명 추가 프로그램")
|
||||
print("="*60)
|
||||
|
||||
if update_sipjeondaebotang():
|
||||
print("\n✅ 업데이트 작업이 완료되었습니다.")
|
||||
else:
|
||||
print("\n❌ 업데이트 중 오류가 발생했습니다.")
|
||||
@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
쌍화탕 처방의 당귀를 일당귀로 수정
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def update_danggui():
|
||||
"""쌍화탕의 당귀를 일당귀로 수정"""
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 현재 쌍화탕에 등록된 당귀 확인
|
||||
print("🔍 현재 쌍화탕 처방의 당귀 확인...")
|
||||
cursor.execute("""
|
||||
SELECT f.formula_name, fi.ingredient_code, hm.herb_name, fi.grams_per_cheop
|
||||
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_name LIKE '%쌍화%'
|
||||
AND hm.herb_name LIKE '%당귀%'
|
||||
""")
|
||||
|
||||
current = cursor.fetchall()
|
||||
print(f"현재 상태:")
|
||||
for name, code, herb, amount in current:
|
||||
print(f" - {name}: {herb} ({code}) - {amount}g")
|
||||
|
||||
# 당귀(3105H1AHM)를 일당귀(3403H1AHM)로 변경
|
||||
print(f"\n✏️ 당귀(3105H1AHM) → 일당귀(3403H1AHM)로 변경 중...")
|
||||
|
||||
# 쌍화탕 처방 ID 확인
|
||||
cursor.execute("""
|
||||
SELECT formula_id
|
||||
FROM formulas
|
||||
WHERE formula_name LIKE '%쌍화%'
|
||||
""")
|
||||
|
||||
formula_ids = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
if formula_ids:
|
||||
# 당귀를 일당귀로 수정
|
||||
cursor.execute("""
|
||||
UPDATE formula_ingredients
|
||||
SET ingredient_code = '3403H1AHM'
|
||||
WHERE ingredient_code = '3105H1AHM'
|
||||
AND formula_id IN ({})
|
||||
""".format(','.join('?' * len(formula_ids))), formula_ids)
|
||||
|
||||
updated_count = cursor.rowcount
|
||||
print(f"✅ {updated_count}개 항목 수정됨")
|
||||
|
||||
# 변경 후 확인
|
||||
print(f"\n🔍 수정 후 확인...")
|
||||
cursor.execute("""
|
||||
SELECT f.formula_name, fi.ingredient_code, hm.herb_name, fi.grams_per_cheop
|
||||
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_name LIKE '%쌍화%'
|
||||
AND hm.herb_name LIKE '%당귀%'
|
||||
""")
|
||||
|
||||
updated = cursor.fetchall()
|
||||
print(f"수정된 상태:")
|
||||
for name, code, herb, amount in updated:
|
||||
print(f" - {name}: {herb} ({code}) - {amount}g")
|
||||
|
||||
conn.commit()
|
||||
print(f"\n✅ 쌍화탕 당귀 수정 완료!")
|
||||
|
||||
# 전체 처방 구성 확인
|
||||
print(f"\n📋 수정된 쌍화탕 전체 구성:")
|
||||
print("-"*60)
|
||||
|
||||
for formula_id in formula_ids:
|
||||
cursor.execute("""
|
||||
SELECT f.formula_name
|
||||
FROM formulas f
|
||||
WHERE f.formula_id = ?
|
||||
""", (formula_id,))
|
||||
|
||||
formula_name = cursor.fetchone()[0]
|
||||
print(f"\n{formula_name}:")
|
||||
|
||||
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
|
||||
""", (formula_id,))
|
||||
|
||||
for herb, amount, notes in cursor.fetchall():
|
||||
marker = "✅" if herb == "일당귀" else " "
|
||||
print(f" {marker} {herb}: {amount}g ({notes if notes else ''})")
|
||||
else:
|
||||
print("❌ 쌍화탕 처방을 찾을 수 없습니다.")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ 데이터베이스 오류: {e}")
|
||||
conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🌿 쌍화탕 당귀 수정 프로그램")
|
||||
print("="*60)
|
||||
|
||||
if update_danggui():
|
||||
print("\n✅ 수정 작업이 완료되었습니다.")
|
||||
else:
|
||||
print("\n❌ 수정 중 오류가 발생했습니다.")
|
||||
@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
월비탕 처방의 진피초를 진피(陳皮)로 수정
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def update_jinpi():
|
||||
"""진피초를 진피로 수정"""
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 현재 진피초로 등록된 월비탕 처방 확인
|
||||
print("🔍 현재 월비탕 처방에 등록된 진피 확인...")
|
||||
cursor.execute("""
|
||||
SELECT f.formula_name, fi.ingredient_code, hm.herb_name
|
||||
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 LIKE 'WBT%'
|
||||
AND hm.herb_name LIKE '%진피%'
|
||||
ORDER BY f.formula_code
|
||||
""")
|
||||
|
||||
current = cursor.fetchall()
|
||||
print(f"현재 상태:")
|
||||
for name, code, herb in current:
|
||||
print(f" - {name}: {herb} ({code})")
|
||||
|
||||
# 진피초(3632H1AHM)를 진피(陳皮)(3466H1AHM)로 변경
|
||||
print(f"\n✏️ 진피초(3632H1AHM) → 진피(陳皮)(3466H1AHM)로 변경 중...")
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE formula_ingredients
|
||||
SET ingredient_code = '3466H1AHM'
|
||||
WHERE ingredient_code = '3632H1AHM'
|
||||
AND formula_id IN (
|
||||
SELECT formula_id
|
||||
FROM formulas
|
||||
WHERE formula_code LIKE 'WBT%'
|
||||
)
|
||||
""")
|
||||
|
||||
updated_count = cursor.rowcount
|
||||
print(f"✅ {updated_count}개 항목 수정됨")
|
||||
|
||||
# 변경 후 확인
|
||||
print(f"\n🔍 수정 후 확인...")
|
||||
cursor.execute("""
|
||||
SELECT f.formula_name, fi.ingredient_code, hm.herb_name
|
||||
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 LIKE 'WBT%'
|
||||
AND hm.herb_name LIKE '%진피%'
|
||||
ORDER BY f.formula_code
|
||||
""")
|
||||
|
||||
updated = cursor.fetchall()
|
||||
print(f"수정된 상태:")
|
||||
for name, code, herb in updated:
|
||||
print(f" - {name}: {herb} ({code})")
|
||||
|
||||
conn.commit()
|
||||
print(f"\n✅ 진피 수정 완료!")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ 데이터베이스 오류: {e}")
|
||||
conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🌿 월비탕 진피 수정 프로그램")
|
||||
print("="*60)
|
||||
|
||||
if update_jinpi():
|
||||
print("\n✅ 수정 작업이 완료되었습니다.")
|
||||
else:
|
||||
print("\n❌ 수정 중 오류가 발생했습니다.")
|
||||
@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
삼소음 처방 데이터 검증 스크립트
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def verify_samsoeun():
|
||||
"""추가된 삼소음 처방 검증"""
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("🔍 삼소음 처방 상세 검증")
|
||||
print("="*70)
|
||||
|
||||
# 삼소음 처방 정보 조회
|
||||
cursor.execute("""
|
||||
SELECT f.formula_id, f.formula_code, f.formula_name, f.description,
|
||||
f.base_cheop, f.base_pouches
|
||||
FROM formulas f
|
||||
WHERE f.formula_code = 'SSE001'
|
||||
""")
|
||||
|
||||
formula = cursor.fetchone()
|
||||
|
||||
if formula:
|
||||
formula_id, code, name, description, base_cheop, base_pouches = formula
|
||||
|
||||
print(f"\n📝 {name} ({code})")
|
||||
print(f" ID: {formula_id}")
|
||||
print(f" 설명: {description}")
|
||||
print(f" 기본 첩수: {base_cheop}첩")
|
||||
print(f" 기본 포수: {base_pouches}포")
|
||||
|
||||
# 약재 구성 상세 조회
|
||||
print(f"\n 약재 구성 (1첩 기준):")
|
||||
print(" " + "-"*60)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT hm.herb_name, fi.grams_per_cheop, fi.notes, hm.ingredient_code
|
||||
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()
|
||||
total_1cheop = 0
|
||||
total_20cheop = 0 # 20첩(1제) 기준
|
||||
|
||||
print(f" {'약재명':15s} | {'1첩(g)':>8s} | {'20첩(g)':>8s} | {'효능'}")
|
||||
print(" " + "-"*60)
|
||||
|
||||
for herb_name, grams, notes, code in ingredients:
|
||||
total_1cheop += grams
|
||||
grams_20 = grams * 20
|
||||
total_20cheop += grams_20
|
||||
print(f" {herb_name:15s} | {grams:8.1f} | {grams_20:8.1f} | {notes}")
|
||||
|
||||
print(" " + "-"*60)
|
||||
print(f" {'총 용량':15s} | {total_1cheop:8.1f} | {total_20cheop:8.1f} |")
|
||||
|
||||
# 원본 데이터와 비교
|
||||
print(f"\n📊 원본 데이터와 비교:")
|
||||
print(" " + "-"*60)
|
||||
|
||||
original_data = {
|
||||
"인삼": (4, 80),
|
||||
"소엽": (4, 80),
|
||||
"전호": (4, 80),
|
||||
"반하": (4, 80),
|
||||
"갈근": (4, 80),
|
||||
"적복령": (4, 80),
|
||||
"대조": (4, 80),
|
||||
"진피": (3, 60),
|
||||
"길경": (3, 60),
|
||||
"지각": (3, 60),
|
||||
"감초": (3, 60),
|
||||
"건강": (1, 20)
|
||||
}
|
||||
|
||||
print(f" {'약재':10s} | {'원본 1첩':>10s} | {'DB 1첩':>10s} | {'일치여부'}")
|
||||
print(" " + "-"*60)
|
||||
|
||||
# DB 데이터를 딕셔너리로 변환
|
||||
db_data = {}
|
||||
for herb_name, grams, notes, code in ingredients:
|
||||
# 약재명 정규화
|
||||
if "자소엽" in herb_name:
|
||||
key = "소엽"
|
||||
elif "복령" in herb_name:
|
||||
key = "적복령"
|
||||
elif "대추" in herb_name:
|
||||
key = "대조"
|
||||
elif "진피" in herb_name:
|
||||
key = "진피"
|
||||
else:
|
||||
key = herb_name
|
||||
|
||||
db_data[key] = grams
|
||||
|
||||
# 비교
|
||||
all_match = True
|
||||
for herb, (orig_1, orig_20) in original_data.items():
|
||||
db_amount = db_data.get(herb, 0)
|
||||
match = "✅" if abs(orig_1 - db_amount) < 0.01 else "❌"
|
||||
if match == "❌":
|
||||
all_match = False
|
||||
print(f" {herb:10s} | {orig_1:10.1f}g | {db_amount:10.1f}g | {match}")
|
||||
|
||||
print("\n" + "="*70)
|
||||
if all_match:
|
||||
print("✅ 모든 약재가 원본 데이터와 일치합니다!")
|
||||
else:
|
||||
print("⚠️ 일부 약재가 원본 데이터와 일치하지 않습니다.")
|
||||
|
||||
else:
|
||||
print("❌ 삼소음 처방을 찾을 수 없습니다.")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify_samsoeun()
|
||||
@ -1,89 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
월비탕 처방 데이터 검증 스크립트
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
def verify_wolbitang():
|
||||
"""추가된 월비탕 처방 검증"""
|
||||
|
||||
conn = sqlite3.connect('database/kdrug.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("🔍 월비탕 처방 상세 검증")
|
||||
print("="*70)
|
||||
|
||||
# 각 처방별 상세 정보 조회
|
||||
cursor.execute("""
|
||||
SELECT f.formula_id, f.formula_code, f.formula_name, f.description
|
||||
FROM formulas f
|
||||
WHERE f.formula_code LIKE 'WBT%'
|
||||
ORDER BY f.formula_code
|
||||
""")
|
||||
|
||||
formulas = cursor.fetchall()
|
||||
|
||||
for formula_id, formula_code, formula_name, description in formulas:
|
||||
print(f"\n📝 {formula_name} ({formula_code})")
|
||||
print(f" 설명: {description}")
|
||||
print(f" 약재 구성:")
|
||||
|
||||
# 각 처방의 약재 상세 조회
|
||||
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
|
||||
""", (formula_id,))
|
||||
|
||||
ingredients = cursor.fetchall()
|
||||
total_grams = 0
|
||||
|
||||
for herb_name, grams, notes in ingredients:
|
||||
print(f" - {herb_name:8s}: {grams:6.3f}g ({notes})")
|
||||
total_grams += grams
|
||||
|
||||
print(f" 총 용량: {total_grams:.3f}g")
|
||||
|
||||
# 단계별 용량 변화 비교
|
||||
print(f"\n{'='*70}")
|
||||
print("📊 단계별 약재 용량 변화:")
|
||||
print("-"*70)
|
||||
|
||||
# 약재별 단계별 용량 조회
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT hm.herb_name
|
||||
FROM formula_ingredients fi
|
||||
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
|
||||
JOIN formulas f ON fi.formula_id = f.formula_id
|
||||
WHERE f.formula_code LIKE 'WBT%'
|
||||
ORDER BY hm.herb_name
|
||||
""")
|
||||
|
||||
herbs = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
print(f"{'약재명':10s} | {'1차':>8s} | {'2차':>8s} | {'3차':>8s} | {'4차':>8s}")
|
||||
print("-"*50)
|
||||
|
||||
for herb in herbs:
|
||||
amounts = []
|
||||
for stage in range(1, 5):
|
||||
cursor.execute("""
|
||||
SELECT fi.grams_per_cheop
|
||||
FROM formula_ingredients fi
|
||||
JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
|
||||
JOIN formulas f ON fi.formula_id = f.formula_id
|
||||
WHERE f.formula_code = ? AND hm.herb_name = ?
|
||||
""", (f'WBT001-{stage}', herb))
|
||||
|
||||
result = cursor.fetchone()
|
||||
amounts.append(f"{result[0]:.3f}g" if result else "-")
|
||||
|
||||
print(f"{herb:10s} | {amounts[0]:>8s} | {amounts[1]:>8s} | {amounts[2]:>8s} | {amounts[3]:>8s}")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
verify_wolbitang()
|
||||
Loading…
Reference in New Issue
Block a user