From ad9ac396e202eaafac4ffc3f0cdededcd91f994f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Wed, 18 Feb 2026 04:44:48 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개발/테스트 스크립트를 dev_scripts/ 폴더로 이동 - 스크린샷을 screenshots/ 폴더로 이동 - 백업 파일 보존 (.backup) - 처방 관련 추가 스크립트 포함 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- add_prescription_data.py | 168 + add_sample_herb_data.py | 456 ++- add_wolbitang_prescriptions.py | 180 + .../analyze_db_structure.py | 0 .../analyze_excel_formats.py | 0 dev_scripts/analyze_inventory_discrepancy.py | 244 ++ dev_scripts/analyze_inventory_full.py | 315 ++ dev_scripts/analyze_price_difference.py | 186 + .../analyze_product_code.py | 0 .../analyze_product_deep.py | 0 .../check_and_create_efficacy_tags.py | 0 .../check_custom_prescription.py | 0 .../check_formula_columns.py | 0 dev_scripts/check_herb_data.py | 45 + dev_scripts/check_lot_creation_methods.py | 143 + dev_scripts/check_missing_lots.py | 179 + dev_scripts/check_purchase_structure.py | 51 + dev_scripts/check_purchase_tables.py | 35 + .../check_samsoeun_ingredients.py | 0 .../check_sipjeondaebotang.py | 0 .../check_ssanghwatang.py | 0 dev_scripts/check_tables.py | 19 + .../check_totals.py | 0 .../check_wolbitang_ingredients.py | 0 dev_scripts/debug_api_calculation.py | 115 + .../debug_receipt_detail.py | 0 dev_scripts/final_inventory_analysis.py | 193 + dev_scripts/final_price_analysis.py | 183 + dev_scripts/final_verification.py | 145 + .../test_compound_e2e.py | 0 .../test_compound_page.py | 0 test_db.py => dev_scripts/test_db.py | 0 test_direct.py => dev_scripts/test_direct.py | 0 dev_scripts/test_direct_api.py | 84 + .../test_frontend.py | 0 .../test_herb_dropdown_bug.py | 0 dev_scripts/test_herb_info_page.py | 192 + dev_scripts/test_herb_info_ui.py | 162 + .../test_improved_import.py | 0 dev_scripts/test_js_debug.py | 134 + .../test_lot_validation.py | 0 .../test_multi_lot_compound.py | 0 test_simple.py => dev_scripts/test_simple.py | 0 .../test_upload_api.py | 0 find_duplicate_issue.py | 192 + get_ingredient_codes.py | 43 + migrations/add_herb_extended_info_tables.py | 2 +- sample/자산관련.md | 55 + .../direct_test.png | Bin screenshots/herb_info_error.png | Bin 0 -> 92606 bytes screenshots/herb_info_page.png | Bin 0 -> 64760 bytes .../purchase_test.png | Bin static/app.js.backup | 3172 +++++++++++++++++ templates/index.html.backup | 1578 ++++++++ 54 files changed, 8030 insertions(+), 241 deletions(-) create mode 100644 add_prescription_data.py create mode 100644 add_wolbitang_prescriptions.py rename analyze_db_structure.py => dev_scripts/analyze_db_structure.py (100%) rename analyze_excel_formats.py => dev_scripts/analyze_excel_formats.py (100%) create mode 100644 dev_scripts/analyze_inventory_discrepancy.py create mode 100644 dev_scripts/analyze_inventory_full.py create mode 100644 dev_scripts/analyze_price_difference.py rename analyze_product_code.py => dev_scripts/analyze_product_code.py (100%) rename analyze_product_deep.py => dev_scripts/analyze_product_deep.py (100%) rename check_and_create_efficacy_tags.py => dev_scripts/check_and_create_efficacy_tags.py (100%) rename check_custom_prescription.py => dev_scripts/check_custom_prescription.py (100%) rename check_formula_columns.py => dev_scripts/check_formula_columns.py (100%) create mode 100644 dev_scripts/check_herb_data.py create mode 100644 dev_scripts/check_lot_creation_methods.py create mode 100644 dev_scripts/check_missing_lots.py create mode 100644 dev_scripts/check_purchase_structure.py create mode 100644 dev_scripts/check_purchase_tables.py rename check_samsoeun_ingredients.py => dev_scripts/check_samsoeun_ingredients.py (100%) rename check_sipjeondaebotang.py => dev_scripts/check_sipjeondaebotang.py (100%) rename check_ssanghwatang.py => dev_scripts/check_ssanghwatang.py (100%) create mode 100644 dev_scripts/check_tables.py rename check_totals.py => dev_scripts/check_totals.py (100%) rename check_wolbitang_ingredients.py => dev_scripts/check_wolbitang_ingredients.py (100%) create mode 100644 dev_scripts/debug_api_calculation.py rename debug_receipt_detail.py => dev_scripts/debug_receipt_detail.py (100%) create mode 100644 dev_scripts/final_inventory_analysis.py create mode 100644 dev_scripts/final_price_analysis.py create mode 100644 dev_scripts/final_verification.py rename test_compound_e2e.py => dev_scripts/test_compound_e2e.py (100%) rename test_compound_page.py => dev_scripts/test_compound_page.py (100%) rename test_db.py => dev_scripts/test_db.py (100%) rename test_direct.py => dev_scripts/test_direct.py (100%) create mode 100644 dev_scripts/test_direct_api.py rename test_frontend.py => dev_scripts/test_frontend.py (100%) rename test_herb_dropdown_bug.py => dev_scripts/test_herb_dropdown_bug.py (100%) create mode 100644 dev_scripts/test_herb_info_page.py create mode 100644 dev_scripts/test_herb_info_ui.py rename test_improved_import.py => dev_scripts/test_improved_import.py (100%) create mode 100644 dev_scripts/test_js_debug.py rename test_lot_validation.py => dev_scripts/test_lot_validation.py (100%) rename test_multi_lot_compound.py => dev_scripts/test_multi_lot_compound.py (100%) rename test_simple.py => dev_scripts/test_simple.py (100%) rename test_upload_api.py => dev_scripts/test_upload_api.py (100%) create mode 100644 find_duplicate_issue.py create mode 100644 get_ingredient_codes.py create mode 100644 sample/자산관련.md rename direct_test.png => screenshots/direct_test.png (100%) create mode 100644 screenshots/herb_info_error.png create mode 100644 screenshots/herb_info_page.png rename purchase_test.png => screenshots/purchase_test.png (100%) create mode 100644 static/app.js.backup create mode 100644 templates/index.html.backup diff --git a/add_prescription_data.py b/add_prescription_data.py new file mode 100644 index 0000000..6062dd3 --- /dev/null +++ b/add_prescription_data.py @@ -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() \ No newline at end of file diff --git a/add_sample_herb_data.py b/add_sample_herb_data.py index cec7e2c..019443d 100644 --- a/add_sample_herb_data.py +++ b/add_sample_herb_data.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -한약재 샘플 데이터 추가 스크립트 -주요 약재들의 확장 정보와 효능 태그를 추가합니다. +한약재 샘플 데이터 추가 - 십전대보탕 구성 약재 """ import sqlite3 @@ -17,171 +16,173 @@ def add_herb_extended_data(): conn = get_connection() cursor = conn.cursor() - # 주요 약재들의 확장 정보 - herbs_data = [ + # 십전대보탕 구성 약재들의 실제 ingredient_code 사용 + herb_data = [ { 'ingredient_code': '3400H1AHM', # 인삼 - 'property': '온', - 'taste': '감,미고', - 'meridian_tropism': '비,폐,심', - 'main_effects': '대보원기, 보비익폐, 생진지갈, 안신증지', - 'indications': '기허증, 비허증, 폐허증, 심기허증, 진액부족, 당뇨병', - 'contraindications': '실증, 열증, 음허화왕', - 'precautions': '복용 중 무 섭취 금지, 고혈압 환자 주의', - 'dosage_range': '3-9g', - 'dosage_max': '30g', - 'active_compounds': '인삼사포닌(ginsenoside Rb1, Rg1, Rg3), 다당체, 아미노산', - 'pharmacological_effects': '면역증강, 항피로, 항산화, 혈당조절, 인지능력개선', - 'clinical_applications': '만성피로, 면역력저하, 당뇨병 보조치료, 노인성 인지저하', - 'tags': [('보기', 5), ('보양', 4), ('안신', 3), ('진통', 2)] + 'property': '온(溫)', + 'taste': '감(甘), 미고(微苦)', + 'meridian_tropism': '폐(肺), 비(脾), 심(心)', + 'main_effects': '대보원기, 보비익폐, 생진지갈, 안신익지', + 'indications': '기허증, 피로, 식욕부진, 설사, 호흡곤란, 자한, 양위, 소갈, 건망, 불면', + 'dosage_range': '1~3돈(3~9g)', + 'precautions': '실증, 열증자 신중 투여', + 'preparation_method': '수치법: 홍삼, 백삼, 당삼 등으로 가공', + 'tags': [ + ('보기', 5), + ('보혈', 3), + ('안신', 4), + ] }, { - 'ingredient_code': '3400H1ADL', # 감초 - 'property': '평', - 'taste': '감', - 'meridian_tropism': '비,위,폐,심', - 'main_effects': '보비익기, 청열해독, 거담지해, 완급지통, 조화제약', - 'indications': '비허증, 해수, 인후통, 소화성궤양, 경련성 통증', - 'contraindications': '습증, 수종, 고혈압', - 'precautions': '장기복용 시 부종 주의, 칼륨 감소 주의', - 'dosage_range': '2-10g', - 'dosage_max': '30g', - 'active_compounds': 'glycyrrhizin, liquiritin, flavonoid, triterpenoid', - 'pharmacological_effects': '항염증, 항궤양, 간보호, 진해거담, 항알레르기', - 'clinical_applications': '위염, 위궤양, 기관지염, 약물조화, 간염', - 'tags': [('보기', 3), ('청열', 3), ('해독', 4), ('거담', 3), ('항염', 4)] + 'ingredient_code': '3007H1AHM', # 감초 + 'property': '평(平)', + 'taste': '감(甘)', + 'meridian_tropism': '심(心), 폐(肺), 비(脾), 위(胃)', + 'main_effects': '화중완급, 윤폐지해, 해독', + 'indications': '복통, 기침, 인후통, 소화불량, 약물중독', + 'dosage_range': '1~3돈(3~9g)', + 'precautions': '장기복용시 부종 주의', + 'preparation_method': '자감초(炙甘草) 등', + 'tags': [ + ('보기', 3), + ('해독', 4), + ('윤조', 3), + ('청열', 2), + ('항염', 3), + ] }, { - 'ingredient_code': '3400H1ACD', # 당귀 - 'property': '온', - 'taste': '감,신', - 'meridian_tropism': '간,심,비', - 'main_effects': '보혈활혈, 조경지통, 윤장통변', - 'indications': '혈허증, 월경부조, 무월경, 변비, 타박상', - 'contraindications': '설사, 습성체질', - 'precautions': '과량 복용 시 설사 주의', - 'dosage_range': '5-15g', - 'dosage_max': '30g', - 'active_compounds': 'ligustilide, n-butylidene phthalide, ferulic acid', - 'pharmacological_effects': '혈액순환개선, 항혈전, 자궁수축조절, 진정진통', - 'clinical_applications': '빈혈, 월경불순, 산후조리, 혈액순환장애', - 'tags': [('보혈', 5), ('활혈', 5), ('진통', 3)] + 'ingredient_code': '3204H1AHM', # 백출 + 'property': '온(溫)', + 'taste': '감(甘), 고(苦)', + 'meridian_tropism': '비(脾), 위(胃)', + 'main_effects': '건비익기, 조습이수, 지한, 안태', + 'indications': '비허설사, 수종, 담음, 자한, 태동불안', + 'dosage_range': '2~4돈(6~12g)', + 'precautions': '음허내열자 신중', + 'preparation_method': '토백출, 생백출', + 'tags': [ + ('보기', 4), + ('이수', 4), + ('건비', 5), + ] }, { - 'ingredient_code': '3400H1AGN', # 황기 - 'property': '온', - 'taste': '감', - 'meridian_tropism': '비,폐', - 'main_effects': '보기승양, 고표지한, 이수소종, 탁독배농', - 'indications': '기허증, 자한, 부종, 탈항, 자궁탈수', - 'contraindications': '표실증, 음허화왕', - 'precautions': '감기 초기 금지', - 'dosage_range': '10-30g', - 'dosage_max': '60g', - 'active_compounds': 'astragaloside, polysaccharide, flavonoid', - 'pharmacological_effects': '면역조절, 항바이러스, 항산화, 신기능보호', - 'clinical_applications': '면역력저하, 만성신장염, 당뇨병, 심부전', - 'tags': [('보기', 5), ('이수', 3), ('해표', 2)] + 'ingredient_code': '3215H1AHM', # 복령 + 'property': '평(平)', + 'taste': '감(甘), 담(淡)', + 'meridian_tropism': '심(心), 폐(肺), 비(脾), 신(腎)', + 'main_effects': '이수삼습, 건비영심, 안신', + 'indications': '소변불리, 수종, 설사, 불면, 심계', + 'dosage_range': '3~5돈(9~15g)', + 'precautions': '음허자 신중', + 'preparation_method': '백복령, 적복령', + 'tags': [ + ('이수', 5), + ('안신', 3), + ('건비', 3), + ] }, { - 'ingredient_code': '3400H1AEW', # 작약 - 'property': '량', - 'taste': '고,산', - 'meridian_tropism': '간,비', - 'main_effects': '양혈조경, 유간지통, 렴음지한', - 'indications': '혈허증, 월경부조, 간혈부족, 자한도한', - 'contraindications': '양허설사', - 'precautions': '한성약물과 병용 주의', - 'dosage_range': '6-15g', - 'dosage_max': '30g', - 'active_compounds': 'paeoniflorin, albiflorin, benzoic acid', - 'pharmacological_effects': '진정진통, 항경련, 항염증, 면역조절', - 'clinical_applications': '월경통, 근육경련, 두통, 자가면역질환', - 'tags': [('보혈', 4), ('평간', 4), ('진통', 4)] + 'ingredient_code': '3419H1AHM', # 작약 + 'property': '미한(微寒)', + 'taste': '고(苦), 산(酸)', + 'meridian_tropism': '간(肝), 비(脾)', + 'main_effects': '양혈렴음, 유간지통, 평간양', + 'indications': '혈허, 복통, 사지경련, 두훈, 월경불순', + 'dosage_range': '2~4돈(6~12g)', + 'precautions': '비허설사자 신중', + 'preparation_method': '백작약, 적작약', + 'tags': [ + ('보혈', 4), + ('진경', 4), + ('평간', 3), + ] }, { - 'ingredient_code': '3400H1ACF', # 천궁 - 'property': '온', - 'taste': '신', - 'meridian_tropism': '간,담,심포', + 'ingredient_code': '3475H1AHM', # 천궁 + 'property': '온(溫)', + 'taste': '신(辛)', + 'meridian_tropism': '간(肝), 담(膽), 심포(心包)', 'main_effects': '활혈행기, 거풍지통', - 'indications': '혈어증, 두통, 월경불순, 풍습비통', - 'contraindications': '음허화왕, 월경과다', - 'precautions': '출혈 경향 환자 주의', - 'dosage_range': '3-10g', - 'dosage_max': '15g', - 'active_compounds': 'ligustilide, senkyunolide, ferulic acid', - 'pharmacological_effects': '혈관확장, 항혈전, 진정진통, 항염증', - 'clinical_applications': '편두통, 혈관성 두통, 어혈증, 월경통', - 'tags': [('활혈', 5), ('이기', 4), ('진통', 5)] + 'indications': '혈체, 두통, 현훈, 월경불순, 복통', + 'dosage_range': '1~2돈(3~6g)', + 'precautions': '음허화왕자 신중', + 'preparation_method': '주천궁', + 'tags': [ + ('활혈', 5), + ('거풍', 3), + ('지통', 4), + ] }, { - 'ingredient_code': '3400H1ACG', # 지황(숙지황) - 'property': '온', - 'taste': '감', - 'meridian_tropism': '간,신', - 'main_effects': '보혈자음, 익정전수', - 'indications': '혈허증, 간신음허, 수발조백, 유정도한', - 'contraindications': '비허설사, 담습', - 'precautions': '소화불량 주의', - 'dosage_range': '10-30g', - 'dosage_max': '60g', - 'active_compounds': 'catalpol, rehmannioside, aucubin', - 'pharmacological_effects': '조혈촉진, 면역조절, 혈당강하, 신경보호', - 'clinical_applications': '빈혈, 당뇨병, 치매예방, 불임증', - 'tags': [('보혈', 5), ('보음', 5)] + 'ingredient_code': '3105H1AHM', # 당귀 + 'property': '온(溫)', + 'taste': '감(甘), 신(辛)', + 'meridian_tropism': '간(肝), 심(心), 비(脾)', + 'main_effects': '보혈활혈, 조경지통, 윤장통변', + 'indications': '혈허, 월경불순, 복통, 변비, 타박상', + 'dosage_range': '2~4돈(6~12g)', + 'precautions': '습성설사자 신중', + 'preparation_method': '주당귀, 당귀신, 당귀미', + 'tags': [ + ('보혈', 5), + ('활혈', 4), + ('윤조', 3), + ] }, { - 'ingredient_code': '3400H1AFJ', # 백출 - 'property': '온', - 'taste': '고,감', - 'meridian_tropism': '비,위', - 'main_effects': '건비익기, 조습이수, 지한안태', - 'indications': '비허증, 식욕부진, 설사, 수종, 자한', - 'contraindications': '음허조갈', - 'precautions': '진액부족 시 주의', - 'dosage_range': '6-15g', - 'dosage_max': '30g', - 'active_compounds': 'atractylenolide, atractylon', - 'pharmacological_effects': '위장운동촉진, 이뇨, 항염증, 항종양', - 'clinical_applications': '만성설사, 부종, 임신오조', - 'tags': [('보기', 4), ('이수', 4), ('소화', 3)] + 'ingredient_code': '3583H1AHM', # 황기 + 'property': '온(溫)', + 'taste': '감(甘)', + 'meridian_tropism': '폐(肺), 비(脾)', + 'main_effects': '보기승양, 고표지한, 이수소종, 탈독생기', + 'indications': '기허, 자한, 설사, 탈항, 수종, 창양', + 'dosage_range': '3~6돈(9~18g)', + 'precautions': '표실사 및 음허자 신중', + 'preparation_method': '밀자황기', + 'tags': [ + ('보기', 5), + ('승양', 4), + ('고표', 4), + ] }, { - 'ingredient_code': '3400H1AGM', # 복령 - 'property': '평', - 'taste': '감,담', - 'meridian_tropism': '심,비,폐,신', - 'main_effects': '이수삼습, 건비안신', - 'indications': '수종, 소변불리, 비허설사, 불면, 심계', - 'contraindications': '음허진액부족', - 'precautions': '이뇨제와 병용 주의', - 'dosage_range': '10-15g', - 'dosage_max': '30g', - 'active_compounds': 'pachymic acid, polysaccharide', - 'pharmacological_effects': '이뇨, 진정, 항염증, 면역조절', - 'clinical_applications': '부종, 불면증, 만성설사', - 'tags': [('이수', 5), ('안신', 3), ('보기', 2)] + 'ingredient_code': '3384H1AHM', # 육계 + 'property': '대열(大熱)', + 'taste': '감(甘), 신(辛)', + 'meridian_tropism': '신(腎), 비(脾), 심(心), 간(肝)', + 'main_effects': '보화조양, 산한지통, 온경통맥', + 'indications': '양허, 냉증, 요통, 복통, 설사', + 'dosage_range': '0.5~1돈(1.5~3g)', + 'precautions': '음허화왕자, 임신부 금기', + 'preparation_method': '육계심, 계피', + 'tags': [ + ('보양', 5), + ('온리', 5), + ('산한', 4), + ] }, { - 'ingredient_code': '3400H1AGI', # 반하 - 'property': '온', - 'taste': '신', - 'meridian_tropism': '비,위,폐', - 'main_effects': '조습화담, 강역지구, 소비산결', - 'indications': '습담, 구토, 해수담다, 현훈', - 'contraindications': '음허조해, 임신', - 'precautions': '임산부 금기, 생품 독성 주의', - 'dosage_range': '5-10g', - 'dosage_max': '15g', - 'active_compounds': 'ephedrine, β-sitosterol', - 'pharmacological_effects': '진토, 진해거담, 항종양', - 'clinical_applications': '임신오조, 기관지염, 현훈증', - 'tags': [('거담', 5), ('소화', 3)] - } + 'ingredient_code': '3299H1AHM', # 숙지황 + 'property': '온(溫)', + 'taste': '감(甘)', + 'meridian_tropism': '간(肝), 신(腎)', + 'main_effects': '자음보혈, 익정전수', + 'indications': '혈허, 음허, 요슬산연, 유정, 붕루', + 'dosage_range': '3~6돈(9~18g)', + 'precautions': '비허설사, 담다자 신중', + 'preparation_method': '숙지황 제법', + 'tags': [ + ('보혈', 5), + ('자음', 5), + ('보신', 4), + ] + }, ] - for herb in herbs_data: + for herb in herb_data: # herb_master_extended 업데이트 cursor.execute(""" UPDATE herb_master_extended @@ -190,53 +191,49 @@ def add_herb_extended_data(): meridian_tropism = ?, main_effects = ?, indications = ?, - contraindications = ?, - precautions = ?, dosage_range = ?, - dosage_max = ?, - active_compounds = ?, - pharmacological_effects = ?, - clinical_applications = ?, + precautions = ?, + preparation_method = ?, updated_at = CURRENT_TIMESTAMP WHERE ingredient_code = ? """, ( - herb['property'], herb['taste'], herb['meridian_tropism'], - herb['main_effects'], herb['indications'], herb['contraindications'], - herb['precautions'], herb['dosage_range'], herb['dosage_max'], - herb['active_compounds'], herb['pharmacological_effects'], - herb['clinical_applications'], herb['ingredient_code'] + herb['property'], + herb['taste'], + herb['meridian_tropism'], + herb['main_effects'], + herb['indications'], + herb['dosage_range'], + herb['precautions'], + herb['preparation_method'], + herb['ingredient_code'] )) - # herb_id 조회 - cursor.execute(""" - SELECT herb_id FROM herb_master_extended - WHERE ingredient_code = ? - """, (herb['ingredient_code'],)) + # 효능 태그 매핑 + for tag_name, strength in herb.get('tags', []): + # 태그 ID 조회 + cursor.execute(""" + SELECT tag_id FROM herb_efficacy_tags + WHERE tag_name = ? + """, (tag_name,)) - result = cursor.fetchone() - if result: - herb_id = result[0] + tag_result = cursor.fetchone() + if tag_result: + tag_id = tag_result[0] - # 효능 태그 매핑 - for tag_name, strength in herb.get('tags', []): - # 태그 ID 조회 + # 기존 태그 삭제 cursor.execute(""" - SELECT tag_id FROM herb_efficacy_tags - WHERE tag_name = ? - """, (tag_name,)) + DELETE FROM herb_item_tags + WHERE ingredient_code = ? AND tag_id = ? + """, (herb['ingredient_code'], tag_id)) - tag_result = cursor.fetchone() - if tag_result: - tag_id = tag_result[0] + # 태그 매핑 추가 + cursor.execute(""" + INSERT INTO herb_item_tags + (ingredient_code, tag_id, strength) + VALUES (?, ?, ?) + """, (herb['ingredient_code'], tag_id, strength)) - # 태그 매핑 추가 - 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']} 데이터 추가 완료") + print(f"✅ {herb['ingredient_code']} 데이터 추가 완료") conn.commit() conn.close() @@ -248,85 +245,64 @@ def add_prescription_rules(): # 몇 가지 대표적인 배합 규칙 추가 rules = [ - # 상수(相須) - 서로 도와서 효과를 증강 { - 'herb1': '인삼', 'herb2': '황기', - 'relationship': '상수', - 'description': '두 약재가 함께 사용되면 보기 효과가 증강됨', - 'severity': 0 + 'herb1': '인삼', + 'herb2': '황기', + 'rule_type': '상수', + 'description': '보기작용 상승효과', + 'clinical_note': '기허증에 병용시 효과 증대' }, { - 'herb1': '당귀', 'herb2': '천궁', - 'relationship': '상수', - 'description': '혈액순환 개선 효과가 증강됨', - 'severity': 0 - }, - # 상사(相使) - 한 약이 다른 약의 효능을 도움 - { - 'herb1': '반하', 'herb2': '생강', - 'relationship': '상사', - 'description': '생강이 반하의 독성을 감소시킴', - 'severity': 0 - }, - # 상반(相反) - 함께 사용하면 독성이나 부작용 발생 - { - 'herb1': '감초', 'herb2': '감수', - 'relationship': '상반', - 'description': '십팔반(十八反) - 함께 사용 금기', - 'severity': 5, - 'is_absolute': True + 'herb1': '당귀', + 'herb2': '천궁', + 'rule_type': '상수', + 'description': '활혈작용 상승효과', + 'clinical_note': '혈허, 혈체에 병용' }, { - 'herb1': '인삼', 'herb2': '오령지', - 'relationship': '상반', - 'description': '십구외(十九畏) - 함께 사용 주의', - 'severity': 4, - 'is_absolute': False + 'herb1': '반하', + 'herb2': '생강', + 'rule_type': '상수', + 'description': '반하의 독성 감소, 진토작용 증강', + 'clinical_note': '구토, 오심에 병용' + }, + { + 'herb1': '감초', + 'herb2': '감수', + 'rule_type': '상반', + 'description': '효능 상반', + 'clinical_note': '병용 금지' + }, + { + 'herb1': '인삼', + 'herb2': '오령지', + 'rule_type': '상외', + 'description': '효능 감소', + 'clinical_note': '병용시 주의' } ] for rule in rules: - # herb_id 조회 cursor.execute(""" - SELECT herb_id FROM herb_master_extended - WHERE name_korean = ? - """, (rule['herb1'],)) - herb1_result = cursor.fetchone() - - cursor.execute(""" - 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']} 규칙 추가") + INSERT OR IGNORE INTO prescription_rules + (herb1_name, herb2_name, rule_type, description, clinical_notes) + VALUES (?, ?, ?, ?, ?) + """, (rule['herb1'], rule['herb2'], rule['rule_type'], + rule['description'], rule['clinical_note'])) + print(f"✅ {rule['herb1']} - {rule['herb2']} 규칙 추가") conn.commit() conn.close() def main(): - """메인 실행 함수""" - print("\n" + "="*80) - print("한약재 샘플 데이터 추가") - print("="*80 + "\n") + print("=" * 80) + print("한약재 샘플 데이터 추가 - 십전대보탕 구성 약재") + print("=" * 80) try: - # 1. 약재 확장 정보 및 태그 추가 - print("1. 약재 확장 정보 추가 중...") + print("\n1. 약재 확장 정보 추가 중...") add_herb_extended_data() - # 2. 처방 규칙 추가 print("\n2. 처방 배합 규칙 추가 중...") add_prescription_rules() diff --git a/add_wolbitang_prescriptions.py b/add_wolbitang_prescriptions.py new file mode 100644 index 0000000..ac5c0ef --- /dev/null +++ b/add_wolbitang_prescriptions.py @@ -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❌ 처방 추가 중 오류가 발생했습니다.") \ No newline at end of file diff --git a/analyze_db_structure.py b/dev_scripts/analyze_db_structure.py similarity index 100% rename from analyze_db_structure.py rename to dev_scripts/analyze_db_structure.py diff --git a/analyze_excel_formats.py b/dev_scripts/analyze_excel_formats.py similarity index 100% rename from analyze_excel_formats.py rename to dev_scripts/analyze_excel_formats.py diff --git a/dev_scripts/analyze_inventory_discrepancy.py b/dev_scripts/analyze_inventory_discrepancy.py new file mode 100644 index 0000000..81629a7 --- /dev/null +++ b/dev_scripts/analyze_inventory_discrepancy.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/analyze_inventory_full.py b/dev_scripts/analyze_inventory_full.py new file mode 100644 index 0000000..9f214aa --- /dev/null +++ b/dev_scripts/analyze_inventory_full.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/analyze_price_difference.py b/dev_scripts/analyze_price_difference.py new file mode 100644 index 0000000..3499d46 --- /dev/null +++ b/dev_scripts/analyze_price_difference.py @@ -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() \ No newline at end of file diff --git a/analyze_product_code.py b/dev_scripts/analyze_product_code.py similarity index 100% rename from analyze_product_code.py rename to dev_scripts/analyze_product_code.py diff --git a/analyze_product_deep.py b/dev_scripts/analyze_product_deep.py similarity index 100% rename from analyze_product_deep.py rename to dev_scripts/analyze_product_deep.py diff --git a/check_and_create_efficacy_tags.py b/dev_scripts/check_and_create_efficacy_tags.py similarity index 100% rename from check_and_create_efficacy_tags.py rename to dev_scripts/check_and_create_efficacy_tags.py diff --git a/check_custom_prescription.py b/dev_scripts/check_custom_prescription.py similarity index 100% rename from check_custom_prescription.py rename to dev_scripts/check_custom_prescription.py diff --git a/check_formula_columns.py b/dev_scripts/check_formula_columns.py similarity index 100% rename from check_formula_columns.py rename to dev_scripts/check_formula_columns.py diff --git a/dev_scripts/check_herb_data.py b/dev_scripts/check_herb_data.py new file mode 100644 index 0000000..ad00848 --- /dev/null +++ b/dev_scripts/check_herb_data.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/check_lot_creation_methods.py b/dev_scripts/check_lot_creation_methods.py new file mode 100644 index 0000000..27cc985 --- /dev/null +++ b/dev_scripts/check_lot_creation_methods.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/check_missing_lots.py b/dev_scripts/check_missing_lots.py new file mode 100644 index 0000000..6da25a4 --- /dev/null +++ b/dev_scripts/check_missing_lots.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/check_purchase_structure.py b/dev_scripts/check_purchase_structure.py new file mode 100644 index 0000000..61dd94e --- /dev/null +++ b/dev_scripts/check_purchase_structure.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/check_purchase_tables.py b/dev_scripts/check_purchase_tables.py new file mode 100644 index 0000000..8f2c0db --- /dev/null +++ b/dev_scripts/check_purchase_tables.py @@ -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() \ No newline at end of file diff --git a/check_samsoeun_ingredients.py b/dev_scripts/check_samsoeun_ingredients.py similarity index 100% rename from check_samsoeun_ingredients.py rename to dev_scripts/check_samsoeun_ingredients.py diff --git a/check_sipjeondaebotang.py b/dev_scripts/check_sipjeondaebotang.py similarity index 100% rename from check_sipjeondaebotang.py rename to dev_scripts/check_sipjeondaebotang.py diff --git a/check_ssanghwatang.py b/dev_scripts/check_ssanghwatang.py similarity index 100% rename from check_ssanghwatang.py rename to dev_scripts/check_ssanghwatang.py diff --git a/dev_scripts/check_tables.py b/dev_scripts/check_tables.py new file mode 100644 index 0000000..15aff88 --- /dev/null +++ b/dev_scripts/check_tables.py @@ -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() \ No newline at end of file diff --git a/check_totals.py b/dev_scripts/check_totals.py similarity index 100% rename from check_totals.py rename to dev_scripts/check_totals.py diff --git a/check_wolbitang_ingredients.py b/dev_scripts/check_wolbitang_ingredients.py similarity index 100% rename from check_wolbitang_ingredients.py rename to dev_scripts/check_wolbitang_ingredients.py diff --git a/dev_scripts/debug_api_calculation.py b/dev_scripts/debug_api_calculation.py new file mode 100644 index 0000000..389b746 --- /dev/null +++ b/dev_scripts/debug_api_calculation.py @@ -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() \ No newline at end of file diff --git a/debug_receipt_detail.py b/dev_scripts/debug_receipt_detail.py similarity index 100% rename from debug_receipt_detail.py rename to dev_scripts/debug_receipt_detail.py diff --git a/dev_scripts/final_inventory_analysis.py b/dev_scripts/final_inventory_analysis.py new file mode 100644 index 0000000..e5fd331 --- /dev/null +++ b/dev_scripts/final_inventory_analysis.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/final_price_analysis.py b/dev_scripts/final_price_analysis.py new file mode 100644 index 0000000..66c14a7 --- /dev/null +++ b/dev_scripts/final_price_analysis.py @@ -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() \ No newline at end of file diff --git a/dev_scripts/final_verification.py b/dev_scripts/final_verification.py new file mode 100644 index 0000000..dcac69e --- /dev/null +++ b/dev_scripts/final_verification.py @@ -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() \ No newline at end of file diff --git a/test_compound_e2e.py b/dev_scripts/test_compound_e2e.py similarity index 100% rename from test_compound_e2e.py rename to dev_scripts/test_compound_e2e.py diff --git a/test_compound_page.py b/dev_scripts/test_compound_page.py similarity index 100% rename from test_compound_page.py rename to dev_scripts/test_compound_page.py diff --git a/test_db.py b/dev_scripts/test_db.py similarity index 100% rename from test_db.py rename to dev_scripts/test_db.py diff --git a/test_direct.py b/dev_scripts/test_direct.py similarity index 100% rename from test_direct.py rename to dev_scripts/test_direct.py diff --git a/dev_scripts/test_direct_api.py b/dev_scripts/test_direct_api.py new file mode 100644 index 0000000..eb6cd5d --- /dev/null +++ b/dev_scripts/test_direct_api.py @@ -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}") \ No newline at end of file diff --git a/test_frontend.py b/dev_scripts/test_frontend.py similarity index 100% rename from test_frontend.py rename to dev_scripts/test_frontend.py diff --git a/test_herb_dropdown_bug.py b/dev_scripts/test_herb_dropdown_bug.py similarity index 100% rename from test_herb_dropdown_bug.py rename to dev_scripts/test_herb_dropdown_bug.py diff --git a/dev_scripts/test_herb_info_page.py b/dev_scripts/test_herb_info_page.py new file mode 100644 index 0000000..67794e1 --- /dev/null +++ b/dev_scripts/test_herb_info_page.py @@ -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() diff --git a/dev_scripts/test_herb_info_ui.py b/dev_scripts/test_herb_info_ui.py new file mode 100644 index 0000000..24c7b6c --- /dev/null +++ b/dev_scripts/test_herb_info_ui.py @@ -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테스트 완료!") \ No newline at end of file diff --git a/test_improved_import.py b/dev_scripts/test_improved_import.py similarity index 100% rename from test_improved_import.py rename to dev_scripts/test_improved_import.py diff --git a/dev_scripts/test_js_debug.py b/dev_scripts/test_js_debug.py new file mode 100644 index 0000000..ebb00b4 --- /dev/null +++ b/dev_scripts/test_js_debug.py @@ -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()) \ No newline at end of file diff --git a/test_lot_validation.py b/dev_scripts/test_lot_validation.py similarity index 100% rename from test_lot_validation.py rename to dev_scripts/test_lot_validation.py diff --git a/test_multi_lot_compound.py b/dev_scripts/test_multi_lot_compound.py similarity index 100% rename from test_multi_lot_compound.py rename to dev_scripts/test_multi_lot_compound.py diff --git a/test_simple.py b/dev_scripts/test_simple.py similarity index 100% rename from test_simple.py rename to dev_scripts/test_simple.py diff --git a/test_upload_api.py b/dev_scripts/test_upload_api.py similarity index 100% rename from test_upload_api.py rename to dev_scripts/test_upload_api.py diff --git a/find_duplicate_issue.py b/find_duplicate_issue.py new file mode 100644 index 0000000..d7c715d --- /dev/null +++ b/find_duplicate_issue.py @@ -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() \ No newline at end of file diff --git a/get_ingredient_codes.py b/get_ingredient_codes.py new file mode 100644 index 0000000..99c3154 --- /dev/null +++ b/get_ingredient_codes.py @@ -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() \ No newline at end of file diff --git a/migrations/add_herb_extended_info_tables.py b/migrations/add_herb_extended_info_tables.py index 21aa3fa..a5d2495 100644 --- a/migrations/add_herb_extended_info_tables.py +++ b/migrations/add_herb_extended_info_tables.py @@ -245,7 +245,7 @@ def create_disease_herb_mapping(): recommendation_grade VARCHAR(10), -- 권고등급 clinical_notes TEXT, - references TEXT, + reference_sources TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) diff --git a/sample/자산관련.md b/sample/자산관련.md new file mode 100644 index 0000000..280ef50 --- /dev/null +++ b/sample/자산관련.md @@ -0,0 +1,55 @@ +Traceback (most recent call last): + File "/root/kdrug/analyze_inventory_full.py", line 315, in + 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. 재고 보정 내역 +------------------------------------------------------------ \ No newline at end of file diff --git a/direct_test.png b/screenshots/direct_test.png similarity index 100% rename from direct_test.png rename to screenshots/direct_test.png diff --git a/screenshots/herb_info_error.png b/screenshots/herb_info_error.png new file mode 100644 index 0000000000000000000000000000000000000000..910b992546287ed1c7f8eef6c70009d21767b6f1 GIT binary patch literal 92606 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

{?B_e2s$oytyN?jeMH9sU zra_RyB=A?eW&Y2iJ&b)jS(-BMBF@wLjk*)96!=1KiPKvTe*T2ApYP>3@XEtJECUzz zopZftLU8NqI;Q}Q^)lt61yq=WMN%C$BjsEHMt54VCr(pLu`<8Wi3}F9L}sJ?x1hv1 zpaUY3;fsJ-2RB5i&W6-L;a&H^)MEJ=b_0jehh;S3X0r+Y1ozG0Dz_^x)KpjXv3uPwu57&B^qz3^y>S{!TDffw;3&p-P z$mQs^qHfVFrqHCh>ORTCwlYkaEeqIGdFu{Mp1>SU=aOkhXu^g^HpXU$)ILE zefJhAaDxU93F0r%0nn2Z{?p7vRcr6eef9W>|8fBW;s4+Q+Ej>gv0Rg8;^3}t22#Uh zW_hekE|vq>ys11nF*)fK5mzUpW36;|wZ@?bd!i3Bog124{+6uixL=u->$Ug^^ec~m zu^KFE?S_onn|^y8U?RK-FDmL$u|}`meO}7-eNx4UpNuhcsdkfoafb8b1e*Ktn~xU5 z2*bGyd5zmBN0fXQpWm~x!c+_e!De@ehTH&@p_e3Qt1^0n-#!-?T5}fL7)dkLlLdo( zcvd0F>4{hFQAbPF4V}#NHRNb+bL_S0o4{^e1g6L)das!z+emJjGxd26GD5Bfwx$Z%;kR?*WK zXMF>L_6BXO`l-!;ldC*B_Ru2-u4>8FaKG~=18zWZx{a3}MXy~PyRB;n>OcAphpZ_v zIX)zFkm-$q*+rKKxQV^){>3dDxUEJ6jh_a?rbS%mf%h0V`DI3sHC+#`KRT&a4<*UZ zk__;kE#akc__1#vL#A%R{jz+Z%kgNPaG7S?RiK+LX?8CI5z8U1*}zFTdN0y{bt2V3QDas<9DQ0J~0D&ILyV-%;cP z=>$;S0jNl(G`-PL^f6CDDFK8e_;+&4-gQk{o1U+MamZ&DCh#0Lc%4oJvQ|I7-==-B zZ;;0Y1eK3O;@k(FQ6OCOC=GV-_}K$n`#%ebp&5;ZoWZDPRq4;!Mm-K30|yJ#UQZd% zsO++e!s`P0LOd&W3ee-09T$?-OTkU|0-H(rE~uP)QkSEt?1Q>#E4UX$i^JX@-MqjT z_6ovw^XmK{W^mo?T_K^2#-eoFrupS!?ZB>d<3V>H*LlpL#xL@$BkH}RFwfp7gq;~F zz5yVSkzI0b?8@}3{Rc-&Xnjq6Z8fB}x}kOe8_YW8>6Q%a+>-5mwP9OuCbYa@^2dcd6yF320N6=^&1Nxez0(3)6pyCRZZ><`nh@ZMO1*HL!F)#iev{ErJevP1<1A#ehJ=8Y^ylwY zF`{cri=EBM`O!ERTWfngYlZxVdXuaCRKcW?(&&w?O{n#g*oCri2vVnF1Uk&nC2Y%m z#@H8KknZc!du#>tKOOOSh*D=qd*Qb)4Aa!Y_=H* zDpY6Ec`Ze;Co!ENfLEVUYH+1tG$Cq}P?OojV>V1kAkao|Zm;evq#4)rdPvpZX=&u` zVYm4#QjJm@vegk8r9Mfq`K2wl?^1Zvtu5H#ILf_thjlLaT{d>+ol%AQD0+d{5&gG5 zJ9!!=>te*`WB zYtjZtHRDxD6Gwlw++r*UI`|obDl@7^%tz#)AH-hY)B?VKrw`sU)yTQ-Q?a~H#GYha5euRO{M!%}B7L|!l}`4dK0TVj2zQ4_)eWIW;t zo@wM3b*@MOed;N~FLN4^g%?WN$qQNNZRYp0p@Jw*DmEIbpll!IYxdOCMUX`yM*dt zFBLMyeWhJB$$|(^62cA&tY^*{_-=XFDh~gk`LGA!{*E{V{hOY0#?%A6=q}e+;;KWa zzKh+2lV7GYaO~Zn>m~o$-d5cm-S3sc+3_6*irw`)XQcz?M7?nzU7nqcsZ+x&S6;qf2t0;L9tZfF}^ zSna)lr#;Hu4P^hIt35a1zAtG1hyhkmTBRjsfsNTF==-Yp$JtChdrZ#IZvE_sqxx>i zRSS>(fF}4(d#86=wReWg=hkMmKH-?@N)vC7^C3ZLDmIIOue*CTrQjEFD;b{qf|ieD zbXG=faj>|rpxPC*5_KAy-jFRlhqtI-Gt)+dw;vZZr1~wLOtW6-AEV20AU~_jAPzi5 z`fRotyLXaq9>@BW3*BN*_55M!fDmT#%pD~gg@$kK|Ho@sy1L@d*&6r%m!xM3G8Yv? zUY%WD@~-mAY0gXiC^=BuA{w+owx<&`bCU;c3BKwXEHYp}hY8}_yZ0k1VeYJJVJ>}F^jF9>AwAaHSRomIh*D_O7y92=rIHsruav5xXVcPtzyFgq}=j* zQtxS=nbxZxQLRd4cp>Hm{*_Bp;5-BOiO$$$(!r(XhStwJ_3vDEbk4eK5dTW<=?D z&+36XH}zO}ml$AfE``}_GKxM$$opdpufvcO@C-I`G z9wP~L;^rcL!bcunu>Z0&GQH@Kd`fHp*&cYcQYkpPCg3zYs^S3RISlJ1RlobVXl3y{ zCrbu)tNM>sXsPvVzydW9;beKqM^&W5rNYC%4Bau3g+@#>Ddf)>ti`-mQ_U4i#6C20 zE`lxWj}LJ~?a|6EGC<Gy z$h*MBQU$^tkc85sP5VJ)m2TYTRrB*{T;KVEJ8>@_$AwtTdmX?9E#vVD)eG#zoZ)o| z7IU6~j=NNCvg8s@=7Qay2G_cx%syJtok5(J8veB#?az~sDd@l6R>LonB}%b)@e48> zVM&u{$ZG?fW>1pm+Pnz3a`mFTs%NPify&_@sGzspUoDRZP>bL3w+*zL|97eHE8t&V z>A_2p;CmEoJX7Hf;rZwHv?}GbbpO@wn97YB>Z^Fb(HGI5Mo^G%iKLtVXl&{KmCn>u z|FE}?nEd*Rr|P+L32ry@tE+71<)0Lw#Q*E3Ps~irLU$)-Fx+(wkDkW7e22Y`S{~uT z2^Ye{ioT(!|LAgKIw<5}d!GObSh0Jv^PYLgT)%seCinf=*_tWw8 zXx#5V-?Em||GR#mnTuH)#P{bNC z!HV56bx)7#3R74NVpVDHy;u3q(-IDh$uMoK3GS8`y$v7E96!Z;{@2!y_0U?()+_0! z9YDcxFCynG9K%xDj*5zJYk0-SsB zI4HUGkdHQ8=Z5!sein=y zA2+$yy(UU+T9J)RT9J70oLL|4vuiiXcrv82HDZK|JSXBQA|I}@;wf=DJj89j$h#g% zTd~#lvP~wj9c_eo$|WOX1t%BS4nOlDrY5*ozW_LKiMCr4vYxxGRO(>!hC{sw$)_~z zl>U10=MNqew8X+JzDPQY%-Ir;XrFyn4WyNnn;uH-s*Wbfa~e5 zKCcj|)z@+(kg4;bl}W5V1((d$5Y=ztf)fCjvc~YS4pA--c6SpzfscF$=a7|&^@GC5 zR{WtZlkr5pnKa#hCV4@Z4!ZtJ@W(x+=Pm9^(pWiDA!@*ofY>+&O_KJS{bzh&Y>(3$ zFQjjFEr?BbmJb2?Lsl92UoN1q+_y4Hu$VtznMp6vxoXC@H!1QydAK~?uDC2Mmro~Y z@I1ZA!8cm+KVP%?Nx~caW&PCO`IuRqc8W{draN>j*9XFx3KYZApxKkrZDRAtX{a;> zzlxyZL#cIUn5i=j{;-!A9*KETl*xwu*Hv#gRkbiUG{i2+cBJy7K0z5#o5MB>VZ`^tCe(gx?z;&ja?4uC7MXlu(E7+a7oSh+v znc}STLQB)e!O$@EwWMC3)x2i95qe$qL2URvM3af(%*xmcN@eETO;zt`*ethfQ2_DO;csgT7mo^wO? zn#>jB+Cmfyd$3ym>s{6Kn!zhy=W(o&+w-6Piv943BCWfl4zj@!Bft&@2bt}l2|mXw zFnOD(z{1f0x#X6MK7X{rAM3aR`TM|?{X??5Ch9EvpS%*2zC9C!W^t(7USTqRe2!t# zo9hR9QpD;3u&HR{+IZ$BP}Z4n=1ItDU`6K>6>s*OceDzNXC7PsI3lVW1Bb zstua_Hcw7c)|Gb4;{$LzI9oyDwfUy{(YL9bAI(IE!F9hZq zW5=z$Iy*xQR5e+@Ws`_eOaURrJ_{`U+z7gv6JPw*;aAigWbmrZ*2G$^!xL^Q%!Ntl?zUN~hp zi%03U%q*@g-3mc3&WHQksI77&%R}hTe=o#1S|~10e@F{Snwk)1VnUAYHsADItOQJa ze=V!0P8~ry=Nm!AAAMu4a;G+jF>`;SrkD%YL__20nrl{UM0EQa2{{D??*wzf;6sX?ewN@iUa6b|D8NhDPn*Wf$y_>qD7Y4R<%`pC2anRag z!lA2II%mF=VfL;e|5vz?RCy^=+?86|Eo=fm4G{B$y$~tNQ@RL^bhf6@ds%D0LCP1{ z{e_aBU%;j&nW1^47g}VD+!W_$4u1R;|1k_YX)9lm!iP)}xL~I}G!PzXJ>gh`+%u@% zqLOHu1976FL>u?EqG2tMBCrD+^`uhj+2C)`D(E(9uDu28wW66bq`k!U-vy-VKrQCr z*ujD57?|hhrPVg6KwG^i@2iP*TIu9#%hc0(tPAeTnR&cIE=S#UX6AfoIQfrA#QKIN z(~g6%F7QV8d+e$lpV0E~V$rT|@oQdD?LKdJAFx~9b(tRuW9l=#Fz5Im&Lk~s+DiJ< zfk+c9zKkV2YN@K*(|Qkg3t}`hJNKU9hGp)K)9l^PhZ|F+#>w<(Mkg)%SqF`ov9RuM zDMZMv-cnxZPBFb(+j?;*4@aXhRv;FcaJwGX)eVl05{wwjd704gEbWoN$3Rd89bh@FIew!Ol9)=KdJ`jj1@%_57 zzI8z@DnoaJnrC7sc<1BZ@yQET;rroi?{+>ZZbqf>g7^emHLCb{gWq1Kgqbf0HlkwPQz15gy z(x+%}}LL`xneZOzO#EO#6;e3ketIjW4z%Ng2-oKZE<*K;hT=1vI7Wi|{nylX9Gs?0T_ z5u1dcCD&Rb=XK0y!YYTnF}9qxIc#w73e1#l=c-ESQ8S|@`~Is=&+BdVAc2@RC#Vp@ z3y5YYeLYYz0bt^2-%3LkGL*JhNn_!O+v7%zh6k%j@X>;>OhrsA+YwhUNQv=!KwwVJ~B?h>R$H;u71}0Z?d!7 zqgVrmv0G>VS&$v9Q>8joKa8bHzQtU&XIZZ&_PZg#OOaFU8)({nYN$%}`4}}%POylj zn`W9UQJGCl9qtRLW*SA`X?Q{uTy9#&pl-9iz;VYO2IJb-b$ktNJy{e3-gr^A9rWu4 zsmzz1hNeV-M6Y( z*@YPBMT*%kSb794SieIJRrTsoNgH=w$a}U>jTuY{1H+2414NLS6w^5v zzIiTxJ1rc2;S*Aw5uvUN7Cg@Y6E69Jgw~H|s7=(dG5bU(I&uK*q9V#oRAM@S(j8X|NlG7pe zNeM)Mkun~$12-$O$}D}#;SyO!<<``4W6Y{NBFJS~kacB#;>BR7!ETk3lGpYEr?_6Q zLCeNT4!o#n85QXN1+KzkCjFc^<95)e5Yr`P-}%<$%fglRa&VtsW|sAq)X}+VSwFqW zyo0@#4MwofgY}MBA?x$)^K-xbOIb82? zfHTIAO~rh-=3Ruh1Rk0c`;6k_g9->j?3P!^loRLyO|Q!p&w%G zU7C{$nq!Fyp&rrJ1uQXSvN35XrKl)XHr;39lQU-QvMXks*PHdRBJHTTrh7}XbYUQH zy-(L@!H4UZ|0WrFyZ2iItedf~p6;Hj=losa<#<4O#d7 z(9PLyGA~+c(^z&g!(qZg6mY5~_0J#MG^|*mybT9Rbn>4>F@1+f^AFnz{klscKI?^* z$z3q%Ywux2GRsH@Rl=gEf4>XlK2Ct*?$AR}cd^`O2&&<;ygqm!{;K6^DTEQmYmm?M{m zpM|$8pC%qxFJi)veD#BsariCWwYP0)UDR3Dmt0?_H>Wb+`?Bv)%xSM#fi8F{bw|qv zy+GSjyTkoF{Pb(57By(|sn0m3y04JdWsRaNmH*Fal4V;u&R@i&k9gi>tY?~)tF)_b z{ee!(JX$dRsvc0!C(Oyob#fMk)acL8Kw1SiyUw4nfyDVW0(9W<4Ne_wU90|==jMGq zOj+U-($GR>oY=nm0LILl$~fdHesG&m6UNoh)>dLQ#bXiHMv=E$p|X6zwEY6K$@H8~ zixV3Pam8mY!mj6u`5>=c{pzbu+Q64dcLy3!>5-Q)BKGGj9#~CN-fXKPjmRfg6x5Fg zEvGA1KciIL7#@Nlm68n=nFDgjO}SwD!O@DF>XDVV0Na<+NYuy%Qsq*BKjjrcPk+Yk z@Y{Qw3RKcCX*i?<&tAi$@rKje75|XK0aR+gN46Go-UeGCc~e`t8#NUJ_3sXUPt0V> z`9esJkKSRPg8k+Xv#U;KKEzQYGWZO zOrM>1h&!*1>sB*FFGWTph9gp8L&nYaatYuh7D9V=t~ehB$0l8qsL*}MiCv{E+e5FZ zG<`*NDJG)}N0T7BxT;gT*T#N_;9;H}e?4;*SAV=Wt>rlEhf%j2nwO z?@x9G%SH_N4Q1-fqP%0dr40ux=B6K=^BYL)dB=OpBWe2;3MlTA*P3!2ddY^a&blmD z&u+*ZS^Qi@xEQ32rN#gh?OPd7%uGj_)V{oXK@2#NwX|Lco@c&ldC>@qDx;nQ_dGYb zAt5krJuF@EYoObIL(!#oms$0*x$xe3&z#dABC*r_Vs&^^Vvx>BJXeU@E@0lUwi6TYCunk`!jq=KfV1g7XYvFLnyJW zR-0XaoD7X#9dFU^iYcAR-yB(_0GkhtB@^LF>q~S+!HTeg6PYt_L=BN>mbNT%em3i) z?S%y7mkSA-TH_nq^KJ9KVimt)({?0QYh9$UD^8x8A=>ZMvtfEFfGl-?>eDqWOYp)r z@*;=uj(hfd#X3&0$Yr%EGw}~TxfCU(abCAZ(=cJsw%P;M1!u>-ZH30or=E(2EbFaD z<2OL-O?w2#T@&`>kd(`p0ypeafNN#JO9v;xVLq76A`5rEbzQi(NXV66oH%c}ML#LO-K6S&$5Qc0!QX$0c4hzgEucCa8gzb9dQU}`;jTR)rXW@3M6#)Lph zoK9z$4~LI-;H$=H7#+#&J-Lb7PBqBQ1kMOnNL4pXdt3NT$uFaF&YS(T)d36Nx^E6A z1R3zlzgnvmgh=ce3euF=&ui#mLwq6CW_3;PO^AJ(Y>X2m1CD7`ydJ3N-L8>6L2mE$ zgms$bECPKbAXudEnTga5f(RJmzQ<$*voF^H?fjH8!Wt&&mG5U{l1!`yUIz%fNc-HK z>ZK@KY6SZ8#qqQ*QHs0rsCeI8dg&3EDf$E56Onu5qlE$Sf9G0ABt0C-TjAIMfqs4|Mt+w;x(t4efc!D4qs6l*4T)7OiYXIQG2? ztQ-qGl-GSSHuTa#5koVMnu3DjWZywC7}`s56_m=)^DUfk{6POg1|HnJUa1OaND*pn zsAkS66Es;ApgRa@${zR~3M?1WB`z^RWr3y(^Hzk%`xJ)@=JGM#Yi8Zbz`tV%Omq}Q z-0{>}C6}gRb>=LA{0lo#%dHzK9_C&OMS6}Dg9)yLj6q)8wJrNZPe+i3z` z-RRsz(SkO7T!&xf%G+`E9~)hDzr7@O8|wH2@2{KaUd2*{KjYcYSmrL6JI6n+sM5Xr zUJlDzDTm)zFF*(cwR13JK%T?HEX}kkyLjNl%6`AW8L%uaX>h$Eu4Q z{unc*R(sMH54c9oxALm7eBuHL`e=6A>ow8FouWedDRqG*)j$eztzbB(0TQZF&NX1e z0Oyxtz4S@nr^>f?;i)P4eOPGJN|lT6Z{ka7wusuKr$8JkK)PcfAdE~J?Bb*TR8h^n zDaFz~lc87Cdnac`846H{i^q2zwF9f?6CcywOv;y)94rn9HW@lR_l`-_R?F8Il}e3< z-%TW)S3f8QQ}}}$<>y!#fyS4Js2Hf59^YC;e0whhLCSa8-D}xO8DQ>9D!pE7LO0!7 zswh@{FfYR7MtQe%PZGs3eC+Rk@TrQBE3jtu6S98iCG(;*>6R!!A%FhEsg0-Qji?^2 z9>Omod1n2gPRr>+UrXrjPi%BkB$d?Zc zW|1P)mndcXEqtM}o&>^vUZF#5wx%C;k@35b!9I5w+K&`y-yk|kVy?NuH%a=lV||(u zxASlSBICESLp}9#UwM8zX2c)z;i5ITu!sBa>-nD1pcm8Cyv7J80qx>#{}$cmAEQEX zfK#7(w$u}+fP~%t8oxi+NrDQPl%WN|kT@vgS3@N|R59FPCAkWq9cv$j8Bb_-t0;R{ z(ddyJs^|fUO5Ppdo=bcu-v*y?Fx#Pg5zLtXe*Sa)cL4|BLe}DK2^Vg%qdaD`xcp_h zqYf?ZKdFB86=->G>XIghW>YUdku{d}Y1}Xw&h;pAfjW45U?o?NZ*TT<2=w^{B|k@% zdkwU;#o68t68lT-)?%ll#s+fx`4DBcoUBn9fIPsN549vpOBF!)d8#fXZ136c6e~e* zK%ps4Mg(a<+bza3N)hLiDj!lwnELu%-G_^bWsZGu^5)Oei#tTkei{%Or5!O`c-FrpcjDbsjH0L$eSxGbrq@=)-iEXZxV|UVj4we zxBecfkk^cnQP{n(bx4Ve!N9*{swk-jaXjr9Yk8eVuUViET@@A8{gDf&OP6bm!k&co z_EtzimFSvWuk2d1W_@IXv~+(-&x2-#N$g3V zIon$Qoo_;jsv%lgb^UW>u&%b$D>$C2s#=WyH8THxQ5RvZqdd1}yvxEb$$D3Lq+PgV z+U$JnI7;o(=IO%*jf5k*~J*+yT+$@^4)mVr)O-AIK4HmlUS*vSuIF-|hn`xbl zt|TjCs1x7TuT9vukWJ0FtF%;%iP(Xrc%xLh1~z9&C4MobG`2_14w>urw*JXQb3paX ztTckiN2T^DL$9>^974P7v}YhU%>g9^G*M^g)9H9qBHP2F zffKdId0gaqMhdZ<^p|0f&Kg;|#?zncddLo2Gm3GmrixwBrvPMRY8%O6g0ymh$t$aL zqu?}3YX67}t(@q9=EieFhDZEve%>E43#iueLH{f|AlyiyrkJ-M5eX~%AUb`8>CaN~NR)h;0zI34Pyo0-?WN@&)$ue^%2#kV$Ms!sXG z0?r#ml2a9B!zHSMh$~-C7JbmPH!TWvy~(~bpa4H@Nm<-+F)-;wuWtN zPDW}uAwCD{wG#hUTfNa?!jEa^l_ip4XVhEPIV(S~VM7T`)Dy-j734C@4!+=106xVp z3Dk_pGXY&X(TOC;F-WMw(RbU=^BbvY5pQY1yz7t&JOS(BoEK-|$1x6}YFkIFVaoAf zoyDCJ<;!)Aee}R4#ZM9{&)gXj z92Skgz7vm4OH5Bsz7RS1QbDr4mb_di>6PN`Y`S!y?quL>Xz1=>=G=O?fda9$vH z+k4a5hN5{$o6zjgi|_(;JgzBI=CkSp9v#=@zl+>7rJt+Q@K7zl&KTcSGlDmzeAS8j z%t}Czru+#A0gR`m&IfD`f9%6qxza);BR(_}ep<>`8ecXy?chR(y_2)ES8m@w!te4LdS=J3U3qvvoRPul31+otiMi%TEiL8gR$^Qr~!u zH7kdJMdhFhWsh%a{;PSu!()oCBepCbBDCO#oTt64QhI$;)16OgJy zJ+Awm`q)`a4&zhk`oJ2ReVR&Pe!q!`*ZO+nuYR~!EZlJ_(^7A7S(NmWz?WBF;_x$_ z;OL)&>1S%b`HlAQ#L;)lXxY3CPeiydK*7zild?*UI9haQDr-JvSA1n5QWXQniwxe} zfnvZRpXq!5>aX2Wrh>(rD{UfNLDF+l$y4FIv=ArQD+|cNyg2EQB?+538N$y-D0iBw&iaqSacXe8o>F51W+xjnL-VMPQNOqy z9?%klPmI7xd4O7hEf}cSBMNC1Vkukwznlof-Sx1Ajj-hM~CT> z;RAeOQd07okvApJsc6f4Db)v?*BW2W@<#vJr)>Kgs;a9}67*1Ky2?hqG$N6%XzOL% zR*n8@tZ+P@F<5->O7ll;X^_GQisz!MSD;zXkIqGcJCa8CSbe&sU$#I=vc(ZaO_FnE zg_xyq3f#aFWwkDLYlw3oj8-m5Y%egKtbsIL77IP8KXv3Ubdx$EmHTPCa(mqHI)E zx{7B3|0S}>ZJnWq&6`^iU=4}$5o~K?Vh~Cgs|#z>R_voF;X@#^Y^S~735l9y4zdb3 zEakjI;1d9VL{+WL1(Lk2WH;41gy`g{uM<@|jO6jwdXArF>L6vT^}VG!U9O{>Gz&5p zYj}sReh)!B{DD2LSxHlCb=lQlG5RhRQ9B9qRLqgTBy4WxyO9^}WB?ixqnI>OGO|-- zah8U$Vu8bEkZqf(=;quxYE^52EDxIF5YEC~CKGXd>KlOkeQ737HEB*KY%2S}We)Hk zz48*|Oxfvi(PuyA=?+^SbS{!=Jzo(&nvs>oD2z9+$`TxV*TSmpzeEXjyK@v1U zgNNYmu7Tk0?(XguLVy6l-QC@THtz1;2~Oj#-94Qr&+pFtuQhA#op*CycUPUNQ@dpE z&;HhJLmvwL0`(GxBzqIRJ%qbT^FY2L;4Oo^(|JP|Vza@O>lbN$pe5drjZGUssRHl$ zpHBJZEf~%Fpem=2>$DAzdRu49apS{K@nIkM1)ug->Vm7xzI`IFW$f+>H4cH$m@75@?QGre0VXG z5TUKyVem81$DtPnd?{X0NZu{4Z824%>a$z>iY29?vbjSV%639PgqOz_ughZigpss{ zy2ctvR}(zUakf#}Oy(Pc5Oo27AOEZXaPATd9+R0p)mNszC+p7yijS15ZKK_FD6hf~ zmme5cu(;SR*W=FV!=*>0t<&m9(Sp)E6h!l;R|Xt^eSi0gV5aj+@er-iI2_)#%@@VY z&W-Dg))#OhuT&F;0aETQ8m83=d>faFt@2#)d&N1=Iie6lkPQe%u&gUo8dd%PFmP`5*|E%! zC6M3C5n8L}B`Gzc9)#RaY6^Wws>KbRq2*6Svo)rKDx@&bjqP7zup1>QXR3*t=f;a!Ol#SeECe@jYTs9!I=KC;{L5zU$ zRmVf#5=B1`g6D1L(`k$5)rf2P_TKAvl76g#o7LEqf9w4+IOfLLEpt({Vd&TzEB&7C zK^3Ki8@Dp^INfzGl5;;*(D01=dtoLJIuMn)Yb(Le4$JGrqbw4njb(pAL-Sysh?DcR z0A+o%v&}|H@fk2;d%v48s;G!5$XSb#tZ*uNvMxuJ@Tok7l(@|coD+&!y>>nA{;I6% z9e84QBb>*bu|c%U++_nsK49s90kVn5b@`FID5YCdRW7LEh30)Bb5 zc?2ZwIL=^EO^x)ZWC$~c^x;hf48+e5t~#z{_l?oXt3@bOqjrw6d>ksLkd6y9KQ}D< zuW{qdt`3)M3H1H={vJ6gfR<|m39TM_H06u=BH_ry5R*1dkDC6n_Di``6(*tmOCXe2 zsU^NKtjzi_aK9pTQ2?pSmYX{}$-||$2)Tj%5BY#H4^F>Y8Z8Q|w5-uyFYqfu*l8`z zwxAEEoD%^z$}i)#mAi%ZUlI>OM5KaZ7++yc__Y#zH}8Z+&i92e)%h@;sJUnR?h zg-8r{tlhGuYTVcU`ju$RHF7AreD=2;Hvqjh>dBPvsx?`_2A_)#`5A9c2X$IJyy0C0 z7aVUUgpm?a?hrwmY99@m%Ux7kUlgi;$AiVNnS3s>52{TIWSV5`m(Xqbyla?)Q?RC| z=jrmp#M%+}M8=R2CQcnGhhtZaJZI#7$$+^l63oF>R2k&9+2n|K_DIhOoGN~lO3z7- zE%yOADeAo)Zu;n6oLER*sd6@sj$heYWK#F@8I9~w22j%KWCtATeU0cD24H28c`lsk zRE|H`j*Ys}BlZayjc(0U4}McG?gV!1Zlb>|_Hh07`@{M1V2jh+Mq^BKkIRQz+hl(q zPnwmN!AC+Y{rXm){=Q}IAq=7{-a1%0uN_|ggNV97Bqrk*3Vx2@^(q?tkjGJ0WvI-` z@i+_p@zi!OgO*Ft`j&zzYG`uox%s^_Ts z1C_UN?Q|w1?zNsCzjU-*S{IX(HIi}F2+To{r3jsU-hAOn_06nc+b*6gv8up&4?&Gr zmP|PQbuKsc$1Ae97;e?(mW){?Rck`>{9A6tvztx&3m_}+uXzKBYFx}pD(d~KTG`C+ zEZ1eFM`N`Rf8;C>_F4S3BaRovue%>;1F6^%8PDB~FwGTwLL<1p(?^duk<=BA#%jtL z+H z%gdzEnxzxyh2Oq;4JP?8DTzKnUC4W)ep8QpPh)p|mD6^XsgoAfHUO!mIsgo>M|DN} zuVG6}VZVTJR%#o-_WP600cQMc@ay0mJsocFEMM@|RWP+i6^FXa)>H_PL<^{{}=ZD1x9=p?N>5}S*oYs_x*n8|I7fBnDMIVO&mRJ`Ib~9twXNM z_nMyLhs7$t@5+BBy;(VKWH&JF0C)2g#)`weyu^M>OXVAD%U(!xQH+Mqr;Jzs4COB= z5ydlBQtae-!o$jZuO9{lAWIaNY$}&&#G|CE@Dd3Eam{q%^nVwBI06w@)b44we@XCe zl)2P&k-z1@alHQ*Gl%=A9aXTg!(_Q7Ybm{Q@=sqp&6jY~ZcdxN66r4`qp3`K*j3GqKysh_OlNWJxnv;yPgvk;JmO(FKsiV1FPHx88|&6zp?e)ZAOvJMy0a#(@qOyDkC`@k*w!cYF zL6PoPD`ewUeZI#z1+ppq(Y-! z`XbmOoUhjQU_FwYG`%hzBf#{7@V^t#|D-bZf57^TA;$*&G3^0-fmgqU5oG9xm;|4Z z(D3nRYuGdXDNke!CR)b$W$R)=zoP4hlE)i@1{Vp8C3Fo03{{70;fL*fFh6*|Qk$nA zpY|X1wp;`exKN9;i;1eSu0W#t)1f%?h0fzi!B8dPgfn7;cAH)9V`gE$Ci`F^#u^r3 za>;y?eY|-#7{L-hHlZVxI!?E{SR( z=X1;L`Q1&Uv0B>IV%&M4&ky7aJ0EW52;sDrWK>(tZEEsJVSlPkTB}NC?g9ts0UX*7 zh2p8giHW%mk3@ko=jNFLD={_>SIEa8Q!8D24l;h}h;$D6*?HaY)(9J*#f3lzzl)7l zHVw&hz^{JyxJ^1Oh7`?`ALsjZ<>THa1AC3!LOSa)l5hs~KBNzn^zY)LNtkdfv9r?n z%?Sg+c`+Tf`kx-2nT}!rXHg|2!56SmlIBa)WV=0q&dwdpF}<9&;l1A5N-OJ0+_mPf z9D|6g5@L-PxcM%?(86MX3W1GtiQMR5`GaOBg_tFaOk|!+a#k8Wqso!sTE?W2YgSd{ zY(aJLL;I6w?W_W82`;U-uFw9#D(M-v#yUl(|9QQ(KatG7@${j@K2>P2ZjSM-IGy{& zR|_JRcGAQD_yT;{!7YsAV*pmUPHV=bHSEOWlrk$;nC~hfWLqsQWhoGg ztem6CzI$s0`Yt`CBOfwB@5p)uDz?`orkYIKDbA2CO17{tZ;0F$4GDDn61rH7b#nL6 zIGI#zVJpgI^HCN>Jg$6Hh1d!(L3I~xQw*d_GjUYp!^UiweW(EFIdBw~Sw`plz`@^C z#Rd9am%as$`B)!MYO^cuX|(gT&+rF=K?~_j+b}vi&>#}`$T2qIh|LAQEZxQ28Y5n% z2tM~k$2ob<$FAp%%9@eYf+MKvu9an7s=2jBT%4CIMFq#Ju(K0d8jKExUJ5w(AUM>C zHDPYWb+fR_+m@<5aC7HSD3P}JN04*O2VD~%Mbgj5QNx(1S?rv(jOrVA9PE!J{=#RR zIn72QYqLD6u^zlzT5KDG1I)Q%pUggcMf?~IYUM@f54Dusl@C?TDI<$s#jl{P+!FwL z?xDFdN^6uKk39im5{ig`wYQ;#Uh)2cD*Q=2p!;xPHngQlr}eMY2wcm>O|9G~tGoSn z8-v7reEe#D-27Bq?{7#w+ToxM=zBZfBbW+jz;|i3n;O4Xdw9EBvZ8q7_I8SeWx|(R zZX(WfT}A2U)!3IO!NHY&$>~fzWSm zl{{J7Lf&(VW8kQY3OdQEll!v)bt_{3XSyo(i}O&hnZE=W2Ka_`o>hJA(C z`I@9E_dc@Tv6i`SBHh*xpy&EBoeA74m@#Q52zESRtOZJhfdIzZ#6I_Hp&M<~C?L~E zzF(rs>tNB6?iq-EWM5K9f?}-ab@a}4$CqYwb^7b&-Wm3N6{vr@pC8zo&F8i>{m>)+n}0u8OUetBrSUCesYRA$@=48s*~p_pT|cxFFDg zLmJT3N_asXG_R|BC$4vqhPzXEr`T(R!l&UXvV-&7GyFk9H9Vo(+~rmAYa6@s%6M~& zT^wmPHWW4DapfhHD4G99eu6V(bQ9#1`PG6kdt12(!R0tq*SQQnx7BAxvg=^+;b2`Y zK(soaG^WT?R_gYHaSbhM0ay_ikKMG0Lwnup;Zaf8n<@?wUJ9I z?DbcXrN|1d^g{|kJ=-t85!pb?I+gL(dFsi7?Jc&k1gu5)P54loSw4K03K2Q%d%Eyc zRbg{&Fc)8qnG8KL+E`qvN=VUND?e1GY!9?sD}LS9!_3+i3>}vC_cAB-%hOUBKAm4v zDsF;pJS`+k%yHjRA6+JC0^wn=Gn}B^in(3dzx1&hw+)RYW|j1@R*GM|DtQmjiEhK3 zc|4*mZp?aYqWxXgDVB;(nL%Q}g_0G6epXHeND9sX-Ia|jb#bqYs-2n9!epe8i)%;P zV_;?3%mgDoOMdk(A7OJobE!#-!}Kjc(+c2O&}Ct5fx!tsdfoxd+^wn_llk!s%vmzs zjwdl2#%7?3+5hO|e0PX*o};N>zUV;9_Hjv$q-o+DKkAV8=BlYcZyK#WZtSi3WOj?w z;Zx+xr`xjY#a(*MCI3$6r)0-_KkC_aY16)DaG6nq_z=2ySL-9Eo1Nnuf`uMfw&Daq zg8di+O)2X-otZaVuU+aX`5t-}@krX*8}C!dKp#&T1+AxRcob=g04AQ`q?}@IhH_J- z=+oAEfzLCIriKXHrJCT%lsaCihOj~xK~j2+2A;VY?mJUOh7j6!)Cwi5AwPUdYEY)W z=J`Cc+%ETEg{{mLKj_%r1v~5=3w{`$7|pbo98fN!<*tfqxUjrE3#WB*a`tHUbZc6w zu29m5y`y>5*%=UiR5Va33$ITAHsV)@X3!ixl4R(*-U#!9idy;ZobyJTWNyl3lH=kO z?N)FAsF4&ay#2fs!;$t*V3_;|ImBA6{u{e+*lw#L;lhYOYj;xh82mhSf}RqYcEzP5 z5%eLV{D@z*&5{%doxAWr8FZWD94=XFM~J>{8vUaR0@s1+#^ys|Ux%jcl_=m$og({K zh{fzWTfAk5H8@ShGMt%}K51%2I28K^X~aIM8F<$Bl+Ik8aO*#!H!C2WaI1K}VK|Y& zdG`(5P>0iY@j`D(^OeBE>BhsVG;$fu_G%ovupQ6+DTXibtO!6cW_qmI{?g{))%fqL zxBx=o7r^kdoyPk6p4^LC3tarcq!_j`I!H(f|N9UYL)A54&#>3 zK1at-_PZ5Q6Q({;EEnHK4N3SV3x}DOkmNsU4>*qkl_ z7P_M#)9>e zN0a!=s+1zTW0nv}fc&EMapDI}kVL4Xk@Z#8bE*GfVB%TD{osX|WPH_E3sk2jR!=S_KgPXUQY=TxMC0P#rzo%8YpN*g zCH??zykh-Xx7KL9v5p{HN)KTq1b{YV`A4^#j#yXWrZ+6t_M&#oy+28IecYZp6VhsC z?=fH5oQ|7+*L6AfQ#;ZkN>TrI;pyOF^O-X{m=qRP;9esU^MT%j<1#*x-m>xxF9n-z6>W4xic z-_*CULJ%emI&Fv1PTeJy2_GaEG!FyL-C|k`y%+5?ml7&ENThkyo4S-^Q#wfs{5rs1-(`eY32Ud+c1F^Ag$u5@!W|T^RjBR3k&BRzW;XIDyPv-{Rq)^T2_{bT&sP_3gTx*ZaP4ChDH`8rNt|Zq_zJLFSgJYG|rkK9Fz0l({5GBnmUsY`n zWhKuEHC(UT`>_eM9Nb%JeT>$7`%5RlFtz6@=e}CFjNdie(&)QouJRSz@?>IqYB=>8B`pX1knZ+3aTZ?T-3nL$V7k|Q|qj6{;XoBt5Y-6K5wG3&DUv=jeU=5HnRzvIDg&WfB4+_4-gK}@DbN*>ENy6u$CeaJQG_5=n9I?ja6F9_e%TVmum0?;X$ zXHfVrc{g1>z+5G>jnU))KEZgAv1uvv0NAGL*6xV`l~Q+!$)CSccKW+=u+LAAS~{;( zm#gQIJ^(l^zJy|DH6>D`j;oqvYyE>KNsiDxaC7di!oFj$7IZk@RH*P1rt%`G(4T>% zf-{SXct|{dGZ=&=xvH2wA2MIeLcZtn+0MtqdV;ENJ;Hv%-N}X;)yFbCVMZ_<1x3y7 z6Gq_BC9SwN?Fi+z@Ey?&?$Ag_9#@t8eActC438i`f*a$JVTF$$^`1ps+Hd1H^#^Ou zge_HQ9$kbvo4pCQrLOa`o@I-JV0+{CyeoydNBfu9`k*dktmtE&d~_YQnZDFp?aXNZ z!KSGgVb@Z#dHPoe=s{UNSe+Op=Q)JREwdE{$^vbE3<@m@c$P2E^95vP$6ISo7Jbc4 zny%K{^H;J8@6zRq?@HV&9?{4?ayo|h!;9IhK_lLo z5ZlmBFtM;`Hs4OU!V6q(bGM~~tJR9y2iEk&_z_$8I`X#=10W)L0&*w`MS`ghFEQ>m zuP})3rfYaLB5Le2`RoD*hCaNrY9r|Tv-@I%tFX@r0fKq9rhw7Uw;SbEv>0Xcw%d=O zT<4?cP8;954Dqo5UzosKzIvfC8%yxUWK%&~I#~h5n>u`aN?K`tM0Kx#5@~ingf^po zXOwj8#2fJ8IpOUqoAs}B+n0Irmi5c}ZI4gQuC*=swnH&=UYXDs8qP0d82(DcjacS6HZe|Bl0Va47<63wb;!_h?vDf zg8&>}A@C>BKtM%}W0JSI^^nJD41>!xXzMp<+h&YwE+C~0xn5OutMBfcB$^frrIOJs zpK2R#CzD|cXedovMS{8}K1Utq@B4d?z^z77!cUL4m4(NdA5E7&SAlx62p>*-@B$g? z`}>fX@O)vWX&b99(QSMtqsB^}5aL$L6*(3kMsy=@7Re#7{ zX*pqgm&I&5lG!E{AA8zYsoRR5AC#|5HsSvk5C7t(eGMH9+&lx`e#RT63W<2Z-v#_k zP!=k9NqU?m47sA{$^FGWcn2D8*{&@$<<-oLOZEZ}M@;ZP*{`?XE^M*&fOvKqI)#!P zFgu;}OLW}0VriKS& zoN1vxkd4GVZ4A}$n~vM-On&CtHhsa4-EVE5xb(S7llW?s^ivb!W9>A2OcMI{OWz1` z3#RErOd6$N*iibvC$2etoa~sDq)fRyUKRZC>o)u3U7)~anp$S3`c`QH^^a5b^^Krm z6_e`0$eVpl;Q)#e@|hCFk^>}JzA4i$HMOhd7Hw%_DK83MRahvCbrwvl-4abVm;A;l zn^mwpW)ys?D2;{Eh=J6Iq~}AnDzD|-^U6WGa8&0s1`X$TbhItAAG8L{%R~tUgRTpkoi(Xrn z|ELvjsn%ao_>~B2QJ2R~;ArfLr0|($ou)Y;H}-J5*FqlRpm_Y9U~mJWbTCrja%^X_ zEt4wsvJd7$t01bMm6aW;;UQrTXxdqkVh9QYe1S`aC9XBPRW^Sjb)Yai9RiqV88(a; zpG1cU8Z3qo^mWLjn`kMis&O)ln_ILUE6ey~7nI0{fV5!;RBrYKB2tGHwQ>#b)Vnuv zt>MUB(p>tfINnB6-p(sW%dgr^=*K?@HK;G4rZ%E!@tp||ENr_q5olF8>0wb!j z)a$R?Tf{>w1jl8DfW}VxNboC*JT{HdTt;i}A&rO44qYz_m5r9~JMX-;16^E~lq0!g z*zR%R=CNeo@-%<)*_2VmV9IK%>Ui-mgJDTh#UcihoKrXma5lU?nmWUd5$@l*;N7Y< zO~Dcj^HQ@qseB7;BTD{YE-yl8#S(?ce>ukL?J@pi8R%&K`svLzd95fBA$}aqHkxy9 zCy|vB6>9e=fcg2SG4#PqdSFEF%)0{j+iS)*BBtu`L3sW|J$AWBFPAX-MppnaQU31a z<@JK0XeYkE`WL6A+LE%+G8E>;L+W2@$E~ifu8all%ztS%f1{ifC@chI>fTg~3YM?4 zzVhf3))LH|j}i}iF&A1EBYymF0B@}8%Daj?ZIe{_p(OWv>YK-CANoQuZ}B#mLt*h= zoqf65A9B^9=sH`#^fS=nYKuJ-*m$N0N5Ic_LAWR*Fn*qKSD&GmvzdPJJj5IjJ#%B@ zM?hCc*b07b>mE(HB#OaMr1p~ng>JK7Rm5Q1a>bl={#b7PdG5AP7b2HP2Sa8$sVX5p ze2CK_`xh@`{5qp%QMm9i7ItG+?vH^?e}2Ib;}GEA`T9gbP3aZ zSFM2K(1kDtYUZ59;0iplMCtZq#;BbMQA9+sfB6jjRy$D{K46@r0pdT=2CUB%bl6n= z6lEKJoj(GWE9uU1P&qY?9F4SSKvmbzACq*JbW#=em-+eTjbE4hoY?d^v<+^=@0ppa zHnp$HtK!VM@vSKsLBBTZ7xXr!F56|t1R{y`g$@iBH?$+Vanu(D$vl3zmB?$@TsDH6 z3clKG;2W2xe$gqlPvinaW1_pdI=|;69xJdlW7OJmUxxCo_-6@_4Ppk~FtZ|nQKL{z1>nkzRy+e~!JczZ{5TP=3-RPk|{2CxK;RFrki)R4l?F$bgH&(*#1Zgn@(k|6nwfaTf^kC4_N^0 z+K3UzM76KhW>mikHU&nTDiV;o<~iorK#Il@g#1CXdEq^W2i#xaI<#ysAMnB}_a;)0 zC_?8yjoM#cW;?Bp*YXf|g84DOSbQ0{IxskkKsAtzNm0drDX%Sd_rgL4#XFfo)xZ@| zL#Fx^>eE+qm!y}Sq?Zx2v8e?E+jF7}a?RNAA1~O>6#C0-(?)Gt#@ye$$KKJ9-<*s= z*YAKRh6j1B7l&07O6facQUxo|HK*#`efb2whT>9zEdIu22$~Q{;m3@BeMMUbRBw)s zr&wmMUl-hz^ZHlPPbCnyy7#q~)Ksr?0m7PRIhlFw^baPZL`#xhJxr;Vl*d!(w#&C`C8eo>H5AC$yNzp)}TV@!PBLL|J0Uy==GvNM7R0q-F!8Y z`q|?WC@BRQ^+zq8bZ64Yu~2|XOgz;PmjiecMBK%C82}OK662(AIwatzvgJIZox3aC zfUea?7vPuu!6$k`=p1Ef`Olz!UBePL$ zvk{a>lLi@dlVpn2*eV%pYsMI*gmBtukAw8?L}^D-*eg#Mr(^z6YlAUHHmL+gbYO=k zlfD5toRzltSk4AR=Tce?7yUNA5e&n&tTWYFKAkl0?c{!yUv}V3V>NLE%g}dOE=lp% zg&fu8JD$#5zY(3?Wy>g1nyciR8f!69YrPsv>QzXMk|<~;A3la||JB-po3oB3_;qXx z$fOS5>)aeUxk)42nGLetO5gW95Ddsaha^vD884aC7`$HVc#M3-n~f#u&c(=;$(_1f(BhVqGRUZXR0shK(iDY!l!rW;C zDHIh`2bOy9=dt1=C`4Q)bDB&SF`tD1UOOYFb#~nP=*jNK?&dixF=fS|0?K#V;Kx@m z59TJ9-xaGp;A5ytdjn01^fDD0su(NQDxZ6@zWpEy*MnT2@nc-Mvc;SN_q&yp%XkeT zYD|_t0T5(ktu_E_fJV%g;Isy{?!Qoi2zjs3w7LyvFy!T9-7C26$i9Zzw^+uk_#xuR zuIvKj*y)6zZVPqb>bw6$9IXU?2N~O+@mW3QMkB5YUil~({OWaMtN+x za=FNP@-&Edt$N>9g5Rpmva6V+heF?FD=8~0d&Kop=neTayi+9uG71hV7fdcpq~S81 zpA`pvP30q_2~=Gn)vP^**5KQjFx z2QFVe%=v}$Y#Yd^g!Z!;Jy&kN=Q`T0{p~^$UJvt*gPw?Qx?!=@S4(qc>i8 zbYJBto^)jx+Yi)quFewJZ7FMVmtm4ij!B9J4VBy}^9o6>wo8@L?bpRDl@G-0ZI3rt zPHE#M@CJx<*UkI<0+T6w!f|D4HkT%_IToh$mL475{1qvN@W#EiGsz;CKkpCJW4`K%MI0_&2sh!AdZ03r2bw3;c}J|lY`n+u$Do?yppn*!CkS@=(MSJu zx|GD2PvR68mVMO`qsUitKRBd46qY?GTU12<)ifbnaEKXUQIog(4`xtqY|-A$o9D2a zIM&!$+qk2ntD~iUVDtm-c~(ghKt2`vNCs11DJV6chd#^dkbvZR8g^BL;d3crQEesS zIRvB75YD*5h#hbxEG?JmZP7ma&qdCr@MdgOFV|J0fCZ}Qcf4;(%JoM@r+s6;q{bEn z*Y@UgH+tUIQ~zTkaMOipOmMos=z)TME2H?rn*5R(*$v6Q+L{2wq@CMaO7dpH09B=I zSUzwtwO5>vsztUT`?epaBO4!}(_CJP-|YL0+5t@!Vg zK|Q;&Ws5cN?)HUMJbxJmzEyIjrL>*!Ms->p+P`zeG#r5y(v29QzY@{zHH?$~f8#PT$oJ~A0tZ+f9MAu|+VyZGN@A6OQ zREN0A$gz>El9h`ZcGB*1K#CN2clkB*SBySbtg4#IN;lT$EMnOLMo-1*C9yacZ=) zYrQw0r)@bkB{XX$BU6sEdvTdGEV05IZVecI>A0`v;PyO2?x_qaI@{so&C*M$c z+$@nZbF%$P_8?$v;Aqq~J}F;TcX0@zN(c9&QS#(usfX^vOkU&?)SBjZn>>a-ckEk< zkb$ey&zWt>+GIes*3%ckk8c=nJY)W?kKCdbJul%uzJPy_H-BBZsTFqrDp@M*o$o3& zU`&axo1l%ng3fo0*^K&<42Jw)=aGVYII3Oint=6!VJYhArPx_|_&!O-pQF{K<3B=2 ze*s{56MwNGk!_0zI0nU)F1G8$?@GdXluh+pgzXwN?T~M=71DROl^M_oG5HZS5`g_m zz3W!Fqn1ZbgSomelGs@3=M*jm`8v9&3Yk_O-BTHm*bRrSGy|HrRYvuvxi(OzMnrUR z$R^d_rc5XP5#?%|Ty^nEhG2K@^_H);59;_#Y!Y$l-q}tTCDHflC0C~JX~^!<1@Ggt z@h)%FQ`C&If4n8~E^m1iFT18utfRAxB&RK~x=@~;giVpN>Joji_#-wtxy@nRCA6s% zL827V>f6Ut;IhljXO~l;%7E%Dwv!gFgU89<4L<&S8TAVmT9`Gk$ye~?GKlo`l=XO_ z&B3RS9PvJYix!_0;$yWPz4+f}(I&Y_cYTg$R9+CGPuSmFDCxkcH6K-mb#y!K>8nA& zu7&l0dwsdF>;Jhuavv?Pe-`-fENFe`!pkq#A;5N_?R(8a={yDDnfx7Ez_(L^n@>*r z2JqdVVYW8P(Yp1e9u5t@B$SWU8tZ^463v$S=aFbxQg;*ZxCtz^u|qusuJTcFqHoCM zUXr1WSZ+R2OV57wy{qKhigsf{%&cv0awYs z{I63#^FMO*efqT$<@o!926zC>Q$k(z9mD{4o_o zEM))mR12)9zF7aBSE5FnhS!1F#SF*^%+xvAH~Q6CJ~5wgNqliqp*+Kw+eP4M<*FmJ z>+r);@mhuC?crT((-z=z(a53MVB3aj>1KgjPyYvU$L0SP`@EqsAa24BEy@M<@pb-ERsdHN8Wm8#i2qHDV4DB&68vxPg&I2OZv0rp2EtYVc-F|V>D|+!F|VD z`Md8nUwx~G@2_^CW(JPv&fe!Y_XuU|F@dBe>pfNU{QqD;6CbCQoNP)H5UpO?AM!%O z`mUt2%nhG*&AO&4Df^Ib!$^2YHtd^8)gvp$GPjOh<-l;`*!NMX$C}`DhE?~|&*dn6 z|JkZFy|Q9$J$1m9dr+lZ>Ww&Cotd4TZHxM%*!!A8H)0vBVDMP3k|%D(cEz!M^Hl43 zVBAdkI>oEjQ)YVqg0phYbfj*(LEyY;)5*;JWMVdbE#z#R_NOOjO~8hc&Xv!(cf=5_ z=dQ%{SZQeGJI?`DEu!yt`G$c$Lvv2fG?Y)ppWM`B0S7i|F=I%bRZe=q) zAr~bOhue1)PU#vND?BNwB---oaPs5R9{TJUah5@azTUDt1-V&xK99cH>8q_-Bx?RNS`n&3Y38Tl_dYK$k>-APW=!TJ#)&O6dc*~sX&v{*N0cHNJ0 z^koq`*};NSpAOdT53p-M*~(^z0`?oX)}OPplFJ%B?KRz*yubDc7M6cUNmRqB>kxr( z7=E1mnG&B!R)_jtxPm*vO||z@`+2Q%V7CUR3~8_H=_nWOQ&n#v7GAp`03G75m@~t<3|47{ZyKkzNYRh7;6XZnbY7bScHcQqt&A1l{KB+HaGcJ6NUIPP z+YjGF*Uu-+UxC{NS}rkfW}9q)lZ5pB+E-kw81d;I68yq<&iG3fkA-pSZ6+!S9^uCR z8=|YiuRd9&-#~RTo6%dl=jn6=T2kybXUiCrf-yPwt8u{}&+ZUg6H~m443-eH-J8Cd3J4L&%~{{Bv+FHlG}b}z>9sz{ORk2$NtJT zHV1qX%-b@6oP4$Oo3zS9Wz#EB!4ABMzcm+fGt!@Wy_JRyif`5C0t4_lt74uP%MQM0 zq~#NF7RVFs3z2{|W47o5o^1j*uYgZ>6oq8~QrRX#aF_4RQ0G?9g`(!Me7pw#tA_3S z7yv;G;yY%N2r+OSyqX_lAT0!#Czm|796i`%%aVT&%Wm0H!eD?*yFY~{cf}7nya&*z z8%g??Y2q1ZeI?5wBFKMIC*ral_w1M!v-K??;=rUq2~8`1b8T3mX#i^Yqv7jKvY181 zlz}xHtf`S=GiNGuK(K$sOAI^R)sBu=;LYh5*J!gR8nzzJ zj~KmA{jXyK8vB3f@`BFdyj#!7}8%^z^ji)*9t_BNt`MoNC1%&svgT5kSPeQH zq602**v{Pnz=DwxYV)*N+tW~cYL|;=>N!((9EK%m^n+P(m|}_U;T=6wFWdIT!DU7Z zI#Z-HbL(5Htjj|Al;7y<9v1T{(pVVX$TLtbQf1V~XL;~7oh(1s9P~Yz2RS#i`IM8y zJ#WtT()W2b{YA*}VwWO7=0@iN*diz!i5wPld9ISnlbXum0c{?WgHFR0fND^2NsA7~ zhqken+Zsj~9O1v>y&mXM!`|wrV2yk@dOvmNmc?-*vB=ZHp~;X`S14cqMv6V8-Y7Nv zB};qT@H+nq#L*G zL%~ET$cM|6S~C;UZvV~Vkz{n73`O>ntj=uaWifIph-=?habXMifPhjJdz=K}Yd~0+ z&qAs#j<|$L{8wl?H3r@@a~KEn?-&7;Ew-5-rKgc5Q@e4|_NW5hdKVPCG1N)aE~}mo z!c8*e?Uo}p6S=3Md&4<4fXao-CuAJ!K6CX4k3=DeZJ-U*J-AEedTJzhj|?@t%AYX5 zBk-x6v?1d@iY4TJc1gA?DS88YwW2p~3O9OFJC8@n-3(IXZZ^A23>@6U)P8Q#YY+d# zM1#nT7ZFSw&K%f!Ea`mK(hhyQa~ZfxS}@ceYEfN%|27)>b70PInTVfk_6#L-%ZLD3 zdHz9dGxx@@6A&V%j^W4U9?}TYjFEQ6Fl&wYDJTAx=?4KyB0{hn_7_I}sQ06~%`Ssw zI(xR;!huP?#3Tx8uM7Yq?Cc)UpT;ZJCnaOMuo3PuqS_jk>$ucYa>-y$94h*gcmno7)0OSBSSay7Xw!i*tu_ zXU`;4?x8J*bDnKixHSxEmBb?+ib-j z3wJaS3!+4ZTUe=@gwB)}2G2-16!72W$mzRr1aqHvo<%FxG)>B+-|rUAti{`8TbEA> ziLT>;A6*ZJbIxDBSw%uyLdSI!&drGVa1-HyxzU#J>i2XSe$u{qqwB+zT#9etMs&aj z%i2l??VUrQc-tb+<+rJr01p5()w>46N>UTzf7abAY%+io`e@{K0X`W25WHa;QSmJ? zFbiyCVrP-iwy13O?7g!_8X$mGaV&JR4cr50990n=29H7cp-(k{5NH}j!xhQZZ^a?t zb#Aq@=<};UsLM*AB*uO9U1>{66!O98@2?q9W`qbae_9o&n892d>L=J(pp4YQal6xe>xSIWfScvj(L!BKvcI*ffRI2`o&zDX z1|fhl81cM5FvB(S?l8CP=-Fiv@12U1E!eTG4$W34ab_}fNZCvV%IIAcOMMJI8GAy2 zwiL=A5AA5 z5miUPc-wXZ4N(JE*y?%hGn7Cg&Eq(%07Flras?Xbc)J*YJ^P~{wf7FYqxrJr`^-D) zn5@@NF3QV+qJj5sn+%wVBE$s4q0c6ud@w#!!V;vzyHv6Y$;o~otX5E99ft;q`OTlc z{5(IAYbbSI?Acia0DCUpQqU?(iGO4(O@A0z0_ZmxvhFmji22Y;?cA$hX*zM$5PwB| zzRY8IKhN=!<;$THJPA|gvQ?iW-Dj9|J<|#2maq@^6%IZC>XLR%7()0Qg)%nlx)%#2 z9BXUYZ7C@Etf#QAxOihYj8&%PRzOn1H1S|tA^?^M+Vn!LI%z-mH>e}iXMpB*EqXT# z)tr0ZO-T~@Q;@ZtvT{d$)$k;pp6niViC*V#y^@|I-1|oPI+KUa;i#ULo-J`<{5Quc z`A@^cyr~PXr|GDco6Y3S$KPPi2=lN-gv-}M><9KBOUj@WxPd%~S0~V*mG#?@r3&67 z@~bJIGC5tpVw=Es)DQ?jxMql|*!;Uo5i(Vn~(ikq$p!+j?Zqx zK>|OiAmz^Cj4RO9FRREV2ijc5LRjNJ-L<29{~uofKgnbp7E523;M910#~d`PhjRPo zY7gzQ!5rjb%@ZQQ+01W0c<>vjxJEx zEJ&bK{XZec5lCTsF^@b7EHpn2YnFVFRxXz1LKrj|CgnT3To;%4sUdDwMHtmkLk{e) z$6SZUz}5qRugZpHAoS+*#)rHjQ&O1`bM!JF38g|3ty{>8f$t}TVd zKnNx3#qH4YTV{+=QJAb;-tN00dtDA_;5WnP+}qLPb+jnBDEj9?$vAH&o1xZvO+^#H z6WBqLbSi?KRV#T?Ftcqr#cv_}OqlE>vn>&rp(ZP310T6N4-5p^04DFC_g*F*HBkk4 z`lk~9!N846UAvS3td?lXOjF_GsTu#IxmJI_l2D&SV6R!z;m6zr!a~}D>ZcMWE~hl6 z&g$=AwVI}Q9?&#ISQW=d6&rs^+~jgQB?nxsDXQf`^1?A16k?l=hdj{ZrWFb}e-Hx* z3pdVkh=ZWf)ZUQ0>CH0rbf0L9i&XxAsRxdb816|@W&)Uj)-E#%nF7$Vc;|;DjT-c* z0-#4qy}#9Xrpr*D`yRC64cHPxY(Bt=MfSURU(I#W&&b!cDw`LA1@_;xLcgsQ*vV`~ zb7R@+M(=*cjwhS}`jceEe(DEqT%qii1O|ZG!D))#F-b?E*Js=P!bu7FSqTm+lX(qk zI*!_WOFx*X8L?qO@mu+qQCJaTeWuP{0++v@M`VRe!T$F?me_wlp4)!R9vc2a;NXja z9xVognsCya^0|*{pSpL|=7Q>T#pkRzU`$}#-`q9xoqDKQL}pgji0fNJh|zteY3oAQ z)bY*5gTCn)fq*RZ`7Ae$x*{iQIchy1K>tG*m@s;6kd{qc)j#oLhIw3vm0=dHw4$Qp zky=X~7^F-TV94SJsaYTadK7p~bf9v7((@YP)~0lEU3`&R;(F?$6Wf|JgZW*S`o(yDJd z1ge{#Gty`at@AyyIV^~N6J7~tu+a27k^b6_gD>#nVxvJv3ts>i09Ay`$6kJ25?a4m z^_`vn*1sOV`GTQ6+6!|za`xYMse`YH&MJMHaD~OhvHc33TF=2LcBjL=t%b&a+%Z+J z;Y7A?10m!vC&;pA_U>iQ1v$9F8&w5xVftiz-XeiGkoV5SS+DKxbl32CJNWu(i(`Yl zTsj3Ixx25yl$e0~oX^K=OKrCMkMm}yjh>H ztl+F(e&xF<40sKf%L8}t$AcYOZzS;lAye_M_wav_()hprp_MT*G#WML`*-;%)r^() z`)}V+4-U-~gfFeR7Q34tou$y7Ek-*k zP^3SNr@=0`zb01S+m&C-e^A}~ab#t{SHJ-2Ud%BIlC`+5duM0>mH$?1-#zOMeaG@0 zg37-&FSwa)XpIGzBrwrR9Pqf+w`0aP)ch?ftIC=c~kQ$bWkDiI7`d@2Bk5S_$X{HkRZ zf~z3up5>QZf_RlE1y;HBlzPv-lv4Y@XnV`3INB~u7Xk^vLkJK&Kmx%D?gS6++PJ&B z69NQxcZbFu8fk*NYvUf=wUOz(^38m6X3hCE^%H7Q4^@xueedf=U){n0&G31xLD}o9eKLjTnn*uS}i%bvHvX1?agE$$LWgfj9<>oo533nsXk#oL)Kvu`%0qC z=WxHcnFM0YgpkA3!I=!^WJvb(FTaWd-d$47{8O3{RFADV^A26T=Y;U%#o z_{JEYgXG;C15r2&N-5r&18LBdUkU26wXuYLUzcnDaH7nfNv7*T5kgXdws=J{y*yu2 z)9!5!#i48Aox0S?j}!DU2V!>XiZqSh9&FF32P+;=2fwye&n##{xnioH*mV=%&LQdKY0otD3kuVZCi$~njd$);#XYltB{(_}@mzb^#C z;^tDY`v&QF{vK1Cxu=5Z?!5X`*z@;^ynpTZss0#~8=NC3@(w0#DyT%6>bySjo*qd+ zDc?xts2Ggos-_Y$urV=N8|e(E9T2@fYF^-ot?_nuMy}YuY=TC$I5D>A=MT!t4$J4U zezo3rD-Z!v^U!onHP>4at4W>;)se%|2WyVlp{BFin|&3}ERPc(_p4y9L5q=yHlbtNTH!-C0)*m{y^Rjg*yLE!={7rR0cex!5FGfQy#g zY80L&JQGHwV&kR7(C7Iz8daj%{<%8z{m^g)cIAh4vDv9fv#lh~XTgyu*!8FdlNL{k9-%L*xUWYlcRih+x9%aD4xS$CJ(opR|gh9K0 zUm0u5EC-rdMzndscBP`@rK+%9)NP#7;^ffr z31d2l`vVf93$6J+qZAl7ZOG&8#Gl!#HHN~B;>CmUWI|~3_U+ln+ zOU1R*9&BO5R}qsI;kCy+U&nX6wC8rZa4;aGVN-qqPWW{^JvY)EoHnzWytp8yd%-e% z*b14NvdxzY(;Z^A;r~#fO*Df1m7bIM@q2N6DfH2NC&Mzu$&^Ff(YZd@`wFQll1AVu zs&w+&zm4a;<7Eme$OW5#0bTip92LVd-=gbzO;N=fOXgd{FJ&2g!!OTEzw*{Dj_8u$nJoj>2|^(*pZO?D#`(wN zOh>DakB!@(A$e-*{;R7!{iq)eQGBZvzSD=C6 zDR1M2$?-OAQRBg0+?kS=VuOHO$LDuF1*PdPw96}XV6?<~mU~GFwfDgIjMx;Q?3_;8 znk2Fg_n(8;{P%3?&5RSum7guOu5-xDmc_ql&-Vw#?ws~lylz9X3})J>BIoMwrhQ@* zZ}6c}1Z$d!@XF0GeH7>4BF-l%lxDtvSRVNnWEjP;WLe$F{IH1POsk));g0qmvRYzQ zD@u(wrhJ>o;d(p~QVf!AO_rMR? zt>)hZ7Ti(CnmPALqZhlY!QKRWrl6?~2)?tQ@K7|&NOix>ADb^Q!L1Jw# zvGQn}(Th{PXD@n<$*H+;P@nBOd#tSw>=A+bn=F^aE*=I;M})qItNQ%bk&NyPfqNXW z`Lh0e>4Q?jN2^<}ikS^;Lgq7q>gO`ith7Nqai_&wUW~ey%PmfXCu~7&jF?XJLN6YqO|3%BgAf`lTgJT{1AD^+@US=RKTOQ+ zCNI$TsO@US=6wjj}drv z7XeD@qU^O>4$H_Snc7g<0mmbA!zGjvVowi8+ur4|V?7s&sbNdMyk->m_u-<%dYSyw zQF2jJJPo}|qu1BPXH}JA`l=5I^^VcC)`1HaP)d%f%y3KBT9h6QLjFh^U8&(mD_a5W zcR6VQSis>@%s)}%3k{+kFth@exEk{9Hok3Hk|oRNt#35=02fdl-lj<7qtbkM+ejNy zOvkGCZ!DlNp@_%gL~jGu+1X*ALQt7&g6CRv5iN~f_9j9?yIcNcw!G+Y2GI!Wl+rm0 z-@*<={}l_%`S8&X_MM&XHK%ovEkN2==$312<7=Kse%($%c31nhya~G$0ZQC z=!R(EERCl}ZjL^h2HhzaCTg+dJuE$ArTWwsO}M7K3U5*nW%Fsr(K8C|Si$2I&qRmc z;Y_u4o~j?xq{39D-S%=FFOR}86d?QtAHGT@mFYFQX0Se5-! zzl!+oa>-I3O9Atuz}-AX9}jybQir&SsOT78HkNih3EROainKTLo?@slj8rAbSYgD; z{Cs=Lb@;p3OZQpmo;`3FFy~P00p#af*5V8E1>GaQe2oIzjtsDX97CJ6R%5MkO?dLpDkD&lFBKrD z-gHbh90{w)@F`}n`wR<<12qHcv6#UBhK;sme7JWdd^(JVk?Wq{9%FxXrnZdp_SLm( zUQypid|iU~vl$*2ALk5eh+27aSpDpWi6M?EC3sPylqOr-rc2Fk+>~=mJzc+A3%lA0 zXn>=t7B;&b6}!Kvq(VTt^X+ocmEs!qzxM>Pk$7&ztP;qQVXVL##tltx1tcbT#vWZEfj z4G^$>3Kjd9G6dyg?nyhRc7sXg^M<=%dTK-YU{jN!wwvle zDUA9m@p{qo^8H0VZv%Ax`d%Ux*m-{N&YqH4V`IYqeXwRe&+~>cz!pCE&wL51kwdGk zIvwI^$6(9{oGp75y*xOsNOL9Ss9q~V5U7mEjr90q*(kn8(%8R8@DO>}9k~e*$UR;M zI1{kj7yIt9{QxrQN)B5^2xJwXQ@C35$7(9#+kJ4FffqVqwJHNxB{nbihbB>4sjbTH zAQfvex*}1i$9@F|33alsOBc|A1(c% zKnl63sp*UH!~CM6M(2$=b1aw~tm2OxEQby*-aT?7yRw?iU)5waoYS4F&F^!Z3w>tO z{p1DO1f|?OudUB-wvY+bpA_xM0=8RCzQ9wQ1l4EZyvf~;y+l<+S>376~8FZeDa(*1J^EoYlJ+h0CZWH}mBvn$@ zMEj3MsxbRs6py1xC!Dm(N+!gSp_X+9h$5201%&Bi8(B4(%Md8(aNI1A#Nq{beT`Nl zymI4BO@4;(tqz0=(*5D+T{C1RXj~MQJtiJ;wU(VlE0zDn zK#iBBFu}1bqWjoxu1%^b6KS^{A`Q@9%*vbr30Ua)ipn)PBBH;i@!*}PwzZhR){5nj zhdY?RvPWw9$3$s_g@r=P@wy6+%7olJ^Bgx;7}^}uMhijn?$m>*?N-5*t6{V5;RH)PNrMW@C>)s`f#DX3zj8~`?78Ihu(jPj)& zZx%X?7kIcROcs1EanmiyF_i9U-+1y3#X-6IH(Xhi`3Z+@b7O#?L>*2i8j`;$M|)sE zHhSFW+PJY`?b^es7GVj1)!$O2H)B8kezuvQG_D`&ak^&h)3|j=;E-P%4jke;t z<&^U+2)%-;@o1@5e_^bJuZRJ_+*|%JcxRc*H{o_9t>WQs=`P=2>3&#!Cb)mIv3yB! z=;G%V=J7|Pt#4cBMZT-<$ysskPS5;EL6X!-yF*!n+h{j)x@2flHypiV`wFdO7S3o= zg5VIAd#-;4<~gwDlyifb?U~gU7Umi+1;xLlsXl#OS*hyi8-zG^pQDp;AtxQbsjTr& zx-i5fimWGTYSsNP0KV)l@sK61vVWr?eEqo}6wbbu%Ju)*l zH(wU(pwj;f#|cAfzL;Y3H#u*8bzRC_-MYy*wZdYiw}KhA$G6m; z9t;i=M~85c93;x={-V7_^w&-?CtOz?0WrSN%Q4j@$NpbL+kYseu!r4Q{uHdT_!K43 ze)TnSY=e!A*cpbRIJgzvB&mRS|BkgQtS*n=#*C_1rO9eF(XgdpW>NUR^4fMrVPej= zzU+sQbpntMGmy`ETVz z{eLST<{*=ugNnXI|B6Rb!9k_)CY(;B?-Y+(64?{d7+A{dQj|rdJ0=Cy^^agadyJm{ z6eiH8l)PY?vphv)RP;p)*7-&8dbL=m`ESHxrHyFD>Pih|J~8O#@7w)GrS=fC5DJB( zvNt>olX4srsomLUM$G;eh=oX;{;k&+QvX}IY}QKvfMe*Zb9JF&{29?P6ZF?hg^~Ye z-4K}I;8=_8LOeDFL_-3{G9OCG&`V@2Q-j@0MXg7^H(w^zU^03mS*!%}gbZNeF+Rs7 zD5)!p?EhLCE-TOT9{200AhG$=CD6=aHKEztcWk+DaE+ol1!{S7rVP=YYD zAw9R7kEaqOo4%B0q!o7Gmuty^q(~ZwU!BxNezN5Ik(zbWt{u(va-8oq(Bs`ZAIvZ?5rC#ixSC80u9<&GmV>t}x% zgjP;FnNguRLW{kcu;?5BJS-JBxeY)JPvsrx0Wz^rKe*k>DJ(VnTjRfS5l^tclNO28 zgFvTR4~M{=Svv(HM0}3ig;PskGjj^y2uRwloW$?r`a%so8e!T9e8+{YWOe73`|*B* zFNv0Wp#J|o;@ZL z4!8%My%*i+PX4KcCBbF!;wrcn;p><9muASY6rAyT6PlZ8Xjb{tO+i_z2L3ExpX`bG z8oEkv&!fP08vbWq8VRBFAEc~Y{UfO!#uGW7r+(ch6t+~{I4PwwfhlhjAfV2}AESF~ z69es?=+dYS83<{a$-4QuF8*+>h;$wY6TG+z#Xj_OQFhjJ@23_gYqE-M@DzVV*;j>$ zM(f~zj>WxhEAkSN_Lp7VH|J7dTr$}mgcg=$y|XRmoNutX3gWXQ-(27ml^Ca2ETI6z zEKb{wISE|lqt5%#%v7XLQY)9$Pk22m+mimil+&i)(KOr0DkiNjjIyy^pA&nGo&XA4 zYMOF4Tcrxt52t&+^7!(WQOwv3T>Lu{-can7^T!2D#e6H%N_dLBJ02I2ZSogsi2rn& z1sKk`m_;QgRUZ}^%w;_UMF5h5wYGVh~OmzH~GjV~ZM0z%rw= zbNQ3xfs4S0g&653r^8l#vCA#zg&WV1Gg-5>m%)59fuh~5Mv59GyQc7(C%e$fk*2S_Q_!~wYFlkVU7m`Cu@ZVf+(g`q1&gPCF&W&Nn>@ri zHL4K5-M-Iq(RAG9Uh^UmI}OU-Ep8z9gC5rX?OLjC5!S7SGNWzl`SPU3$)z}f(k_JW zn)N6amRQ|NBkj=3>{#9+-{)mFu%6+8sw1~%xuvk81B&U;Ue3`-mz7g}OV0)G6tBA~ zcRb9g)>HlRgypDjGSsP-T>-8l|MP*SThlcjcOwAL6p$-9L~T~}%S@B#->EMc1~3R~ zj`Ng{4sH-f$*>u1puM|}Ulw=^+If8yrZNX#Um>LOF#$^!k0qeohSR2NCo%+sr){%K z{HP>P&Nnur2KFZJF%r zx$mU^#sak6alxo&DyEmUhD{bvk2-}1*izD=VyIPyEi~YOz( zPXfiXUqK1tr+}&ZQ{@!3uFdhM1*3nu$_UAyu7XzEP{B~qkkC%MkkLCzxC$V?c=Y$R z&pj4qtuY8iZd6TAKfpSKd8EW_amcOX<*rBbi{ zMvFM``u#Gg4o2Yps^LPYXL~(DeQ6V{J!Zs(R(nB>so-%eh({dvW3+})x0YVvW2+K^!A2!%|0aq z8l9Q)il)ucQ_Q1A7P+R?!4%$3w1-Xvc}%nb>)!DQ`Qo?F8QR6FYPYYFmZLc9cHx(U zv`ZqQLGqMj#H%obN`Nq&&Z{eM3yV`iTb=3@4~rHXka04B+QT|j!z5geCwXMMF7{g( zWj>8<+GR|qG3{6)u8fpQuX3#eZord79}0y+67c!`IHcH@4j?gOtBb|VbMR?D#5!cF z#9$)MlyUn@i&Lg&KU~hsn2_HU2tgKoA=D4M*FDKj__0;gYlm?1Xt263zCR&4PVGLg zd-sHrn(lsJEpA~3_nS;JJv)`7(*XDVOVak{o~mP3j}2d~h~~TEL9jbliM&ijkJ068 ztbJ#vY6oZ z_=GNM==e0uShqsX{Y1FdZ4fT+XWSKw(=*Ms2F;JGhugo;TCmdliy_2;#Pu6c1oC^C1&(*!axLf(fG+xEaL-aG{kEQg=eIQ*f1!Ylqs zsQ(O7qd$YR&jkbD66P5n?e$o)gF*ZQW6M1mz=Lw=Oz-wy6+>1PxD}?K@_hmuA<&9E z{hNUEmVr8Olar&tW+nHK2QP|`b-pSXuJv8id1%m9OrgIR(Xb}8e%3N?37DWki#^kB z!P-ZH<6>jWKJn`zq5*;|x^H^3wI3Cib)1+u%OxaEUm1uRFkR&W(DC|*ICjrwY4nfgPA)I6fBL7#mtgFBq>K3>fY*^ z1t-$*@wJs>gJr{b#qHlLd6Bc3uWB3GX~u<!O|ijB`Yo62vdjOaXxFzD{F(*>ln*wdxNfL??LjO}d{-M{Oye{-J}7EH6rC{mWz7J ze6EDt=9|GbL(R(`NMo9f*+tP(qRZdI=e;bYq+Z2ftLz7sUo=n?gTTyQX5X?ZcuOI3 zG!mdwwT3EP0Wu2nxf-_IbwbrE#}9?M4^YV6g=_cVhMLx#V1VwPx}p?)ZgT%HO7cb}#S=EopAo zD(gv-r?3IpX>vP&DoPiWYPg5<{^1z(vi*g16Wuoq0N(v(Z^F8MoX}i^3qv_t;&a9W zldg(=2F!ZnI`-CaUBdSE3cG>q$FC|QrpuMF7Hs={&B7Xp_XY?N4aLFp$z@KOGOw&6 zHT0$=x9FwAQ?|mdyMi<$NZfQ9GX5+tp7o(I6YU`}D>kszwnWK zOIDT29E%ki( z&-wTz<_4X(r+2)v+6^CIdUZhIZuA`Y>~$jzGecJM=spJ!FY0~u=mp4`|0YZGneD5v zg^(#yIzhJJx+HQcxe}sYg5;>(x$m-ykK=BuWl`%7G@9|Zzi7$w62XhG{B;_|HfBX& z1~<0PUS^Hpi~Vk+kobOxaN6hLpKV0A?z>ME5;^#d zaki)VdYFLabCo$cBVS!!?;R(t#8ew&Lu(URdb*(z+T2AahU!X)N|56(_GIX$kE(xPOUOBK zV&po1hwtmtxN$+z*lk4;RS-?X$4k^w(iI|`*vGsVp&Nhv7H5+Qdh#;P~i*X zRLXHQ%F3}~F2UN}cln}O0(h{Tr#y49q;l?{#A_ZrT-+3Sy_4mWtCs?04$gXmo2~_i z6T0)0ju4>pRPp`#zrpCp%6c0Rb6XHS#0ec{Vv%|izvnr5{A@i?R@m~TtG(8hBdEKB zN6QomAtq(P<~LADIKkZ!BTG47eky)+`4X?ta zd{+Bt=uC8VrlwT7#bCdcMEX1mJvnRo&~9TxlkEfA*o^r8NUGM}^6gew?S#<6Y>e3l zz)brc^0_2}e0|(k?V;EA1p1TXing@Cz6Fn7qk8Od912-h`gr`F&C4+N$w9gjN1zhV ztWn;aaDxR^9E_I;a2M~heztbj+m5K^e39j+rXA|*Oas6gy5(L3mH!&`ecDayZON8w z@@L(yMUX|;lWbBHvNyYy=6#)*FmBsz1;(0k&ihLGyb+9cTpAEOwpi7%47&bR8Bf!! zRS_HKp;Qtt8I)k`EM8{Z=ikM%e^2%7erzF~`fYRz1$0UHWgg(JfAz2j2s3@F+^RU# z-s*g|g9u&mIo5imz$XLp+248!&hJZizR>-m-Du>7rChTFb$w1$yITnl-hPsTE4fX@_Nhr(=+09MYDpe7D~1D5ToKE6FVKe#uHPS@--{ zwLTX* zHlI)txoZ2(t`B?NAW-OQ_;PLdJZqxg*cX#gkrch@twEW3uFhi;m%i@i&<*WVx7|k1 z6J-t62LS}yE%e8ov7A-2)wVjPS@x*0YuT&MsOk5sgd3LP*XeR9zh&+cfZa#iHci}% z(f5_`(xu2*e@0|n=bsT-JI4eAzdG<-W28uu6lNZ?N27ZQ|t91gRhT1s29;xD)y{_u06fo zzNuxp*j1*-mj*XJ*-?=J3Q#-#+x&+ek$@4k$VRXW6eDCWbXa~njiw+QbKNqwGMdvi zJ~XmC(>_T*k{e!}MO21h{ckKlIBo<;B8^tWA1CJQ%H$PaQe()t8a**n()jiw-80ox zln5OhQ>2O`VPxW;&;Nd!Uh$E+l-y$ffR~nL!$*f~qFlFFps}N|Wu%=Y?V77aGi|T1 z81=$&io|HEdRbh~sqVvgjeYB`m#-U!!cXkY+_!Pe-9ISaxSY%kDJ^!WITdcSPIhDK z+VH1*pybVpzHy;HuE^+vj`u_tulg)q-`%o>6@!*WEQ8GeBDIL{woP6)>!%7>!o9Dq z8kCH*qByahI=oW^q6(NC`rhRNs!@I^Q&GHNs)W7)Ta)@;3LejMcp!7pSwG;0iB!+| zg5w8M-t4Q*!fC{~8TGjA{g^p%GW|rN4y$PCXLMeDk_sXB7k7qczuQ$Tjim;b1n-=- z=@5S`95 z{lwu?ndzF2lAZ#rm|?&N`yRNjq+Ps9iUbzq)5Owi(OJIkfW>eShx|_hN3W+NE|zTN zD+9uWH)9>Y^Pr%a+6Mbk*G{^SEMGY%e`FEvtlGUOJL02aaGP3=h44GyO%(3tr*tjT zl{XPb!%){STZgP`ZMwtXbBi3~QXHc6Lo!nBAPQ#t!?H$?M9i&@H;Ccsiv`~bDnJW0kjJsp0H{lux+Dk_8UJxr_<2GVisg$`uS+t{nQH>-*rQJeqBel!W(Pm6_w0g;lKNQe1DV|D!|wiAe^%pZ>T0~&-i4g z?jn@08)sJYXRFuIZCCJPW4x6u5iZ?z_d#WY!xnVD>$n^DO7@I~1e{{N{`PdI`#3z@ zQ6RIdK~+5=>x?NOK1!h=KI|f-pxzjLrXLq$HgS+hp?z{nl0(fWvdK?h!On7S@@mh) zB9J`9Iq*Qh*gtfg`SQpeA!|qEc+nw+P)NnHRQbFPEK7-v!~u-_HK_?>KwRA+Wdz}s zoK74$1wp4jJXWJ?wJjYQEp{rM&wSE7pxI}TaFUEr;X1Q|dE<%)0@7$Sgl==P3_|9L z&$^b%@EIrqB1!LkpsT;~zCUK5xh5$LOe)4o6a?{^-&u5_dlpamEmk!f=61=R+@8jC zr|%wOoz9v)yS!@8D_!t)Zs!cVZ!YyzaDL7SRQMKGu?><#)qXHJWD92)QMew+w8)5+_zL{c=VK2#T(BiAIu!M-l_YNv`yu}^m{Pjxoc;;ND-mxv9U_j*D5&7j`~}{$16x2uU}nAMfzMx9e59zr!~`8imfL za;qe`s*Q9%yXOEZw&4%iR9ZkoKvcP%)m@6huBtY-E2^#=zGMv&` zvl3>E$JyGJ$iIsfDuQ=R-{)8%4H&iGW1;R?uDxCAs5<6yNnH~R8ibr$>{;PlspvN@G+KeJ$9LbBB2Xt$i3-^>o#Vj~qVep-V{BF$D^` z!FZ>n@62FSBm|=wxj25FwP`rYAGDv$RtN;)5{t5yTG<92ZujRhQly#9 z&S`yps-2~Fjl(?ScJq|KAZlw{wgb9gTs!cDr$KKQ+EMZ7kAb7L3QxuA2)-);7+qxe z91d3YzX80mwZCKUVH<>T9c4`#Z0O9S80H|Ero!E`wusp9C`wcMD>fZJ2EK2hnJXyh zM5;?RzHMrn+*7U4Zb2Sw_xURr3^5S^b{-CghYv5cK6p{*EUI5v9labqa-Z??OogeO zX`f_1L8%_~#d<01GgC(g==vKP9jmShd4{D=`i4F zT1yv7vTIA#BR1 z`3!2f{M>4Hcp9lIa$K8arF#by*D%;}zr8xpK6{lVlp6Eim6mO9%+egQKV~^tgjjp2 z1=6B)uEajqTjKg2$FaEijKRH;ZZtjMZ_|6 z#FA}AcgyK=ufWqKCf<4FSdEAAc?AMr`YyeRug{Us6Y2h-KovKvKno&GKx6{ zT`Ib$$cNtml{uRBr|l)VJgr1xj~UT%&$AyFC!?B1jjo%EnF-8HVLkfPB;Cf60d7R( zBi}}h7ZzsdeF(s>5{G;X8J;lxgXVsWVQ{kfNx%;JJvkaIqHrea>eN$mM*=-wdAg(= zHZln61EOD&x_sLy^&@F4Lq7bkVhbsV6EWCqsY|5=Q~NC%)mTsAO}t8du&ME6MM4f& z7YChhL4!_W-7M%l?t2Z~6e`v7u@B zWn<+IVN;X4oHRVFghy|Ttq9LpKvKGyc3^b402=OV8(;scCv#`Z1e@(ky3lDPd2NaqIbnph9`P!H@Zy04Yk3>|aKxMbU~zbpGSP zwl@OQS#gBVLpu~D_|4s>`Em&j1C!mPc*CzF$dxy6TrTm?D9Li?tA3I`rG3=_v|L

4*)|6V&s{CT~O*mZ~_-xYAoeh+On-n#R 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 JTK7r#e*nn<0e}Di literal 0 HcmV?d00001 diff --git a/screenshots/herb_info_page.png b/screenshots/herb_info_page.png new file mode 100644 index 0000000000000000000000000000000000000000..e076056e69f4827dfb037e8a61fc20213ae9abe2 GIT binary patch literal 64760 zcmb5VXIN8B*EUR15fM-n5fD)6MXG>+5C!Q7QbP~D_g)i01?kc|NbkLu5Fqs4d+$Be zgb)HB*L~g3`~3Niuk1gWWAE8(X3d(t%6U$Lz9~u*KcaesgM&jXEAvGK2j~7>6K~`p z-rdh9Iyqb%oF_Q4Up}jOq#P^~eo}RB6gWm@GjJFZK5VOGdHS^Qcm7H{uf{~7g8IZ% z&QxQ#QQ4eEccJ@)2>lTA)Ku+WiM(E+Mp3Qn{<|HEiYdJcNlAh)_bFZy%CwWcA>8s| zHY9rT`gP?dnmDU>)=BYU+;;PAXQ-*xKf~W$7W)Ga|82G2dyRwtpXPVjPdE?$y&3%- z=kdQSCS2T?|F#tH@VNhNCETOEoBrMPY5)8h?aXU<0rgS){_idA^dcAt;C<3p)!wdP zDUeu*G-76F?;rUqhI<3t(qUDO`kH2uxT>S$>KUR#t3UCM6}xF|Oheiq^T zf2Fg-C&7WlUNpP$>#VyU-rKa!5&=VJU;cMOfB8Lct{c|0-i+A`apJS5iSp$FxRXBp zt4lRgS@UV|`+gS;mMVsIaBC?}SWx2st9JJc{xd!fH{k_gs}*vC5_xv!!WZZi)Isu` zYTyg0K}wLQcL0`d(J99cy<%Xti<3*A{_iU|+7FHK%ddGbqh`qN>tM#QS63&@$OB?G z?$H*4*f<0xXc5JZ@nqnGhhSy1gJtO;>CHG1uY{)#_ovU&vrt78o^m|3PTy{AeYu@}b>i4AQqO$ep*5cxG(acUl zC^ZJF{aLrakK;(BvapxDjvmjKscrP`90H&&C2XS596%nbSv@zPYrB|WIGKKWi= z#~i98`Shf1hnOJ!cffEGAghSOZShpeE*d4Wj~t4d0E7s9Lo|!@_M4 zOy1l7?5)52tX@vLJvZx?FT`%JZ*#q>DS|liWjG;GU5sn?nUIT@0&gDY_T)@yBLseH$ za*|xFoYvUlxlimW(8aP$2I|d>&WO$a>qFEMCXu7D*mrvkKA)j0U@OEtdZjB7Kulx< zY%$+Mq9}YE>*7Q&ah<1&4|TnGYrW$C__81EG)&k?aP7@xAo7)4!eiqIZHvR$wbS+l#ftfNkG09YV?$|j1GOWo44`aYBEWkU&>T{tYSD9Aum`iHS*31HP%|m zY$}fqC*jiXBKXJTwfQWEmx7q+FYvd3iLwjr-}P4rZqnQ7n%T=_`;BM0Bxm{d9IN5Y z=-)n?8~5nwQ5 zj0l_(R+#EXK5b0exu{;CrjlGryA)fM;@P%&NKGdDnuJzpVtB=xRTWjNi6(H;;8#&ZeUo;?S9s-v7rCsP)mZN!s%AW5EFzqZx_d_K`1&j|1Mz-%2U*+eV_Kl zi$l!U1gnbMGOhi!M>XPDTY0uT?zD+DG!&KO< zvC}MGryH`$$~8Zn%_sHi601LLZm_D9TyZpez zGu~EL7h)Pqz9+7gtER81U=}x3i_CP33-fG|y0;?U7!B-ZS`^{aKeVs`UG3$`DQhK4 zC;s9%VXyPo=(BqzQoZg*+})V$CV`2Mu2f2B!%6Yo9C5R~a`nYT=ac)&TZL8@>15KQ z7JGkVSdUWE)`j<^%ZkUlZifP;&BoOl=|8jT#r>R;VI8AqQL z(yD_k&$bwN13tccvnuPIp43R$AI(Vv%O)C-JgsJS(mR;6?aeuTcpI;d8iY8BEZbf$ z@aPqhcb!H>94dH3O&8QBa5>fTq_9m2-O5Ij7lecqacn0iYRxq@2)*YHDLlJ9)71Wg zJY*!8S|PPc3!!SvkyK5KkfwYnWkscU z2P#6!NieF$wf=955?Tk#TVS7~#O|J7-u)s;Tg=NWp5L5X_#rjZ^|R1?jlLMVvaUs0 zrk1eQ`rv%IjIcF^@`^3$g|-TvnNT}*irm{MT6aeFbp4;&Ib&1cSRpo33Hh=+7uLdD zT5afjS|KV8k5fbzmh3u#+tYd|0lUz+Pa-q1{D50KuimEcNB@cT6U95dIqy#+j#-0X zl9q%Es-l+hWMkK^rl82-eX;m(=T05txz7~jy$$Wf8HXHm7?_v>#P3>VPO#>SX(XgW z?KaAo$>*%IEc18IL+CVDW_)+?R!*lUN(thf)Sef5^iJT98Ifs({c>L+6{O&D7n=PF z5kv%Elt(G|m(mFP@Mc{}KBws0utJQ=Gr4lH2axBc-Q3JyzBR9q9Qt9?KQaoTWT|-j z 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 bKmaC2$OaY}Y4ZGE^PUxD)nuxrEJFSZh~VNH literal 0 HcmV?d00001 diff --git a/purchase_test.png b/screenshots/purchase_test.png similarity index 100% rename from purchase_test.png rename to screenshots/purchase_test.png diff --git a/static/app.js.backup b/static/app.js.backup new file mode 100644 index 0000000..a2879b3 --- /dev/null +++ b/static/app.js.backup @@ -0,0 +1,3172 @@ +// 한약 재고관리 시스템 - Frontend JavaScript + +// 원래 처방 구성 저장용 전역 변수 +let originalFormulaIngredients = {}; + +// 로트 배분 관련 전역 변수 +let currentLotAllocation = { + herbId: null, + requiredQty: 0, + row: null, + data: null +}; + +// 재고 계산 모드 (localStorage에 저장) +let inventoryCalculationMode = localStorage.getItem('inventoryMode') || 'all'; + +$(document).ready(function() { + // 페이지 네비게이션 + $('.sidebar .nav-link').on('click', function(e) { + e.preventDefault(); + const page = $(this).data('page'); + + // Active 상태 변경 + $('.sidebar .nav-link').removeClass('active'); + $(this).addClass('active'); + + // 페이지 전환 + $('.main-content').removeClass('active'); + $(`#${page}`).addClass('active'); + + // 페이지별 데이터 로드 + loadPageData(page); + }); + + // 초기 데이터 로드 + loadPageData('dashboard'); + + // 페이지별 데이터 로드 함수 + function loadPageData(page) { + switch(page) { + case 'dashboard': + loadDashboard(); + break; + case 'patients': + loadPatients(); + break; + case 'purchase': + loadPurchaseReceipts(); + loadSuppliersForSelect(); + break; + case 'formulas': + loadFormulas(); + break; + case 'compound': + loadCompounds(); + loadPatientsForSelect(); + loadFormulasForSelect(); + break; + case 'inventory': + loadInventory(); + break; + case 'herbs': + loadHerbs(); + break; + case 'herb-info': + loadHerbInfo(); + break; + } + } + + // 대시보드 데이터 로드 + function loadDashboard() { + // 환자 수 + $.get('/api/patients', function(response) { + if (response.success) { + $('#totalPatients').text(response.data.length); + } + }); + + // 재고 현황 (저장된 모드 사용) + loadInventorySummary(); + + // 오늘 조제 수 및 최근 조제 내역 + $.get('/api/compounds', function(response) { + if (response.success) { + const today = new Date().toISOString().split('T')[0]; + const todayCompounds = response.data.filter(c => c.compound_date === today); + $('#todayCompounds').text(todayCompounds.length); + + // 최근 조제 내역 (최근 5개) + const tbody = $('#recentCompounds'); + tbody.empty(); + + const recentCompounds = response.data.slice(0, 5); + if (recentCompounds.length > 0) { + recentCompounds.forEach(compound => { + let statusBadge = ''; + switch(compound.status) { + case 'PREPARED': + statusBadge = '조제완료'; + break; + case 'DISPENSED': + statusBadge = '출고완료'; + break; + case 'CANCELLED': + statusBadge = '취소'; + break; + default: + statusBadge = '대기'; + } + + tbody.append(` + + ${compound.compound_date || '-'} + ${compound.patient_name || '직접조제'} + ${compound.formula_name || '직접조제'} + ${compound.je_count}제 + ${compound.pouch_total}개 + ${statusBadge} + + `); + }); + } else { + tbody.html('조제 내역이 없습니다.'); + } + } + }); + } + + // 환자 목록 로드 + function loadPatients() { + $.get('/api/patients', function(response) { + if (response.success) { + const tbody = $('#patientsList'); + tbody.empty(); + + // 각 환자의 처방 횟수를 가져오기 위해 처방 데이터도 로드 + $.get('/api/compounds', function(compoundsResponse) { + const compounds = compoundsResponse.success ? compoundsResponse.data : []; + + // 환자별 처방 횟수 계산 + const compoundCounts = {}; + compounds.forEach(compound => { + if (compound.patient_id) { + compoundCounts[compound.patient_id] = (compoundCounts[compound.patient_id] || 0) + 1; + } + }); + + response.data.forEach(patient => { + const compoundCount = compoundCounts[patient.patient_id] || 0; + + tbody.append(` + + ${patient.name} + ${patient.phone} + ${patient.gender === 'M' ? '남' : patient.gender === 'F' ? '여' : '-'} + ${patient.birth_date || '-'} + + ${compoundCount}회 + + ${patient.notes || '-'} + + + + + + `); + }); + + // 처방내역 버튼 이벤트 + $('.view-patient-compounds').on('click', function() { + const patientId = $(this).data('id'); + const patientName = $(this).data('name'); + viewPatientCompounds(patientId, patientName); + }); + }); + } + }); + } + + // 환자 등록 + $('#savePatientBtn').on('click', function() { + const patientData = { + name: $('#patientName').val(), + phone: $('#patientPhone').val(), + jumin_no: $('#patientJumin').val(), + gender: $('#patientGender').val(), + birth_date: $('#patientBirth').val(), + address: $('#patientAddress').val(), + notes: $('#patientNotes').val() + }; + + $.ajax({ + url: '/api/patients', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(patientData), + success: function(response) { + if (response.success) { + alert('환자가 등록되었습니다.'); + $('#patientModal').modal('hide'); + $('#patientForm')[0].reset(); + loadPatients(); + } + }, + error: function(xhr) { + alert('오류: ' + xhr.responseJSON.error); + } + }); + }); + + // 환자 처방 내역 조회 + function viewPatientCompounds(patientId, patientName) { + // 환자 정보 가져오기 + $.get(`/api/patients/${patientId}`, function(patientResponse) { + if (patientResponse.success) { + const patient = patientResponse.data; + + // 환자 기본 정보 표시 + $('#patientCompoundsName').text(patient.name); + $('#patientInfoName').text(patient.name); + $('#patientInfoPhone').text(patient.phone || '-'); + $('#patientInfoGender').text(patient.gender === 'M' ? '남성' : patient.gender === 'F' ? '여성' : '-'); + $('#patientInfoBirth').text(patient.birth_date || '-'); + + // 환자의 처방 내역 가져오기 + $.get(`/api/patients/${patientId}/compounds`, function(compoundsResponse) { + if (compoundsResponse.success) { + const compounds = compoundsResponse.compounds || []; + + // 통계 계산 + const totalCompounds = compounds.length; + let totalJe = 0; + let totalAmount = 0; + let lastVisit = '-'; + + if (compounds.length > 0) { + compounds.forEach(c => { + totalJe += c.je_count || 0; + totalAmount += c.sell_price_total || 0; + }); + lastVisit = compounds[0].compound_date || '-'; + } + + // 통계 표시 + $('#patientTotalCompounds').text(totalCompounds + '회'); + $('#patientLastVisit').text(lastVisit); + $('#patientTotalJe').text(totalJe + '제'); + $('#patientTotalAmount').text(formatCurrency(totalAmount)); + + // 처방 내역 테이블 표시 + const tbody = $('#patientCompoundsList'); + tbody.empty(); + + if (compounds.length === 0) { + tbody.append(` + + 처방 내역이 없습니다. + + `); + } else { + compounds.forEach((compound, index) => { + // 상태 뱃지 + let statusBadge = ''; + switch(compound.status) { + case 'PREPARED': + statusBadge = '조제완료'; + break; + case 'DISPENSED': + statusBadge = '출고완료'; + break; + case 'CANCELLED': + statusBadge = '취소'; + break; + default: + statusBadge = '대기'; + } + + const detailRowId = `compound-detail-${compound.compound_id}`; + + // 처방명 표시 (가감방 여부 포함) + let formulaDisplay = compound.formula_name || '직접조제'; + if (compound.is_custom && compound.formula_name) { + formulaDisplay = `${compound.formula_name} 가감`; + } + + tbody.append(` + + + + + ${compound.compound_date || '-'} + + ${formulaDisplay} + ${compound.custom_summary ? `
${compound.custom_summary}` : ''} + + ${compound.je_count || 0} + ${compound.cheop_total || 0} + ${compound.pouch_total || 0} + ${formatCurrency(compound.cost_total || 0)} + ${formatCurrency(compound.sell_price_total || 0)} + ${statusBadge} + ${compound.prescription_no || '-'} + + + +

+ + + `); + }); + + // 행 클릭 이벤트 - 상세 정보 토글 + $('.compound-row').on('click', function() { + const compoundId = $(this).data('compound-id'); + const detailRow = $(`#compound-detail-${compoundId}`); + const icon = $(this).find('.toggle-icon'); + + if (detailRow.is(':visible')) { + // 닫기 + detailRow.slideUp(); + icon.removeClass('bi-chevron-down').addClass('bi-chevron-right'); + } else { + // 열기 - 다른 모든 행 닫기 + $('.collapse-row').slideUp(); + $('.toggle-icon').removeClass('bi-chevron-down').addClass('bi-chevron-right'); + + // 현재 행 열기 + detailRow.slideDown(); + icon.removeClass('bi-chevron-right').addClass('bi-chevron-down'); + + // 상세 정보 로드 + loadCompoundDetailInline(compoundId); + } + }); + } + + // 모달 표시 + $('#patientCompoundsModal').modal('show'); + } + }).fail(function() { + alert('처방 내역을 불러오는데 실패했습니다.'); + }); + } + }).fail(function() { + alert('환자 정보를 불러오는데 실패했습니다.'); + }); + } + + // 환자 처방 내역 모달 내에서 조제 상세 정보 로드 (인라인) + function loadCompoundDetailInline(compoundId) { + $.get(`/api/compounds/${compoundId}`, function(response) { + if (response.success && response.data) { + const data = response.data; + + // 구성 약재 테이블 + let ingredientsHtml = ''; + + if (data.ingredients && data.ingredients.length > 0) { + data.ingredients.forEach(ing => { + ingredientsHtml += ` + + + + + + + `; + }); + } else { + ingredientsHtml += ''; + } + ingredientsHtml += '
약재명보험코드첩당용량총용량
${ing.herb_name}${ing.insurance_code || '-'}${ing.grams_per_cheop}g${ing.total_grams}g
약재 정보가 없습니다
'; + + $(`#ingredients-${compoundId}`).html(ingredientsHtml); + + // 재고 소비 내역 테이블 + let consumptionsHtml = ''; + + if (data.consumptions && data.consumptions.length > 0) { + let totalCost = 0; + data.consumptions.forEach(con => { + totalCost += con.cost_amount || 0; + consumptionsHtml += ` + + + + + + + + + `; + }); + consumptionsHtml += ` + + + + + `; + } else { + consumptionsHtml += ''; + } + consumptionsHtml += '
약재명원산지도매상사용량단가원가
${con.herb_name}${con.origin_country || '-'}${con.supplier_name || '-'}${con.quantity_used}g${formatCurrency(con.unit_cost_per_g)}/g${formatCurrency(con.cost_amount)}
총 원가:${formatCurrency(totalCost)}
재고 소비 내역이 없습니다
'; + + $(`#consumptions-${compoundId}`).html(consumptionsHtml); + } + }).fail(function() { + $(`#ingredients-${compoundId}`).html('
데이터를 불러오는데 실패했습니다.
'); + $(`#consumptions-${compoundId}`).html('
데이터를 불러오는데 실패했습니다.
'); + }); + } + + // 처방 목록 로드 + function loadFormulas() { + $.get('/api/formulas', function(response) { + if (response.success) { + const tbody = $('#formulasList'); + tbody.empty(); + + response.data.forEach(formula => { + tbody.append(` + + ${formula.formula_code || '-'} + ${formula.formula_name} + ${formula.base_cheop}첩 + ${formula.base_pouches}파우치 + + + + + + + + `); + }); + + // 구성 약재 보기 + $('.view-ingredients').on('click', function() { + const formulaId = $(this).data('id'); + $.get(`/api/formulas/${formulaId}/ingredients`, function(response) { + if (response.success) { + let ingredientsList = response.data.map(ing => + `${ing.herb_name}: ${ing.grams_per_cheop}g` + ).join(', '); + alert('구성 약재:\n' + ingredientsList); + } + }); + }); + } + }); + } + + // 처방 구성 약재 추가 (모달) + let formulaIngredientCount = 0; + $('#addFormulaIngredientBtn').on('click', function() { + formulaIngredientCount++; + $('#formulaIngredients').append(` + + + + + + + + + + + + + + + `); + + // 약재 목록 로드 + const selectElement = $(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .herb-select`); + loadHerbsForSelect(selectElement); + + // 삭제 버튼 이벤트 + $(`#formulaIngredients tr[data-row="${formulaIngredientCount}"] .remove-ingredient`).on('click', function() { + $(this).closest('tr').remove(); + }); + }); + + // 처방 저장 + $('#saveFormulaBtn').on('click', function() { + const ingredients = []; + $('#formulaIngredients tr').each(function() { + const herbId = $(this).find('.herb-select').val(); + const grams = $(this).find('.grams-input').val(); + + if (herbId && grams) { + ingredients.push({ + herb_item_id: parseInt(herbId), + grams_per_cheop: parseFloat(grams), + notes: $(this).find('.notes-input').val() + }); + } + }); + + const formulaData = { + formula_code: $('#formulaCode').val(), + formula_name: $('#formulaName').val(), + formula_type: $('#formulaType').val(), + base_cheop: parseInt($('#baseCheop').val()), + base_pouches: parseInt($('#basePouches').val()), + description: $('#formulaDescription').val(), + ingredients: ingredients + }; + + $.ajax({ + url: '/api/formulas', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(formulaData), + success: function(response) { + if (response.success) { + alert('처방이 등록되었습니다.'); + $('#formulaModal').modal('hide'); + $('#formulaForm')[0].reset(); + $('#formulaIngredients').empty(); + loadFormulas(); + } + }, + error: function(xhr) { + alert('오류: ' + xhr.responseJSON.error); + } + }); + }); + + // 조제 관리 + $('#newCompoundBtn').on('click', function() { + $('#compoundForm').show(); + $('#compoundEntryForm')[0].reset(); + $('#compoundIngredients').empty(); + }); + + $('#cancelCompoundBtn').on('click', function() { + $('#compoundForm').hide(); + }); + + // 제수 변경 시 첩수 자동 계산 + $('#jeCount').on('input', function() { + const jeCount = parseFloat($(this).val()) || 0; + const cheopTotal = jeCount * 20; + const pouchTotal = jeCount * 30; + + $('#cheopTotal').val(cheopTotal); + $('#pouchTotal').val(pouchTotal); + + // 약재별 총 용량 재계산 + updateIngredientTotals(); + }); + + // 처방 선택 시 구성 약재 로드 + $('#compoundFormula').on('change', function() { + const formulaId = $(this).val(); + + // 원래 처방 구성 초기화 + originalFormulaIngredients = {}; + $('#customPrescriptionBadge').remove(); // 커스텀 뱃지 제거 + + if (!formulaId) { + $('#compoundIngredients').empty(); + return; + } + + // 직접조제인 경우 + if (formulaId === 'custom') { + $('#compoundIngredients').empty(); + // 빈 행 하나 추가 + addEmptyIngredientRow(); + return; + } + + // 등록된 처방인 경우 + $.get(`/api/formulas/${formulaId}/ingredients`, function(response) { + if (response.success) { + $('#compoundIngredients').empty(); + + // 원래 처방 구성 저장 + response.data.forEach(ing => { + originalFormulaIngredients[ing.ingredient_code] = { + herb_name: ing.herb_name, + grams_per_cheop: ing.grams_per_cheop + }; + }); + + response.data.forEach(ing => { + const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; + const totalGrams = ing.grams_per_cheop * cheopTotal; + + // 제품 선택 옵션 생성 + let productOptions = ''; + if (ing.available_products && ing.available_products.length > 0) { + ing.available_products.forEach(product => { + const specInfo = product.specification ? ` [${product.specification}]` : ''; + productOptions += ``; + }); + } + + $('#compoundIngredients').append(` + + + ${ing.herb_name} + ${ing.total_available_stock > 0 + ? `(총 ${ing.total_available_stock.toFixed(0)}g 사용 가능)` + : '(재고 없음)'} + + + + + ${totalGrams.toFixed(1)} + +
+ + +
+ + 대기중 + + + + + `); + + // 첫 번째 제품 자동 선택 및 원산지 로드 + const tr = $(`tr[data-ingredient-code="${ing.ingredient_code}"]`); + if (ing.available_products && ing.available_products.length > 0) { + const firstProduct = ing.available_products[0]; + tr.find('.product-select').val(firstProduct.herb_item_id); + tr.attr('data-herb-id', firstProduct.herb_item_id); + // 원산지/로트 옵션 로드 + loadOriginOptions(firstProduct.herb_item_id, totalGrams); + } + }); + + // 재고 확인 + checkStockForCompound(); + + // 제품 선택 변경 이벤트 + $('.product-select').on('change', function() { + const herbId = $(this).val(); + const row = $(this).closest('tr'); + + if (herbId) { + row.attr('data-herb-id', herbId); + const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; + const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0; + const totalGrams = gramsPerCheop * cheopTotal; + + // 원산지/로트 옵션 로드 + loadOriginOptions(herbId, totalGrams); + } else { + row.attr('data-herb-id', ''); + row.find('.origin-select').empty().append('').prop('disabled', true); + row.find('.stock-status').text('대기중'); + } + }); + + // 용량 변경 이벤트 + $('.grams-per-cheop').on('input', function() { + updateIngredientTotals(); + + // 원산지 옵션 다시 로드 + const row = $(this).closest('tr'); + const herbId = row.attr('data-herb-id'); + if (herbId) { + const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; + const gramsPerCheop = parseFloat($(this).val()) || 0; + const totalGrams = gramsPerCheop * cheopTotal; + loadOriginOptions(herbId, totalGrams); + } + }); + + // 삭제 버튼 이벤트 + $('.remove-compound-ingredient').on('click', function() { + $(this).closest('tr').remove(); + updateIngredientTotals(); // 총용량 재계산 및 커스텀 감지 + }); + } + }); + }); + + // 약재별 총 용량 업데이트 + function updateIngredientTotals() { + const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; + + $('#compoundIngredients tr').each(function() { + const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val()) || 0; + const totalGrams = gramsPerCheop * cheopTotal; + $(this).find('.total-grams').text(totalGrams.toFixed(1)); + }); + + checkStockForCompound(); + // 커스텀 처방 감지 호출 + checkCustomPrescription(); + } + + // 커스텀 처방 감지 함수 + function checkCustomPrescription() { + const formulaId = $('#compoundFormula').val(); + + // 처방이 선택되지 않았거나 직접조제인 경우 리턴 + if (!formulaId || formulaId === 'custom' || Object.keys(originalFormulaIngredients).length === 0) { + $('#customPrescriptionBadge').remove(); + return; + } + + // 현재 약재 구성 수집 + const currentIngredients = {}; + $('#compoundIngredients tr').each(function() { + const ingredientCode = $(this).attr('data-ingredient-code'); + const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val()); + + if (ingredientCode && gramsPerCheop > 0) { + currentIngredients[ingredientCode] = gramsPerCheop; + } + }); + + // 변경사항 감지 + const customDetails = []; + let isCustom = false; + + // 추가된 약재 확인 + for (const code in currentIngredients) { + if (!originalFormulaIngredients[code]) { + const herbName = $(`tr[data-ingredient-code="${code}"] .herb-select-compound option:selected`).data('herb-name') || code; + customDetails.push(`${herbName} ${currentIngredients[code]}g 추가`); + isCustom = true; + } + } + + // 삭제된 약재 확인 + for (const code in originalFormulaIngredients) { + if (!currentIngredients[code]) { + customDetails.push(`${originalFormulaIngredients[code].herb_name} 제거`); + isCustom = true; + } + } + + // 용량 변경된 약재 확인 + for (const code in currentIngredients) { + if (originalFormulaIngredients[code]) { + const originalGrams = originalFormulaIngredients[code].grams_per_cheop; + const currentGrams = currentIngredients[code]; + + if (Math.abs(originalGrams - currentGrams) > 0.01) { + const herbName = originalFormulaIngredients[code].herb_name; + customDetails.push(`${herbName} ${originalGrams}g→${currentGrams}g`); + isCustom = true; + } + } + } + + // 커스텀 뱃지 표시/숨기기 + $('#customPrescriptionBadge').remove(); + + if (isCustom) { + const badgeHtml = ` +
+ 가감방 + ${customDetails.join(' | ')} +
+ `; + + // 처방 선택 영역 아래에 추가 + $('#compoundFormula').closest('.col-md-6').append(badgeHtml); + } + } + + // 재고 확인 + function checkStockForCompound() { + $('#compoundIngredients tr').each(function() { + const herbId = $(this).data('herb-id'); + const totalGrams = parseFloat($(this).find('.total-grams').text()) || 0; + const $stockStatus = $(this).find('.stock-status'); + + // TODO: API 호출로 실제 재고 확인 + $stockStatus.text('재고 확인 필요'); + }); + } + + // 조제 약재 추가 + // 빈 약재 행 추가 함수 + function addEmptyIngredientRow() { + const newRow = $(` + + + + + + + + 0.0 + +
+ + +
+ + - + + + + + `); + + $('#compoundIngredients').append(newRow); + + // 약재 목록 로드 + const herbSelect = newRow.find('.herb-select-compound'); + loadHerbsForSelect(herbSelect); + + // 약재(마스터) 선택 시 제품 옵션 로드 + newRow.find('.herb-select-compound').on('change', function() { + const ingredientCode = $(this).val(); + const herbName = $(this).find('option:selected').data('herb-name'); + if (ingredientCode) { + const row = $(this).closest('tr'); + row.attr('data-ingredient-code', ingredientCode); + + // 제품 목록 로드 + loadProductOptions(row, ingredientCode, herbName); + + // 제품 선택 활성화 + row.find('.product-select').prop('disabled', false); + + // 원산지 선택 초기화 및 비활성화 + row.find('.origin-select').empty().append('').prop('disabled', true); + } else { + const row = $(this).closest('tr'); + row.attr('data-ingredient-code', ''); + row.attr('data-herb-id', ''); + row.find('.product-select').empty().append('').prop('disabled', true); + row.find('.origin-select').empty().append('').prop('disabled', true); + } + }); + + // 제품 선택 이벤트 + newRow.find('.product-select').on('change', function() { + const herbId = $(this).val(); + const row = $(this).closest('tr'); + + if (herbId) { + row.attr('data-herb-id', herbId); + + // 원산지 선택 활성화 + row.find('.origin-select').prop('disabled', false); + + // 원산지 옵션 로드 + const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; + const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0; + const totalGrams = gramsPerCheop * cheopTotal; + loadOriginOptions(herbId, totalGrams); + } else { + row.attr('data-herb-id', ''); + row.find('.origin-select').empty().append('').prop('disabled', true); + } + }); + + // 이벤트 바인딩 + newRow.find('.grams-per-cheop').on('input', function() { + updateIngredientTotals(); + // 원산지 옵션 다시 로드 + const herbId = $(this).closest('tr').attr('data-herb-id'); + if (herbId) { + const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; + const gramsPerCheop = parseFloat($(this).val()) || 0; + const totalGrams = gramsPerCheop * cheopTotal; + loadOriginOptions(herbId, totalGrams); + } + }); + + newRow.find('.remove-compound-ingredient').on('click', function() { + $(this).closest('tr').remove(); + updateIngredientTotals(); + }); + } + + $('#addIngredientBtn').on('click', function() { + addEmptyIngredientRow(); + }); + + // 기존 약재 추가 버튼 (기존 코드 삭제) + /* + $('#addIngredientBtn').on('click', function() { + const newRow = $(` + + + + + + + + 0.0 + - + + + + + `); + + $('#compoundIngredients').append(newRow); + + // 약재 목록 로드 + loadHerbsForSelect(newRow.find('.herb-select-compound')); + + // 이벤트 바인딩 + newRow.find('.grams-per-cheop').on('input', updateIngredientTotals); + newRow.find('.remove-compound-ingredient').on('click', function() { + $(this).closest('tr').remove(); + updateIngredientTotals(); // 총용량 재계산 및 커스텀 감지 + }); + newRow.find('.herb-select-compound').on('change', function() { + const herbId = $(this).val(); + $(this).closest('tr').attr('data-herb-id', herbId); + updateIngredientTotals(); + }); + }); + */ + + // 조제 실행 + $('#compoundEntryForm').on('submit', function(e) { + e.preventDefault(); + + // getIngredientDataForCompound 함수 사용하여 lot_assignments 포함 + const ingredients = getIngredientDataForCompound(); + + const compoundData = { + patient_id: $('#compoundPatient').val() ? parseInt($('#compoundPatient').val()) : null, + formula_id: $('#compoundFormula').val() ? parseInt($('#compoundFormula').val()) : null, + je_count: parseFloat($('#jeCount').val()), + cheop_total: parseFloat($('#cheopTotal').val()), + pouch_total: parseFloat($('#pouchTotal').val()), + ingredients: ingredients + }; + + $.ajax({ + url: '/api/compounds', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(compoundData), + success: function(response) { + if (response.success) { + alert(`조제가 완료되었습니다.\n원가: ${formatCurrency(response.total_cost)}`); + $('#compoundForm').hide(); + loadCompounds(); + } + }, + error: function(xhr) { + alert('오류: ' + xhr.responseJSON.error); + } + }); + }); + + // 조제 내역 로드 + function loadCompounds() { + $.get('/api/compounds', function(response) { + const tbody = $('#compoundsList'); + tbody.empty(); + + if (response.success && response.data.length > 0) { + // 통계 업데이트 + const today = new Date().toISOString().split('T')[0]; + const currentMonth = new Date().toISOString().slice(0, 7); + + let todayCount = 0; + let monthCount = 0; + + response.data.forEach((compound, index) => { + // 통계 계산 + if (compound.compound_date === today) todayCount++; + if (compound.compound_date && compound.compound_date.startsWith(currentMonth)) monthCount++; + + // 상태 뱃지 + let statusBadge = ''; + switch(compound.status) { + case 'PREPARED': + statusBadge = '조제완료'; + break; + case 'DISPENSED': + statusBadge = '출고완료'; + break; + case 'CANCELLED': + statusBadge = '취소'; + break; + default: + statusBadge = '대기'; + } + + const row = $(` + + ${response.data.length - index} + ${compound.compound_date || ''}
${compound.created_at ? compound.created_at.split(' ')[1] : ''} + ${compound.patient_name || '직접조제'} + ${compound.patient_phone || '-'} + ${compound.formula_name || '직접조제'} + ${compound.je_count || 0} + ${compound.cheop_total || 0} + ${compound.pouch_total || 0} + ${formatCurrency(compound.cost_total || 0)} + ${formatCurrency(compound.sell_price_total || 0)} + ${statusBadge} + ${compound.prescription_no || '-'} + + + + + `); + tbody.append(row); + }); + + // 통계 업데이트 + $('#todayCompoundCount').text(todayCount); + $('#monthCompoundCount').text(monthCount); + + // 상세보기 버튼 이벤트 + $('.view-compound-detail').on('click', function() { + const compoundId = $(this).data('id'); + viewCompoundDetail(compoundId); + }); + } else { + tbody.html('조제 내역이 없습니다.'); + $('#todayCompoundCount').text(0); + $('#monthCompoundCount').text(0); + } + }).fail(function() { + $('#compoundsList').html('데이터를 불러오는데 실패했습니다.'); + }); + } + + // 조제 상세보기 + function viewCompoundDetail(compoundId) { + $.get(`/api/compounds/${compoundId}`, function(response) { + if (response.success && response.data) { + const data = response.data; + + // 환자 정보 + $('#detailPatientName').text(data.patient_name || '직접조제'); + $('#detailPatientPhone').text(data.patient_phone || '-'); + $('#detailCompoundDate').text(data.compound_date || '-'); + + // 처방 정보 (가감방 표시 포함) + let formulaDisplay = data.formula_name || '직접조제'; + if (data.is_custom && data.formula_name) { + formulaDisplay += ' 가감'; + if (data.custom_summary) { + formulaDisplay += `
${data.custom_summary}`; + } + } + $('#detailFormulaName').html(formulaDisplay); // text() 대신 html() 사용 + $('#detailPrescriptionNo').text(data.prescription_no || '-'); + $('#detailQuantities').text(`${data.je_count}제 / ${data.cheop_total}첩 / ${data.pouch_total}파우치`); + + // 처방 구성 약재 + const ingredientsBody = $('#detailIngredients'); + ingredientsBody.empty(); + if (data.ingredients && data.ingredients.length > 0) { + data.ingredients.forEach(ing => { + ingredientsBody.append(` + + ${ing.herb_name} + ${ing.insurance_code || '-'} + ${ing.grams_per_cheop}g + ${ing.total_grams}g + ${ing.notes || '-'} + + `); + }); + } + + // 재고 소비 내역 + const consumptionsBody = $('#detailConsumptions'); + consumptionsBody.empty(); + if (data.consumptions && data.consumptions.length > 0) { + data.consumptions.forEach(con => { + consumptionsBody.append(` + + ${con.herb_name} + ${con.origin_country || '-'} + ${con.supplier_name || '-'} + ${con.quantity_used}g + ${formatCurrency(con.unit_cost_per_g)}/g + ${formatCurrency(con.cost_amount)} + + `); + }); + } + + // 총 원가 + $('#detailTotalCost').text(formatCurrency(data.cost_total || 0)); + + // 비고 + $('#detailNotes').text(data.notes || ''); + + // 부모 모달(환자 처방 내역)을 임시로 숨기고 조제 상세 모달 열기 + const parentModal = $('#patientCompoundsModal'); + const wasParentOpen = parentModal.hasClass('show'); + + if (wasParentOpen) { + // 부모 모달 숨기기 (DOM에서 제거하지 않음) + parentModal.modal('hide'); + + // 조제 상세 모달이 닫힐 때 부모 모달 다시 열기 + $('#compoundDetailModal').off('hidden.bs.modal').on('hidden.bs.modal', function() { + parentModal.modal('show'); + }); + } + + // 조제 상세 모달 열기 + $('#compoundDetailModal').modal('show'); + } + }).fail(function() { + alert('조제 상세 정보를 불러오는데 실패했습니다.'); + }); + } + + // 재고 현황 로드 + function loadInventory() { + $.get('/api/inventory/summary', function(response) { + if (response.success) { + const tbody = $('#inventoryList'); + tbody.empty(); + + let totalValue = 0; + let herbsInStock = 0; + + // 주성분코드 기준 보유 현황 표시 + if (response.summary) { + const summary = response.summary; + const coverageHtml = ` +
+
+
+
📊 급여 약재 보유 현황
+
+
+ 전체 급여 약재: ${summary.total_ingredient_codes || 454}개 주성분 +
+
+ 보유 약재: ${summary.owned_ingredient_codes || 0}개 주성분 +
+
+ 보유율: + ${summary.coverage_rate || 0}% +
+
+
+
+
+
+ ${summary.owned_ingredient_codes || 0} / ${summary.total_ingredient_codes || 454} +
+
+
+
+
+ + ※ 건강보험 급여 한약재 ${summary.total_ingredient_codes || 454}개 주성분 중 ${summary.owned_ingredient_codes || 0}개 보유 + +
+
+ `; + + // 재고 테이블 위에 통계 표시 + if ($('#inventoryCoverage').length === 0) { + $('#inventoryList').parent().before(`
${coverageHtml}
`); + } else { + $('#inventoryCoverage').html(coverageHtml); + } + } + + response.data.forEach(item => { + // 원산지가 여러 개인 경우 표시 + const originBadge = item.origin_count > 1 + ? `${item.origin_count}개 원산지` + : ''; + + // 효능 태그 표시 + let efficacyTags = ''; + if (item.efficacy_tags && item.efficacy_tags.length > 0) { + efficacyTags = item.efficacy_tags.map(tag => + `${tag}` + ).join(''); + } + + // 가격 범위 표시 (원산지가 여러 개이고 가격차가 있는 경우) + let priceDisplay = item.avg_price ? formatCurrency(item.avg_price) : '-'; + if (item.origin_count > 1 && item.min_price && item.max_price && item.min_price !== item.max_price) { + priceDisplay = `${formatCurrency(item.min_price)} ~ ${formatCurrency(item.max_price)}`; + } + + // 통계 업데이트 + totalValue += item.total_value || 0; + if (item.total_quantity > 0) herbsInStock++; + + tbody.append(` + + ${item.insurance_code || '-'} + ${item.herb_name}${originBadge}${efficacyTags} + ${item.total_quantity.toFixed(1)} + ${item.lot_count} + ${priceDisplay} + ${formatCurrency(item.total_value)} + + + + + + `); + }); + + // 통계 업데이트 + $('#totalInventoryValue').text(formatCurrency(totalValue)); + $('#totalHerbsInStock').text(`${herbsInStock}종`); + + // 클릭 이벤트 바인딩 + $('.view-inventory-detail').on('click', function(e) { + e.stopPropagation(); + const herbId = $(this).data('herb-id'); + showInventoryDetail(herbId); + }); + + // 입출고 내역 버튼 이벤트 + $('.view-stock-ledger').on('click', function(e) { + e.stopPropagation(); + const herbId = $(this).data('herb-id'); + const herbName = $(this).data('herb-name'); + viewStockLedger(herbId, herbName); + }); + } + }); + } + + // 재고 상세 모달 표시 + function showInventoryDetail(herbId) { + $.get(`/api/inventory/detail/${herbId}`, function(response) { + if (response.success) { + const data = response.data; + + // 원산지별 재고 정보 HTML 생성 + let originsHtml = ''; + data.origins.forEach(origin => { + originsHtml += ` +
+
+
+ ${origin.origin_country} + ${origin.total_quantity.toFixed(1)}g +
+
+
+
+
+ 평균 단가:
+ ${formatCurrency(origin.avg_price)}/g +
+
+ 재고 가치:
+ ${formatCurrency(origin.total_value)} +
+
+ + + + + + + + + + + + `; + + origin.lots.forEach(lot => { + // variant 속성들을 뱃지로 표시 + let variantBadges = ''; + if (lot.form) variantBadges += `${lot.form}`; + if (lot.processing) variantBadges += `${lot.processing}`; + if (lot.grade) variantBadges += `${lot.grade}`; + + originsHtml += ` + + + + + + + + `; + }); + + originsHtml += ` + +
로트ID품명수량단가입고일도매상
#${lot.lot_id} + ${lot.display_name ? `${lot.display_name}` : '-'} + ${variantBadges} + ${lot.quantity_onhand.toFixed(1)}g${formatCurrency(lot.unit_price_per_g)}${lot.received_date}${lot.supplier_name || '-'}
+
+
`; + }); + + // 모달 생성 및 표시 + const modalHtml = ` + `; + + // 기존 모달 제거 + $('#inventoryDetailModal').remove(); + $('body').append(modalHtml); + + // 모달 표시 + const modal = new bootstrap.Modal(document.getElementById('inventoryDetailModal')); + modal.show(); + } + }); + } + + // 약재 목록 로드 + function loadHerbs() { + $.get('/api/herbs', function(response) { + if (response.success) { + const tbody = $('#herbsList'); + tbody.empty(); + + response.data.forEach(herb => { + tbody.append(` + + ${herb.insurance_code || '-'} + ${herb.herb_name} + ${herb.specification || '-'} + ${herb.current_stock ? herb.current_stock.toFixed(1) + 'g' : '0g'} + + + + + `); + }); + } + }); + } + + // 입고장 목록 로드 + function loadPurchaseReceipts() { + const startDate = $('#purchaseStartDate').val(); + const endDate = $('#purchaseEndDate').val(); + const supplierId = $('#purchaseSupplier').val(); + + let url = '/api/purchase-receipts?'; + if (startDate) url += `start_date=${startDate}&`; + if (endDate) url += `end_date=${endDate}&`; + if (supplierId) url += `supplier_id=${supplierId}`; + + $.get(url, function(response) { + if (response.success) { + const tbody = $('#purchaseReceiptsList'); + tbody.empty(); + + if (response.data.length === 0) { + tbody.append('입고장이 없습니다.'); + return; + } + + response.data.forEach(receipt => { + tbody.append(` + + ${receipt.receipt_date} + ${receipt.supplier_name} + ${receipt.line_count}개 + ${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'} + ${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'} + ${receipt.source_file || '-'} + + + + + + `); + }); + + // 이벤트 바인딩 + $('.view-receipt').on('click', function() { + const receiptId = $(this).data('id'); + viewReceiptDetail(receiptId); + }); + + $('.delete-receipt').on('click', function() { + const receiptId = $(this).data('id'); + if (confirm('정말 이 입고장을 삭제하시겠습니까? 사용되지 않은 재고만 삭제 가능합니다.')) { + deleteReceipt(receiptId); + } + }); + } + }); + } + + // 입고장 상세 보기 + function viewReceiptDetail(receiptId) { + $.get(`/api/purchase-receipts/${receiptId}`, function(response) { + if (response.success) { + const data = response.data; + let linesHtml = ''; + + data.lines.forEach(line => { + // display_name이 있으면 표시, 없으면 herb_name + const displayName = line.display_name || line.herb_name; + + // variant 속성들을 뱃지로 표시 + let variantBadges = ''; + if (line.form) variantBadges += `${line.form}`; + if (line.processing) variantBadges += `${line.processing}`; + if (line.grade) variantBadges += `${line.grade}`; + + linesHtml += ` + + +
${line.herb_name}
+ ${line.display_name ? `${line.display_name}` : ''} + ${variantBadges} + + ${line.insurance_code || '-'} + ${line.origin_country || '-'} + ${line.quantity_g}g + ${formatCurrency(line.unit_price_per_g)} + ${formatCurrency(line.line_total)} + ${line.current_stock}g + + `; + }); + + const modalHtml = ` + + `; + + // 기존 모달 제거 + $('#receiptDetailModal').remove(); + $('body').append(modalHtml); + $('#receiptDetailModal').modal('show'); + } + }); + } + + // 입고장 삭제 + function deleteReceipt(receiptId) { + $.ajax({ + url: `/api/purchase-receipts/${receiptId}`, + method: 'DELETE', + success: function(response) { + if (response.success) { + alert(response.message); + loadPurchaseReceipts(); + } + }, + error: function(xhr) { + alert('오류: ' + xhr.responseJSON.error); + } + }); + } + + // 입고장 조회 버튼 + $('#searchPurchaseBtn').on('click', function() { + loadPurchaseReceipts(); + }); + + // 도매상 목록 로드 (셀렉트 박스용) + function loadSuppliersForSelect() { + $.get('/api/suppliers', function(response) { + if (response.success) { + const select = $('#uploadSupplier'); + select.empty().append(''); + + response.data.forEach(supplier => { + select.append(``); + }); + + // 필터용 셀렉트 박스도 업데이트 + const filterSelect = $('#purchaseSupplier'); + filterSelect.empty().append(''); + response.data.forEach(supplier => { + filterSelect.append(``); + }); + } + }); + } + + // 도매상 등록 + $('#saveSupplierBtn').on('click', function() { + const supplierData = { + name: $('#supplierName').val(), + business_no: $('#supplierBusinessNo').val(), + contact_person: $('#supplierContactPerson').val(), + phone: $('#supplierPhone').val(), + address: $('#supplierAddress').val() + }; + + if (!supplierData.name) { + alert('도매상명은 필수입니다.'); + return; + } + + $.ajax({ + url: '/api/suppliers', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(supplierData), + success: function(response) { + if (response.success) { + alert('도매상이 등록되었습니다.'); + $('#supplierModal').modal('hide'); + $('#supplierForm')[0].reset(); + loadSuppliersForSelect(); + } + }, + error: function(xhr) { + alert('오류: ' + xhr.responseJSON.error); + } + }); + }); + + // 입고장 업로드 + $('#purchaseUploadForm').on('submit', function(e) { + e.preventDefault(); + + const supplierId = $('#uploadSupplier').val(); + if (!supplierId) { + alert('도매상을 선택해주세요.'); + return; + } + + const formData = new FormData(); + const fileInput = $('#purchaseFile')[0]; + + if (fileInput.files.length === 0) { + alert('파일을 선택해주세요.'); + return; + } + + formData.append('file', fileInput.files[0]); + formData.append('supplier_id', supplierId); + + $('#uploadResult').html('
업로드 중...
'); + + $.ajax({ + url: '/api/upload/purchase', + method: 'POST', + data: formData, + processData: false, + contentType: false, + success: function(response) { + if (response.success) { + let summaryHtml = ''; + if (response.summary) { + summaryHtml = `
+ + 형식: ${response.summary.format}
+ 처리: ${response.summary.processed_rows}개 라인
+ 품목: ${response.summary.total_items}종
+ 수량: ${response.summary.total_quantity}
+ 금액: ${response.summary.total_amount} +
`; + } + + $('#uploadResult').html( + `
+ ${response.message} + ${summaryHtml} +
` + ); + $('#purchaseUploadForm')[0].reset(); + + // 입고장 목록 새로고침 + loadPurchaseReceipts(); + } + }, + error: function(xhr) { + $('#uploadResult').html( + `
+ 오류: ${xhr.responseJSON.error} +
` + ); + } + }); + }); + + // 검색 기능 + $('#patientSearch').on('keyup', function() { + const value = $(this).val().toLowerCase(); + $('#patientsList tr').filter(function() { + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1); + }); + }); + + $('#inventorySearch').on('keyup', function() { + const value = $(this).val().toLowerCase(); + $('#inventoryList tr').filter(function() { + $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1); + }); + }); + + // 헬퍼 함수들 + function loadPatientsForSelect() { + $.get('/api/patients', function(response) { + if (response.success) { + const select = $('#compoundPatient'); + select.empty().append(''); + + response.data.forEach(patient => { + select.append(``); + }); + } + }); + } + + function loadFormulasForSelect() { + $.get('/api/formulas', function(response) { + if (response.success) { + const select = $('#compoundFormula'); + select.empty().append(''); + + // 직접조제 옵션 추가 + select.append(''); + + // 등록된 처방 추가 + if (response.data.length > 0) { + select.append(''); + response.data.forEach(formula => { + select.append(``); + }); + select.append(''); + } + } + }); + } + + function loadHerbsForSelect(selectElement) { + $.get('/api/herbs/masters', function(response) { + if (response.success) { + selectElement.empty().append(''); + + // 재고가 있는 약재만 필터링하여 표시 + const herbsWithStock = response.data.filter(herb => herb.has_stock === 1); + + herbsWithStock.forEach(herb => { + // ingredient_code를 value로 사용하고, 한글명(한자명) 형식으로 표시 + let displayName = herb.herb_name; + if (herb.herb_name_hanja) { + displayName += ` (${herb.herb_name_hanja})`; + } + selectElement.append(``); + }); + } + }).fail(function(error) { + console.error('Failed to load herbs:', error); + }); + } + + // ingredient_code 기반으로 제품 옵션 로드 + function loadProductOptions(row, ingredientCode, herbName) { + $.get(`/api/herbs/by-ingredient/${ingredientCode}`, function(response) { + if (response.success) { + const productSelect = row.find('.product-select'); + productSelect.empty(); + + if (response.data.length === 0) { + productSelect.append(''); + productSelect.prop('disabled', true); + } else { + productSelect.append(''); + response.data.forEach(product => { + const stockInfo = product.stock_quantity > 0 ? `(재고: ${product.stock_quantity.toFixed(1)}g)` : '(재고 없음)'; + const companyInfo = product.company_name ? `[${product.company_name}]` : ''; + productSelect.append(``); + }); + productSelect.prop('disabled', false); + } + } + }).fail(function() { + console.error(`Failed to load products for ingredient code: ${ingredientCode}`); + }); + } + + // 원산지별 재고 옵션 로드 + function loadOriginOptions(herbId, requiredQty) { + $.get(`/api/herbs/${herbId}/available-lots`, function(response) { + if (response.success) { + const selectElement = $(`tr[data-herb-id="${herbId}"] .origin-select`); + selectElement.empty(); + + const origins = response.data.origins; + + if (origins.length === 0) { + selectElement.append(''); + selectElement.prop('disabled', true); + $(`tr[data-herb-id="${herbId}"] .stock-status`) + .html('재고 없음'); + } else { + selectElement.append(''); + + // 로트가 2개 이상인 경우 수동 배분 옵션 추가 + const totalLots = origins.reduce((sum, o) => sum + o.lot_count, 0); + if (totalLots > 1) { + selectElement.append(''); + } + + origins.forEach(origin => { + const stockStatus = origin.total_quantity >= requiredQty ? '' : ' (재고 부족)'; + const priceInfo = `${formatCurrency(origin.min_price)}/g`; + + // 해당 원산지의 display_name 목록 생성 + let displayNames = []; + if (origin.lots && origin.lots.length > 0) { + origin.lots.forEach(lot => { + if (lot.display_name) { + displayNames.push(lot.display_name); + } + }); + } + + // 고유한 display_name만 표시 (중복 제거) + const uniqueDisplayNames = [...new Set(displayNames)]; + const displayNameText = uniqueDisplayNames.length > 0 + ? ` [${uniqueDisplayNames.join(', ')}]` + : ''; + + const option = ``; + selectElement.append(option); + }); + + selectElement.prop('disabled', false); + + // 원산지 선택 변경 이벤트 (수동 배분 모달 트리거) + selectElement.off('change').on('change', function() { + const selectedValue = $(this).val(); + const row = $(this).closest('tr'); + + if (selectedValue === 'manual') { + // 현재 행의 실제 필요량 재계산 + const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0; + const cheopTotal = parseFloat($('#cheopTotal').val()) || 0; + const actualRequiredQty = gramsPerCheop * cheopTotal; + + // 수동 배분 모달 열기 (재계산된 필요량 사용) + openLotAllocationModal(herbId, actualRequiredQty, row, response.data); + } else { + // 기존 자동/원산지 선택 - lot_assignments 제거 + row.removeAttr('data-lot-assignments'); + } + }); + + // 재고 상태 업데이트 + const totalAvailable = response.data.total_quantity; + const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`); + + if (totalAvailable >= requiredQty) { + statusElement.html(`충분 (${totalAvailable.toFixed(1)}g)`); + } else { + statusElement.html(`부족 (${totalAvailable.toFixed(1)}g)`); + } + } + } + }); + } + + // 재고 원장 보기 + let currentLedgerData = []; // 원본 데이터 저장 + + function viewStockLedger(herbId, herbName) { + const url = herbId ? `/api/stock-ledger?herb_id=${herbId}` : '/api/stock-ledger'; + + $.get(url, function(response) { + if (response.success) { + // 원본 데이터 저장 + currentLedgerData = response.ledger; + + // 헤더 업데이트 + if (herbName) { + $('#stockLedgerModal .modal-title').html(` ${herbName} 입출고 원장`); + } else { + $('#stockLedgerModal .modal-title').html(` 전체 입출고 원장`); + } + + // 필터 적용하여 표시 + applyLedgerFilters(); + + // 약재 필터 옵션 업데이트 + const herbFilter = $('#ledgerHerbFilter'); + if (herbFilter.find('option').length <= 1) { + response.summary.forEach(herb => { + herbFilter.append(``); + }); + } + + $('#stockLedgerModal').modal('show'); + } + }).fail(function() { + alert('입출고 내역을 불러오는데 실패했습니다.'); + }); + } + + // 필터 적용 함수 + function applyLedgerFilters() { + const typeFilter = $('#ledgerTypeFilter').val(); + const tbody = $('#stockLedgerList'); + tbody.empty(); + + // 필터링된 데이터 + let filteredData = currentLedgerData; + + // 타입 필터 적용 + if (typeFilter) { + filteredData = currentLedgerData.filter(entry => entry.event_type === typeFilter); + } + + // 데이터 표시 + filteredData.forEach(entry => { + let typeLabel = ''; + let typeBadge = ''; + switch(entry.event_type) { + case 'PURCHASE': + case 'RECEIPT': + typeLabel = '입고'; + typeBadge = 'badge bg-success'; + break; + case 'CONSUME': + typeLabel = '출고'; + typeBadge = 'badge bg-danger'; + break; + case 'ADJUST': + typeLabel = '보정'; + typeBadge = 'badge bg-warning'; + break; + default: + typeLabel = entry.event_type; + typeBadge = 'badge bg-secondary'; + } + + const quantity = Math.abs(entry.quantity_delta); + const sign = entry.quantity_delta > 0 ? '+' : '-'; + const quantityDisplay = entry.quantity_delta > 0 + ? `+${quantity.toFixed(1)}g` + : `-${quantity.toFixed(1)}g`; + + const referenceInfo = entry.patient_name + ? `${entry.patient_name}` + : entry.supplier_name || '-'; + + tbody.append(` + + ${entry.event_time} + ${typeLabel} + ${entry.herb_name} + ${quantityDisplay} + ${entry.unit_cost_per_g ? formatCurrency(entry.unit_cost_per_g) + '/g' : '-'} + ${entry.origin_country || '-'} + ${referenceInfo} + ${entry.reference_no || '-'} + + `); + }); + + // 데이터가 없는 경우 + if (filteredData.length === 0) { + tbody.append(` + + 데이터가 없습니다. + + `); + } + } + + // 입출고 원장 모달 버튼 이벤트 + $('#showStockLedgerBtn').on('click', function() { + viewStockLedger(null, null); + }); + + // 필터 변경 이벤트 + $('#ledgerHerbFilter').on('change', function() { + const herbId = $(this).val(); + + // 약재 필터 변경 시 데이터 재로드 + if (herbId) { + const herbName = $('#ledgerHerbFilter option:selected').text(); + viewStockLedger(herbId, herbName); + } else { + viewStockLedger(null, null); + } + }); + + // 타입 필터 변경 이벤트 (현재 데이터에서 필터링만) + $('#ledgerTypeFilter').on('change', function() { + applyLedgerFilters(); + }); + + // ==================== 재고 보정 ==================== + + // 재고 보정 모달 열기 + $('#showStockAdjustmentBtn').on('click', function() { + // 현재 날짜 설정 + $('#adjustmentDate').val(new Date().toISOString().split('T')[0]); + $('#adjustmentItemsList').empty(); + $('#stockAdjustmentForm')[0].reset(); + $('#stockAdjustmentModal').modal('show'); + }); + + // 재고 보정 내역 모달 열기 + $('#showAdjustmentHistoryBtn').on('click', function() { + loadAdjustmentHistory(); + }); + + // 재고 보정 내역 로드 + function loadAdjustmentHistory() { + $.get('/api/stock-adjustments', function(response) { + if (response.success) { + const tbody = $('#adjustmentHistoryList'); + tbody.empty(); + + if (response.data.length === 0) { + tbody.append(` + + 보정 내역이 없습니다. + + `); + } else { + response.data.forEach(adj => { + // 보정 유형 한글 변환 + let typeLabel = ''; + switch(adj.adjustment_type) { + case 'LOSS': typeLabel = '감모/손실'; break; + case 'FOUND': typeLabel = '발견'; break; + case 'RECOUNT': typeLabel = '재고조사'; break; + case 'DAMAGE': typeLabel = '파손'; break; + case 'EXPIRE': typeLabel = '유통기한 경과'; break; + default: typeLabel = adj.adjustment_type; + } + + tbody.append(` + + ${adj.adjustment_date} + ${adj.adjustment_no} + ${typeLabel} + ${adj.detail_count || 0}개 + ${adj.created_by || '-'} + ${adj.notes || '-'} + + + + + `); + }); + + // 상세보기 버튼 이벤트 + $('.view-adjustment-detail').on('click', function() { + const adjustmentId = $(this).data('id'); + viewAdjustmentDetail(adjustmentId); + }); + } + + $('#adjustmentHistoryModal').modal('show'); + } + }).fail(function() { + alert('보정 내역을 불러오는데 실패했습니다.'); + }); + } + + // 재고 보정 상세 조회 + function viewAdjustmentDetail(adjustmentId) { + $.get(`/api/stock-adjustments/${adjustmentId}`, function(response) { + if (response.success) { + const data = response.data; + + // 보정 정보 표시 + $('#detailAdjustmentNo').text(data.adjustment_no); + $('#detailAdjustmentDate').text(data.adjustment_date); + + // 보정 유형 한글 변환 + let typeLabel = ''; + switch(data.adjustment_type) { + case 'LOSS': typeLabel = '감모/손실'; break; + case 'FOUND': typeLabel = '발견'; break; + case 'RECOUNT': typeLabel = '재고조사'; break; + case 'DAMAGE': typeLabel = '파손'; break; + case 'EXPIRE': typeLabel = '유통기한 경과'; break; + default: typeLabel = data.adjustment_type; + } + $('#detailAdjustmentType').html(`${typeLabel}`); + $('#detailAdjustmentCreatedBy').text(data.created_by || '-'); + $('#detailAdjustmentNotes').text(data.notes || '-'); + + // 보정 상세 항목 표시 + const itemsBody = $('#detailAdjustmentItems'); + itemsBody.empty(); + + if (data.details && data.details.length > 0) { + data.details.forEach(item => { + const delta = item.quantity_delta; + let deltaHtml = ''; + if (delta > 0) { + deltaHtml = `+${delta.toFixed(1)}g`; + } else if (delta < 0) { + deltaHtml = `${delta.toFixed(1)}g`; + } else { + deltaHtml = '0g'; + } + + itemsBody.append(` + + ${item.herb_name} + ${item.insurance_code || '-'} + ${item.origin_country || '-'} + #${item.lot_id} + ${item.quantity_before.toFixed(1)}g + ${item.quantity_after.toFixed(1)}g + ${deltaHtml} + ${item.reason || '-'} + + `); + }); + } + + // 보정 상세 모달 표시 + $('#adjustmentDetailModal').modal('show'); + } + }).fail(function() { + alert('보정 상세 정보를 불러오는데 실패했습니다.'); + }); + } + + // 보정 대상 약재 추가 + let adjustmentItemCount = 0; + $('#addAdjustmentItemBtn').on('click', function() { + addAdjustmentItemRow(); + }); + + function addAdjustmentItemRow() { + adjustmentItemCount++; + const rowId = `adj-item-${adjustmentItemCount}`; + + const newRow = $(` + + + + + + + + - + + + + - + + + + + + + + `); + + $('#adjustmentItemsList').append(newRow); + + // 약재 목록 로드 + loadHerbsForSelect(newRow.find('.adj-herb-select')); + + // 약재 선택 이벤트 + newRow.find('.adj-herb-select').on('change', function() { + const herbId = $(this).val(); + const row = $(this).closest('tr'); + + if (herbId) { + loadLotsForAdjustment(herbId, row); + } else { + row.find('.adj-lot-select').empty().append('').prop('disabled', true); + row.find('.before-qty').text('-'); + row.find('.after-qty-input').val(''); + row.find('.delta-qty').text('-'); + } + }); + + // 로트 선택 이벤트 + newRow.find('.adj-lot-select').on('change', function() { + const selectedOption = $(this).find('option:selected'); + const row = $(this).closest('tr'); + + if (selectedOption.val()) { + const beforeQty = parseFloat(selectedOption.data('qty')) || 0; + row.find('.before-qty').text(beforeQty.toFixed(1) + 'g'); + row.data('before-qty', beforeQty); + + // 기존 변경후 값이 있으면 델타 재계산 + const afterQty = parseFloat(row.find('.after-qty-input').val()); + if (!isNaN(afterQty)) { + updateDelta(row, beforeQty, afterQty); + } + } else { + row.find('.before-qty').text('-'); + row.find('.after-qty-input').val(''); + row.find('.delta-qty').text('-'); + } + }); + + // 변경후 수량 입력 이벤트 + newRow.find('.after-qty-input').on('input', function() { + const row = $(this).closest('tr'); + const beforeQty = row.data('before-qty') || 0; + const afterQty = parseFloat($(this).val()) || 0; + + updateDelta(row, beforeQty, afterQty); + }); + + // 삭제 버튼 + newRow.find('.remove-adj-item').on('click', function() { + $(this).closest('tr').remove(); + }); + } + + // 약재별 로트 목록 로드 + function loadLotsForAdjustment(herbId, row) { + $.get(`/api/inventory/detail/${herbId}`, function(response) { + if (response.success) { + const lotSelect = row.find('.adj-lot-select'); + lotSelect.empty(); + lotSelect.append(''); + + const data = response.data; + + // 원산지별로 로트 표시 + data.origins.forEach(origin => { + const optgroup = $(``); + + origin.lots.forEach(lot => { + optgroup.append(` + + `); + }); + + lotSelect.append(optgroup); + }); + + lotSelect.prop('disabled', false); + } + }).fail(function() { + alert('재고 정보를 불러오는데 실패했습니다.'); + }); + } + + // 델타 계산 및 표시 + function updateDelta(row, beforeQty, afterQty) { + const delta = afterQty - beforeQty; + const deltaElement = row.find('.delta-qty'); + + if (delta > 0) { + deltaElement.html(`+${delta.toFixed(1)}g`); + } else if (delta < 0) { + deltaElement.html(`${delta.toFixed(1)}g`); + } else { + deltaElement.html('0g'); + } + + row.data('delta', delta); + } + + // 재고 보정 저장 버튼 + $('#saveAdjustmentBtn').on('click', function() { + saveStockAdjustment(); + }); + + // 재고 보정 저장 + $('#stockAdjustmentForm').on('submit', function(e) { + e.preventDefault(); + saveStockAdjustment(); + }); + + function saveStockAdjustment() { + const items = []; + let hasError = false; + + $('#adjustmentItemsList tr').each(function() { + const herbId = $(this).find('.adj-herb-select').val(); + const lotId = $(this).find('.adj-lot-select').val(); + const beforeQty = $(this).data('before-qty'); + const afterQty = parseFloat($(this).find('.after-qty-input').val()); + const delta = $(this).data('delta'); + const reason = $(this).find('.reason-input').val(); + + if (!herbId || !lotId) { + hasError = true; + return false; + } + + items.push({ + herb_item_id: parseInt(herbId), + lot_id: parseInt(lotId), + quantity_before: beforeQty, + quantity_after: afterQty, + quantity_delta: delta, + reason: reason + }); + }); + + if (hasError) { + alert('모든 항목의 약재와 로트를 선택해주세요.'); + return; + } + + if (items.length === 0) { + alert('보정할 항목을 추가해주세요.'); + return; + } + + const adjustmentData = { + adjustment_date: $('#adjustmentDate').val(), + adjustment_type: $('#adjustmentType').val(), + created_by: $('#adjustmentCreatedBy').val() || 'SYSTEM', + notes: $('#adjustmentNotes').val(), + details: items // API expects 'details', not 'items' + }; + + $.ajax({ + url: '/api/stock-adjustments', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(adjustmentData), + success: function(response) { + if (response.success) { + alert(`재고 보정이 완료되었습니다.\n보정번호: ${response.adjustment_no}\n항목 수: ${items.length}개`); + $('#stockAdjustmentModal').modal('hide'); + + // 재고 목록 새로고침 + loadInventory(); + } + }, + error: function(xhr) { + alert('오류: ' + (xhr.responseJSON?.error || '재고 보정 실패')); + } + }); + } + + function formatCurrency(amount) { + if (amount === null || amount === undefined) return '0원'; + return new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format(amount); + } + + // === 재고 자산 계산 설정 기능 === + + // 재고 현황 로드 (모드 포함) + function loadInventorySummary() { + const mode = localStorage.getItem('inventoryMode') || 'all'; + + $.get(`/api/inventory/summary?mode=${mode}`, function(response) { + if (response.success) { + $('#totalHerbs').text(response.data.length); + $('#inventoryValue').text(formatCurrency(response.summary.total_value)); + + // 모드 표시 업데이트 + if (response.summary.calculation_mode) { + $('#inventoryMode').text(response.summary.calculation_mode.mode_label); + + // 설정 모달이 열려있으면 정보 표시 + if ($('#inventorySettingsModal').hasClass('show')) { + updateModeInfo(response.summary.calculation_mode); + } + } + } + }); + } + + // 재고 계산 설정 저장 + window.saveInventorySettings = function() { + const selectedMode = $('input[name="inventoryMode"]:checked').val(); + + // localStorage에 저장 + localStorage.setItem('inventoryMode', selectedMode); + inventoryCalculationMode = selectedMode; + + // 재고 현황 다시 로드 + loadInventorySummary(); + + // 모달 닫기 + $('#inventorySettingsModal').modal('hide'); + + // 성공 메시지 + showToast('success', '재고 계산 설정이 변경되었습니다.'); + } + + // 모드 정보 업데이트 + function updateModeInfo(modeInfo) { + let infoHtml = ''; + + if (modeInfo.mode === 'all' && modeInfo.no_receipt_lots !== undefined) { + if (modeInfo.no_receipt_lots > 0) { + infoHtml = ` +

• 입고장 없는 LOT: ${modeInfo.no_receipt_lots}개

+

• 해당 재고 가치: ${formatCurrency(modeInfo.no_receipt_value)}

+ `; + } else { + infoHtml = '

• 모든 LOT이 입고장과 연결되어 있습니다.

'; + } + } else if (modeInfo.mode === 'receipt_only') { + infoHtml = '

• 입고장과 연결된 LOT만 계산합니다.

'; + } else if (modeInfo.mode === 'verified') { + infoHtml = '

• 검증 확인된 LOT만 계산합니다.

'; + } + + if (infoHtml) { + $('#modeInfoContent').html(infoHtml); + $('#modeInfo').show(); + } else { + $('#modeInfo').hide(); + } + } + + // 설정 모달이 열릴 때 현재 모드 설정 + $('#inventorySettingsModal').on('show.bs.modal', function() { + const currentMode = localStorage.getItem('inventoryMode') || 'all'; + $(`input[name="inventoryMode"][value="${currentMode}"]`).prop('checked', true); + + // 현재 모드 정보 로드 + $.get(`/api/inventory/summary?mode=${currentMode}`, function(response) { + if (response.success && response.summary.calculation_mode) { + updateModeInfo(response.summary.calculation_mode); + } + }); + }); + + // 모드 선택 시 즉시 정보 업데이트 + $('input[name="inventoryMode"]').on('change', function() { + const selectedMode = $(this).val(); + + // 선택한 모드의 정보를 미리보기로 로드 + $.get(`/api/inventory/summary?mode=${selectedMode}`, function(response) { + if (response.success && response.summary.calculation_mode) { + updateModeInfo(response.summary.calculation_mode); + } + }); + }); + + // Toast 메시지 표시 함수 (없으면 추가) + function showToast(type, message) { + const toastHtml = ` + + `; + + // Toast 컨테이너가 없으면 생성 + if (!$('#toastContainer').length) { + $('body').append('
'); + } + + const $toast = $(toastHtml); + $('#toastContainer').append($toast); + + const toast = new bootstrap.Toast($toast[0]); + toast.show(); + + // 5초 후 자동 제거 + setTimeout(() => { + $toast.remove(); + }, 5000); + } + + // ==================== 주성분코드 기반 약재 관리 ==================== + let allHerbMasters = []; // 전체 약재 데이터 저장 + let currentFilter = 'all'; // 현재 필터 상태 + + // 약재 마스터 목록 로드 + function loadHerbMasters() { + $.get('/api/herbs/masters', function(response) { + if (response.success) { + allHerbMasters = response.data; + + // 통계 정보 표시 + const summary = response.summary; + $('#herbMasterSummary').html(` +
+
+
📊 급여 약재 현황
+
+
+ 전체: ${summary.total_herbs}개 주성분 +
+
+ 재고 있음: ${summary.herbs_with_stock}개 +
+
+ 재고 없음: ${summary.herbs_without_stock}개 +
+
+ 보유율: ${summary.coverage_rate}% +
+
+
+
+
+
+ ${summary.herbs_with_stock} / ${summary.total_herbs} +
+
+
+
+ `); + + // 목록 표시 + displayHerbMasters(allHerbMasters); + } + }); + } + + // 약재 목록 표시 + function displayHerbMasters(herbs) { + const tbody = $('#herbMastersList'); + tbody.empty(); + + // 필터링 + let filteredHerbs = herbs; + if (currentFilter === 'stock') { + filteredHerbs = herbs.filter(h => h.has_stock); + } else if (currentFilter === 'no-stock') { + filteredHerbs = herbs.filter(h => !h.has_stock); + } + + // 검색 필터 + const searchText = $('#herbSearch').val().toLowerCase(); + if (searchText) { + filteredHerbs = filteredHerbs.filter(h => + h.herb_name.toLowerCase().includes(searchText) || + h.ingredient_code.toLowerCase().includes(searchText) + ); + } + + // 효능 필터 + const efficacyFilter = $('#efficacyFilter').val(); + if (efficacyFilter) { + filteredHerbs = filteredHerbs.filter(h => + h.efficacy_tags && h.efficacy_tags.includes(efficacyFilter) + ); + } + + // 표시 + filteredHerbs.forEach(herb => { + // 효능 태그 표시 + let efficacyTags = ''; + if (herb.efficacy_tags && herb.efficacy_tags.length > 0) { + efficacyTags = herb.efficacy_tags.map(tag => + `${tag}` + ).join(''); + } + + // 상태 표시 + const statusBadge = herb.has_stock + ? '재고 있음' + : '재고 없음'; + + // 재고량 표시 + const stockDisplay = herb.stock_quantity > 0 + ? `${herb.stock_quantity.toFixed(1)}g` + : '-'; + + // 평균단가 표시 + const priceDisplay = herb.avg_price > 0 + ? formatCurrency(herb.avg_price) + : '-'; + + tbody.append(` + + ${herb.ingredient_code} + ${herb.herb_name} + ${efficacyTags} + ${stockDisplay} + ${priceDisplay} + ${herb.product_count || 0}개 + ${statusBadge} + + + + + `); + }); + + if (filteredHerbs.length === 0) { + tbody.append('표시할 약재가 없습니다.'); + } + } + + // 약재 상세 보기 + function viewHerbDetail(ingredientCode) { + // TODO: 약재 상세 모달 구현 + console.log('View detail for:', ingredientCode); + } + + // 필터 버튼 이벤트 + $('#herbs .btn-group button[data-filter]').on('click', function() { + $('#herbs .btn-group button').removeClass('active'); + $(this).addClass('active'); + currentFilter = $(this).data('filter'); + displayHerbMasters(allHerbMasters); + }); + + // 검색 이벤트 + $('#herbSearch').on('keyup', function() { + displayHerbMasters(allHerbMasters); + }); + + // 효능 필터 이벤트 + $('#efficacyFilter').on('change', function() { + displayHerbMasters(allHerbMasters); + }); + + // 약재 관리 페이지가 활성화되면 데이터 로드 + $('.nav-link[data-page="herbs"]').on('click', function() { + setTimeout(() => loadHerbMasters(), 100); + }); + + // ==================== 로트 배분 모달 관련 함수들 ==================== + + // 로트 배분 모달 열기 + window.openLotAllocationModal = function(herbId, requiredQty, row, data) { + // 디버깅: 전달받은 필요량 확인 + console.log('로트 배분 모달 열기:', { + herbId: herbId, + requiredQty: requiredQty, + herbName: data.herb_name, + gramsPerCheop: row.find('.grams-per-cheop').val(), + cheopTotal: $('#cheopTotal').val() + }); + + currentLotAllocation = { + herbId: herbId, + requiredQty: requiredQty, + row: row, + data: data + }; + + // 모달 초기화 + $('#lotAllocationHerbName').text(data.herb_name); + $('#lotAllocationRequired').text(requiredQty.toFixed(1)); + $('#lotAllocationError').addClass('d-none'); + + // 로트 목록 생성 + const tbody = $('#lotAllocationList'); + tbody.empty(); + + // 모든 로트를 하나의 목록으로 표시 + let allLots = []; + data.origins.forEach(origin => { + origin.lots.forEach(lot => { + allLots.push({ + ...lot, + origin: origin.origin_country + }); + }); + }); + + // 단가 순으로 정렬 + allLots.sort((a, b) => a.unit_price_per_g - b.unit_price_per_g); + + allLots.forEach(lot => { + tbody.append(` + + #${lot.lot_id} + ${lot.origin || '미지정'} + ${lot.quantity_onhand.toFixed(1)}g + ${formatCurrency(lot.unit_price_per_g)}/g + + + + 0원 + + `); + }); + + // 입력 이벤트 + $('.lot-allocation-input').on('input', function() { + updateLotAllocationSummary(); + }); + + $('#lotAllocationModal').modal('show'); + }; + + // 로트 배분 합계 업데이트 + function updateLotAllocationSummary() { + let totalQty = 0; + let totalCost = 0; + + $('.lot-allocation-input').each(function() { + const qty = parseFloat($(this).val()) || 0; + const price = parseFloat($(this).data('price')) || 0; + const subtotal = qty * price; + + totalQty += qty; + totalCost += subtotal; + + // 소계 표시 + $(this).closest('tr').find('.lot-subtotal').text(formatCurrency(subtotal) + '원'); + }); + + $('#lotAllocationTotal').text(totalQty.toFixed(1)); + $('#lotAllocationSumQty').text(totalQty.toFixed(1) + 'g'); + $('#lotAllocationSumCost').text(formatCurrency(totalCost) + '원'); + + // 검증 + const required = currentLotAllocation.requiredQty; + const diff = Math.abs(totalQty - required); + + if (diff > 0.01) { + $('#lotAllocationError') + .removeClass('d-none') + .text(`필요량(${required.toFixed(1)}g)과 배분 합계(${totalQty.toFixed(1)}g)가 일치하지 않습니다.`); + $('#lotAllocationConfirmBtn').prop('disabled', true); + } else { + $('#lotAllocationError').addClass('d-none'); + $('#lotAllocationConfirmBtn').prop('disabled', false); + } + } + + // 자동 배분 + $('#lotAllocationAutoBtn').on('click', function() { + let remaining = currentLotAllocation.requiredQty; + + $('.lot-allocation-input').each(function() { + const maxAvailable = parseFloat($(this).data('max')); + const allocate = Math.min(remaining, maxAvailable); + + $(this).val(allocate.toFixed(1)); + remaining -= allocate; + + if (remaining <= 0) return false; // break + }); + + updateLotAllocationSummary(); + }); + + // 로트 배분 확인 + $('#lotAllocationConfirmBtn').on('click', function() { + const allocations = []; + + $('.lot-allocation-input').each(function() { + const qty = parseFloat($(this).val()) || 0; + if (qty > 0) { + const lotId = $(this).closest('tr').data('lot-id'); + allocations.push({ + lot_id: lotId, + quantity: qty + }); + } + }); + + // 현재 행에 로트 배분 정보 저장 + currentLotAllocation.row.attr('data-lot-assignments', JSON.stringify(allocations)); + + // 상태 표시 업데이트 + const statusElement = currentLotAllocation.row.find('.stock-status'); + statusElement.html(`수동 배분 (${allocations.length}개 로트)`); + + $('#lotAllocationModal').modal('hide'); + }); + + // 조제 실행 시 lot_assignments 포함 + window.getIngredientDataForCompound = function() { + const ingredients = []; + + $('#compoundIngredients tr').each(function() { + const herbId = $(this).attr('data-herb-id'); + if (herbId) { + const ingredient = { + herb_item_id: parseInt(herbId), + grams_per_cheop: parseFloat($(this).find('.grams-per-cheop').val()) || 0, + total_grams: parseFloat($(this).find('.total-grams').text()) || 0, + origin: $(this).find('.origin-select').val() || 'auto' + }; + + // 수동 로트 배분이 있는 경우 + const lotAssignments = $(this).attr('data-lot-assignments'); + if (lotAssignments) { + ingredient.lot_assignments = JSON.parse(lotAssignments); + ingredient.origin = 'manual'; // origin을 manual로 설정 + } + + ingredients.push(ingredient); + } + }); + + return ingredients; + }; + + // ==================== 약재 정보 시스템 ==================== + + // 약재 정보 페이지 로드 + window.loadHerbInfo = function loadHerbInfo() { + loadAllHerbsInfo(); + loadEfficacyTags(); + + // 뷰 전환 버튼 + $('#herb-info button[data-view]').on('click', function() { + $('#herb-info button[data-view]').removeClass('active'); + $(this).addClass('active'); + + const view = $(this).data('view'); + if (view === 'search') { + $('#herb-search-section').show(); + $('#herb-efficacy-section').hide(); + loadAllHerbsInfo(); + } else if (view === 'efficacy') { + $('#herb-search-section').hide(); + $('#herb-efficacy-section').show(); + loadEfficacyTagButtons(); + } else if (view === 'category') { + $('#herb-search-section').show(); + $('#herb-efficacy-section').hide(); + loadHerbsByCategory(); + } + }); + + // 검색 버튼 + $('#herbSearchBtn').off('click').on('click', function() { + const searchTerm = $('#herbSearchInput').val(); + searchHerbs(searchTerm); + }); + + // 엔터 키로 검색 + $('#herbSearchInput').off('keypress').on('keypress', function(e) { + if (e.which === 13) { + searchHerbs($(this).val()); + } + }); + + // 필터 변경 + $('#herbInfoEfficacyFilter, #herbInfoPropertyFilter').off('change').on('change', function() { + filterHerbs(); + }); + } + + // 모든 약재 정보 로드 + window.loadAllHerbsInfo = function loadAllHerbsInfo() { + $.get('/api/herbs/masters', function(response) { + if (response.success) { + displayHerbCards(response.data); + } + }); + } + + // 약재 카드 표시 + window.displayHerbCards = function displayHerbCards(herbs) { + const grid = $('#herbInfoGrid'); + grid.empty(); + + if (herbs.length === 0) { + grid.html('
검색 결과가 없습니다.
'); + return; + } + + herbs.forEach(herb => { + // 재고 상태에 따른 배지 색상 + const stockBadge = herb.has_stock ? + '재고있음' : + '재고없음'; + + // 효능 태그 HTML + let tagsHtml = ''; + if (herb.efficacy_tags && herb.efficacy_tags.length > 0) { + tagsHtml = herb.efficacy_tags.slice(0, 3).map(tag => + `${tag}` + ).join(''); + if (herb.efficacy_tags.length > 3) { + tagsHtml += `+${herb.efficacy_tags.length - 3}`; + } + } + + const card = ` +
+
+
+
+
+ ${herb.herb_name} + ${herb.herb_name_hanja ? `(${herb.herb_name_hanja})` : ''} +
+ ${stockBadge} +
+

+ ${herb.ingredient_code} +

+
+ ${tagsHtml || '태그 없음'} +
+ ${herb.main_effects ? + `

${herb.main_effects.substring(0, 50)}...

` : + '

효능 정보 없음

' + } +
+
+
+ `; + grid.append(card); + }); + + // 카드 클릭 이벤트 + $('.herb-info-card').off('click').on('click', function() { + const ingredientCode = $(this).data('ingredient-code'); + showHerbDetail(ingredientCode); + }); + } + + // 약재 상세 정보 표시 + function showHerbDetail(ingredientCode) { + // herb_master_extended에서 herb_id 찾기 + $.get('/api/herbs/masters', function(response) { + if (response.success) { + const herb = response.data.find(h => h.ingredient_code === ingredientCode); + if (herb && herb.herb_id) { + // 확장 정보 조회 + $.get(`/api/herbs/${herb.herb_id}/extended`, function(detailResponse) { + displayHerbDetailModal(detailResponse); + }).fail(function() { + // 확장 정보가 없으면 기본 정보만 표시 + displayHerbDetailModal(herb); + }); + } + } + }); + } + + // 상세 정보 모달 표시 + function displayHerbDetailModal(herb) { + $('#herbDetailName').text(herb.name_korean || herb.herb_name || '-'); + $('#herbDetailHanja').text(herb.name_hanja || herb.herb_name_hanja || ''); + + // 기본 정보 + $('#detailIngredientCode').text(herb.ingredient_code || '-'); + $('#detailLatinName').text(herb.name_latin || herb.herb_name_latin || '-'); + $('#detailMedicinalPart').text(herb.medicinal_part || '-'); + $('#detailOriginPlant').text(herb.origin_plant || '-'); + + // 성미귀경 + $('#detailProperty').text(herb.property || '-'); + $('#detailTaste').text(herb.taste || '-'); + $('#detailMeridian').text(herb.meridian_tropism || '-'); + + // 효능효과 + $('#detailMainEffects').text(herb.main_effects || '-'); + $('#detailIndications').text(herb.indications || '-'); + + // 효능 태그 + if (herb.efficacy_tags && herb.efficacy_tags.length > 0) { + const tagsHtml = herb.efficacy_tags.map(tag => { + const strength = tag.strength || 3; + const sizeClass = strength >= 4 ? 'fs-5' : 'fs-6'; + return `${tag.name || tag}`; + }).join(''); + $('#detailEfficacyTags').html(tagsHtml); + } else { + $('#detailEfficacyTags').html('태그 없음'); + } + + // 용법용량 + $('#detailDosageRange').text(herb.dosage_range || '-'); + $('#detailDosageMax').text(herb.dosage_max || '-'); + $('#detailPreparation').text(herb.preparation_method || '-'); + + // 안전성 + $('#detailContraindications').text(herb.contraindications || '-'); + $('#detailPrecautions').text(herb.precautions || '-'); + + // 성분정보 + $('#detailActiveCompounds').text(herb.active_compounds || '-'); + + // 임상응용 + $('#detailPharmacological').text(herb.pharmacological_effects || '-'); + $('#detailClinical').text(herb.clinical_applications || '-'); + + // 정보 수정 버튼 + $('#editHerbInfoBtn').off('click').on('click', function() { + editHerbInfo(herb.herb_id || herb.ingredient_code); + }); + + $('#herbDetailModal').modal('show'); + } + + // 약재 검색 + function searchHerbs(searchTerm) { + if (!searchTerm) { + loadAllHerbsInfo(); + return; + } + + $.get('/api/herbs/masters', function(response) { + if (response.success) { + const filtered = response.data.filter(herb => { + const term = searchTerm.toLowerCase(); + return (herb.herb_name && herb.herb_name.toLowerCase().includes(term)) || + (herb.herb_name_hanja && herb.herb_name_hanja.includes(term)) || + (herb.ingredient_code && herb.ingredient_code.toLowerCase().includes(term)) || + (herb.main_effects && herb.main_effects.toLowerCase().includes(term)) || + (herb.efficacy_tags && herb.efficacy_tags.some(tag => tag.toLowerCase().includes(term))); + }); + displayHerbCards(filtered); + } + }); + } + + // 필터 적용 + function filterHerbs() { + const efficacyFilter = $('#herbInfoEfficacyFilter').val(); + const propertyFilter = $('#herbInfoPropertyFilter').val(); + + $.get('/api/herbs/masters', function(response) { + if (response.success) { + let filtered = response.data; + + if (efficacyFilter) { + filtered = filtered.filter(herb => + herb.efficacy_tags && herb.efficacy_tags.includes(efficacyFilter) + ); + } + + if (propertyFilter) { + filtered = filtered.filter(herb => + herb.property === propertyFilter + ); + } + + displayHerbCards(filtered); + } + }); + } + + // 효능 태그 로드 + function loadEfficacyTags() { + $.get('/api/efficacy-tags', function(tags) { + const select = $('#herbInfoEfficacyFilter'); + select.empty().append(''); + + tags.forEach(tag => { + select.append(``); + }); + }); + } + + // 효능 태그 버튼 표시 + function loadEfficacyTagButtons() { + $.get('/api/efficacy-tags', function(tags) { + const container = $('#efficacyTagsContainer'); + container.empty(); + + // 카테고리별로 그룹화 + const grouped = {}; + tags.forEach(tag => { + if (!grouped[tag.category]) { + grouped[tag.category] = []; + } + grouped[tag.category].push(tag); + }); + + // 카테고리별로 표시 + Object.keys(grouped).forEach(category => { + const categoryHtml = ` +
+
${category}
+
+ ${grouped[category].map(tag => ` + + `).join('')} +
+
+ `; + container.append(categoryHtml); + }); + + // 태그 버튼 클릭 이벤트 + $('.efficacy-tag-btn').on('click', function() { + $(this).toggleClass('active'); + const selectedTags = $('.efficacy-tag-btn.active').map(function() { + return $(this).data('tag'); + }).get(); + + if (selectedTags.length > 0) { + searchByEfficacyTags(selectedTags); + } else { + loadAllHerbsInfo(); + } + }); + }); + } + + // 효능 태그로 검색 + function searchByEfficacyTags(tags) { + const queryString = tags.map(tag => `tags=${encodeURIComponent(tag)}`).join('&'); + $.get(`/api/herbs/search-by-efficacy?${queryString}`, function(herbs) { + displayHerbCards(herbs); + }); + } + + // 약재 정보 수정 (추후 구현) + function editHerbInfo(herbId) { + // herbId는 향후 수정 기능 구현시 사용 예정 + console.log('Edit herb info for ID:', herbId); + alert('약재 정보 수정 기능은 준비 중입니다.'); + // TODO: 정보 수정 폼 구현 + } +}); \ No newline at end of file diff --git a/templates/index.html.backup b/templates/index.html.backup new file mode 100644 index 0000000..5b03308 --- /dev/null +++ b/templates/index.html.backup @@ -0,0 +1,1578 @@ + + + + + + 한약 재고관리 시스템 + + + + + + +
+ +
+
+ + + + +
+ +
+

대시보드

+
+
+
+
총 환자수
+
0
+
+
+
+
+
재고 품목
+
0
+
+
+
+
+
오늘 조제
+
0
+
+
+
+
+
+ 재고 자산 + +
+
0
+ 전체 재고 +
+
+
+ +
+
+
+
+
최근 조제 내역
+
+
+ + + + + + + + + + + + + + +
조제일환자명처방명제수파우치상태
+
+
+
+
+
+ + +
+
+

환자 관리

+ +
+
+
+
+ +
+ + + + + + + + + + + + + + + +
환자명전화번호성별생년월일처방 횟수메모작업
+
+
+
+ + +
+
+

입고 관리

+
+ + +
+
+
입고장 목록
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + +
입고일공급업체품목 수총 금액총 수량파일명작업
+
+
+ + +
+
+
새 입고 등록 (Excel 업로드)
+ +
+
+
+
+
+ + +
+
+ + +
+
+
+ Excel 형식: 한의사랑, 한의정보 (자동 감지)
+ Excel 내 업체명은 제조사(제약사)로 저장됩니다 +
+ +
+
+
+
+
+ + +
+
+

처방 관리

+ +
+
+
+ + + + + + + + + + + + + + +
처방코드처방명기본 첩수기본 파우치구성 약재작업
+
+
+
+ + +
+
+

조제 관리

+ +
+ + +
+
+
+
조제 내역
+
+ 오늘 조제: 0 + 이번달 조제: 0 +
+
+
+
+
+
+ +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
#조제일시환자명연락처처방명제수첩수파우치원가판매가상태처방전번호작업
+
+ +
+
+ + + +
+ + +
+
+

재고 현황

+
+ + + +
+
+ +
+
+
+
총 재고 금액
+

₩0

+
+
+
+
+
재고 보유 약재
+

0종

+
+
+
+
+
오늘 입고
+

0건

+
+
+
+
+
오늘 출고
+

0건

+
+
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + +
보험코드약재명현재 재고(g)로트 수평균 단가재고 금액작업
+
+
+
+ + + + + + + + + + + + +
+ + +
+
+

약재 관리 (주성분코드 기준)

+
+
+ + + +
+ +
+
+ + +
+ +
+ + +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + +
주성분코드약재명효능재고량평균단가제품수상태작업
+
+
+
+ + +
+
+

한약재 정보 시스템

+
+ + + +
+
+ + +
+
+
+
+ + + +
+
+
+
+ + +
+
+
+
+ + + + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file