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

168
add_prescription_data.py Normal file
View File

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

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
한약재 샘플 데이터 추가 스크립트 한약재 샘플 데이터 추가 - 십전대보탕 구성 약재
주요 약재들의 확장 정보와 효능 태그를 추가합니다.
""" """
import sqlite3 import sqlite3
@ -17,171 +16,173 @@ def add_herb_extended_data():
conn = get_connection() conn = get_connection()
cursor = conn.cursor() cursor = conn.cursor()
# 주요 약재들의 확장 정보 # 십전대보탕 구성 약재들의 실제 ingredient_code 사용
herbs_data = [ herb_data = [
{ {
'ingredient_code': '3400H1AHM', # 인삼 'ingredient_code': '3400H1AHM', # 인삼
'property': '', 'property': '(溫)',
'taste': ',미고', 'taste': '(甘), 미고(微苦)',
'meridian_tropism': '비,폐,심', 'meridian_tropism': '폐(肺), 비(脾), 심(心)',
'main_effects': '대보원기, 보비익폐, 생진지갈, 안신', 'main_effects': '대보원기, 보비익폐, 생진지갈, 안신',
'indications': '기허증, 비허증, 폐허증, 심기허증, 진액부족, 당뇨병', 'indications': '기허증, 피로, 식욕부진, 설사, 호흡곤란, 자한, 양위, 소갈, 건망, 불면',
'contraindications': '실증, 열증, 음허화왕', 'dosage_range': '1~3돈(3~9g)',
'precautions': '복용 중 무 섭취 금지, 고혈압 환자 주의', 'precautions': '실증, 열증자 신중 투여',
'dosage_range': '3-9g', 'preparation_method': '수치법: 홍삼, 백삼, 당삼 등으로 가공',
'dosage_max': '30g', 'tags': [
'active_compounds': '인삼사포닌(ginsenoside Rb1, Rg1, Rg3), 다당체, 아미노산', ('보기', 5),
'pharmacological_effects': '면역증강, 항피로, 항산화, 혈당조절, 인지능력개선', ('보혈', 3),
'clinical_applications': '만성피로, 면역력저하, 당뇨병 보조치료, 노인성 인지저하', ('안신', 4),
'tags': [('보기', 5), ('보양', 4), ('안신', 3), ('진통', 2)] ]
}, },
{ {
'ingredient_code': '3400H1ADL', # 감초 'ingredient_code': '3007H1AHM', # 감초
'property': '', 'property': '평(平)',
'taste': '', 'taste': '감(甘)',
'meridian_tropism': '비,위,폐,심', 'meridian_tropism': '심(心), 폐(肺), 비(脾), 위(胃)',
'main_effects': '보비익기, 청열해독, 거담지해, 완급지통, 조화제약', 'main_effects': '화중완급, 윤폐지해, 해독',
'indications': '비허증, 해수, 인후통, 소화성궤양, 경련성 통증', 'indications': '복통, 기침, 인후통, 소화불량, 약물중독',
'contraindications': '습증, 수종, 고혈압', 'dosage_range': '1~3돈(3~9g)',
'precautions': '장기복용 시 부종 주의, 칼륨 감소 주의', 'precautions': '장기복용시 부종 주의',
'dosage_range': '2-10g', 'preparation_method': '자감초(炙甘草) 등',
'dosage_max': '30g', 'tags': [
'active_compounds': 'glycyrrhizin, liquiritin, flavonoid, triterpenoid', ('보기', 3),
'pharmacological_effects': '항염증, 항궤양, 간보호, 진해거담, 항알레르기', ('해독', 4),
'clinical_applications': '위염, 위궤양, 기관지염, 약물조화, 간염', ('윤조', 3),
'tags': [('보기', 3), ('청열', 3), ('해독', 4), ('거담', 3), ('항염', 4)] ('청열', 2),
('항염', 3),
]
}, },
{ {
'ingredient_code': '3400H1ACD', # 당귀 'ingredient_code': '3204H1AHM', # 백출
'property': '', 'property': '(溫)',
'taste': ',신', 'taste': '(甘), 고(苦)',
'meridian_tropism': '간,심,비', 'meridian_tropism': '비(脾), 위(胃)',
'main_effects': '보혈활혈, 조경지통, 윤장통변', 'main_effects': '건비익기, 조습이수, 지한, 안태',
'indications': '혈허증, 월경부조, 무월경, 변비, 타박상', 'indications': '비허설사, 수종, 담음, 자한, 태동불안',
'contraindications': '설사, 습성체질', 'dosage_range': '2~4돈(6~12g)',
'precautions': '과량 복용 시 설사 주의', 'precautions': '음허내열자 신중',
'dosage_range': '5-15g', 'preparation_method': '토백출, 생백출',
'dosage_max': '30g', 'tags': [
'active_compounds': 'ligustilide, n-butylidene phthalide, ferulic acid', ('보기', 4),
'pharmacological_effects': '혈액순환개선, 항혈전, 자궁수축조절, 진정진통', ('이수', 4),
'clinical_applications': '빈혈, 월경불순, 산후조리, 혈액순환장애', ('건비', 5),
'tags': [('보혈', 5), ('활혈', 5), ('진통', 3)] ]
}, },
{ {
'ingredient_code': '3400H1AGN', # 황기 'ingredient_code': '3215H1AHM', # 복령
'property': '', 'property': '평(平)',
'taste': '', 'taste': '(甘), 담(淡)',
'meridian_tropism': '비,폐', 'meridian_tropism': '심(心), 폐(肺), 비(脾), 신(腎)',
'main_effects': '보기승양, 고표지한, 이수소종, 탁독배농', 'main_effects': '이수삼습, 건비영심, 안신',
'indications': '기허증, 자한, 부종, 탈항, 자궁탈수', 'indications': '소변불리, 수종, 설사, 불면, 심계',
'contraindications': '표실증, 음허화왕', 'dosage_range': '3~5돈(9~15g)',
'precautions': '감기 초기 금지', 'precautions': '음허자 신중',
'dosage_range': '10-30g', 'preparation_method': '백복령, 적복령',
'dosage_max': '60g', 'tags': [
'active_compounds': 'astragaloside, polysaccharide, flavonoid', ('이수', 5),
'pharmacological_effects': '면역조절, 항바이러스, 항산화, 신기능보호', ('안신', 3),
'clinical_applications': '면역력저하, 만성신장염, 당뇨병, 심부전', ('건비', 3),
'tags': [('보기', 5), ('이수', 3), ('해표', 2)] ]
}, },
{ {
'ingredient_code': '3400H1AEW', # 작약 'ingredient_code': '3419H1AHM', # 작약
'property': '', 'property': '미한(微寒)',
'taste': ',산', 'taste': '(苦), (酸)',
'meridian_tropism': ',비', 'meridian_tropism': '(肝), (脾)',
'main_effects': '양혈조경, 유간지통, 렴음지한', 'main_effects': '양혈렴음, 유간지통, 평간양',
'indications': '혈허증, 월경부조, 간혈부족, 자한도한', 'indications': '혈허, 복통, 사지경련, 두훈, 월경불순',
'contraindications': '양허설사', 'dosage_range': '2~4돈(6~12g)',
'precautions': '한성약물과 병용 주의', 'precautions': '비허설사자 신중',
'dosage_range': '6-15g', 'preparation_method': '백작약, 적작약',
'dosage_max': '30g', 'tags': [
'active_compounds': 'paeoniflorin, albiflorin, benzoic acid', ('보혈', 4),
'pharmacological_effects': '진정진통, 항경련, 항염증, 면역조절', ('진경', 4),
'clinical_applications': '월경통, 근육경련, 두통, 자가면역질환', ('평간', 3),
'tags': [('보혈', 4), ('평간', 4), ('진통', 4)] ]
}, },
{ {
'ingredient_code': '3400H1ACF', # 천궁 'ingredient_code': '3475H1AHM', # 천궁
'property': '', 'property': '(溫)',
'taste': '', 'taste': '(辛)',
'meridian_tropism': ',담,심포', 'meridian_tropism': '(肝), (膽), 심포(心包)',
'main_effects': '활혈행기, 거풍지통', 'main_effects': '활혈행기, 거풍지통',
'indications': '어증, 두통, 월경불순, 풍습비', 'indications': '체, 두통, 현훈, 월경불순, 복',
'contraindications': '음허화왕, 월경과다', 'dosage_range': '1~2돈(3~6g)',
'precautions': '출혈 경향 환자 주의', 'precautions': '음허화왕자 신중',
'dosage_range': '3-10g', 'preparation_method': '주천궁',
'dosage_max': '15g', 'tags': [
'active_compounds': 'ligustilide, senkyunolide, ferulic acid', ('활혈', 5),
'pharmacological_effects': '혈관확장, 항혈전, 진정진통, 항염증', ('거풍', 3),
'clinical_applications': '편두통, 혈관성 두통, 어혈증, 월경통', ('지통', 4),
'tags': [('활혈', 5), ('이기', 4), ('진통', 5)] ]
}, },
{ {
'ingredient_code': '3400H1ACG', # 지황(숙지황) 'ingredient_code': '3105H1AHM', # 당귀
'property': '', 'property': '(溫)',
'taste': '', 'taste': '(甘), 신(辛)',
'meridian_tropism': ',신', 'meridian_tropism': '(肝), 심(心), 비(脾)',
'main_effects': '보혈자음, 익정전수', 'main_effects': '보혈활혈, 조경지통, 윤장통변',
'indications': '혈허증, 간신음허, 수발조백, 유정도한', 'indications': '혈허, 월경불순, 복통, 변비, 타박상',
'contraindications': '비허설사, 담습', 'dosage_range': '2~4돈(6~12g)',
'precautions': '소화불량 주의', 'precautions': '습성설사자 신중',
'dosage_range': '10-30g', 'preparation_method': '주당귀, 당귀신, 당귀미',
'dosage_max': '60g', 'tags': [
'active_compounds': 'catalpol, rehmannioside, aucubin', ('보혈', 5),
'pharmacological_effects': '조혈촉진, 면역조절, 혈당강하, 신경보호', ('활혈', 4),
'clinical_applications': '빈혈, 당뇨병, 치매예방, 불임증', ('윤조', 3),
'tags': [('보혈', 5), ('보음', 5)] ]
}, },
{ {
'ingredient_code': '3400H1AFJ', # 백출 'ingredient_code': '3583H1AHM', # 황기
'property': '', 'property': '(溫)',
'taste': '고,', 'taste': '(甘)',
'meridian_tropism': '비,위', 'meridian_tropism': '폐(肺), 비(脾)',
'main_effects': '건비익기, 조습이수, 지한안태', 'main_effects': '보기승양, 고표지한, 이수소종, 탈독생기',
'indications': '비허증, 식욕부진, 설사, 수종, 자한', 'indications': '기허, 자한, 설사, 탈항, 수종, 창양',
'contraindications': '음허조갈', 'dosage_range': '3~6돈(9~18g)',
'precautions': '진액부족 시 주의', 'precautions': '표실사 및 음허자 신중',
'dosage_range': '6-15g', 'preparation_method': '밀자황기',
'dosage_max': '30g', 'tags': [
'active_compounds': 'atractylenolide, atractylon', ('보기', 5),
'pharmacological_effects': '위장운동촉진, 이뇨, 항염증, 항종양', ('승양', 4),
'clinical_applications': '만성설사, 부종, 임신오조', ('고표', 4),
'tags': [('보기', 4), ('이수', 4), ('소화', 3)] ]
}, },
{ {
'ingredient_code': '3400H1AGM', # 복령 'ingredient_code': '3384H1AHM', # 육계
'property': '', 'property': '대열(大熱)',
'taste': ',담', 'taste': '(甘), 신(辛)',
'meridian_tropism': '심,비,폐,신', 'meridian_tropism': '신(腎), 비(脾), 심(心), 간(肝)',
'main_effects': '이수삼습, 건비안신', 'main_effects': '보화조양, 산한지통, 온경통맥',
'indications': '수종, 소변불리, 비허설사, 불면, 심계', 'indications': '양허, 냉증, 요통, 복통, 설사',
'contraindications': '음허진액부족', 'dosage_range': '0.5~1돈(1.5~3g)',
'precautions': '이뇨제와 병용 주의', 'precautions': '음허화왕자, 임신부 금기',
'dosage_range': '10-15g', 'preparation_method': '육계심, 계피',
'dosage_max': '30g', 'tags': [
'active_compounds': 'pachymic acid, polysaccharide', ('보양', 5),
'pharmacological_effects': '이뇨, 진정, 항염증, 면역조절', ('온리', 5),
'clinical_applications': '부종, 불면증, 만성설사', ('산한', 4),
'tags': [('이수', 5), ('안신', 3), ('보기', 2)] ]
}, },
{ {
'ingredient_code': '3400H1AGI', # 반하 'ingredient_code': '3299H1AHM', # 숙지황
'property': '', 'property': '(溫)',
'taste': '', 'taste': '감(甘)',
'meridian_tropism': '비,위,폐', 'meridian_tropism': '간(肝), 신(腎)',
'main_effects': '조습화담, 강역지구, 소비산결', 'main_effects': '자음보혈, 익정전수',
'indications': '습담, 구토, 해수담다, 현훈', 'indications': '혈허, 음허, 요슬산연, 유정, 붕루',
'contraindications': '음허조해, 임신', 'dosage_range': '3~6돈(9~18g)',
'precautions': '임산부 금기, 생품 독성 주의', 'precautions': '비허설사, 담다자 신중',
'dosage_range': '5-10g', 'preparation_method': '숙지황 제법',
'dosage_max': '15g', 'tags': [
'active_compounds': 'ephedrine, β-sitosterol', ('보혈', 5),
'pharmacological_effects': '진토, 진해거담, 항종양', ('자음', 5),
'clinical_applications': '임신오조, 기관지염, 현훈증', ('보신', 4),
'tags': [('거담', 5), ('소화', 3)] ]
} },
] ]
for herb in herbs_data: for herb in herb_data:
# herb_master_extended 업데이트 # herb_master_extended 업데이트
cursor.execute(""" cursor.execute("""
UPDATE herb_master_extended UPDATE herb_master_extended
@ -190,53 +191,49 @@ def add_herb_extended_data():
meridian_tropism = ?, meridian_tropism = ?,
main_effects = ?, main_effects = ?,
indications = ?, indications = ?,
contraindications = ?,
precautions = ?,
dosage_range = ?, dosage_range = ?,
dosage_max = ?, precautions = ?,
active_compounds = ?, preparation_method = ?,
pharmacological_effects = ?,
clinical_applications = ?,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE ingredient_code = ? WHERE ingredient_code = ?
""", ( """, (
herb['property'], herb['taste'], herb['meridian_tropism'], herb['property'],
herb['main_effects'], herb['indications'], herb['contraindications'], herb['taste'],
herb['precautions'], herb['dosage_range'], herb['dosage_max'], herb['meridian_tropism'],
herb['active_compounds'], herb['pharmacological_effects'], herb['main_effects'],
herb['clinical_applications'], herb['ingredient_code'] herb['indications'],
herb['dosage_range'],
herb['precautions'],
herb['preparation_method'],
herb['ingredient_code']
)) ))
# herb_id 조회 # 효능 태그 매핑
cursor.execute(""" for tag_name, strength in herb.get('tags', []):
SELECT herb_id FROM herb_master_extended # 태그 ID 조회
WHERE ingredient_code = ? cursor.execute("""
""", (herb['ingredient_code'],)) SELECT tag_id FROM herb_efficacy_tags
WHERE tag_name = ?
""", (tag_name,))
result = cursor.fetchone() tag_result = cursor.fetchone()
if result: if tag_result:
herb_id = result[0] tag_id = tag_result[0]
# 효능 태그 매핑 # 기존 태그 삭제
for tag_name, strength in herb.get('tags', []):
# 태그 ID 조회
cursor.execute(""" cursor.execute("""
SELECT tag_id FROM herb_efficacy_tags DELETE FROM herb_item_tags
WHERE tag_name = ? WHERE ingredient_code = ? AND tag_id = ?
""", (tag_name,)) """, (herb['ingredient_code'], tag_id))
tag_result = cursor.fetchone() # 태그 매핑 추가
if tag_result: cursor.execute("""
tag_id = tag_result[0] INSERT INTO herb_item_tags
(ingredient_code, tag_id, strength)
VALUES (?, ?, ?)
""", (herb['ingredient_code'], tag_id, strength))
# 태그 매핑 추가 print(f"{herb['ingredient_code']} 데이터 추가 완료")
cursor.execute("""
INSERT OR REPLACE INTO herb_item_tags
(herb_id, tag_id, strength)
VALUES (?, ?, ?)
""", (herb_id, tag_id, strength))
print(f"{herb['ingredient_code']} 데이터 추가 완료")
conn.commit() conn.commit()
conn.close() conn.close()
@ -248,85 +245,64 @@ def add_prescription_rules():
# 몇 가지 대표적인 배합 규칙 추가 # 몇 가지 대표적인 배합 규칙 추가
rules = [ rules = [
# 상수(相須) - 서로 도와서 효과를 증강
{ {
'herb1': '인삼', 'herb2': '황기', 'herb1': '인삼',
'relationship': '상수', 'herb2': '황기',
'description': '두 약재가 함께 사용되면 보기 효과가 증강됨', 'rule_type': '상수',
'severity': 0 'description': '보기작용 상승효과',
'clinical_note': '기허증에 병용시 효과 증대'
}, },
{ {
'herb1': '당귀', 'herb2': '천궁', 'herb1': '당귀',
'relationship': '상수', 'herb2': '천궁',
'description': '혈액순환 개선 효과가 증강됨', 'rule_type': '상수',
'severity': 0 'description': '활혈작용 상승효과',
}, 'clinical_note': '혈허, 혈체에 병용'
# 상사(相使) - 한 약이 다른 약의 효능을 도움
{
'herb1': '반하', 'herb2': '생강',
'relationship': '상사',
'description': '생강이 반하의 독성을 감소시킴',
'severity': 0
},
# 상반(相反) - 함께 사용하면 독성이나 부작용 발생
{
'herb1': '감초', 'herb2': '감수',
'relationship': '상반',
'description': '십팔반(十八反) - 함께 사용 금기',
'severity': 5,
'is_absolute': True
}, },
{ {
'herb1': '인삼', 'herb2': '오령지', 'herb1': '반하',
'relationship': '상반', 'herb2': '생강',
'description': '십구외(十九畏) - 함께 사용 주의', 'rule_type': '상수',
'severity': 4, 'description': '반하의 독성 감소, 진토작용 증강',
'is_absolute': False 'clinical_note': '구토, 오심에 병용'
},
{
'herb1': '감초',
'herb2': '감수',
'rule_type': '상반',
'description': '효능 상반',
'clinical_note': '병용 금지'
},
{
'herb1': '인삼',
'herb2': '오령지',
'rule_type': '상외',
'description': '효능 감소',
'clinical_note': '병용시 주의'
} }
] ]
for rule in rules: for rule in rules:
# herb_id 조회
cursor.execute(""" cursor.execute("""
SELECT herb_id FROM herb_master_extended INSERT OR IGNORE INTO prescription_rules
WHERE name_korean = ? (herb1_name, herb2_name, rule_type, description, clinical_notes)
""", (rule['herb1'],)) VALUES (?, ?, ?, ?, ?)
herb1_result = cursor.fetchone() """, (rule['herb1'], rule['herb2'], rule['rule_type'],
rule['description'], rule['clinical_note']))
cursor.execute(""" print(f"{rule['herb1']} - {rule['herb2']} 규칙 추가")
SELECT herb_id FROM herb_master_extended
WHERE name_korean = ?
""", (rule['herb2'],))
herb2_result = cursor.fetchone()
if herb1_result and herb2_result:
cursor.execute("""
INSERT OR REPLACE INTO prescription_rules
(herb1_id, herb2_id, relationship_type, description,
severity_level, is_absolute)
VALUES (?, ?, ?, ?, ?, ?)
""", (
herb1_result[0], herb2_result[0],
rule['relationship'], rule['description'],
rule['severity'], rule.get('is_absolute', False)
))
print(f"{rule['herb1']} - {rule['herb2']} 규칙 추가")
conn.commit() conn.commit()
conn.close() conn.close()
def main(): def main():
"""메인 실행 함수""" print("=" * 80)
print("\n" + "="*80) print("한약재 샘플 데이터 추가 - 십전대보탕 구성 약재")
print("한약재 샘플 데이터 추가") print("=" * 80)
print("="*80 + "\n")
try: try:
# 1. 약재 확장 정보 및 태그 추가 print("\n1. 약재 확장 정보 추가 중...")
print("1. 약재 확장 정보 추가 중...")
add_herb_extended_data() add_herb_extended_data()
# 2. 처방 규칙 추가
print("\n2. 처방 배합 규칙 추가 중...") print("\n2. 처방 배합 규칙 추가 중...")
add_prescription_rules() add_prescription_rules()

View File

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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,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,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())

192
find_duplicate_issue.py Normal file
View File

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

43
get_ingredient_codes.py Normal file
View File

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

View File

@ -245,7 +245,7 @@ def create_disease_herb_mapping():
recommendation_grade VARCHAR(10), -- 권고등급 recommendation_grade VARCHAR(10), -- 권고등급
clinical_notes TEXT, clinical_notes TEXT,
references TEXT, reference_sources TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )

55
sample/자산관련.md Normal file
View File

@ -0,0 +1,55 @@
Traceback (most recent call last):
File "/root/kdrug/analyze_inventory_full.py", line 315, in <module>
analyze_inventory_discrepancy()
File "/root/kdrug/analyze_inventory_full.py", line 261, in analyze_inventory_discrepancy
cursor.execute("""
sqlite3.OperationalError: no such column: quantity
================================================================================
재고 자산 금액 불일치 상세 분석
분석 시간: 2026-02-18 01:23:14
================================================================================
1. 현재 시스템 재고 자산 (inventory_lots 테이블)
------------------------------------------------------------
💰 총 재고 자산: ₩1,529,434
📦 활성 LOT 수: 30개
⚖️ 총 재고량: 86,420.0g
🌿 약재 종류: 28종
2. 입고장 데이터 분석 (purchase_receipts + purchase_receipt_lines)
------------------------------------------------------------
📋 총 입고 금액: ₩1,551,900
📑 입고장 수: 1건
📝 입고 라인 수: 29개
⚖️ 총 입고량: 88,000.0g
최근 입고장 5건:
- PR-20260211-0001 (20260211): ₩1,551,900
3. LOT-입고장 매칭 분석
------------------------------------------------------------
✅ 입고장과 연결된 LOT: 30개 (₩1,529,434)
❌ 입고장 없는 LOT: 0개 (₩0)
4. 입고장 라인별 LOT 생성 확인
------------------------------------------------------------
📝 전체 입고 라인: 30개
✅ LOT 생성된 라인: 30개
❌ LOT 없는 라인: 0개
5. 재고 자산 차이 분석
------------------------------------------------------------
💰 현재 LOT 재고 가치: ₩1,529,434
📋 원본 입고 금액: ₩1,616,400
📊 차이: ₩-86,966
6. 출고 및 소비 내역
------------------------------------------------------------
처방전 테이블이 없습니다.
🏭 복합제 소비 금액: ₩77,966
⚖️ 복합제 소비량: 4,580.0g
📦 복합제 수: 8개
7. 재고 보정 내역
------------------------------------------------------------

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

3172
static/app.js.backup Normal file

File diff suppressed because it is too large Load Diff

1578
templates/index.html.backup Normal file

File diff suppressed because it is too large Load Diff