diff --git a/.claude/project_state.md b/.claude/project_state.md new file mode 100644 index 0000000..f952b56 --- /dev/null +++ b/.claude/project_state.md @@ -0,0 +1,67 @@ +# 한약 재고관리 시스템 - 프로젝트 상태 + +## ✅ 해결된 이슈 (2026-02-15) +1. **입고장 상세보기 500 에러** - 해결 완료! + - 원인: receipt_date가 튜플 문자열로 저장됨, total_amount가 bytes로 저장됨 + - 해결: 데이터베이스 값 수정 완료 +2. **Flask 프로세스 중복 실행 문제** - 해결 완료! + - 해결: run_server.sh 스크립트 생성으로 단일 프로세스 관리 + +## 📝 최근 수정 사항 (2026-02-15) + +### ✅ 완료된 작업 +1. **총금액 계산 문제 해결** + - `app.py` 쿼리에서 중복된 `pr.total_amount` 제거 + - `SUM(prl.line_total) as total_amount` 사용 + - API가 정확한 총금액 1,551,900원 반환 + +2. **UI 개선** + - 입고장 목록에서 총금액을 굵은 파란색으로 강조 + - 총수량을 작은 회색 글씨로 표시 + - 테이블 헤더 순서 변경 (총금액이 먼저) + +3. **원산지 데이터 처리** + - Excel에서 원산지(origin_country) 데이터 읽기 확인 + - 데이터베이스 저장 확인 + - 입고장 상세 화면에 원산지 표시 + +### 🔧 해결 필요 +1. **입고장 상세보기 오류** + - 증상: 상세보기 버튼 클릭 시 500 에러 + - 위치: `/api/purchase-receipts/` API + - 원인: 조사 중 + +2. **Flask 프로세스 관리** + - 문제: 여러 Flask 프로세스가 중복 실행 + - 해결 방법: 단일 프로세스로 관리 필요 + +## 🗄️ 데이터베이스 상태 +- 입고장 1건 (ID: 6, 날짜: 2026-02-11) +- 총 29개 품목, 총금액 1,551,900원 +- 도매상: (주)휴먼허브 + +## 🌐 서버 정보 +- **포트**: 5001 +- **모드**: Debug (자동 재시작 활성화) +- **URL**: http://localhost:5001 + +## 📂 주요 파일 +- `app.py` - Flask 백엔드 +- `database/kdrug.db` - SQLite 데이터베이스 +- `templates/index.html` - 프론트엔드 HTML +- `static/app.js` - JavaScript +- `excel_processor.py` - Excel 파일 처리 + +## 🔄 Flask 서버 관리 명령어 +```bash +# 모든 Flask 프로세스 종료 +lsof -ti:5001 | xargs -r kill -9 + +# Flask 서버 시작 (디버그 모드) +source venv/bin/activate && python app.py +``` + +## 📋 다음 작업 +1. 입고장 상세보기 500 에러 수정 +2. Flask 프로세스 중복 실행 방지 설정 +3. 에러 로깅 개선 \ No newline at end of file diff --git a/analyze_product_code.py b/analyze_product_code.py new file mode 100644 index 0000000..1925805 --- /dev/null +++ b/analyze_product_code.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +한약재 제품 코드 엑셀 파일 분석 +""" + +import pandas as pd +import openpyxl + +def analyze_excel_file(): + file_path = 'sample/(게시)한약재제품코드_2510.xlsx' + + # 엑셀 파일 열기 + wb = openpyxl.load_workbook(file_path, read_only=True) + + print("=== 엑셀 파일 시트 목록 ===") + for i, sheet_name in enumerate(wb.sheetnames, 1): + print(f"{i}. {sheet_name}") + + # 4번째 시트 데이터 읽기 + if len(wb.sheetnames) >= 4: + sheet_name = wb.sheetnames[3] # 0-based index + print(f"\n=== 4번째 시트 '{sheet_name}' 분석 ===") + + # pandas로 데이터 읽기 + df = pd.read_excel(file_path, sheet_name=sheet_name) + + print(f"\n데이터 크기: {df.shape[0]}행 x {df.shape[1]}열") + print(f"\n컬럼 목록:") + for i, col in enumerate(df.columns, 1): + # NaN이 아닌 값들의 예시 + non_null_count = df[col].notna().sum() + sample_values = df[col].dropna().head(3).tolist() + print(f" {i}. {col} (유효값: {non_null_count}개)") + if sample_values: + print(f" 예시: {sample_values[:3]}") + + print(f"\n=== 데이터 샘플 (처음 10행) ===") + pd.set_option('display.max_columns', None) + pd.set_option('display.width', None) + pd.set_option('display.max_colwidth', 50) + print(df.head(10)) + + # 주요 컬럼 분석 + if '주성분코드' in df.columns: + print(f"\n=== 주성분코드 분석 ===") + print(f"유일한 주성분코드 수: {df['주성분코드'].nunique()}") + print(f"주성분코드 샘플: {df['주성분코드'].unique()[:10].tolist()}") + + if '제품명' in df.columns: + print(f"\n=== 제품명 분석 ===") + print(f"유일한 제품 수: {df['제품명'].nunique()}") + print(f"제품명 샘플: {df['제품명'].head(10).tolist()}") + + # 컬럼 정보를 더 자세히 분석 + print(f"\n=== 데이터 타입 및 null 값 정보 ===") + print(df.info()) + + wb.close() + +if __name__ == "__main__": + analyze_excel_file() \ No newline at end of file diff --git a/analyze_product_deep.py b/analyze_product_deep.py new file mode 100644 index 0000000..bdb0147 --- /dev/null +++ b/analyze_product_deep.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +한약재 제품 코드 심층 분석 +""" + +import pandas as pd + +def deep_analyze(): + file_path = 'sample/(게시)한약재제품코드_2510.xlsx' + sheet_name = '한약재 제품코드_20250930기준(유효코드만 공지)' + + # 데이터 읽기 - 제품코드를 문자열로 읽어서 0 유지 + df = pd.read_excel(file_path, sheet_name=sheet_name, dtype={'제품코드': str}) + + print("=== 한약재 제품 코드 데이터 심층 분석 ===") + print(f"전체 데이터: {len(df):,}개 제품") + print(f"유일한 주성분코드: {df['주성분코드'].nunique()}개") + print(f"유일한 약재 품목명: {df['한약재 품목명'].nunique()}개") + print(f"유일한 업체: {df['업체명'].nunique()}개") + + # 주성분코드별 통계 + print("\n=== 주성분코드별 제품 수 (상위 20개) ===") + ingredient_stats = df.groupby(['주성분코드', '한약재 품목명']).size().reset_index(name='제품수') + ingredient_stats = ingredient_stats.sort_values('제품수', ascending=False).head(20) + + for _, row in ingredient_stats.iterrows(): + print(f" {row['주성분코드']} ({row['한약재 품목명']}): {row['제품수']}개 제품") + + # 업체별 통계 + print("\n=== 업체별 제품 수 (상위 10개) ===") + company_stats = df['업체명'].value_counts().head(10) + for company, count in company_stats.items(): + print(f" {company}: {count}개 제품") + + # 규격별 분석 + print("\n=== 약품 규격 분석 ===") + spec_stats = df['약품규격(단위)'].value_counts() + print("규격 단위별 제품 수:") + for spec, count in spec_stats.items(): + print(f" {spec}: {count}개") + + # 특정 약재들 확인 + print("\n=== 주요 약재 확인 ===") + target_herbs = ['건강', '감초', '당귀', '황기', '숙지황', '백출', '천궁', '육계', '백작약', '인삼', '생강', '대추'] + + for herb in target_herbs: + herb_data = df[df['한약재 품목명'] == herb] + if not herb_data.empty: + unique_code = herb_data['주성분코드'].iloc[0] if len(herb_data) > 0 else 'N/A' + product_count = len(herb_data) + company_count = herb_data['업체명'].nunique() + print(f" {herb}: 주성분코드={unique_code}, {product_count}개 제품, {company_count}개 업체") + else: + print(f" {herb}: 데이터 없음") + + # 한 약재에 여러 제품이 있는 예시 - 건강 + print("\n=== '건강' 약재의 제품 예시 (처음 10개) ===") + gangang_data = df[df['한약재 품목명'] == '건강'].head(10) + if not gangang_data.empty: + for _, row in gangang_data.iterrows(): + # 제품코드를 9자리로 표시 (0 패딩) + product_code = str(row['제품코드']).zfill(9) + print(f" 업체: {row['업체명']}, 제품명: {row['제품명']}, 제품코드: {product_code}, 규격: {row['약품규격(숫자)']} {row['약품규격(단위)']}") + + # 현재 시스템과의 비교 + print("\n=== 현재 DB 설계와의 차이점 ===") + print("1. 현재 시스템:") + print(" - herb_items: 약재 기본 정보 (예: 건강)") + print(" - inventory_lots: 로트별 재고 (원산지, 가격 등)") + print("\nㅇ2. 제품코드 시스템:") + print(" - 주성분코드: 약재별 고유 코드 (예: 3050H1AHM = 건강)") + print(" - 제품코드: 업체별 제품 고유코드") + print(" - 표준코드/대표코드: 바코드 시스템") + print(" - 규격: 포장 단위 (500g, 1000g 등)") + + print("\n=== 시사점 ===") + print("- 54,000개 이상의 유통 제품이 454개 주성분코드로 분류됨") + print("- 같은 약재(주성분)라도 업체별로 다른 제품명과 코드를 가짐") + print("- 제품별로 다양한 포장 규격 존재 (-, 500g, 600g, 1000g 등)") + print("- 표준코드(바코드)를 통한 제품 식별 가능") + +if __name__ == "__main__": + deep_analyze() \ No newline at end of file diff --git a/app.py b/app.py index e5ff71a..422057d 100644 --- a/app.py +++ b/app.py @@ -159,6 +159,78 @@ def get_herbs(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/herbs/masters', methods=['GET']) +def get_herb_masters(): + """주성분코드 기준 전체 약재 목록 조회 (454개)""" + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + m.ingredient_code, + m.herb_name, + m.herb_name_hanja, + m.herb_name_latin, + -- 재고 정보 + COALESCE(inv.total_quantity, 0) as stock_quantity, + COALESCE(inv.lot_count, 0) as lot_count, + COALESCE(inv.avg_price, 0) as avg_price, + CASE WHEN inv.total_quantity > 0 THEN 1 ELSE 0 END as has_stock, + -- 효능 태그 + GROUP_CONCAT(DISTINCT et.tag_name) as efficacy_tags, + -- 제품 정보 + COUNT(DISTINCT p.company_name) as company_count, + COUNT(DISTINCT p.product_id) as product_count + FROM herb_masters m + LEFT JOIN ( + -- 재고 정보 서브쿼리 + SELECT + h.ingredient_code, + SUM(il.quantity_onhand) as total_quantity, + COUNT(DISTINCT il.lot_id) as lot_count, + AVG(il.unit_price_per_g) as avg_price + FROM herb_items h + INNER 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.ingredient_code + ) inv ON m.ingredient_code = inv.ingredient_code + LEFT JOIN herb_products p ON m.ingredient_code = p.ingredient_code + LEFT JOIN herb_items hi ON m.ingredient_code = hi.ingredient_code + LEFT JOIN herb_item_tags hit ON hi.herb_item_id = hit.herb_item_id + LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id + WHERE m.is_active = 1 + GROUP BY m.ingredient_code, m.herb_name, inv.total_quantity, inv.lot_count, inv.avg_price + ORDER BY has_stock DESC, m.herb_name + """) + + herbs = [] + for row in cursor.fetchall(): + herb = dict(row) + # 효능 태그를 리스트로 변환 + if herb['efficacy_tags']: + herb['efficacy_tags'] = herb['efficacy_tags'].split(',') + else: + herb['efficacy_tags'] = [] + herbs.append(herb) + + # 통계 정보 + total_herbs = len(herbs) + herbs_with_stock = sum(1 for h in herbs if h['has_stock']) + coverage_rate = round(herbs_with_stock * 100 / total_herbs, 1) if total_herbs > 0 else 0 + + return jsonify({ + 'success': True, + 'data': herbs, + 'summary': { + 'total_herbs': total_herbs, + 'herbs_with_stock': herbs_with_stock, + 'herbs_without_stock': total_herbs - herbs_with_stock, + 'coverage_rate': coverage_rate + } + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + # ==================== 처방 관리 API ==================== @app.route('/api/formulas', methods=['GET']) @@ -723,6 +795,149 @@ def delete_purchase_receipt(receipt_id): # ==================== 조제 관리 API ==================== +@app.route('/api/compounds', methods=['GET']) +def get_compounds(): + """조제 목록 조회""" + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + c.compound_id, + c.patient_id, + p.name as patient_name, + p.phone as patient_phone, + c.formula_id, + f.formula_name, + f.formula_code, + c.compound_date, + c.je_count, + c.cheop_total, + c.pouch_total, + c.cost_total, + c.sell_price_total, + c.prescription_no, + c.status, + c.notes, + c.created_at, + c.created_by + FROM compounds c + LEFT JOIN patients p ON c.patient_id = p.patient_id + LEFT JOIN formulas f ON c.formula_id = f.formula_id + ORDER BY c.created_at DESC + LIMIT 100 + """) + compounds = [dict(row) for row in cursor.fetchall()] + return jsonify({'success': True, 'data': compounds}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/compounds/', methods=['GET']) +def get_compound_detail(compound_id): + """조제 상세 정보 조회""" + try: + with get_db() as conn: + cursor = conn.cursor() + + # 조제 마스터 정보 + cursor.execute(""" + SELECT + c.*, + p.name as patient_name, + p.phone as patient_phone, + f.formula_name, + f.formula_code + FROM compounds c + LEFT JOIN patients p ON c.patient_id = p.patient_id + LEFT JOIN formulas f ON c.formula_id = f.formula_id + WHERE c.compound_id = ? + """, (compound_id,)) + compound = dict(cursor.fetchone()) + + # 조제 약재 구성 + cursor.execute(""" + SELECT + ci.*, + h.herb_name, + h.insurance_code + FROM compound_ingredients ci + JOIN herb_items h ON ci.herb_item_id = h.herb_item_id + WHERE ci.compound_id = ? + ORDER BY ci.compound_ingredient_id + """, (compound_id,)) + ingredients = [dict(row) for row in cursor.fetchall()] + + # 소비 내역 + cursor.execute(""" + SELECT + cc.*, + h.herb_name, + il.origin_country, + il.supplier_id, + s.name as supplier_name + FROM compound_consumptions cc + JOIN herb_items h ON cc.herb_item_id = h.herb_item_id + JOIN inventory_lots il ON cc.lot_id = il.lot_id + LEFT JOIN suppliers s ON il.supplier_id = s.supplier_id + WHERE cc.compound_id = ? + ORDER BY cc.consumption_id + """, (compound_id,)) + consumptions = [dict(row) for row in cursor.fetchall()] + + compound['ingredients'] = ingredients + compound['consumptions'] = consumptions + + return jsonify({'success': True, 'data': compound}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/patients//compounds', methods=['GET']) +def get_patient_compounds(patient_id): + """환자별 처방 기록 조회""" + try: + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + c.compound_id, + c.formula_id, + f.formula_name, + f.formula_code, + c.compound_date, + c.je_count, + c.cheop_total, + c.pouch_total, + c.cost_total, + c.sell_price_total, + c.prescription_no, + c.status, + c.notes, + c.created_at, + c.created_by + FROM compounds c + LEFT JOIN formulas f ON c.formula_id = f.formula_id + WHERE c.patient_id = ? + ORDER BY c.compound_date DESC, c.created_at DESC + """, (patient_id,)) + compounds = [dict(row) for row in cursor.fetchall()] + + # 환자 정보도 함께 반환 + cursor.execute(""" + SELECT patient_id, name, phone, gender, birth_date, notes + FROM patients + WHERE patient_id = ? + """, (patient_id,)) + patient_row = cursor.fetchone() + patient = dict(patient_row) if patient_row else None + + return jsonify({ + 'success': True, + 'patient': patient, + 'compounds': compounds + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + @app.route('/api/compounds', methods=['POST']) def create_compound(): """조제 실행""" @@ -845,6 +1060,116 @@ def create_compound(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 +# ==================== 재고 원장 API ==================== + +@app.route('/api/stock-ledger', methods=['GET']) +def get_stock_ledger(): + """재고 원장 (입출고 내역) 조회""" + try: + herb_id = request.args.get('herb_id') + limit = request.args.get('limit', 100, type=int) + + with get_db() as conn: + cursor = conn.cursor() + + if herb_id: + cursor.execute(""" + SELECT + sl.ledger_id, + sl.event_type, + sl.event_time, + h.herb_name, + h.insurance_code, + sl.quantity_delta, + sl.unit_cost_per_g, + sl.reference_table, + sl.reference_id, + il.origin_country, + s.name as supplier_name, + CASE + WHEN sl.event_type = 'PURCHASE' THEN pr.receipt_no + WHEN sl.event_type = 'CONSUME' THEN c.compound_id + ELSE NULL + END as reference_no, + CASE + WHEN sl.event_type = 'CONSUME' THEN p.name + ELSE NULL + END as patient_name + FROM stock_ledger sl + JOIN herb_items h ON sl.herb_item_id = h.herb_item_id + LEFT JOIN inventory_lots il ON sl.lot_id = il.lot_id + LEFT JOIN suppliers s ON il.supplier_id = s.supplier_id + LEFT JOIN purchase_receipts pr ON sl.reference_table = 'purchase_receipts' AND sl.reference_id = pr.receipt_id + LEFT JOIN compounds c ON sl.reference_table = 'compounds' AND sl.reference_id = c.compound_id + LEFT JOIN patients p ON c.patient_id = p.patient_id + WHERE sl.herb_item_id = ? + ORDER BY sl.event_time DESC + LIMIT ? + """, (herb_id, limit)) + else: + cursor.execute(""" + SELECT + sl.ledger_id, + sl.event_type, + sl.event_time, + h.herb_name, + h.insurance_code, + sl.quantity_delta, + sl.unit_cost_per_g, + sl.reference_table, + sl.reference_id, + il.origin_country, + s.name as supplier_name, + CASE + WHEN sl.event_type = 'PURCHASE' THEN pr.receipt_no + WHEN sl.event_type = 'CONSUME' THEN c.compound_id + ELSE NULL + END as reference_no, + CASE + WHEN sl.event_type = 'CONSUME' THEN p.name + ELSE NULL + END as patient_name + FROM stock_ledger sl + JOIN herb_items h ON sl.herb_item_id = h.herb_item_id + LEFT JOIN inventory_lots il ON sl.lot_id = il.lot_id + LEFT JOIN suppliers s ON il.supplier_id = s.supplier_id + LEFT JOIN purchase_receipts pr ON sl.reference_table = 'purchase_receipts' AND sl.reference_id = pr.receipt_id + LEFT JOIN compounds c ON sl.reference_table = 'compounds' AND sl.reference_id = c.compound_id + LEFT JOIN patients p ON c.patient_id = p.patient_id + ORDER BY sl.event_time DESC + LIMIT ? + """, (limit,)) + + ledger_entries = [dict(row) for row in cursor.fetchall()] + + # 약재별 현재 재고 요약 + cursor.execute(""" + SELECT + h.herb_item_id, + h.herb_name, + h.insurance_code, + COALESCE(SUM(il.quantity_onhand), 0) as total_stock, + COUNT(DISTINCT il.lot_id) as active_lots, + AVG(il.unit_price_per_g) as avg_price + FROM herb_items h + LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id + AND il.is_depleted = 0 + WHERE h.is_active = 1 + GROUP BY h.herb_item_id + HAVING total_stock > 0 + ORDER BY h.herb_name + """) + + stock_summary = [dict(row) for row in cursor.fetchall()] + + return jsonify({ + 'success': True, + 'ledger': ledger_entries, + 'summary': stock_summary + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + # ==================== 조제용 재고 조회 API ==================== @app.route('/api/herbs//available-lots', methods=['GET']) diff --git a/backups/kdrug_full_backup_20260215_before_refactoring.tar.gz b/backups/kdrug_full_backup_20260215_before_refactoring.tar.gz new file mode 100644 index 0000000..ba37e14 Binary files /dev/null and b/backups/kdrug_full_backup_20260215_before_refactoring.tar.gz differ diff --git a/check_totals.py b/check_totals.py new file mode 100644 index 0000000..0c3522b --- /dev/null +++ b/check_totals.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sqlite3 + +# 데이터베이스 연결 +conn = sqlite3.connect('database/kdrug.db') +conn.row_factory = sqlite3.Row +cursor = conn.cursor() + +print("=== 입고장별 총금액 확인 ===\n") + +# 각 입고장의 라인별 총액 확인 +cursor.execute(""" + SELECT + pr.receipt_id, + pr.receipt_date, + s.name as supplier_name, + COUNT(prl.line_id) as line_count, + SUM(prl.quantity_g) as total_quantity, + SUM(prl.line_total) as calculated_total + FROM purchase_receipts pr + JOIN suppliers s ON pr.supplier_id = s.supplier_id + LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id + GROUP BY pr.receipt_id + ORDER BY pr.receipt_date DESC +""") + +results = cursor.fetchall() + +for row in results: + print(f"입고장 ID: {row['receipt_id']}") + print(f" 날짜: {row['receipt_date']}") + print(f" 도매상: {row['supplier_name']}") + print(f" 품목 수: {row['line_count']}개") + print(f" 총 수량: {row['total_quantity']}g") + print(f" 총 금액: {row['calculated_total']:,.0f}원" if row['calculated_total'] else " 총 금액: 0원") + print("-" * 40) + +print("\n=== 입고장 라인 상세 (첫 번째 입고장) ===\n") + +# 첫 번째 입고장의 라인 상세 확인 +if results: + first_receipt_id = results[0]['receipt_id'] + cursor.execute(""" + SELECT + herb_item_id, + quantity_g, + unit_price_per_g, + line_total + FROM purchase_receipt_lines + WHERE receipt_id = ? + LIMIT 5 + """, (first_receipt_id,)) + + lines = cursor.fetchall() + for line in lines: + print(f"약재 ID: {line['herb_item_id']}") + print(f" 수량: {line['quantity_g']}g") + print(f" 단가: {line['unit_price_per_g']}원/g") + print(f" 라인 총액: {line['line_total']}원") + print(f" 계산 검증: {line['quantity_g']} × {line['unit_price_per_g']} = {line['quantity_g'] * line['unit_price_per_g']}원") + print() + +conn.close() \ No newline at end of file diff --git a/debug_receipt_detail.py b/debug_receipt_detail.py new file mode 100644 index 0000000..af60303 --- /dev/null +++ b/debug_receipt_detail.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +입고장 상세보기 오류 디버그 +""" + +import sqlite3 +import traceback + +def debug_receipt_detail(): + conn = sqlite3.connect('database/kdrug.db') + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + receipt_id = 6 + + print("=== 1. 입고장 헤더 조회 ===") + try: + cursor.execute(""" + SELECT + pr.*, + s.name as supplier_name, + s.business_no as supplier_business_no, + s.phone as supplier_phone + FROM purchase_receipts pr + JOIN suppliers s ON pr.supplier_id = s.supplier_id + WHERE pr.receipt_id = ? + """, (receipt_id,)) + + receipt = cursor.fetchone() + if receipt: + receipt_dict = dict(receipt) + print("헤더 조회 성공!") + for key, value in receipt_dict.items(): + print(f" {key}: {value} (type: {type(value).__name__})") + else: + print("입고장을 찾을 수 없습니다.") + return + except Exception as e: + print(f"헤더 조회 오류: {e}") + traceback.print_exc() + return + + print("\n=== 2. 입고장 상세 라인 조회 ===") + try: + cursor.execute(""" + SELECT + prl.*, + h.herb_name, + h.insurance_code, + il.lot_id, + il.quantity_onhand as current_stock + FROM purchase_receipt_lines prl + JOIN herb_items h ON prl.herb_item_id = h.herb_item_id + LEFT JOIN inventory_lots il ON prl.line_id = il.receipt_line_id + WHERE prl.receipt_id = ? + ORDER BY prl.line_id + """, (receipt_id,)) + + lines = cursor.fetchall() + print(f"라인 수: {len(lines)}개") + + if lines: + first_line = dict(lines[0]) + print("\n첫 번째 라인 데이터:") + for key, value in first_line.items(): + print(f" {key}: {value} (type: {type(value).__name__})") + except Exception as e: + print(f"라인 조회 오류: {e}") + traceback.print_exc() + + print("\n=== 3. JSON 변환 테스트 ===") + try: + import json + + # receipt_data 구성 + receipt_data = dict(receipt) + receipt_data['lines'] = [dict(row) for row in lines] + + # JSON 변환 시도 + json_str = json.dumps(receipt_data, ensure_ascii=False, default=str) + print("JSON 변환 성공!") + print(f"JSON 길이: {len(json_str)} 문자") + + except Exception as e: + print(f"JSON 변환 오류: {e}") + traceback.print_exc() + + # 문제가 되는 필드 찾기 + print("\n각 필드별 JSON 변환 테스트:") + for key, value in receipt_data.items(): + try: + json.dumps({key: value}, default=str) + print(f" ✓ {key}: OK") + except Exception as field_error: + print(f" ✗ {key}: {field_error}") + print(f" 값: {value}") + print(f" 타입: {type(value)}") + + conn.close() + +if __name__ == "__main__": + debug_receipt_detail() \ No newline at end of file diff --git a/direct_test.png b/direct_test.png new file mode 100644 index 0000000..e174e9c Binary files /dev/null and b/direct_test.png differ diff --git a/docs/주성분코드_기반_개선_계획.md b/docs/주성분코드_기반_개선_계획.md new file mode 100644 index 0000000..47038c7 --- /dev/null +++ b/docs/주성분코드_기반_개선_계획.md @@ -0,0 +1,128 @@ +# 📋 주성분코드 기반 약재 관리 체계 개선 계획 + +## 🎯 목표 +**"454개 주성분코드를 기준으로 약재를 관리하고, 입고되지 않은 약재도 처방 가능한 체계 구축"** + +## 🏗️ 현재 문제점 +1. 약재 관리가 입고된 제품 중심 (28개만 표시) +2. 입고되지 않은 약재는 처방 생성 불가 +3. 보험코드와 주성분코드가 혼재 + +## ✨ 개선 후 모습 + +### 3단계 계층 구조 +``` +1. 주성분코드 (454개) - 약재 마스터 + ↓ +2. 처방 구성 - 주성분코드 기반 + ↓ +3. 실제 조제 - 입고된 제품으로 매핑 +``` + +## 📝 구현 단계 + +### Phase 1: 약재 관리 UI 개선 (우선) +#### 1-1. 약재 목록 페이지 개선 +- **현재**: 입고된 약재만 표시 (28개) +- **개선**: + ``` + 전체 454개 주성분코드 표시 + ✅ 재고 있음 (28개) - 녹색 표시 + ⬜ 재고 없음 (426개) - 회색 표시 + ``` + +#### 1-2. API 수정 +- `/api/herbs/masters` - 454개 전체 약재 (재고 유무 표시) +- `/api/herbs/inventory` - 재고 있는 약재만 (현재 방식 유지) + +#### 1-3. 필터링 기능 +- 전체 보기 / 재고 있음 / 재고 없음 +- 효능별 필터 +- 검색 기능 + +### Phase 2: 처방 관리 개선 +#### 2-1. 처방 생성 개선 +- 454개 주성분코드에서 선택 +- 재고 없는 약재도 선택 가능 +- 재고 상태 시각적 표시 + +#### 2-2. 처방 구성 표시 +``` +쌍화탕 (12개 약재) +✅ 건강 (재고: 6,500g) +✅ 감초 (재고: 5,000g) +⚠️ 특정약재 (재고: 0g) - 입고 필요 +``` + +### Phase 3: 조제 프로세스 개선 +#### 3-1. 조제 시 자동 매핑 +``` +주성분코드 → 실제 제품 선택 +3017H1AHM (건강) → + • 경희한약 건강 500g (페루산) + • 고강제약 건강 600g (한국산) +``` + +#### 3-2. 재고 부족 알림 +- 조제 불가능한 약재 강조 +- 대체 가능 제품 제안 + +### Phase 4: 입고 관리 개선 +#### 4-1. 제품 매핑 +- 입고 시 주성분코드 자동 매핑 +- 바코드로 제품 식별 + +## 🚀 구현 순서 + +### Step 1: 약재 마스터 API (30분) +1. `/api/herbs/masters` API 생성 +2. 454개 전체 약재 + 재고 상태 반환 + +### Step 2: 약재 관리 UI (1시간) +1. 약재 관리 페이지 새로 구성 +2. 필터링 기능 추가 +3. 재고 상태 표시 + +### Step 3: 처방 관리 수정 (1시간) +1. formula_ingredients를 주성분코드 기반으로 변경 +2. 처방 생성 UI 수정 +3. 재고 체크 로직 분리 + +### Step 4: 조제 프로세스 (1시간) +1. 주성분코드 → 제품 매핑 로직 +2. 제품 선택 UI +3. 재고 부족 처리 + +### Step 5: 테스트 및 마무리 (30분) +1. 전체 프로세스 테스트 +2. 버그 수정 +3. 문서 업데이트 + +## 📊 예상 효과 + +### Before +- 28개 약재만 관리 +- 입고된 약재로만 처방 생성 +- 제한적인 시스템 + +### After +- 454개 전체 급여 약재 관리 +- 재고 없어도 처방 생성 가능 +- 표준화된 체계 +- 확장 가능한 구조 + +## ⚠️ 주의사항 +1. **하위 호환성**: 기존 데이터 유지 +2. **단계적 적용**: 한 번에 하나씩 구현 +3. **백업**: 각 단계별 백업 + +## 🔍 검증 기준 +1. 454개 약재 모두 표시되는가? +2. 재고 없는 약재로 처방 생성 가능한가? +3. 조제 시 적절한 제품이 매핑되는가? +4. 기존 기능이 정상 작동하는가? + +--- +작성일: 2026-02-15 +예상 소요시간: 4시간 +우선순위: 높음 \ No newline at end of file diff --git a/fix_database.py b/fix_database.py new file mode 100644 index 0000000..f91d6f2 --- /dev/null +++ b/fix_database.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +데이터베이스 데이터 수정 +""" + +import sqlite3 + +def fix_database(): + conn = sqlite3.connect('database/kdrug.db') + cursor = conn.cursor() + + print("=== 데이터베이스 수정 시작 ===") + + # 1. receipt_date 수정 (튜플 문자열을 일반 문자열로) + print("\n1. receipt_date 수정...") + cursor.execute(""" + UPDATE purchase_receipts + SET receipt_date = '20260211' + WHERE receipt_id = 6 + """) + print(f" receipt_date 수정 완료: {cursor.rowcount}건") + + # 2. total_amount 계산 및 수정 + print("\n2. total_amount 재계산...") + cursor.execute(""" + UPDATE purchase_receipts + SET total_amount = ( + SELECT SUM(line_total) + FROM purchase_receipt_lines + WHERE receipt_id = 6 + ) + WHERE receipt_id = 6 + """) + print(f" total_amount 수정 완료: {cursor.rowcount}건") + + # 변경 사항 저장 + conn.commit() + + # 수정 결과 확인 + print("\n=== 수정 후 데이터 확인 ===") + cursor.execute(""" + SELECT receipt_date, total_amount + FROM purchase_receipts + WHERE receipt_id = 6 + """) + result = cursor.fetchone() + print(f" receipt_date: {result[0]} (type: {type(result[0]).__name__})") + print(f" total_amount: {result[1]:,.0f}원 (type: {type(result[1]).__name__})") + + conn.close() + print("\n✅ 데이터베이스 수정 완료!") + +if __name__ == "__main__": + fix_database() \ No newline at end of file diff --git a/purchase_test.png b/purchase_test.png new file mode 100644 index 0000000..1af181e Binary files /dev/null and b/purchase_test.png differ diff --git a/sample/(게시)한약재제품코드_2510.xlsx b/sample/(게시)한약재제품코드_2510.xlsx new file mode 100644 index 0000000..5f45202 Binary files /dev/null and b/sample/(게시)한약재제품코드_2510.xlsx differ diff --git a/static/app.js b/static/app.js index f46a686..90e7ac0 100644 --- a/static/app.js +++ b/static/app.js @@ -532,8 +532,145 @@ $(document).ready(function() { // 조제 내역 로드 function loadCompounds() { - // TODO: 조제 내역 API 구현 필요 - $('#compoundsList').html('조제 내역이 없습니다.'); + $.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 || '-'); + + // 처방 정보 + $('#detailFormulaName').text(data.formula_name || '직접조제'); + $('#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 || ''); + + // 모달 표시 + $('#compoundDetailModal').modal('show'); + } + }).fail(function() { + alert('조제 상세 정보를 불러오는데 실패했습니다.'); + }); } // 재고 현황 로드 @@ -543,6 +680,9 @@ $(document).ready(function() { const tbody = $('#inventoryList'); tbody.empty(); + let totalValue = 0; + let herbsInStock = 0; + // 주성분코드 기준 보유 현황 표시 if (response.summary) { const summary = response.summary; @@ -611,23 +751,48 @@ $(document).ready(function() { 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}종`); + // 클릭 이벤트 바인딩 - $('.inventory-row').on('click', function() { + $('.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); + }); } }); } @@ -1128,6 +1293,98 @@ $(document).ready(function() { }); } + // 재고 원장 보기 + function viewStockLedger(herbId, herbName) { + const url = herbId ? `/api/stock-ledger?herb_id=${herbId}` : '/api/stock-ledger'; + + $.get(url, function(response) { + if (response.success) { + const tbody = $('#stockLedgerList'); + tbody.empty(); + + // 헤더 업데이트 + if (herbName) { + $('#stockLedgerModal .modal-title').html(` ${herbName} 입출고 원장`); + } else { + $('#stockLedgerModal .modal-title').html(` 전체 입출고 원장`); + } + + response.ledger.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; + 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 || '-'} + + `); + }); + + // 약재 필터 옵션 업데이트 + const herbFilter = $('#ledgerHerbFilter'); + if (herbFilter.find('option').length <= 1) { + response.summary.forEach(herb => { + herbFilter.append(``); + }); + } + + $('#stockLedgerModal').modal('show'); + } + }).fail(function() { + alert('입출고 내역을 불러오는데 실패했습니다.'); + }); + } + + // 입출고 원장 모달 버튼 이벤트 + $('#showStockLedgerBtn').on('click', function() { + viewStockLedger(null, null); + }); + + // 필터 변경 이벤트 + $('#ledgerHerbFilter, #ledgerTypeFilter').on('change', function() { + const herbId = $('#ledgerHerbFilter').val(); + const typeFilter = $('#ledgerTypeFilter').val(); + + // 재로드 (필터 적용은 프론트엔드에서 처리) + if (herbId) { + const herbName = $('#ledgerHerbFilter option:selected').text(); + viewStockLedger(herbId, herbName); + } else { + viewStockLedger(null, null); + } + }); + function formatCurrency(amount) { if (amount === null || amount === undefined) return '0원'; return new Intl.NumberFormat('ko-KR', { @@ -1135,4 +1392,161 @@ $(document).ready(function() { currency: 'KRW' }).format(amount); } + + // ==================== 주성분코드 기반 약재 관리 ==================== + 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); + }); }); \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index c841df8..a6aa803 100644 --- a/templates/index.html +++ b/templates/index.html @@ -408,53 +408,289 @@
-
-
조제 내역
+
+
+
조제 내역
+
+ 오늘 조제: 0 + 이번달 조제: 0 +
+
- - - - - - - - - - - - - - - -
조제일환자명처방명제수파우치원가상태
+
+
+ +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
#조제일시환자명연락처처방명제수첩수파우치원가판매가상태처방전번호작업
+
+ +
+
+ + +
-

재고 현황

+
+

재고 현황

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

₩0

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

0종

+
+
+
+
+
오늘 입고
+

0건

+
+
+
+
+
오늘 출고
+

0건

+
+
+
+
- - - - - - - - - - - - - - -
보험코드약재명현재 재고(g)로트 수평균 단가재고 금액
+
+ + + + + + + + + + + + + + + +
보험코드약재명현재 재고(g)로트 수평균 단가재고 금액작업
+
+
+
+ + +
@@ -462,24 +698,68 @@
-

약재 관리

- +

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

+
+
+ + + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ +
+
+
+
+
- + - - + + + + + - +
보험코드주성분코드 약재명규격현재 재고효능재고량평균단가제품수상태 작업
diff --git a/test_direct.py b/test_direct.py new file mode 100644 index 0000000..f5ab800 --- /dev/null +++ b/test_direct.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +직접 JavaScript 함수 호출 테스트 +""" + +from playwright.sync_api import sync_playwright +import time + +def test_direct_load(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + print("페이지 로드 중...") + page.goto("http://localhost:5001") + page.wait_for_load_state("networkidle") + time.sleep(2) + + # 직접 입고 관리 탭 활성화 및 함수 호출 + print("\n입고 데이터 직접 로드...") + page.evaluate(""" + // 입고 탭 표시 + document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('show', 'active')); + document.querySelector('#purchase').classList.add('show', 'active'); + + // 직접 함수 호출 + if (typeof loadPurchaseReceipts === 'function') { + loadPurchaseReceipts(); + } else { + console.error('loadPurchaseReceipts 함수를 찾을 수 없습니다'); + } + """) + time.sleep(2) + + # 테이블 내용 확인 + table_html = page.inner_html('#purchaseReceiptsList') + print(f"\n테이블 내용 (처음 500자):\n{table_html[:500]}") + + # 테이블 행 수 확인 + row_count = page.evaluate("document.querySelectorAll('#purchaseReceiptsList tr').length") + print(f"\n테이블 행 수: {row_count}") + + # 첫 번째 행 내용 확인 + if row_count > 0: + first_row = page.evaluate(""" + const row = document.querySelector('#purchaseReceiptsList tr'); + if (row) { + const cells = row.querySelectorAll('td'); + return Array.from(cells).map(cell => cell.textContent.trim()); + } + return null; + """) + if first_row: + print(f"\n첫 번째 행 데이터:") + headers = ['입고일', '공급업체', '품목 수', '총 금액', '총 수량', '파일명', '작업'] + for i, value in enumerate(first_row[:-1]): # 마지막 '작업' 열 제외 + if i < len(headers): + print(f" {headers[i]}: {value}") + + # 스크린샷 + page.screenshot(path="/root/kdrug/direct_test.png") + print("\n스크린샷 저장: /root/kdrug/direct_test.png") + + browser.close() + +if __name__ == "__main__": + test_direct_load() \ No newline at end of file diff --git a/test_frontend.py b/test_frontend.py new file mode 100644 index 0000000..971d0c7 --- /dev/null +++ b/test_frontend.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Playwright로 프론트엔드 확인 +""" + +from playwright.sync_api import sync_playwright +import time +import json + +def test_purchase_receipts(): + with sync_playwright() as p: + # 브라우저 시작 + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + # 페이지 이동 + print("1. 페이지 로드 중...") + page.goto("http://localhost:5001") + page.wait_for_load_state("networkidle") + + # 입고 관리 탭 클릭 + print("2. 입고 관리 탭으로 이동...") + try: + page.click('a[href="#purchase"]', timeout=5000) + except: + # 다른 방법으로 시도 + page.evaluate("document.querySelector('a[href=\"#purchase\"]').click()") + time.sleep(1) + + # API 호출 직접 확인 + print("3. API 직접 호출 확인...") + api_response = page.evaluate(""" + async () => { + const response = await fetch('/api/purchase-receipts'); + const data = await response.json(); + return data; + } + """) + + print("\n=== API 응답 데이터 ===") + print(json.dumps(api_response, indent=2, ensure_ascii=False)) + + # 테이블 내용 확인 + print("\n4. 테이블 렌더링 확인...") + table_rows = page.query_selector_all('#purchaseReceiptsList tr') + + if len(table_rows) == 0: + print(" 테이블에 행이 없습니다.") + # "입고장이 없습니다." 메시지 확인 + empty_message = page.query_selector('#purchaseReceiptsList td') + if empty_message: + print(f" 메시지: {empty_message.text_content()}") + else: + print(f" 테이블 행 수: {len(table_rows)}") + + # 첫 번째 행 상세 확인 + if len(table_rows) > 0: + first_row = table_rows[0] + cells = first_row.query_selector_all('td') + + print("\n 첫 번째 행 내용:") + headers = ['입고일', '공급업체', '품목 수', '총 금액', '총 수량', '파일명', '작업'] + for i, cell in enumerate(cells[:-1]): # 마지막 '작업' 열 제외 + print(f" {headers[i]}: {cell.text_content()}") + + # JavaScript 콘솔 에러 확인 + page.on("console", lambda msg: print(f"콘솔: {msg.text}") if msg.type == "error" else None) + + # 스크린샷 저장 + print("\n5. 스크린샷 저장...") + page.screenshot(path="/root/kdrug/purchase_screenshot.png") + print(" /root/kdrug/purchase_screenshot.png 저장 완료") + + browser.close() + +if __name__ == "__main__": + test_purchase_receipts() \ No newline at end of file diff --git a/test_simple.py b/test_simple.py new file mode 100644 index 0000000..3f7ac78 --- /dev/null +++ b/test_simple.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +간단한 프론트엔드 확인 +""" + +from playwright.sync_api import sync_playwright +import time + +def test_purchase_display(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + page = browser.new_page() + + print("페이지 로드 중...") + page.goto("http://localhost:5001", wait_until="networkidle") + + # 입고 관리 화면으로 직접 이동 + print("\n입고 관리 화면 확인...") + page.goto("http://localhost:5001/#purchase", wait_until="networkidle") + time.sleep(2) # JavaScript 렌더링 대기 + + # API 데이터와 실제 렌더링 비교 + print("\n=== API 데이터 vs 화면 렌더링 확인 ===") + + # API 응답 확인 + api_data = page.evaluate(""" + fetch('/api/purchase-receipts') + .then(response => response.json()) + .then(data => data) + """) + time.sleep(1) + + # 테이블 확인 + table_html = page.evaluate("document.querySelector('#purchaseReceiptsList').innerHTML") + + print(f"\nAPI 응답 총금액: {api_data.get('data', [{}])[0].get('total_amount', 0)}") + + # 화면에 표시된 총금액 찾기 + try: + total_amount_cell = page.query_selector('.fw-bold.text-primary') + if total_amount_cell: + print(f"화면 표시 총금액: {total_amount_cell.text_content()}") + else: + print("총금액 셀을 찾을 수 없습니다.") + except: + pass + + # 테이블 전체 내용 + print("\n테이블 HTML (처음 200자):") + print(table_html[:200] if table_html else "테이블이 비어있음") + + # 스크린샷 + page.screenshot(path="/root/kdrug/purchase_test.png") + print("\n스크린샷 저장: /root/kdrug/purchase_test.png") + + browser.close() + +if __name__ == "__main__": + test_purchase_display() \ No newline at end of file diff --git a/uploads/20260215_083747_xlsx b/uploads/20260215_083747_xlsx new file mode 100644 index 0000000..0cc74c9 Binary files /dev/null and b/uploads/20260215_083747_xlsx differ