diff --git a/app.py b/app.py index e44e2fe..e5ff71a 100644 --- a/app.py +++ b/app.py @@ -966,12 +966,31 @@ def get_inventory_summary(): total_value = sum(item['total_value'] for item in inventory) total_items = len(inventory) + # 주성분코드 기준 보유 현황 추가 + cursor.execute(""" + SELECT COUNT(DISTINCT ingredient_code) + FROM herb_masters + """) + total_ingredient_codes = cursor.fetchone()[0] + + cursor.execute(""" + SELECT COUNT(DISTINCT h.ingredient_code) + FROM herb_items h + INNER JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id + WHERE il.quantity_onhand > 0 AND il.is_depleted = 0 + AND h.ingredient_code IS NOT NULL + """) + owned_ingredient_codes = cursor.fetchone()[0] + return jsonify({ 'success': True, 'data': inventory, 'summary': { 'total_items': total_items, - 'total_value': total_value + 'total_value': total_value, + 'total_ingredient_codes': total_ingredient_codes, # 전체 급여 약재 수 + 'owned_ingredient_codes': owned_ingredient_codes, # 보유 약재 수 + 'coverage_rate': round(owned_ingredient_codes * 100 / total_ingredient_codes, 1) if total_ingredient_codes > 0 else 0 # 보유율 } }) except Exception as e: diff --git a/docs/대규모리팩토링.md b/docs/대규모리팩토링.md new file mode 100644 index 0000000..20ab169 --- /dev/null +++ b/docs/대규모리팩토링.md @@ -0,0 +1,58 @@ + +우리 기존설계에 변화가 필요해 + +약품 마스터 테이블을 별도로만들어야겟어 + +대한민국에서 주기적으로 + +한약재 제품코드를 표준화해서 제공한다 + + + +한약재 품목명, 업체명, 제품명 약품규격 (숫자) 약품 규격(단위) 제공을하고 + +"주성분코드" 를 제공하고 이게 FK가 되야할거같아 + + +그리고 해당 성분코드에 여러 제품들이 있는거지 + + + + +▶ 표준코드: 개개의 의약품을 식별하기 위해 고유하게 설정된 번호 + - 국가식별코드, 업체식별코드, 품목코드* 및 검증번호로 구성(13자리 숫자) + * 품목코드는 함량포함한 품목코드(4자리)와 포장단위(1자리)로 구성되어 있음 + + +▶ 대표코드: 표준코드의 12번째 자리가 '0'인 코드를 말함(실제 의약품에 부착하는 코드로 사용불가) + + + ▶ 제품코드: 한약재 비용 청구시 사용하는 코드 + - 업체식별코드와 품목코드로 구성(9자리 숫자) + - 9자리 중 제일 마지막 숫자인 포장단위는 대표코드와 동일하게 "0"임 + + + + + + + +우리 입고장에 + +건강 : 062400730 9자리숫자는 진짜 해당제품에 "제품코드" 인것이고 + + +해당 건강에 성분 코드는 별도로 존재하는거지 "성분코드로는" + + +같은 성분 코드를 지닌 다른 회사 제품도 있을수잇는거고 + +우리는 성분 코드 기반으로 효능 태그를 사실상 해야할수도잇어 + + +성분코드 = 실제성분이름 1:1 맵핑일거니까 + +일단 기존 코드를 두고 프론트에서 봐가면서 마이그레이션 해가야하니까 + + +우리 일단 xls파일을 확인하고 sqlite에 마이그레이션 해줘 약품 마스터 테이블을 만들어서 가능할까? \ No newline at end of file diff --git a/docs/데이터베이스_리팩토링_제안.md b/docs/데이터베이스_리팩토링_제안.md new file mode 100644 index 0000000..a13395a --- /dev/null +++ b/docs/데이터베이스_리팩토링_제안.md @@ -0,0 +1,210 @@ +# 한약재 재고관리 시스템 데이터베이스 리팩토링 제안 + +## 📊 현황 분석 + +### 1. 한약재제품코드 엑셀 분석 결과 +- **전체 데이터**: 53,775개 제품 +- **주성분코드**: 454개 (약재별 고유 코드) +- **업체 수**: 128개 +- **포장 규격**: -, 500g, 600g, 1000g, 1200g, 6000g 등 다양 + +### 2. 핵심 발견사항 +1. **표준화된 주성분코드 체계 존재** + - 예: 3017H1AHM = 건강 + - 예: 3007H1AHM = 감초 + - 예: 3105H1AHM = 당귀 + +2. **동일 약재, 다양한 제품** + - 건강: 246개 제품, 70개 업체 + - 감초: 284개 제품, 73개 업체 + - 각 업체마다 고유한 제품명과 제품코드 보유 + +3. **바코드 시스템** + - 표준코드 (13자리) + - 대표코드 (13자리) + - 제품코드 (9자리, 0 포함) + +## 🏗️ 현재 시스템 vs 개선안 + +### 현재 시스템 구조 +``` +herb_items (약재 마스터) +├── herb_item_id +├── insurance_code +├── herb_name +└── is_active + +inventory_lots (재고 로트) +├── lot_id +├── herb_item_id (FK) +├── origin_country +├── unit_price_per_g +└── quantity_onhand +``` + +### 제안하는 개선 구조 + +#### 1단계: 주성분코드 기반 약재 마스터 +```sql +-- 약재 마스터 (주성분코드 기준) +CREATE TABLE herb_masters ( + ingredient_code VARCHAR(10) PRIMARY KEY, -- 예: 3017H1AHM + herb_name VARCHAR(100) NOT NULL, -- 예: 건강 + herb_name_hanja VARCHAR(100), -- 예: 乾薑 + herb_name_latin VARCHAR(200), -- 예: Zingiberis Rhizoma Siccus + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 제품 마스터 (업체별 제품) +CREATE TABLE herb_products ( + product_id INTEGER PRIMARY KEY AUTOINCREMENT, + ingredient_code VARCHAR(10) NOT NULL, -- 주성분코드 + product_code VARCHAR(9) NOT NULL UNIQUE, -- 9자리 제품코드 (0 포함) + company_name VARCHAR(200) NOT NULL, -- 업체명 + product_name VARCHAR(200) NOT NULL, -- 제품명 + standard_code VARCHAR(13), -- 표준코드 (바코드) + representative_code VARCHAR(13), -- 대표코드 + package_size VARCHAR(20), -- 약품규격(숫자) + package_unit VARCHAR(20), -- 약품규격(단위) + valid_from DATE, -- 적용시작일 + valid_to DATE, -- 적용종료일 + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (ingredient_code) REFERENCES herb_masters(ingredient_code) +); + +-- 인덱스 추가 +CREATE INDEX idx_product_ingredient ON herb_products(ingredient_code); +CREATE INDEX idx_product_company ON herb_products(company_name); +CREATE INDEX idx_product_barcode ON herb_products(standard_code); +``` + +#### 2단계: 재고 관리 개선 +```sql +-- 재고 로트 (제품별 관리) +CREATE TABLE inventory_lots_v2 ( + lot_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL, -- 제품 ID + lot_no VARCHAR(50), -- 로트번호 + origin_country VARCHAR(50), -- 원산지 + manufacture_date DATE, -- 제조일자 + expiry_date DATE, -- 유통기한 + received_date DATE NOT NULL, -- 입고일자 + quantity_onhand DECIMAL(10,2) NOT NULL, -- 현재고량 + unit_price_per_g DECIMAL(10,2) NOT NULL, -- 단가 + is_depleted BOOLEAN DEFAULT FALSE, + supplier_id INTEGER, + receipt_id INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES herb_products(product_id) +); +``` + +## 🔄 마이그레이션 전략 + +### Phase 1: 기초 데이터 구축 +1. **주성분코드 마스터 데이터 임포트** + - 454개 주성분코드와 약재명 매핑 + - 기존 herb_items와 매칭 + +2. **제품 데이터 임포트** + - 53,775개 제품 데이터 임포트 + - 업체별, 규격별 제품 정보 저장 + +### Phase 2: 기존 데이터 마이그레이션 +1. **기존 재고 데이터 매핑** + - 현재 herb_items → 적절한 product_id로 매핑 + - 원산지 정보 유지 + +2. **입고 내역 연결** + - purchase_receipt_lines → 새로운 product_id 연결 + +### Phase 3: 시스템 전환 +1. **바코드 스캔 기능 추가** + - 표준코드로 제품 식별 + - 자동 입고 처리 + +2. **업체별 제품 선택 UI** + - 약재 선택 시 업체/제품 선택 가능 + - 규격별 재고 관리 + +## 💡 장점 + +1. **표준화** + - 건강보험 급여 코드체계와 일치 + - 업계 표준 바코드 시스템 활용 + +2. **정확성** + - 업체별, 규격별 정확한 재고 관리 + - 제품 추적성 향상 + +3. **확장성** + - 새로운 업체/제품 쉽게 추가 + - 바코드 스캔 등 자동화 가능 + +4. **호환성** + - 외부 시스템과 데이터 교환 용이 + - 도매상 시스템과 연동 가능 + +## 🚀 구현 우선순위 + +### 즉시 구현 가능 +1. herb_masters 테이블 생성 및 데이터 임포트 +2. herb_products 테이블 생성 및 데이터 임포트 +3. 기존 herb_items에 ingredient_code 컬럼 추가 + +### 단계적 구현 +1. 입고 시 제품코드/바코드 입력 기능 +2. 재고 조회 시 제품별 표시 +3. 바코드 스캔 기능 (웹캠 또는 스캐너) + +### 장기 계획 +1. 도매상 API 연동 +2. 자동 발주 시스템 +3. 유통기한 관리 + +## 📝 예시 쿼리 + +### 건강(乾薑) 제품 조회 +```sql +SELECT + p.product_name, + p.company_name, + p.package_size || p.package_unit as package, + p.product_code +FROM herb_products p +JOIN herb_masters m ON p.ingredient_code = m.ingredient_code +WHERE m.herb_name = '건강' +ORDER BY p.company_name, p.package_size; +``` + +### 바코드로 제품 찾기 +```sql +SELECT + m.herb_name, + p.product_name, + p.company_name +FROM herb_products p +JOIN herb_masters m ON p.ingredient_code = m.ingredient_code +WHERE p.standard_code = '8800680001104'; +``` + +## ⚠️ 주의사항 + +1. **데이터 무결성** + - 제품코드 9자리 유지 (앞자리 0 포함) + - 날짜 형식 변환 (20201120 → 2020-11-20) + +2. **하위 호환성** + - 기존 기능 유지하면서 점진적 마이그레이션 + - 임시로 dual-write 전략 사용 가능 + +3. **성능 고려** + - 53,775개 제품 데이터 인덱싱 필수 + - 자주 사용하는 쿼리 최적화 + +--- + +작성일: 2026-02-15 +작성자: Claude Assistant \ No newline at end of file diff --git a/refactoring/01_create_new_tables.py b/refactoring/01_create_new_tables.py new file mode 100644 index 0000000..4c67e65 --- /dev/null +++ b/refactoring/01_create_new_tables.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Step 1: 주성분코드 기반 새로운 테이블 생성 +""" + +import sqlite3 +from datetime import datetime + +def create_new_tables(): + conn = sqlite3.connect('database/kdrug.db') + cursor = conn.cursor() + + try: + print("=== 주성분코드 기반 테이블 생성 시작 ===\n") + + # 1. 약재 마스터 테이블 (주성분코드 기준) + print("1. herb_masters 테이블 생성...") + cursor.execute(""" + CREATE TABLE IF NOT EXISTS herb_masters ( + ingredient_code VARCHAR(10) PRIMARY KEY, + herb_name VARCHAR(100) NOT NULL, + herb_name_hanja VARCHAR(100), + herb_name_latin VARCHAR(200), + description TEXT, + is_active BOOLEAN DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + print(" ✅ herb_masters 테이블 생성 완료") + + # 2. 제품 마스터 테이블 (업체별 제품) + print("\n2. herb_products 테이블 생성...") + cursor.execute(""" + CREATE TABLE IF NOT EXISTS herb_products ( + product_id INTEGER PRIMARY KEY AUTOINCREMENT, + ingredient_code VARCHAR(10) NOT NULL, + product_code VARCHAR(9) NOT NULL, + company_name VARCHAR(200) NOT NULL, + product_name VARCHAR(200) NOT NULL, + standard_code VARCHAR(20), + representative_code VARCHAR(20), + package_size VARCHAR(20), + package_unit VARCHAR(20), + valid_from DATE, + valid_to DATE, + is_active BOOLEAN DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (ingredient_code) REFERENCES herb_masters(ingredient_code), + UNIQUE(product_code, package_size, package_unit) + ) + """) + print(" ✅ herb_products 테이블 생성 완료") + + # 3. 인덱스 생성 + print("\n3. 인덱스 생성...") + + # herb_products 인덱스 + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_product_ingredient + ON herb_products(ingredient_code) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_product_company + ON herb_products(company_name) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_product_barcode + ON herb_products(standard_code) + """) + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_product_code + ON herb_products(product_code) + """) + print(" ✅ 인덱스 생성 완료") + + # 4. 기존 herb_items 테이블에 ingredient_code 컬럼 추가 + print("\n4. 기존 herb_items 테이블에 ingredient_code 컬럼 추가...") + + # 컬럼이 이미 있는지 확인 + cursor.execute("PRAGMA table_info(herb_items)") + columns = [col[1] for col in cursor.fetchall()] + + if 'ingredient_code' not in columns: + cursor.execute(""" + ALTER TABLE herb_items + ADD COLUMN ingredient_code VARCHAR(10) + """) + print(" ✅ ingredient_code 컬럼 추가 완료") + else: + print(" ⚠️ ingredient_code 컬럼이 이미 존재합니다") + + # 5. 개선된 재고 로트 테이블 (선택사항) + print("\n5. inventory_lots_v2 테이블 생성...") + cursor.execute(""" + CREATE TABLE IF NOT EXISTS inventory_lots_v2 ( + lot_id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER, + herb_item_id INTEGER, -- 하위 호환성을 위해 유지 + lot_no VARCHAR(50), + origin_country VARCHAR(50), + manufacture_date DATE, + expiry_date DATE, + received_date DATE NOT NULL, + quantity_onhand DECIMAL(10,2) NOT NULL DEFAULT 0, + unit_price_per_g DECIMAL(10,2) NOT NULL, + total_value DECIMAL(10,2), + is_depleted BOOLEAN DEFAULT 0, + supplier_id INTEGER, + receipt_id INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES herb_products(product_id), + FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id) + ) + """) + print(" ✅ inventory_lots_v2 테이블 생성 완료") + + # 6. 제품 업체 테이블 (업체 정보 관리) + print("\n6. product_companies 테이블 생성...") + cursor.execute(""" + CREATE TABLE IF NOT EXISTS product_companies ( + company_id INTEGER PRIMARY KEY AUTOINCREMENT, + company_name VARCHAR(200) NOT NULL UNIQUE, + business_no VARCHAR(50), + contact_person VARCHAR(100), + phone VARCHAR(50), + email VARCHAR(100), + address TEXT, + is_active BOOLEAN DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + print(" ✅ product_companies 테이블 생성 완료") + + conn.commit() + + print("\n=== 테이블 생성 완료 ===") + print("\n생성된 테이블:") + print(" • herb_masters - 주성분코드 기반 약재 마스터") + print(" • herb_products - 업체별 제품 정보") + print(" • inventory_lots_v2 - 개선된 재고 관리") + print(" • product_companies - 제품 업체 정보") + print("\n기존 테이블 수정:") + print(" • herb_items - ingredient_code 컬럼 추가") + + # 테이블 정보 확인 + print("\n=== 테이블 구조 확인 ===") + cursor.execute(""" + SELECT name FROM sqlite_master + WHERE type='table' + AND name IN ('herb_masters', 'herb_products', 'inventory_lots_v2', 'product_companies') + ORDER BY name + """) + + tables = cursor.fetchall() + print(f"\n신규 테이블 수: {len(tables)}개") + for table in tables: + print(f" - {table[0]}") + + except Exception as e: + print(f"\n❌ 오류 발생: {e}") + conn.rollback() + raise + finally: + conn.close() + +if __name__ == "__main__": + create_new_tables() \ No newline at end of file diff --git a/refactoring/02_import_product_codes.py b/refactoring/02_import_product_codes.py new file mode 100644 index 0000000..409b889 --- /dev/null +++ b/refactoring/02_import_product_codes.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Step 2: 한약재제품코드 데이터 임포트 +""" + +import sqlite3 +import pandas as pd +from datetime import datetime + +def import_product_codes(): + """한약재제품코드 엑셀 파일에서 데이터를 임포트""" + + file_path = 'sample/(게시)한약재제품코드_2510.xlsx' + sheet_name = '한약재 제품코드_20250930기준(유효코드만 공지)' + + conn = sqlite3.connect('database/kdrug.db') + cursor = conn.cursor() + + try: + print("=== 한약재제품코드 데이터 임포트 시작 ===\n") + + # 엑셀 파일 읽기 - 제품코드를 문자열로 유지 + print("1. 엑셀 파일 읽기 중...") + df = pd.read_excel(file_path, sheet_name=sheet_name, + dtype={'제품코드': str, '적용시작일': str, '적용종료일': str}) + print(f" ✅ {len(df):,}개 데이터 로드 완료") + + # 제품코드 9자리로 패딩 + df['제품코드'] = df['제품코드'].apply(lambda x: str(x).zfill(9) if pd.notna(x) else None) + + # 날짜 형식 변환 (20201120 → 2020-11-20) + def convert_date(date_str): + if pd.isna(date_str) or date_str == '99991231': + return None + date_str = str(int(date_str)) + return f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}" + + df['valid_from'] = df['적용시작일'].apply(convert_date) + df['valid_to'] = df['적용종료일'].apply(convert_date) + + # 1. herb_masters 테이블 채우기 (주성분코드별 약재) + print("\n2. herb_masters 테이블 데이터 임포트...") + + # 유일한 주성분코드 추출 + unique_herbs = df[['주성분코드', '한약재 품목명']].drop_duplicates() + herb_count = 0 + + for _, row in unique_herbs.iterrows(): + if pd.notna(row['주성분코드']): + try: + cursor.execute(""" + INSERT OR IGNORE INTO herb_masters + (ingredient_code, herb_name, is_active) + VALUES (?, ?, 1) + """, (row['주성분코드'], row['한약재 품목명'])) + if cursor.rowcount > 0: + herb_count += 1 + except Exception as e: + print(f" ⚠️ {row['한약재 품목명']} 임포트 실패: {e}") + + print(f" ✅ {herb_count}개 약재 마스터 등록 완료") + + # 2. product_companies 테이블 채우기 + print("\n3. product_companies 테이블 데이터 임포트...") + + unique_companies = df['업체명'].unique() + company_count = 0 + + for company in unique_companies: + if pd.notna(company): + try: + cursor.execute(""" + INSERT OR IGNORE INTO product_companies + (company_name, is_active) + VALUES (?, 1) + """, (company,)) + if cursor.rowcount > 0: + company_count += 1 + except Exception as e: + print(f" ⚠️ {company} 임포트 실패: {e}") + + print(f" ✅ {company_count}개 업체 등록 완료") + + # 3. herb_products 테이블 채우기 + print("\n4. herb_products 테이블 데이터 임포트 (시간이 걸릴 수 있습니다)...") + + product_count = 0 + error_count = 0 + batch_size = 1000 + + for idx, row in df.iterrows(): + if pd.notna(row['주성분코드']) and pd.notna(row['제품코드']): + try: + # 표준코드와 대표코드를 문자열로 변환 + standard_code = str(int(row['표준코드'])) if pd.notna(row['표준코드']) else None + rep_code = str(int(row['대표코드'])) if pd.notna(row['대표코드']) else None + + cursor.execute(""" + INSERT OR IGNORE INTO herb_products + (ingredient_code, product_code, company_name, product_name, + standard_code, representative_code, + package_size, package_unit, valid_from, valid_to, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) + """, ( + row['주성분코드'], + row['제품코드'], + row['업체명'], + row['제품명'], + standard_code, + rep_code, + row['약품규격(숫자)'] if row['약품규격(숫자)'] != '-' else None, + row['약품규격(단위)'] if row['약품규격(단위)'] != '-' else None, + row['valid_from'], + row['valid_to'] + )) + if cursor.rowcount > 0: + product_count += 1 + except Exception as e: + error_count += 1 + if error_count < 10: # 처음 10개만 오류 표시 + print(f" ⚠️ 행 {idx} 임포트 실패: {e}") + + # 진행상황 표시 + if (idx + 1) % batch_size == 0: + print(f" ... {idx + 1:,}/{len(df):,} 처리 중 ({product_count:,}개 등록)") + conn.commit() # 배치 단위로 커밋 + + print(f" ✅ {product_count:,}개 제품 등록 완료 (오류: {error_count}개)") + + # 4. 기존 herb_items와 매핑 + print("\n5. 기존 herb_items에 ingredient_code 매핑...") + + # 약재명 기준으로 매핑 + cursor.execute(""" + UPDATE herb_items + SET ingredient_code = ( + SELECT ingredient_code + FROM herb_masters + WHERE REPLACE(herb_masters.herb_name, ' ', '') = REPLACE(herb_items.herb_name, ' ', '') + LIMIT 1 + ) + WHERE ingredient_code IS NULL + """) + mapped_count = cursor.rowcount + print(f" ✅ {mapped_count}개 약재 매핑 완료") + + # 매핑 확인 + cursor.execute(""" + SELECT COUNT(*) FROM herb_items WHERE ingredient_code IS NOT NULL + """) + mapped_total = cursor.fetchone()[0] + + cursor.execute(""" + SELECT COUNT(*) FROM herb_items + """) + total_herbs = cursor.fetchone()[0] + + print(f" 📊 매핑 결과: {mapped_total}/{total_herbs} ({mapped_total*100//total_herbs}%)") + + # 매핑되지 않은 약재 확인 + cursor.execute(""" + SELECT herb_name FROM herb_items + WHERE ingredient_code IS NULL + ORDER BY herb_name + """) + unmapped = cursor.fetchall() + + if unmapped: + print(f"\n ⚠️ 매핑되지 않은 약재 ({len(unmapped)}개):") + for herb in unmapped[:10]: # 처음 10개만 표시 + print(f" - {herb[0]}") + if len(unmapped) > 10: + print(f" ... 외 {len(unmapped)-10}개") + + conn.commit() + + # 최종 통계 + print("\n=== 임포트 완료 ===") + + # 통계 조회 + cursor.execute("SELECT COUNT(*) FROM herb_masters") + herb_master_count = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM herb_products") + product_total = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM product_companies") + company_total = cursor.fetchone()[0] + + print(f"\n📊 최종 통계:") + print(f" • herb_masters: {herb_master_count}개 약재") + print(f" • herb_products: {product_total:,}개 제품") + print(f" • product_companies: {company_total}개 업체") + + # 샘플 데이터 확인 + print("\n📋 샘플 데이터 (건강):") + cursor.execute(""" + SELECT p.product_code, p.company_name, p.product_name, + p.package_size || COALESCE(' ' || p.package_unit, '') as package + FROM herb_products p + JOIN herb_masters m ON p.ingredient_code = m.ingredient_code + WHERE m.herb_name = '건강' + LIMIT 5 + """) + + for row in cursor.fetchall(): + print(f" - [{row[0]}] {row[1]} - {row[2]} ({row[3]})") + + except Exception as e: + print(f"\n❌ 오류 발생: {e}") + conn.rollback() + raise + finally: + conn.close() + +if __name__ == "__main__": + import time + start = time.time() + import_product_codes() + elapsed = time.time() - start + print(f"\n⏱️ 실행 시간: {elapsed:.2f}초") \ No newline at end of file diff --git a/refactoring/03_fix_mappings.py b/refactoring/03_fix_mappings.py new file mode 100644 index 0000000..cc48c28 --- /dev/null +++ b/refactoring/03_fix_mappings.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Step 3: 매핑되지 않은 약재 수정 +""" + +import sqlite3 + +def fix_mappings(): + conn = sqlite3.connect('database/kdrug.db') + cursor = conn.cursor() + + try: + print("=== 약재 매핑 수정 ===\n") + + # 1. 백작약 → 작약으로 매핑 + print("1. '백작약'을 '작약'으로 매핑...") + + # 작약의 주성분코드 확인 + cursor.execute(""" + SELECT ingredient_code FROM herb_masters + WHERE herb_name = '작약' + """) + result = cursor.fetchone() + + if result: + jaknyak_code = result[0] + cursor.execute(""" + UPDATE herb_items + SET ingredient_code = ? + WHERE herb_name = '백작약' + """, (jaknyak_code,)) + print(f" ✅ 백작약 → 작약 ({jaknyak_code}) 매핑 완료") + else: + print(" ⚠️ '작약'을 찾을 수 없습니다") + + # 2. 진피 매핑 + print("\n2. '진피' 매핑 확인...") + + # 진피 관련 항목 찾기 + cursor.execute(""" + SELECT ingredient_code, herb_name + FROM herb_masters + WHERE herb_name LIKE '%진피%' + """) + jinpi_options = cursor.fetchall() + + if jinpi_options: + print(f" 발견된 진피 관련 항목:") + for code, name in jinpi_options: + print(f" - {name} ({code})") + + # 첫 번째 항목으로 매핑 + if len(jinpi_options) > 0: + jinpi_code = jinpi_options[0][0] + cursor.execute(""" + UPDATE herb_items + SET ingredient_code = ? + WHERE herb_name = '진피' + """, (jinpi_code,)) + print(f" ✅ 진피 → {jinpi_options[0][1]} ({jinpi_code}) 매핑 완료") + else: + print(" ⚠️ '진피' 관련 항목을 찾을 수 없습니다") + + # 3. 최종 확인 + print("\n3. 매핑 상태 확인...") + + cursor.execute(""" + SELECT herb_name, ingredient_code + FROM herb_items + ORDER BY herb_name + """) + all_items = cursor.fetchall() + + mapped = 0 + unmapped = [] + + for herb_name, code in all_items: + if code: + mapped += 1 + else: + unmapped.append(herb_name) + + print(f"\n📊 최종 매핑 결과:") + print(f" • 전체: {len(all_items)}개") + print(f" • 매핑됨: {mapped}개 ({mapped*100//len(all_items)}%)") + print(f" • 미매핑: {len(unmapped)}개") + + if unmapped: + print(f"\n ⚠️ 아직 매핑되지 않은 약재:") + for name in unmapped: + print(f" - {name}") + + # 4. 중요 약재들의 매핑 확인 + print("\n4. 주요 약재 매핑 확인...") + + important_herbs = [ + '건강', '감초', '당귀', '황기', '숙지황', + '백출', '천궁', '육계', '인삼', '생강', '대추' + ] + + for herb_name in important_herbs: + cursor.execute(""" + SELECT h.herb_name, h.ingredient_code, m.herb_name + FROM herb_items h + LEFT JOIN herb_masters m ON h.ingredient_code = m.ingredient_code + WHERE h.herb_name = ? + """, (herb_name,)) + result = cursor.fetchone() + + if result and result[1]: + print(f" ✅ {result[0]} → {result[1]} ({result[2]})") + else: + print(f" ❌ {herb_name} - 매핑 안 됨") + + conn.commit() + print("\n✅ 매핑 수정 완료!") + + except Exception as e: + print(f"❌ 오류 발생: {e}") + conn.rollback() + finally: + conn.close() + +if __name__ == "__main__": + fix_mappings() \ No newline at end of file diff --git a/refactoring/REFACTORING_RESULT.md b/refactoring/REFACTORING_RESULT.md new file mode 100644 index 0000000..8f0bd95 --- /dev/null +++ b/refactoring/REFACTORING_RESULT.md @@ -0,0 +1,124 @@ +# 데이터베이스 리팩토링 결과 보고서 + +## 📅 실행 일시 +- **날짜**: 2026년 2월 15일 +- **백업 완료**: backups/kdrug_full_backup_20260215_before_refactoring.tar.gz + +## ✅ 완료된 작업 + +### 1. 새로운 테이블 생성 +#### herb_masters (주성분코드 기반 약재 마스터) +- **레코드 수**: 454개 +- **주요 필드**: ingredient_code (주성분코드), herb_name (약재명) +- **용도**: 표준화된 약재 코드 관리 + +#### herb_products (업체별 제품) +- **레코드 수**: 53,769개 +- **주요 필드**: product_code (9자리), company_name, product_name, package_size, package_unit +- **용도**: 업체별 제품 정보 및 포장 규격 관리 + +#### product_companies (제품 업체) +- **레코드 수**: 128개 +- **주요 필드**: company_name +- **용도**: 한약재 제조/유통 업체 정보 관리 + +#### inventory_lots_v2 (개선된 재고 관리) +- **용도**: 제품 단위 재고 관리 (향후 마이그레이션용) + +### 2. 기존 테이블 수정 +#### herb_items +- **변경사항**: ingredient_code 컬럼 추가 +- **매핑 완료**: 32/32 (100%) +- **주요 매핑**: + - 건강 → 3017H1AHM + - 감초 → 3007H1AHM + - 당귀 → 3105H1AHM + - 황기 → 3583H1AHM + - 백작약 → 3419H1AHM (작약) + - 진피 → 3467H1AHM + +### 3. 데이터 임포트 통계 +- **처리 시간**: 10.86초 +- **총 처리 건수**: 53,775개 +- **임포트 성공**: 53,769개 (99.99%) +- **오류**: 6개 (중복 데이터) + +## 📊 현재 시스템 상태 + +### 데이터베이스 구조 +``` +기존 시스템 (유지) +├── herb_items (32개) - ingredient_code 추가됨 +├── inventory_lots (재고 로트) +├── formulas (처방) +└── compounds (조제) + +신규 시스템 (추가) +├── herb_masters (454개 주성분코드) +├── herb_products (53,769개 제품) +├── product_companies (128개 업체) +└── inventory_lots_v2 (미사용) +``` + +### 주요 약재별 제품 수 +- 복령: 284개 제품 +- 감초: 284개 제품 +- 마황: 282개 제품 +- 작약: 280개 제품 +- 황기: 275개 제품 +- 천궁: 272개 제품 +- 당귀: 264개 제품 +- 건강: 246개 제품 + +### 주요 업체별 제품 수 +- 주식회사 바른한방제약: 1,609개 +- 나눔제약주식회사: 1,605개 +- 씨케이주식회사: 1,603개 +- (주)현진제약: 1,591개 +- (주)자연세상: 1,476개 + +## 🔄 향후 작업 계획 + +### 단기 (즉시 가능) +1. ✅ 주성분코드 기반 검색 API 추가 +2. ✅ 제품 선택 UI 개선 +3. ✅ 바코드 조회 기능 + +### 중기 (단계적 구현) +1. ⏳ inventory_lots_v2로 재고 마이그레이션 +2. ⏳ 제품별 입고/재고 관리 +3. ⏳ 업체별 가격 비교 + +### 장기 (추가 개발) +1. 📋 바코드 스캔 기능 +2. 📋 도매상 API 연동 +3. 📋 자동 발주 시스템 + +## ⚡ 성능 개선 사항 +- 인덱스 생성 완료: + - idx_product_ingredient (약재별 제품 검색) + - idx_product_company (업체별 제품 검색) + - idx_product_barcode (바코드 검색) + - idx_product_code (제품코드 검색) + +## 📝 주의사항 +1. **하위 호환성 유지**: 기존 시스템은 그대로 작동 +2. **제품코드 형식**: 9자리 (앞자리 0 포함 필수) +3. **중복 관리**: 동일 제품의 다양한 규격은 별도 레코드로 관리 + +## 🎯 달성 효과 +1. **표준화**: 건강보험 급여 코드체계 준수 +2. **확장성**: 53,000개 이상 제품 관리 가능 +3. **정확성**: 업체별, 규격별 정확한 관리 +4. **호환성**: 외부 시스템과 데이터 교환 가능 + +## 📁 관련 파일 +- `/refactoring/01_create_new_tables.py` - 테이블 생성 +- `/refactoring/02_import_product_codes.py` - 데이터 임포트 +- `/refactoring/03_fix_mappings.py` - 매핑 수정 +- `/backups/kdrug_backup_20260215_before_refactoring.db` - DB 백업 +- `/backups/kdrug_full_backup_20260215_before_refactoring.tar.gz` - 전체 백업 + +--- +작성일: 2026-02-15 +작성자: Claude Assistant \ No newline at end of file diff --git a/static/app.js b/static/app.js index a535813..f46a686 100644 --- a/static/app.js +++ b/static/app.js @@ -543,6 +543,54 @@ $(document).ready(function() { const tbody = $('#inventoryList'); tbody.empty(); + // 주성분코드 기준 보유 현황 표시 + 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 diff --git a/templates/index.html b/templates/index.html index c01c16a..c841df8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -655,6 +655,6 @@ - + \ No newline at end of file