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

21
app.py
View File

@ -966,12 +966,31 @@ def get_inventory_summary():
total_value = sum(item['total_value'] for item in inventory) total_value = sum(item['total_value'] for item in inventory)
total_items = len(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({ return jsonify({
'success': True, 'success': True,
'data': inventory, 'data': inventory,
'summary': { 'summary': {
'total_items': total_items, '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: except Exception as e:

View File

@ -0,0 +1,58 @@
우리 기존설계에 변화가 필요해
약품 마스터 테이블을 별도로만들어야겟어
대한민국에서 주기적으로
한약재 제품코드를 표준화해서 제공한다
한약재 품목명, 업체명, 제품명 약품규격 (숫자) 약품 규격(단위) 제공을하고
"주성분코드" 를 제공하고 이게 FK가 되야할거같아
그리고 해당 성분코드에 여러 제품들이 있는거지
▶ 표준코드: 개개의 의약품을 식별하기 위해 고유하게 설정된 번호
- 국가식별코드, 업체식별코드, 품목코드* 및 검증번호로 구성(13자리 숫자)
* 품목코드는 함량포함한 품목코드(4자리)와 포장단위(1자리)로 구성되어 있음
▶ 대표코드: 표준코드의 12번째 자리가 '0'인 코드를 말함(실제 의약품에 부착하는 코드로 사용불가)
▶ 제품코드: 한약재 비용 청구시 사용하는 코드
- 업체식별코드와 품목코드로 구성(9자리 숫자)
- 9자리 중 제일 마지막 숫자인 포장단위는 대표코드와 동일하게 "0"임
우리 입고장에
건강 : 062400730 9자리숫자는 진짜 해당제품에 "제품코드" 인것이고
해당 건강에 성분 코드는 별도로 존재하는거지 "성분코드로는"
같은 성분 코드를 지닌 다른 회사 제품도 있을수잇는거고
우리는 성분 코드 기반으로 효능 태그를 사실상 해야할수도잇어
성분코드 = 실제성분이름 1:1 맵핑일거니까
일단 기존 코드를 두고 프론트에서 봐가면서 마이그레이션 해가야하니까
우리 일단 xls파일을 확인하고 sqlite에 마이그레이션 해줘 약품 마스터 테이블을 만들어서 가능할까?

View File

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

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

View File

@ -543,6 +543,54 @@ $(document).ready(function() {
const tbody = $('#inventoryList'); const tbody = $('#inventoryList');
tbody.empty(); tbody.empty();
// 주성분코드 기준 보유 현황 표시
if (response.summary) {
const summary = response.summary;
const coverageHtml = `
<div class="alert alert-info mb-3">
<div class="row align-items-center">
<div class="col-md-8">
<h6 class="mb-2">📊 급여 약재 보유 현황</h6>
<div class="d-flex align-items-center">
<div class="me-4">
<strong>전체 급여 약재:</strong> ${summary.total_ingredient_codes || 454}
</div>
<div class="me-4">
<strong>보유 약재:</strong> ${summary.owned_ingredient_codes || 0}
</div>
<div>
<strong>보유율:</strong>
<span class="badge bg-primary fs-6">${summary.coverage_rate || 0}%</span>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<div class="progress" style="height: 30px;">
<div class="progress-bar bg-success" role="progressbar"
style="width: ${summary.coverage_rate || 0}%"
aria-valuenow="${summary.coverage_rate || 0}"
aria-valuemin="0" aria-valuemax="100">
${summary.owned_ingredient_codes || 0} / ${summary.total_ingredient_codes || 454}
</div>
</div>
</div>
</div>
<div class="mt-2">
<small class="text-muted">
건강보험 급여 한약재 ${summary.total_ingredient_codes || 454} 주성분 ${summary.owned_ingredient_codes || 0} 보유
</small>
</div>
</div>
`;
// 재고 테이블 위에 통계 표시
if ($('#inventoryCoverage').length === 0) {
$('#inventoryList').parent().before(`<div id="inventoryCoverage">${coverageHtml}</div>`);
} else {
$('#inventoryCoverage').html(coverageHtml);
}
}
response.data.forEach(item => { response.data.forEach(item => {
// 원산지가 여러 개인 경우 표시 // 원산지가 여러 개인 경우 표시
const originBadge = item.origin_count > 1 const originBadge = item.origin_count > 1

View File

@ -655,6 +655,6 @@
<!-- Scripts --> <!-- Scripts -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/app.js"></script> <script src="/static/app.js?v=20260215"></script>
</body> </body>
</html> </html>