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:
parent
2a14af59c3
commit
8f2823e6df
21
app.py
21
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:
|
||||
|
||||
58
docs/대규모리팩토링.md
Normal file
58
docs/대규모리팩토링.md
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
우리 기존설계에 변화가 필요해
|
||||
|
||||
약품 마스터 테이블을 별도로만들어야겟어
|
||||
|
||||
대한민국에서 주기적으로
|
||||
|
||||
한약재 제품코드를 표준화해서 제공한다
|
||||
|
||||
|
||||
|
||||
한약재 품목명, 업체명, 제품명 약품규격 (숫자) 약품 규격(단위) 제공을하고
|
||||
|
||||
"주성분코드" 를 제공하고 이게 FK가 되야할거같아
|
||||
|
||||
|
||||
그리고 해당 성분코드에 여러 제품들이 있는거지
|
||||
|
||||
|
||||
|
||||
|
||||
▶ 표준코드: 개개의 의약품을 식별하기 위해 고유하게 설정된 번호
|
||||
- 국가식별코드, 업체식별코드, 품목코드* 및 검증번호로 구성(13자리 숫자)
|
||||
* 품목코드는 함량포함한 품목코드(4자리)와 포장단위(1자리)로 구성되어 있음
|
||||
|
||||
|
||||
▶ 대표코드: 표준코드의 12번째 자리가 '0'인 코드를 말함(실제 의약품에 부착하는 코드로 사용불가)
|
||||
|
||||
|
||||
▶ 제품코드: 한약재 비용 청구시 사용하는 코드
|
||||
- 업체식별코드와 품목코드로 구성(9자리 숫자)
|
||||
- 9자리 중 제일 마지막 숫자인 포장단위는 대표코드와 동일하게 "0"임
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
우리 입고장에
|
||||
|
||||
건강 : 062400730 9자리숫자는 진짜 해당제품에 "제품코드" 인것이고
|
||||
|
||||
|
||||
해당 건강에 성분 코드는 별도로 존재하는거지 "성분코드로는"
|
||||
|
||||
|
||||
같은 성분 코드를 지닌 다른 회사 제품도 있을수잇는거고
|
||||
|
||||
우리는 성분 코드 기반으로 효능 태그를 사실상 해야할수도잇어
|
||||
|
||||
|
||||
성분코드 = 실제성분이름 1:1 맵핑일거니까
|
||||
|
||||
일단 기존 코드를 두고 프론트에서 봐가면서 마이그레이션 해가야하니까
|
||||
|
||||
|
||||
우리 일단 xls파일을 확인하고 sqlite에 마이그레이션 해줘 약품 마스터 테이블을 만들어서 가능할까?
|
||||
210
docs/데이터베이스_리팩토링_제안.md
Normal file
210
docs/데이터베이스_리팩토링_제안.md
Normal 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
|
||||
172
refactoring/01_create_new_tables.py
Normal file
172
refactoring/01_create_new_tables.py
Normal 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()
|
||||
222
refactoring/02_import_product_codes.py
Normal file
222
refactoring/02_import_product_codes.py
Normal 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}초")
|
||||
126
refactoring/03_fix_mappings.py
Normal file
126
refactoring/03_fix_mappings.py
Normal 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()
|
||||
124
refactoring/REFACTORING_RESULT.md
Normal file
124
refactoring/REFACTORING_RESULT.md
Normal 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
|
||||
@ -543,6 +543,54 @@ $(document).ready(function() {
|
||||
const tbody = $('#inventoryList');
|
||||
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 => {
|
||||
// 원산지가 여러 개인 경우 표시
|
||||
const originBadge = item.origin_count > 1
|
||||
|
||||
@ -655,6 +655,6 @@
|
||||
<!-- Scripts -->
|
||||
<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="/static/app.js"></script>
|
||||
<script src="/static/app.js?v=20260215"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user