chore: 개발 파일 정리 및 구조화

- 개발/테스트 스크립트를 dev_scripts/ 폴더로 이동
- 스크린샷을 screenshots/ 폴더로 이동
- 백업 파일 보존 (.backup)
- 처방 관련 추가 스크립트 포함

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-02-18 04:44:48 +00:00
parent 124bc5eaf8
commit ad9ac396e2
54 changed files with 8030 additions and 241 deletions

View File

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

View File

@@ -0,0 +1,198 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Excel 파일 형식 분석 도구
한의사랑과 한의정보 형식 비교
"""
import pandas as pd
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
def analyze_excel_format(file_path, format_name):
"""Excel 파일 형식 분석"""
print(f"\n{'='*60}")
print(f"📊 {format_name} 형식 분석")
print(f"파일: {file_path}")
print('='*60)
try:
# Excel 파일 읽기
df = pd.read_excel(file_path)
# 기본 정보
print(f"\n1⃣ 기본 정보:")
print(f" - 행 개수: {len(df)}")
print(f" - 열 개수: {len(df.columns)}")
# 컬럼 정보
print(f"\n2⃣ 컬럼 목록:")
for i, col in enumerate(df.columns, 1):
print(f" {i}. {col}")
# 데이터 타입
print(f"\n3⃣ 데이터 타입:")
for col in df.columns:
print(f" - {col}: {df[col].dtype}")
# 샘플 데이터 (처음 3행)
print(f"\n4⃣ 샘플 데이터 (처음 3행):")
print(df.head(3).to_string(index=False))
# 누락 데이터 확인
print(f"\n5⃣ 누락 데이터:")
null_counts = df.isnull().sum()
for col in df.columns:
if null_counts[col] > 0:
print(f" - {col}: {null_counts[col]}개 누락")
if null_counts.sum() == 0:
print(" - 누락 데이터 없음")
# 고유값 개수 (참고용)
print(f"\n6⃣ 고유값 개수:")
for col in df.columns:
unique_count = df[col].nunique()
print(f" - {col}: {unique_count}")
return df
except Exception as e:
print(f"❌ 오류 발생: {str(e)}")
return None
def compare_formats(df1, df2, name1, name2):
"""두 형식 비교"""
print(f"\n{'='*60}")
print(f"🔄 {name1} vs {name2} 형식 비교")
print('='*60)
if df1 is None or df2 is None:
print("비교할 수 없습니다 (데이터 로드 실패)")
return
cols1 = set(df1.columns)
cols2 = set(df2.columns)
# 공통 컬럼
common = cols1.intersection(cols2)
print(f"\n✅ 공통 컬럼 ({len(common)}개):")
for col in sorted(common):
print(f" - {col}")
# 한의사랑에만 있는 컬럼
only_in_1 = cols1 - cols2
if only_in_1:
print(f"\n📌 {name1}에만 있는 컬럼 ({len(only_in_1)}개):")
for col in sorted(only_in_1):
print(f" - {col}")
# 한의정보에만 있는 컬럼
only_in_2 = cols2 - cols1
if only_in_2:
print(f"\n📌 {name2}에만 있는 컬럼 ({len(only_in_2)}개):")
for col in sorted(only_in_2):
print(f" - {col}")
# 컬럼명 매핑 추천
print(f"\n🔗 컬럼 매핑 추천:")
# 가능한 매핑 찾기
mappings = []
# 날짜 관련
date_cols1 = [c for c in cols1 if '' in c or '날짜' in c or 'date' in c.lower()]
date_cols2 = [c for c in cols2 if '' in c or '날짜' in c or 'date' in c.lower()]
if date_cols1 and date_cols2:
mappings.append((date_cols1[0], date_cols2[0], "날짜"))
# 약재명 관련
herb_cols1 = [c for c in cols1 if '약재' in c or '품목' in c or '제품' in c]
herb_cols2 = [c for c in cols2 if '약재' in c or '품목' in c or '제품' in c]
if herb_cols1 and herb_cols2:
mappings.append((herb_cols1[0], herb_cols2[0], "약재명"))
# 수량 관련
qty_cols1 = [c for c in cols1 if '수량' in c or '' in c or '구입량' in c]
qty_cols2 = [c for c in cols2 if '수량' in c or '' in c or '구입량' in c]
if qty_cols1 and qty_cols2:
mappings.append((qty_cols1[0], qty_cols2[0], "수량"))
# 금액 관련
amt_cols1 = [c for c in cols1 if '금액' in c or '' in c or '가격' in c]
amt_cols2 = [c for c in cols2 if '금액' in c or '' in c or '가격' in c]
if amt_cols1 and amt_cols2:
mappings.append((amt_cols1[0], amt_cols2[0], "금액"))
# 업체 관련
supplier_cols1 = [c for c in cols1 if '업체' in c or '도매' in c or '공급' in c]
supplier_cols2 = [c for c in cols2 if '업체' in c or '도매' in c or '공급' in c]
if supplier_cols1 and supplier_cols2:
mappings.append((supplier_cols1[0], supplier_cols2[0], "공급업체"))
# 원산지 관련
origin_cols1 = [c for c in cols1 if '원산지' in c or '산지' in c]
origin_cols2 = [c for c in cols2 if '원산지' in c or '산지' in c]
if origin_cols1 and origin_cols2:
mappings.append((origin_cols1[0], origin_cols2[0], "원산지"))
for col1, col2, mapping_type in mappings:
print(f" - {mapping_type}: [{name1}]{col1} ↔ [{name2}]{col2}")
def main():
"""메인 함수"""
print("\n" + "="*60)
print("🏥 한약 입고장 Excel 형식 분석기")
print("="*60)
# 파일 경로
hanisarang_path = '/root/kdrug/sample/한의사랑.xlsx'
haninfo_path = '/root/kdrug/sample/한의정보.xlsx'
current_path = '/root/kdrug/sample/order_view_20260215154829.xlsx'
# 각 형식 분석
df_hanisarang = None
df_haninfo = None
df_current = None
if os.path.exists(hanisarang_path):
df_hanisarang = analyze_excel_format(hanisarang_path, "한의사랑")
else:
print(f"❌ 한의사랑 파일을 찾을 수 없음: {hanisarang_path}")
if os.path.exists(haninfo_path):
df_haninfo = analyze_excel_format(haninfo_path, "한의정보")
else:
print(f"❌ 한의정보 파일을 찾을 수 없음: {haninfo_path}")
# 현재 사용 중인 형식도 분석
if os.path.exists(current_path):
df_current = analyze_excel_format(current_path, "현재 사용 중")
# 형식 비교
if df_hanisarang is not None and df_haninfo is not None:
compare_formats(df_hanisarang, df_haninfo, "한의사랑", "한의정보")
# 통합 매핑 제안
print(f"\n{'='*60}")
print("💡 통합 컬럼 매핑 제안")
print('='*60)
print("""
시스템에서 사용할 표준 컬럼:
1. insurance_code (보험코드/제품코드)
2. supplier_name (업체명/도매상)
3. herb_name (약재명/품목명)
4. receipt_date (구입일자/입고일)
5. quantity (구입량/수량) - 그램 단위
6. total_amount (구입액/금액)
7. origin_country (원산지)
8. unit_price (단가) - 계산 가능한 경우
각 형식별 매핑 규칙을 자동으로 적용하여
어떤 형식의 Excel 파일도 처리 가능하도록 구현 가능
""")
if __name__ == "__main__":
main()

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
한약재 제품 코드 엑셀 파일 분석
"""
import pandas as pd
import openpyxl
def analyze_excel_file():
file_path = 'sample/(게시)한약재제품코드_2510.xlsx'
# 엑셀 파일 열기
wb = openpyxl.load_workbook(file_path, read_only=True)
print("=== 엑셀 파일 시트 목록 ===")
for i, sheet_name in enumerate(wb.sheetnames, 1):
print(f"{i}. {sheet_name}")
# 4번째 시트 데이터 읽기
if len(wb.sheetnames) >= 4:
sheet_name = wb.sheetnames[3] # 0-based index
print(f"\n=== 4번째 시트 '{sheet_name}' 분석 ===")
# pandas로 데이터 읽기
df = pd.read_excel(file_path, sheet_name=sheet_name)
print(f"\n데이터 크기: {df.shape[0]}행 x {df.shape[1]}")
print(f"\n컬럼 목록:")
for i, col in enumerate(df.columns, 1):
# NaN이 아닌 값들의 예시
non_null_count = df[col].notna().sum()
sample_values = df[col].dropna().head(3).tolist()
print(f" {i}. {col} (유효값: {non_null_count}개)")
if sample_values:
print(f" 예시: {sample_values[:3]}")
print(f"\n=== 데이터 샘플 (처음 10행) ===")
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option('display.max_colwidth', 50)
print(df.head(10))
# 주요 컬럼 분석
if '주성분코드' in df.columns:
print(f"\n=== 주성분코드 분석 ===")
print(f"유일한 주성분코드 수: {df['주성분코드'].nunique()}")
print(f"주성분코드 샘플: {df['주성분코드'].unique()[:10].tolist()}")
if '제품명' in df.columns:
print(f"\n=== 제품명 분석 ===")
print(f"유일한 제품 수: {df['제품명'].nunique()}")
print(f"제품명 샘플: {df['제품명'].head(10).tolist()}")
# 컬럼 정보를 더 자세히 분석
print(f"\n=== 데이터 타입 및 null 값 정보 ===")
print(df.info())
wb.close()
if __name__ == "__main__":
analyze_excel_file()

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
한약재 제품 코드 심층 분석
"""
import pandas as pd
def deep_analyze():
file_path = 'sample/(게시)한약재제품코드_2510.xlsx'
sheet_name = '한약재 제품코드_20250930기준(유효코드만 공지)'
# 데이터 읽기 - 제품코드를 문자열로 읽어서 0 유지
df = pd.read_excel(file_path, sheet_name=sheet_name, dtype={'제품코드': str})
print("=== 한약재 제품 코드 데이터 심층 분석 ===")
print(f"전체 데이터: {len(df):,}개 제품")
print(f"유일한 주성분코드: {df['주성분코드'].nunique()}")
print(f"유일한 약재 품목명: {df['한약재 품목명'].nunique()}")
print(f"유일한 업체: {df['업체명'].nunique()}")
# 주성분코드별 통계
print("\n=== 주성분코드별 제품 수 (상위 20개) ===")
ingredient_stats = df.groupby(['주성분코드', '한약재 품목명']).size().reset_index(name='제품수')
ingredient_stats = ingredient_stats.sort_values('제품수', ascending=False).head(20)
for _, row in ingredient_stats.iterrows():
print(f" {row['주성분코드']} ({row['한약재 품목명']}): {row['제품수']}개 제품")
# 업체별 통계
print("\n=== 업체별 제품 수 (상위 10개) ===")
company_stats = df['업체명'].value_counts().head(10)
for company, count in company_stats.items():
print(f" {company}: {count}개 제품")
# 규격별 분석
print("\n=== 약품 규격 분석 ===")
spec_stats = df['약품규격(단위)'].value_counts()
print("규격 단위별 제품 수:")
for spec, count in spec_stats.items():
print(f" {spec}: {count}")
# 특정 약재들 확인
print("\n=== 주요 약재 확인 ===")
target_herbs = ['건강', '감초', '당귀', '황기', '숙지황', '백출', '천궁', '육계', '백작약', '인삼', '생강', '대추']
for herb in target_herbs:
herb_data = df[df['한약재 품목명'] == herb]
if not herb_data.empty:
unique_code = herb_data['주성분코드'].iloc[0] if len(herb_data) > 0 else 'N/A'
product_count = len(herb_data)
company_count = herb_data['업체명'].nunique()
print(f" {herb}: 주성분코드={unique_code}, {product_count}개 제품, {company_count}개 업체")
else:
print(f" {herb}: 데이터 없음")
# 한 약재에 여러 제품이 있는 예시 - 건강
print("\n=== '건강' 약재의 제품 예시 (처음 10개) ===")
gangang_data = df[df['한약재 품목명'] == '건강'].head(10)
if not gangang_data.empty:
for _, row in gangang_data.iterrows():
# 제품코드를 9자리로 표시 (0 패딩)
product_code = str(row['제품코드']).zfill(9)
print(f" 업체: {row['업체명']}, 제품명: {row['제품명']}, 제품코드: {product_code}, 규격: {row['약품규격(숫자)']} {row['약품규격(단위)']}")
# 현재 시스템과의 비교
print("\n=== 현재 DB 설계와의 차이점 ===")
print("1. 현재 시스템:")
print(" - herb_items: 약재 기본 정보 (예: 건강)")
print(" - inventory_lots: 로트별 재고 (원산지, 가격 등)")
print("\nㅇ2. 제품코드 시스템:")
print(" - 주성분코드: 약재별 고유 코드 (예: 3050H1AHM = 건강)")
print(" - 제품코드: 업체별 제품 고유코드")
print(" - 표준코드/대표코드: 바코드 시스템")
print(" - 규격: 포장 단위 (500g, 1000g 등)")
print("\n=== 시사점 ===")
print("- 54,000개 이상의 유통 제품이 454개 주성분코드로 분류됨")
print("- 같은 약재(주성분)라도 업체별로 다른 제품명과 코드를 가짐")
print("- 제품별로 다양한 포장 규격 존재 (-, 500g, 600g, 1000g 등)")
print("- 표준코드(바코드)를 통한 제품 식별 가능")
if __name__ == "__main__":
deep_analyze()

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
약재 효능 태그 시스템 추가 스크립트
"""
import sqlite3
def check_and_create_efficacy_system():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 1. 현재 테이블 확인
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name
""")
print("=== 현재 테이블 목록 ===")
for table in cursor.fetchall():
print(f" - {table[0]}")
# 2. herb_items 테이블 스키마 확인
print("\n=== herb_items 테이블 구조 ===")
cursor.execute("PRAGMA table_info(herb_items)")
columns = cursor.fetchall()
for col in columns:
print(f" {col[1]} ({col[2]})")
# 3. 효능 태그 테이블 생성
print("\n=== 효능 태그 시스템 생성 ===")
# 효능 마스터 테이블
cursor.execute("""
CREATE TABLE IF NOT EXISTS herb_efficacy_tags (
tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
tag_name VARCHAR(50) NOT NULL UNIQUE,
tag_category VARCHAR(50), -- 보(補), 사(瀉), 온(溫), 량(涼) 등
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
print("✅ herb_efficacy_tags 테이블 생성")
# 약재-효능 연결 테이블 (다대다)
cursor.execute("""
CREATE TABLE IF NOT EXISTS herb_item_tags (
herb_item_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (herb_item_id, tag_id),
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES herb_efficacy_tags(tag_id) ON DELETE CASCADE
)
""")
print("✅ herb_item_tags 테이블 생성")
# 4. 기본 효능 태그 추가
basic_tags = [
('보혈', '', '혈을 보하는 효능'),
('보기', '', '기를 보하는 효능'),
('보양', '', '양기를 보하는 효능'),
('보음', '', '음액을 보하는 효능'),
('활혈', '', '혈액순환을 활발하게 하는 효능'),
('거담', '', '가래를 제거하는 효능'),
('온중', '', '속을 따뜻하게 하는 효능'),
('온양', '', '양기를 따뜻하게 하는 효능'),
('청열', '', '열을 내리는 효능'),
('해표', '', '표증을 해소하는 효능'),
('소화', '', '소화를 돕는 효능'),
('이수', '', '수분대사를 돕는 효능'),
('안신', '', '정신을 안정시키는 효능'),
('지혈', '', '출혈을 멈추는 효능'),
('조화제약', '조화', '여러 약재를 조화롭게 하는 효능'),
('대보원기', '대보', '원기를 크게 보하는 효능'),
('보기건비', '', '기를 보하고 비장을 건강하게 하는 효능'),
('보중익기', '', '중초를 보하고 기를 증진시키는 효능'),
]
for tag_name, category, description in basic_tags:
cursor.execute("""
INSERT OR IGNORE INTO herb_efficacy_tags (tag_name, tag_category, description)
VALUES (?, ?, ?)
""", (tag_name, category, description))
print(f"{len(basic_tags)}개 기본 효능 태그 추가")
# 5. 쌍화탕 약재들에 효능 태그 연결
ssanghwa_herbs = [
('숙지황', '보혈'),
('당귀', '보혈'),
('백작약', '보혈'),
('천궁', '활혈'),
('황기', '보기'),
('인삼', '대보원기'),
('백출', '보기건비'),
('감초', '조화제약'),
('생강', '온중'),
('대추', '보중익기'),
('육계', '온양'),
('건강', '온중'),
]
print("\n=== 약재별 효능 태그 연결 ===")
for herb_name, tag_name in ssanghwa_herbs:
# 약재 ID 찾기
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (herb_name,))
herb_result = cursor.fetchone()
# 태그 ID 찾기
cursor.execute("SELECT tag_id FROM herb_efficacy_tags WHERE tag_name = ?", (tag_name,))
tag_result = cursor.fetchone()
if herb_result and tag_result:
herb_id = herb_result[0]
tag_id = tag_result[0]
cursor.execute("""
INSERT OR IGNORE INTO herb_item_tags (herb_item_id, tag_id)
VALUES (?, ?)
""", (herb_id, tag_id))
print(f"{herb_name}{tag_name}")
else:
if not herb_result:
print(f" ⚠️ {herb_name} 약재 없음")
if not tag_result:
print(f" ⚠️ {tag_name} 태그 없음")
conn.commit()
# 6. 결과 확인 - 약재별 태그 조회
print("\n=== 약재별 효능 태그 확인 ===")
cursor.execute("""
SELECT
h.herb_name,
GROUP_CONCAT(t.tag_name, ', ') as tags
FROM herb_items h
LEFT JOIN herb_item_tags ht ON h.herb_item_id = ht.herb_item_id
LEFT JOIN herb_efficacy_tags t ON ht.tag_id = t.tag_id
WHERE ht.tag_id IS NOT NULL
GROUP BY h.herb_item_id
ORDER BY h.herb_name
""")
results = cursor.fetchall()
for herb_name, tags in results:
print(f" {herb_name}: {tags}")
print("\n✅ 효능 태그 시스템 구축 완료!")
except Exception as e:
print(f"❌ 오류 발생: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == "__main__":
check_and_create_efficacy_system()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sqlite3
# 데이터베이스 연결
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
print("=== 입고장별 총금액 확인 ===\n")
# 각 입고장의 라인별 총액 확인
cursor.execute("""
SELECT
pr.receipt_id,
pr.receipt_date,
s.name as supplier_name,
COUNT(prl.line_id) as line_count,
SUM(prl.quantity_g) as total_quantity,
SUM(prl.line_total) as calculated_total
FROM purchase_receipts pr
JOIN suppliers s ON pr.supplier_id = s.supplier_id
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
GROUP BY pr.receipt_id
ORDER BY pr.receipt_date DESC
""")
results = cursor.fetchall()
for row in results:
print(f"입고장 ID: {row['receipt_id']}")
print(f" 날짜: {row['receipt_date']}")
print(f" 도매상: {row['supplier_name']}")
print(f" 품목 수: {row['line_count']}")
print(f" 총 수량: {row['total_quantity']}g")
print(f" 총 금액: {row['calculated_total']:,.0f}" if row['calculated_total'] else " 총 금액: 0원")
print("-" * 40)
print("\n=== 입고장 라인 상세 (첫 번째 입고장) ===\n")
# 첫 번째 입고장의 라인 상세 확인
if results:
first_receipt_id = results[0]['receipt_id']
cursor.execute("""
SELECT
herb_item_id,
quantity_g,
unit_price_per_g,
line_total
FROM purchase_receipt_lines
WHERE receipt_id = ?
LIMIT 5
""", (first_receipt_id,))
lines = cursor.fetchall()
for line in lines:
print(f"약재 ID: {line['herb_item_id']}")
print(f" 수량: {line['quantity_g']}g")
print(f" 단가: {line['unit_price_per_g']}원/g")
print(f" 라인 총액: {line['line_total']}")
print(f" 계산 검증: {line['quantity_g']} × {line['unit_price_per_g']} = {line['quantity_g'] * line['unit_price_per_g']}")
print()
conn.close()

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
입고장 상세보기 오류 디버그
"""
import sqlite3
import traceback
def debug_receipt_detail():
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
receipt_id = 6
print("=== 1. 입고장 헤더 조회 ===")
try:
cursor.execute("""
SELECT
pr.*,
s.name as supplier_name,
s.business_no as supplier_business_no,
s.phone as supplier_phone
FROM purchase_receipts pr
JOIN suppliers s ON pr.supplier_id = s.supplier_id
WHERE pr.receipt_id = ?
""", (receipt_id,))
receipt = cursor.fetchone()
if receipt:
receipt_dict = dict(receipt)
print("헤더 조회 성공!")
for key, value in receipt_dict.items():
print(f" {key}: {value} (type: {type(value).__name__})")
else:
print("입고장을 찾을 수 없습니다.")
return
except Exception as e:
print(f"헤더 조회 오류: {e}")
traceback.print_exc()
return
print("\n=== 2. 입고장 상세 라인 조회 ===")
try:
cursor.execute("""
SELECT
prl.*,
h.herb_name,
h.insurance_code,
il.lot_id,
il.quantity_onhand as current_stock
FROM purchase_receipt_lines prl
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 prl.receipt_id = ?
ORDER BY prl.line_id
""", (receipt_id,))
lines = cursor.fetchall()
print(f"라인 수: {len(lines)}")
if lines:
first_line = dict(lines[0])
print("\n첫 번째 라인 데이터:")
for key, value in first_line.items():
print(f" {key}: {value} (type: {type(value).__name__})")
except Exception as e:
print(f"라인 조회 오류: {e}")
traceback.print_exc()
print("\n=== 3. JSON 변환 테스트 ===")
try:
import json
# receipt_data 구성
receipt_data = dict(receipt)
receipt_data['lines'] = [dict(row) for row in lines]
# JSON 변환 시도
json_str = json.dumps(receipt_data, ensure_ascii=False, default=str)
print("JSON 변환 성공!")
print(f"JSON 길이: {len(json_str)} 문자")
except Exception as e:
print(f"JSON 변환 오류: {e}")
traceback.print_exc()
# 문제가 되는 필드 찾기
print("\n각 필드별 JSON 변환 테스트:")
for key, value in receipt_data.items():
try:
json.dumps({key: value}, default=str)
print(f"{key}: OK")
except Exception as field_error:
print(f"{key}: {field_error}")
print(f" 값: {value}")
print(f" 타입: {type(value)}")
conn.close()
if __name__ == "__main__":
debug_receipt_detail()

View File

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

View File

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

View File

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

View File

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

View File

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

46
dev_scripts/test_db.py Normal file
View File

@@ -0,0 +1,46 @@
import sqlite3
import json
conn = sqlite3.connect('database/kdrug.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT
pr.receipt_id,
pr.receipt_date,
pr.receipt_no,
pr.total_amount,
pr.source_file,
pr.created_at,
s.name as supplier_name,
s.supplier_id,
COUNT(prl.line_id) as line_count,
SUM(prl.quantity_g) as total_quantity
FROM purchase_receipts pr
JOIN suppliers s ON pr.supplier_id = s.supplier_id
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
GROUP BY pr.receipt_id
LIMIT 1
""")
row = cursor.fetchone()
if row:
print("Row keys:", row.keys())
receipt = dict(row)
for key, value in receipt.items():
print(f"{key}: {value} (type: {type(value).__name__})")
# 타입 변환 시도
if isinstance(receipt.get('receipt_date'), bytes):
receipt['receipt_date'] = receipt['receipt_date'].decode('utf-8')
elif receipt.get('receipt_date'):
receipt['receipt_date'] = str(receipt['receipt_date'])
if isinstance(receipt.get('created_at'), bytes):
receipt['created_at'] = receipt['created_at'].decode('utf-8')
elif receipt.get('created_at'):
receipt['created_at'] = str(receipt['created_at'])
print("\nAfter conversion:")
print(json.dumps(receipt, indent=2))

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
직접 JavaScript 함수 호출 테스트
"""
from playwright.sync_api import sync_playwright
import time
def test_direct_load():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
print("페이지 로드 중...")
page.goto("http://localhost:5001")
page.wait_for_load_state("networkidle")
time.sleep(2)
# 직접 입고 관리 탭 활성화 및 함수 호출
print("\n입고 데이터 직접 로드...")
page.evaluate("""
// 입고 탭 표시
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('show', 'active'));
document.querySelector('#purchase').classList.add('show', 'active');
// 직접 함수 호출
if (typeof loadPurchaseReceipts === 'function') {
loadPurchaseReceipts();
} else {
console.error('loadPurchaseReceipts 함수를 찾을 수 없습니다');
}
""")
time.sleep(2)
# 테이블 내용 확인
table_html = page.inner_html('#purchaseReceiptsList')
print(f"\n테이블 내용 (처음 500자):\n{table_html[:500]}")
# 테이블 행 수 확인
row_count = page.evaluate("document.querySelectorAll('#purchaseReceiptsList tr').length")
print(f"\n테이블 행 수: {row_count}")
# 첫 번째 행 내용 확인
if row_count > 0:
first_row = page.evaluate("""
const row = document.querySelector('#purchaseReceiptsList tr');
if (row) {
const cells = row.querySelectorAll('td');
return Array.from(cells).map(cell => cell.textContent.trim());
}
return null;
""")
if first_row:
print(f"\n첫 번째 행 데이터:")
headers = ['입고일', '공급업체', '품목 수', '총 금액', '총 수량', '파일명', '작업']
for i, value in enumerate(first_row[:-1]): # 마지막 '작업' 열 제외
if i < len(headers):
print(f" {headers[i]}: {value}")
# 스크린샷
page.screenshot(path="/root/kdrug/direct_test.png")
print("\n스크린샷 저장: /root/kdrug/direct_test.png")
browser.close()
if __name__ == "__main__":
test_direct_load()

View File

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

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Playwright로 프론트엔드 확인
"""
from playwright.sync_api import sync_playwright
import time
import json
def test_purchase_receipts():
with sync_playwright() as p:
# 브라우저 시작
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 페이지 이동
print("1. 페이지 로드 중...")
page.goto("http://localhost:5001")
page.wait_for_load_state("networkidle")
# 입고 관리 탭 클릭
print("2. 입고 관리 탭으로 이동...")
try:
page.click('a[href="#purchase"]', timeout=5000)
except:
# 다른 방법으로 시도
page.evaluate("document.querySelector('a[href=\"#purchase\"]').click()")
time.sleep(1)
# API 호출 직접 확인
print("3. API 직접 호출 확인...")
api_response = page.evaluate("""
async () => {
const response = await fetch('/api/purchase-receipts');
const data = await response.json();
return data;
}
""")
print("\n=== API 응답 데이터 ===")
print(json.dumps(api_response, indent=2, ensure_ascii=False))
# 테이블 내용 확인
print("\n4. 테이블 렌더링 확인...")
table_rows = page.query_selector_all('#purchaseReceiptsList tr')
if len(table_rows) == 0:
print(" 테이블에 행이 없습니다.")
# "입고장이 없습니다." 메시지 확인
empty_message = page.query_selector('#purchaseReceiptsList td')
if empty_message:
print(f" 메시지: {empty_message.text_content()}")
else:
print(f" 테이블 행 수: {len(table_rows)}")
# 첫 번째 행 상세 확인
if len(table_rows) > 0:
first_row = table_rows[0]
cells = first_row.query_selector_all('td')
print("\n 첫 번째 행 내용:")
headers = ['입고일', '공급업체', '품목 수', '총 금액', '총 수량', '파일명', '작업']
for i, cell in enumerate(cells[:-1]): # 마지막 '작업' 열 제외
print(f" {headers[i]}: {cell.text_content()}")
# JavaScript 콘솔 에러 확인
page.on("console", lambda msg: print(f"콘솔: {msg.text}") if msg.type == "error" else None)
# 스크린샷 저장
print("\n5. 스크린샷 저장...")
page.screenshot(path="/root/kdrug/purchase_screenshot.png")
print(" /root/kdrug/purchase_screenshot.png 저장 완료")
browser.close()
if __name__ == "__main__":
test_purchase_receipts()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
간단한 프론트엔드 확인
"""
from playwright.sync_api import sync_playwright
import time
def test_purchase_display():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
print("페이지 로드 중...")
page.goto("http://localhost:5001", wait_until="networkidle")
# 입고 관리 화면으로 직접 이동
print("\n입고 관리 화면 확인...")
page.goto("http://localhost:5001/#purchase", wait_until="networkidle")
time.sleep(2) # JavaScript 렌더링 대기
# API 데이터와 실제 렌더링 비교
print("\n=== API 데이터 vs 화면 렌더링 확인 ===")
# API 응답 확인
api_data = page.evaluate("""
fetch('/api/purchase-receipts')
.then(response => response.json())
.then(data => data)
""")
time.sleep(1)
# 테이블 확인
table_html = page.evaluate("document.querySelector('#purchaseReceiptsList').innerHTML")
print(f"\nAPI 응답 총금액: {api_data.get('data', [{}])[0].get('total_amount', 0)}")
# 화면에 표시된 총금액 찾기
try:
total_amount_cell = page.query_selector('.fw-bold.text-primary')
if total_amount_cell:
print(f"화면 표시 총금액: {total_amount_cell.text_content()}")
else:
print("총금액 셀을 찾을 수 없습니다.")
except:
pass
# 테이블 전체 내용
print("\n테이블 HTML (처음 200자):")
print(table_html[:200] if table_html else "테이블이 비어있음")
# 스크린샷
page.screenshot(path="/root/kdrug/purchase_test.png")
print("\n스크린샷 저장: /root/kdrug/purchase_test.png")
browser.close()
if __name__ == "__main__":
test_purchase_display()

View File

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