refactor: 주성분코드 기반 데이터베이스 리팩토링 완료

DB 구조 개선:
- 454개 주성분코드 기반 herb_masters 테이블 생성
- 53,769개 제품 데이터를 herb_products 테이블에 임포트
- 128개 업체 정보를 product_companies 테이블에 추가
- 기존 herb_items에 ingredient_code 매핑 (100% 완료)

UI/API 개선:
- 급여 약재 보유 현황 표시 (28/454 = 6.2%)
- 재고 현황에 프로그레스 바 추가
- 주성분코드 기준 통계 API 추가

문서화:
- 데이터베이스 리팩토링 제안서 작성
- 리팩토링 결과 보고서 작성
- 백업 정보 문서화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-02-15 10:34:32 +00:00
parent 2a14af59c3
commit 8f2823e6df
9 changed files with 981 additions and 2 deletions

View File

@@ -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()

View File

@@ -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}")

View File

@@ -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()

View File

@@ -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