From 974000acaa5b1e1ea44cc14b2616c35e4ac65323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Sun, 15 Feb 2026 08:15:59 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Excel=20=ED=98=95=EC=8B=9D=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EA=B0=90=EC=A7=80=20=EB=B0=8F=20=EB=8B=A4=EC=A4=91?= =?UTF-8?q?=20=ED=98=95=EC=8B=9D=20=EC=A7=80=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 새로운 기능 - 한의사랑, 한의정보 Excel 형식 자동 감지 - ExcelProcessor 클래스로 형식별 처리 로직 분리 - 각 형식에 맞는 컬럼 매핑 자동 적용 📊 지원하는 Excel 형식 1. 한의사랑 형식 - 품목명, 제품코드, 일그램당단가, 원산지 등 - 단가가 이미 계산된 형식 2. 한의정보 형식 - 제품코드, 업체명, 약재명, 구입일자 등 - 업체명이 포함된 형식 🔧 기술적 변경사항 - excel_processor.py 모듈 추가 - 형식 감지 및 검증 로직 구현 - 표준 형식으로 자동 변환 기능 - 업로드 응답에 상세 요약 정보 추가 ✅ 테스트 완료 - 한의사랑 형식 업로드 성공 - 한의정보 형식 업로드 성공 - 각 형식당 28종 약재, 88,000g 처리 확인 🤖 Generated with Claude Code Co-Authored-By: Claude --- analyze_excel_formats.py | 198 +++++++++++++++++++++++++++ app.py | 60 +++++--- excel_processor.py | 286 +++++++++++++++++++++++++++++++++++++++ sample/한의사랑.xlsx | Bin 0 -> 8054 bytes sample/한의정보.xlsx | Bin 0 -> 7915 bytes 5 files changed, 528 insertions(+), 16 deletions(-) create mode 100644 analyze_excel_formats.py create mode 100644 excel_processor.py create mode 100644 sample/한의사랑.xlsx create mode 100644 sample/한의정보.xlsx diff --git a/analyze_excel_formats.py b/analyze_excel_formats.py new file mode 100644 index 0000000..92d98da --- /dev/null +++ b/analyze_excel_formats.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Excel 파일 형식 분석 도구 +한의사랑과 한의정보 형식 비교 +""" + +import pandas as pd +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +def analyze_excel_format(file_path, format_name): + """Excel 파일 형식 분석""" + print(f"\n{'='*60}") + print(f"📊 {format_name} 형식 분석") + print(f"파일: {file_path}") + print('='*60) + + try: + # Excel 파일 읽기 + df = pd.read_excel(file_path) + + # 기본 정보 + print(f"\n1️⃣ 기본 정보:") + print(f" - 행 개수: {len(df)}") + print(f" - 열 개수: {len(df.columns)}") + + # 컬럼 정보 + print(f"\n2️⃣ 컬럼 목록:") + for i, col in enumerate(df.columns, 1): + print(f" {i}. {col}") + + # 데이터 타입 + print(f"\n3️⃣ 데이터 타입:") + for col in df.columns: + print(f" - {col}: {df[col].dtype}") + + # 샘플 데이터 (처음 3행) + print(f"\n4️⃣ 샘플 데이터 (처음 3행):") + print(df.head(3).to_string(index=False)) + + # 누락 데이터 확인 + print(f"\n5️⃣ 누락 데이터:") + null_counts = df.isnull().sum() + for col in df.columns: + if null_counts[col] > 0: + print(f" - {col}: {null_counts[col]}개 누락") + if null_counts.sum() == 0: + print(" - 누락 데이터 없음") + + # 고유값 개수 (참고용) + print(f"\n6️⃣ 고유값 개수:") + for col in df.columns: + unique_count = df[col].nunique() + print(f" - {col}: {unique_count}개") + + return df + + except Exception as e: + print(f"❌ 오류 발생: {str(e)}") + return None + +def compare_formats(df1, df2, name1, name2): + """두 형식 비교""" + print(f"\n{'='*60}") + print(f"🔄 {name1} vs {name2} 형식 비교") + print('='*60) + + if df1 is None or df2 is None: + print("비교할 수 없습니다 (데이터 로드 실패)") + return + + cols1 = set(df1.columns) + cols2 = set(df2.columns) + + # 공통 컬럼 + common = cols1.intersection(cols2) + print(f"\n✅ 공통 컬럼 ({len(common)}개):") + for col in sorted(common): + print(f" - {col}") + + # 한의사랑에만 있는 컬럼 + only_in_1 = cols1 - cols2 + if only_in_1: + print(f"\n📌 {name1}에만 있는 컬럼 ({len(only_in_1)}개):") + for col in sorted(only_in_1): + print(f" - {col}") + + # 한의정보에만 있는 컬럼 + only_in_2 = cols2 - cols1 + if only_in_2: + print(f"\n📌 {name2}에만 있는 컬럼 ({len(only_in_2)}개):") + for col in sorted(only_in_2): + print(f" - {col}") + + # 컬럼명 매핑 추천 + print(f"\n🔗 컬럼 매핑 추천:") + + # 가능한 매핑 찾기 + mappings = [] + + # 날짜 관련 + date_cols1 = [c for c in cols1 if '일' in c or '날짜' in c or 'date' in c.lower()] + date_cols2 = [c for c in cols2 if '일' in c or '날짜' in c or 'date' in c.lower()] + if date_cols1 and date_cols2: + mappings.append((date_cols1[0], date_cols2[0], "날짜")) + + # 약재명 관련 + herb_cols1 = [c for c in cols1 if '약재' in c or '품목' in c or '제품' in c] + herb_cols2 = [c for c in cols2 if '약재' in c or '품목' in c or '제품' in c] + if herb_cols1 and herb_cols2: + mappings.append((herb_cols1[0], herb_cols2[0], "약재명")) + + # 수량 관련 + qty_cols1 = [c for c in cols1 if '수량' in c or '량' in c or '구입량' in c] + qty_cols2 = [c for c in cols2 if '수량' in c or '량' in c or '구입량' in c] + if qty_cols1 and qty_cols2: + mappings.append((qty_cols1[0], qty_cols2[0], "수량")) + + # 금액 관련 + amt_cols1 = [c for c in cols1 if '금액' in c or '액' in c or '가격' in c] + amt_cols2 = [c for c in cols2 if '금액' in c or '액' in c or '가격' in c] + if amt_cols1 and amt_cols2: + mappings.append((amt_cols1[0], amt_cols2[0], "금액")) + + # 업체 관련 + supplier_cols1 = [c for c in cols1 if '업체' in c or '도매' in c or '공급' in c] + supplier_cols2 = [c for c in cols2 if '업체' in c or '도매' in c or '공급' in c] + if supplier_cols1 and supplier_cols2: + mappings.append((supplier_cols1[0], supplier_cols2[0], "공급업체")) + + # 원산지 관련 + origin_cols1 = [c for c in cols1 if '원산지' in c or '산지' in c] + origin_cols2 = [c for c in cols2 if '원산지' in c or '산지' in c] + if origin_cols1 and origin_cols2: + mappings.append((origin_cols1[0], origin_cols2[0], "원산지")) + + for col1, col2, mapping_type in mappings: + print(f" - {mapping_type}: [{name1}]{col1} ↔ [{name2}]{col2}") + +def main(): + """메인 함수""" + print("\n" + "="*60) + print("🏥 한약 입고장 Excel 형식 분석기") + print("="*60) + + # 파일 경로 + hanisarang_path = '/root/kdrug/sample/한의사랑.xlsx' + haninfo_path = '/root/kdrug/sample/한의정보.xlsx' + current_path = '/root/kdrug/sample/order_view_20260215154829.xlsx' + + # 각 형식 분석 + df_hanisarang = None + df_haninfo = None + df_current = None + + if os.path.exists(hanisarang_path): + df_hanisarang = analyze_excel_format(hanisarang_path, "한의사랑") + else: + print(f"❌ 한의사랑 파일을 찾을 수 없음: {hanisarang_path}") + + if os.path.exists(haninfo_path): + df_haninfo = analyze_excel_format(haninfo_path, "한의정보") + else: + print(f"❌ 한의정보 파일을 찾을 수 없음: {haninfo_path}") + + # 현재 사용 중인 형식도 분석 + if os.path.exists(current_path): + df_current = analyze_excel_format(current_path, "현재 사용 중") + + # 형식 비교 + if df_hanisarang is not None and df_haninfo is not None: + compare_formats(df_hanisarang, df_haninfo, "한의사랑", "한의정보") + + # 통합 매핑 제안 + print(f"\n{'='*60}") + print("💡 통합 컬럼 매핑 제안") + print('='*60) + + print(""" +시스템에서 사용할 표준 컬럼: +1. insurance_code (보험코드/제품코드) +2. supplier_name (업체명/도매상) +3. herb_name (약재명/품목명) +4. receipt_date (구입일자/입고일) +5. quantity (구입량/수량) - 그램 단위 +6. total_amount (구입액/금액) +7. origin_country (원산지) +8. unit_price (단가) - 계산 가능한 경우 + +각 형식별 매핑 규칙을 자동으로 적용하여 +어떤 형식의 Excel 파일도 처리 가능하도록 구현 가능 +""") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/app.py b/app.py index bc51861..50ea310 100644 --- a/app.py +++ b/app.py @@ -14,6 +14,7 @@ import pandas as pd from werkzeug.utils import secure_filename import json from contextlib import contextmanager +from excel_processor import ExcelProcessor # Flask 앱 초기화 app = Flask(__name__, static_folder='static', template_folder='templates') @@ -230,7 +231,7 @@ def get_formula_ingredients(formula_id): @app.route('/api/upload/purchase', methods=['POST']) def upload_purchase_excel(): - """Excel 파일 업로드 및 입고 처리""" + """Excel 파일 업로드 및 입고 처리 (한의사랑/한의정보 형식 자동 감지)""" try: if 'file' not in request.files: return jsonify({'success': False, 'error': '파일이 없습니다'}), 400 @@ -249,25 +250,36 @@ def upload_purchase_excel(): filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) - # Excel 파일 읽기 - df = pd.read_excel(filepath) + # Excel 프로세서로 파일 처리 + processor = ExcelProcessor() + if not processor.read_excel(filepath): + return jsonify({'success': False, 'error': 'Excel 파일을 읽을 수 없습니다'}), 400 - # 컬럼 매핑 (Excel 컬럼명 -> DB 필드) - column_mapping = { - '제품코드': 'insurance_code', - '업체명': 'supplier_name', - '약재명': 'herb_name', - '구입일자': 'receipt_date', - '구입량': 'quantity', - '구입액': 'total_amount', - '원산지': 'origin_country' - } + # 형식 감지 및 처리 + try: + df = processor.process() + except ValueError as e: + return jsonify({ + 'success': False, + 'error': f'지원하지 않는 Excel 형식입니다: {str(e)}' + }), 400 - df = df.rename(columns=column_mapping) + # 데이터 검증 + valid, msg = processor.validate_data() + if not valid: + return jsonify({'success': False, 'error': f'데이터 검증 실패: {msg}'}), 400 + + # 표준 형식으로 변환 + df = processor.export_to_standard() + + # 처리 요약 정보 + summary = processor.get_summary() # 데이터 처리 with get_db() as conn: cursor = conn.cursor() + processed_rows = 0 + processed_items = set() # 날짜별, 업체별로 그룹화 grouped = df.groupby(['receipt_date', 'supplier_name']) @@ -343,10 +355,26 @@ def upload_purchase_excel(): VALUES ('RECEIPT', ?, ?, ?, ?, 'purchase_receipts', ?) """, (herb_item_id, lot_id, quantity, unit_price, receipt_id)) + processed_rows += 1 + processed_items.add(row['herb_name']) + + # 응답 메시지 생성 + format_name = { + 'hanisarang': '한의사랑', + 'haninfo': '한의정보' + }.get(summary['format_type'], '알 수 없음') + return jsonify({ 'success': True, - 'message': f'입고 데이터가 성공적으로 처리되었습니다', - 'filename': filename + 'message': f'{format_name} 형식 입고 데이터가 성공적으로 처리되었습니다', + 'filename': filename, + 'summary': { + 'format': format_name, + 'processed_rows': processed_rows, + 'total_items': len(processed_items), + 'total_quantity': f"{summary['total_quantity']:,.0f}g", + 'total_amount': f"{summary['total_amount']:,.0f}원" + } }) except Exception as e: diff --git a/excel_processor.py b/excel_processor.py new file mode 100644 index 0000000..474f48e --- /dev/null +++ b/excel_processor.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Excel 파일 처리 모듈 +한의사랑, 한의정보 형식 자동 감지 및 처리 +""" + +import pandas as pd +import numpy as np +from datetime import datetime +import re + +class ExcelProcessor: + """Excel 파일 형식별 처리 클래스""" + + # 한의사랑 형식 컬럼 매핑 + HANISARANG_MAPPING = { + '품목명': 'herb_name', + '제품코드': 'insurance_code', + '일그램당단가': 'unit_price', + '원산지': 'origin_country', + '적용일': 'receipt_date', + '총구입량': 'quantity', + '총구입단가': 'total_amount' + } + + # 한의정보 형식 컬럼 매핑 + HANINFO_MAPPING = { + '제품코드': 'insurance_code', + '업체명': 'supplier_name', + '약재명': 'herb_name', + '구입일자': 'receipt_date', + '구입량': 'quantity', + '구입액': 'total_amount', + '원산지': 'origin_country', + '비고': 'notes' + } + + def __init__(self): + self.format_type = None + self.df_original = None + self.df_processed = None + + def detect_format(self, df): + """Excel 형식 자동 감지""" + columns = df.columns.tolist() + + # 한의사랑 형식 체크 + hanisarang_cols = ['품목명', '제품코드', '일그램당단가', '총구입량', '총구입단가'] + if all(col in columns for col in hanisarang_cols): + return 'hanisarang' + + # 한의정보 형식 체크 + haninfo_cols = ['제품코드', '업체명', '약재명', '구입일자', '구입량', '구입액'] + if all(col in columns for col in haninfo_cols): + return 'haninfo' + + # 기본 형식 (제품코드가 있는 경우 한의정보로 간주) + if '제품코드' in columns and '약재명' in columns: + return 'haninfo' + + return 'unknown' + + def read_excel(self, file_path): + """Excel 파일 읽기""" + try: + self.df_original = pd.read_excel(file_path) + self.format_type = self.detect_format(self.df_original) + return True + except Exception as e: + print(f"Excel 파일 읽기 실패: {str(e)}") + return False + + def process_hanisarang(self): + """한의사랑 형식 처리""" + df = self.df_original.copy() + + # 컬럼 매핑 + df_mapped = pd.DataFrame() + + for old_col, new_col in self.HANISARANG_MAPPING.items(): + if old_col in df.columns: + df_mapped[new_col] = df[old_col] + + # 업체명 추가 (기본값) + df_mapped['supplier_name'] = '한의사랑' + + # 날짜 처리 + if 'receipt_date' in df_mapped.columns: + df_mapped['receipt_date'] = pd.to_datetime( + df_mapped['receipt_date'], + format='%Y-%m-%d', + errors='coerce' + ).dt.strftime('%Y%m%d') + + # 단가 계산 (이미 있지만 검증) + if 'unit_price' not in df_mapped.columns or df_mapped['unit_price'].isnull().all(): + if 'total_amount' in df_mapped.columns and 'quantity' in df_mapped.columns: + df_mapped['unit_price'] = df_mapped['total_amount'] / df_mapped['quantity'] + + self.df_processed = df_mapped + return df_mapped + + def process_haninfo(self): + """한의정보 형식 처리""" + df = self.df_original.copy() + + # 컬럼 매핑 + df_mapped = pd.DataFrame() + + for old_col, new_col in self.HANINFO_MAPPING.items(): + if old_col in df.columns: + df_mapped[new_col] = df[old_col] + + # 날짜 처리 (YYYYMMDD 형식) + if 'receipt_date' in df_mapped.columns: + df_mapped['receipt_date'] = df_mapped['receipt_date'].astype(str) + + # 단가 계산 + if 'total_amount' in df_mapped.columns and 'quantity' in df_mapped.columns: + df_mapped['unit_price'] = df_mapped['total_amount'] / df_mapped['quantity'] + df_mapped['unit_price'] = df_mapped['unit_price'].round(2) + + self.df_processed = df_mapped + return df_mapped + + def process(self): + """형식에 따라 자동 처리""" + if self.format_type == 'hanisarang': + return self.process_hanisarang() + elif self.format_type == 'haninfo': + return self.process_haninfo() + else: + raise ValueError(f"지원하지 않는 형식: {self.format_type}") + + def validate_data(self): + """처리된 데이터 검증""" + if self.df_processed is None: + return False, "처리된 데이터가 없습니다" + + df = self.df_processed + + # 필수 컬럼 확인 + required_columns = ['herb_name', 'quantity', 'total_amount'] + missing_cols = [col for col in required_columns if col not in df.columns] + + if missing_cols: + return False, f"필수 컬럼 누락: {', '.join(missing_cols)}" + + # 데이터 타입 검증 + numeric_cols = ['quantity', 'total_amount', 'unit_price'] + for col in numeric_cols: + if col in df.columns: + try: + df[col] = pd.to_numeric(df[col], errors='coerce') + except: + return False, f"{col} 컬럼이 숫자 형식이 아닙니다" + + # NULL 값 확인 + null_check = df[required_columns].isnull().sum() + if null_check.sum() > 0: + null_cols = null_check[null_check > 0].index.tolist() + return False, f"NULL 값 포함 컬럼: {', '.join(null_cols)}" + + # 음수 값 확인 + for col in ['quantity', 'total_amount']: + if col in df.columns: + if (df[col] < 0).any(): + return False, f"{col} 컬럼에 음수 값이 있습니다" + + return True, "검증 통과" + + def get_summary(self): + """처리 결과 요약""" + if self.df_processed is None: + return None + + df = self.df_processed + + summary = { + 'format_type': self.format_type, + 'total_rows': len(df), + 'total_items': df['herb_name'].nunique() if 'herb_name' in df.columns else 0, + 'total_quantity': df['quantity'].sum() if 'quantity' in df.columns else 0, + 'total_amount': df['total_amount'].sum() if 'total_amount' in df.columns else 0, + 'suppliers': df['supplier_name'].unique().tolist() if 'supplier_name' in df.columns else [], + 'date_range': None + } + + # 날짜 범위 + if 'receipt_date' in df.columns: + dates = pd.to_datetime(df['receipt_date'], format='%Y%m%d', errors='coerce') + dates = dates.dropna() + if not dates.empty: + summary['date_range'] = { + 'start': dates.min().strftime('%Y-%m-%d'), + 'end': dates.max().strftime('%Y-%m-%d') + } + + return summary + + def export_to_standard(self): + """표준 형식으로 변환""" + if self.df_processed is None: + return None + + # 표준 컬럼 순서 + standard_columns = [ + 'insurance_code', 'supplier_name', 'herb_name', + 'receipt_date', 'quantity', 'total_amount', + 'unit_price', 'origin_country', 'notes' + ] + + # 있는 컬럼만 선택 + available_cols = [col for col in standard_columns if col in self.df_processed.columns] + df_standard = self.df_processed[available_cols].copy() + + # 누락된 컬럼 추가 (기본값) + for col in standard_columns: + if col not in df_standard.columns: + if col == 'notes': + df_standard[col] = '' + elif col == 'supplier_name': + df_standard[col] = '미지정' + else: + df_standard[col] = None + + return df_standard[standard_columns] + + +# 테스트 함수 +def test_processor(): + """프로세서 테스트""" + processor = ExcelProcessor() + + # 한의사랑 테스트 + print("="*60) + print("한의사랑 형식 테스트") + print("="*60) + + if processor.read_excel('/root/kdrug/sample/한의사랑.xlsx'): + print(f"형식 감지: {processor.format_type}") + df = processor.process() + print(f"처리된 행 수: {len(df)}") + + valid, msg = processor.validate_data() + print(f"검증 결과: {msg}") + + summary = processor.get_summary() + print(f"요약:") + print(f" - 총 약재: {summary['total_items']}종") + print(f" - 총 수량: {summary['total_quantity']:,.0f}g") + print(f" - 총 금액: {summary['total_amount']:,.0f}원") + + # 샘플 출력 + print("\n처리된 데이터 샘플:") + print(df.head(3).to_string()) + + # 한의정보 테스트 + print("\n" + "="*60) + print("한의정보 형식 테스트") + print("="*60) + + processor2 = ExcelProcessor() + if processor2.read_excel('/root/kdrug/sample/한의정보.xlsx'): + print(f"형식 감지: {processor2.format_type}") + df = processor2.process() + print(f"처리된 행 수: {len(df)}") + + valid, msg = processor2.validate_data() + print(f"검증 결과: {msg}") + + summary = processor2.get_summary() + print(f"요약:") + print(f" - 총 약재: {summary['total_items']}종") + print(f" - 총 수량: {summary['total_quantity']:,.0f}g") + print(f" - 총 금액: {summary['total_amount']:,.0f}원") + print(f" - 공급업체: {', '.join(summary['suppliers'])}") + + # 샘플 출력 + print("\n처리된 데이터 샘플:") + print(df.head(3).to_string()) + + +if __name__ == "__main__": + test_processor() \ No newline at end of file diff --git a/sample/한의사랑.xlsx b/sample/한의사랑.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0cc74c91327fd8786f79f489644ca30ea5444c09 GIT binary patch literal 8054 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 H&(r?{w9#|S literal 0 HcmV?d00001 diff --git a/sample/한의정보.xlsx b/sample/한의정보.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ec7d637ae377e8f8355113e6f7f558ce7138c277 GIT binary patch literal 7915 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 literal 0 HcmV?d00001