Compare commits
No commits in common. "69be63d00d015b765474253fd2d2f13bf3aa2ab5" and "1679f75d33006a0056f7f761dd79a2038bcb4ee1" have entirely different histories.
69be63d00d
...
1679f75d33
63
app.py
63
app.py
@ -455,62 +455,23 @@ def get_official_formulas():
|
|||||||
search = request.args.get('search', '').strip()
|
search = request.args.get('search', '').strip()
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
base_query = """
|
|
||||||
SELECT of2.official_formula_id, of2.formula_number, of2.formula_name,
|
|
||||||
of2.formula_name_hanja, of2.source_text, of2.description, of2.reference_notes,
|
|
||||||
COUNT(ofi.ingredient_id) as ingredient_count
|
|
||||||
FROM official_formulas of2
|
|
||||||
LEFT JOIN official_formula_ingredients ofi ON of2.official_formula_id = ofi.official_formula_id
|
|
||||||
"""
|
|
||||||
if search:
|
if search:
|
||||||
cursor.execute(base_query + """
|
cursor.execute("""
|
||||||
WHERE of2.formula_name LIKE ? OR of2.formula_name_hanja LIKE ?
|
SELECT official_formula_id, formula_number, formula_name,
|
||||||
OR of2.source_text LIKE ? OR of2.reference_notes LIKE ?
|
formula_name_hanja, source_text, description, reference_notes
|
||||||
GROUP BY of2.official_formula_id
|
FROM official_formulas
|
||||||
ORDER BY of2.formula_number
|
WHERE formula_name LIKE ? OR formula_name_hanja LIKE ?
|
||||||
|
OR source_text LIKE ? OR reference_notes LIKE ?
|
||||||
|
ORDER BY formula_number
|
||||||
""", (f'%{search}%', f'%{search}%', f'%{search}%', f'%{search}%'))
|
""", (f'%{search}%', f'%{search}%', f'%{search}%', f'%{search}%'))
|
||||||
else:
|
else:
|
||||||
cursor.execute(base_query + """
|
cursor.execute("""
|
||||||
GROUP BY of2.official_formula_id
|
SELECT official_formula_id, formula_number, formula_name,
|
||||||
ORDER BY of2.formula_number
|
formula_name_hanja, source_text, description, reference_notes
|
||||||
|
FROM official_formulas
|
||||||
|
ORDER BY formula_number
|
||||||
""")
|
""")
|
||||||
formulas = [dict(row) for row in cursor.fetchall()]
|
formulas = [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
# 등록 여부 판정: official_formula_id FK 매칭 (1차) + 이름 매칭 (fallback)
|
|
||||||
# 1차: formulas.official_formula_id로 직접 연결된 처방
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT official_formula_id, formula_name
|
|
||||||
FROM formulas
|
|
||||||
WHERE is_active = 1 AND official_formula_id IS NOT NULL
|
|
||||||
""")
|
|
||||||
registered_by_id = {}
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
oid = row['official_formula_id']
|
|
||||||
if oid not in registered_by_id:
|
|
||||||
registered_by_id[oid] = []
|
|
||||||
registered_by_id[oid].append(row['formula_name'])
|
|
||||||
|
|
||||||
# 2차 fallback: 이름 기반 매칭용
|
|
||||||
cursor.execute("SELECT formula_name FROM formulas WHERE is_active = 1")
|
|
||||||
my_formula_names = [row['formula_name'] for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
for formula in formulas:
|
|
||||||
oid = formula['official_formula_id']
|
|
||||||
oname = formula['formula_name']
|
|
||||||
|
|
||||||
# 1차: FK 매칭
|
|
||||||
if oid in registered_by_id:
|
|
||||||
formula['is_registered'] = True
|
|
||||||
formula['registered_names'] = registered_by_id[oid]
|
|
||||||
# 2차: 이름 매칭 (정확 매칭 또는 내 처방명에 원방명이 포함)
|
|
||||||
elif any(name == oname or oname in name for name in my_formula_names):
|
|
||||||
matched = [name for name in my_formula_names if name == oname or oname in name]
|
|
||||||
formula['is_registered'] = True
|
|
||||||
formula['registered_names'] = matched
|
|
||||||
else:
|
|
||||||
formula['is_registered'] = False
|
|
||||||
formula['registered_names'] = []
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': formulas})
|
return jsonify({'success': True, 'data': formulas})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|||||||
190
docs/제품 3단계분류.md
190
docs/제품 3단계분류.md
@ -1,190 +0,0 @@
|
|||||||
# 제품 3단계 분류 체계
|
|
||||||
|
|
||||||
> 한약재는 **성분 → 제품 → 로트** 3단계로 식별된다.
|
|
||||||
> 같은 성분코드의 약재도 제조사, 형태, 산지, 등급에 따라 품질과 가격이 크게 다르다.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 3단계 구조
|
|
||||||
|
|
||||||
```
|
|
||||||
[1단계] 성분 (herb_masters) — 약재의 본질
|
|
||||||
└─ ingredient_code: 3002H1AHM = "갈근"
|
|
||||||
│
|
|
||||||
[2단계] 제품 (herb_items) — 도매상별 상품
|
|
||||||
└─ insurance_code: 062401050 = "휴먼갈근" (주식회사휴먼허브)
|
|
||||||
│
|
|
||||||
[3단계] 로트 (inventory_lots) — 입고 건별 실물
|
|
||||||
└─ lot_id: 190 = "갈근.각" (한국산, ₩17/g)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 테이블 매핑
|
|
||||||
|
|
||||||
| 단계 | 테이블 | PK | 식별키 | 행수 | 예시 |
|
|
||||||
|------|--------|-----|-------|------|------|
|
|
||||||
| 성분 | `herb_masters` | herb_id | `ingredient_code` | 454 | 갈근 (3002H1AHM) |
|
|
||||||
| 제품 | `herb_items` | herb_item_id | `insurance_code` | ~30 | 휴먼갈근 (062401050) |
|
|
||||||
| 로트 | `inventory_lots` | lot_id | receipt_line_id | ~30 | 갈근.각 (한국, ₩17) |
|
|
||||||
|
|
||||||
### herb_items 컬럼 역할
|
|
||||||
|
|
||||||
| 컬럼 | 실제 내용 | 예시 |
|
|
||||||
|------|----------|------|
|
|
||||||
| `herb_name` | 제품명 (품명) | "휴먼갈근" |
|
|
||||||
| `insurance_code` | 보험코드 (=product_code) | "062401050" |
|
|
||||||
| `ingredient_code` | 성분코드 (herb_masters FK) | "3002H1AHM" |
|
|
||||||
| `specification` | 제조사명 | "주식회사휴먼허브" |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 로트 세부 분류 (display_name + lot_variants)
|
|
||||||
|
|
||||||
### 2-1. `inventory_lots.display_name`
|
|
||||||
|
|
||||||
**엑셀 입고 시 NULL로 들어감**. AI가 쇼핑몰/카탈로그 정보를 참고하여 사후에 채워넣는 값.
|
|
||||||
|
|
||||||
도매상 카탈로그의 실제 품명으로, 같은 제품(herb_items)이라도 로트마다 다를 수 있다:
|
|
||||||
|
|
||||||
| herb_name (제품) | display_name (로트) | 차이점 |
|
|
||||||
|-----------------|-------------------|--------|
|
|
||||||
| 휴먼건강 | 건강 | 페루산, ₩12/g |
|
|
||||||
| 휴먼건강 | 건강.土 | 한국산(토종), ₩51/g |
|
|
||||||
| 휴먼일당귀 | 일당귀(한국산) | 한국산, ₩19/g |
|
|
||||||
| 휴먼일당귀 | 일당귀.中(1kg) | 중국산 중품, ₩13/g |
|
|
||||||
|
|
||||||
### 2-2. `lot_variants` 테이블 (파싱 결과)
|
|
||||||
|
|
||||||
`display_name`을 구조화된 필드로 파싱한 결과를 저장:
|
|
||||||
|
|
||||||
| 컬럼 | 용도 | 파싱 예시 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `raw_name` | 원본 품명 | "건강.土" |
|
|
||||||
| `form` | 형태 | 각(角), 片(편), 절편, 통 |
|
|
||||||
| `processing` | 포제/가공 | 초(炒), 자(炙), 酒炙, 9증 |
|
|
||||||
| `selection_state` | 선별/원산 | 土(토종), 正, 中, 재배, 야생 |
|
|
||||||
| `grade` | 등급 | 1호, 특, 名品, 소(小) |
|
|
||||||
| `age_years` | 연근 | 4, 6 (년근) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. display_name 명명 패턴 (도매상 기준)
|
|
||||||
|
|
||||||
### 기본 구조
|
|
||||||
```
|
|
||||||
약재명.형태[등급](포장단위)
|
|
||||||
약재명.선별<유통경로>(포장단위)[비고]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 실제 패턴 분석 (30개 로트)
|
|
||||||
|
|
||||||
| display_name | 약재 | form | processing | selection | grade |
|
|
||||||
|-------------|------|------|-----------|-----------|-------|
|
|
||||||
| `갈근.각` | 갈근 | 각(角) | - | - | - |
|
|
||||||
| `감초.1호[야생](1kg)` | 감초 | - | - | 야생 | 1호 |
|
|
||||||
| `건강` | 건강 | - | - | - | - |
|
|
||||||
| `건강.土` | 건강 | - | - | 土(토종) | - |
|
|
||||||
| `길경.片[특]` | 길경 | 片(편) | - | - | 특 |
|
|
||||||
| `세신.中` | 세신 | - | - | 中(중품) | - |
|
|
||||||
| `백출.당[1kg]` | 백출 | - | - | 당(當) | - |
|
|
||||||
| `작약주자.土[酒炙]` | 작약주자 | - | 酒炙 | 土(토종) | - |
|
|
||||||
| `숙지황(9증)(신흥.1kg)[완]` | 숙지황 | - | 9증 | - | 완 |
|
|
||||||
| `육계.YB` | 육계 | - | - | YB | - |
|
|
||||||
| `진피.비열[非熱](1kg)` | 진피 | - | 非熱(비열) | - | - |
|
|
||||||
| `창출[북창술.재배](1kg)` | 창출 | - | - | 재배 | - |
|
|
||||||
| `천궁.일<토매지>(1kg)` | 천궁 | - | - | 일(日) | - |
|
|
||||||
| `황기(직절.小)(1kg)` | 황기 | 직절 | - | - | 小 |
|
|
||||||
| `용안육.名品(1kg)` | 용안육 | - | - | - | 名品 |
|
|
||||||
| `오미자<토매지>(1kg)` | 오미자 | - | - | - | - |
|
|
||||||
| `전호[재배]` | 전호 | - | - | 재배 | - |
|
|
||||||
| `지황.건[회](1kg)` | 지황 | - | 건(乾) | 회(灰) | - |
|
|
||||||
|
|
||||||
### 구분자 규칙
|
|
||||||
|
|
||||||
| 구분자 | 의미 | 예시 |
|
|
||||||
|--------|------|------|
|
|
||||||
| `.` | 주 속성 구분 | `건강.土` → 토종 |
|
|
||||||
| `[...]` | 부가 정보/등급 | `길경.片[특]` → 특등 |
|
|
||||||
| `(...)` | 포장/가공/산지 | `대추(절편)(1kg)` |
|
|
||||||
| `<...>` | 유통경로 | `오미자<토매지>(1kg)` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. AI가 display_name / lot_variants를 채우는 절차
|
|
||||||
|
|
||||||
### 언제 실행하는가
|
|
||||||
|
|
||||||
1. 엑셀 입고 완료 후 (`inventory_lots.display_name = NULL`)
|
|
||||||
2. 도매상 쇼핑몰에서 해당 품목 정보를 AI에게 제공
|
|
||||||
3. AI가 정보를 파싱하여 `display_name` + `lot_variants` 업데이트
|
|
||||||
|
|
||||||
### Step 1: NULL인 로트 확인
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT il.lot_id, h.herb_name, h.insurance_code, il.origin_country,
|
|
||||||
il.unit_price_per_g, il.quantity_received
|
|
||||||
FROM inventory_lots il
|
|
||||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
|
||||||
WHERE il.display_name IS NULL AND il.is_depleted = 0;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: 쇼핑몰/카탈로그 정보 참고
|
|
||||||
|
|
||||||
사용자가 도매상 쇼핑몰에서 해당 제품의 상세 품명을 제공하면,
|
|
||||||
AI가 **가격, 단가, 원산지, 포장단위** 등을 교차 참고하여 올바른 로트에 매칭.
|
|
||||||
|
|
||||||
참고 가능한 매칭 단서:
|
|
||||||
- **보험코드** (insurance_code) — 제품 특정
|
|
||||||
- **원산지** (origin_country) — 같은 제품의 로트 구분
|
|
||||||
- **단가** (unit_price_per_g) — 등급/선별 구분 (土 > 일반, 한국산 > 중국산)
|
|
||||||
- **수량** (quantity_received) — 포장 단위 매칭
|
|
||||||
|
|
||||||
### Step 3: display_name 업데이트
|
|
||||||
|
|
||||||
```sql
|
|
||||||
UPDATE inventory_lots
|
|
||||||
SET display_name = '건강.土'
|
|
||||||
WHERE lot_id = 193;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: lot_variants 파싱 결과 저장
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO lot_variants (lot_id, raw_name, form, processing, selection_state, grade, parsed_method)
|
|
||||||
VALUES (193, '건강.土', NULL, NULL, '土', NULL, 'ai_parsing');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: 검증
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT il.lot_id, il.display_name, h.herb_name, il.origin_country,
|
|
||||||
lv.form, lv.processing, lv.selection_state, lv.grade
|
|
||||||
FROM inventory_lots il
|
|
||||||
JOIN herb_items h ON il.herb_item_id = h.herb_item_id
|
|
||||||
LEFT JOIN lot_variants lv ON il.lot_id = lv.lot_id
|
|
||||||
WHERE il.lot_id = 193;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 활용처
|
|
||||||
|
|
||||||
| 화면 | 사용 값 | 표시 예시 |
|
|
||||||
|------|---------|----------|
|
|
||||||
| 입고장 상세 | `display_name` | 갈근.각, 건강.土 |
|
|
||||||
| 재고 상세 모달 | `display_name` + `origin_country` | 건강.土 (한국) |
|
|
||||||
| 조제 시 로트 선택 | `display_name` + `unit_price` | 건강.土 ₩51/g vs 건강 ₩12/g |
|
|
||||||
| 재고 원장 | `display_name` | 입출고 이력에 로트 구분 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 주의사항
|
|
||||||
|
|
||||||
1. **엑셀 입고 로직은 수정하지 않는다** — `display_name`은 항상 NULL로 입고
|
|
||||||
2. **AI가 사후에 채운다** — 쇼핑몰 정보 기반, `parsed_method = 'ai_parsing'`
|
|
||||||
3. **같은 herb_item_id에 여러 display_name 가능** — 로트마다 다른 실물이므로 정상
|
|
||||||
4. **lot_variants 파싱이 안 되어도 display_name만으로 구분 가능** — 파싱은 선택적
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*이 문서는 kdrug 시스템의 약재 3단계 분류 체계와 AI 기반 로트 분류 절차를 정의합니다.*
|
|
||||||
*최종 수정: 2026-02-18*
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
운영 데이터 초기화 스크립트
|
|
||||||
- 마스터 데이터는 보존
|
|
||||||
- 운영/거래 데이터만 삭제
|
|
||||||
- prescription_rules 중복 정리
|
|
||||||
|
|
||||||
실행: python3 scripts/reset_operational_data.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database', 'kdrug.db')
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 보존할 마스터 테이블 (절대 건드리지 않음)
|
|
||||||
# ============================================================
|
|
||||||
MASTER_TABLES = [
|
|
||||||
'herb_masters', # 454 - 급여 한약재 성분코드 마스터
|
|
||||||
'herb_master_extended', # 454 - 약재 확장 정보 (성미귀경, 효능)
|
|
||||||
'herb_products', # 53,769 - 보험 제품 목록
|
|
||||||
'product_companies', # 128 - 제조/유통사
|
|
||||||
'official_formulas', # 100 - 100처방 원방 마스터
|
|
||||||
'official_formula_ingredients', # 68 - 100처방 구성 약재
|
|
||||||
'herb_efficacy_tags', # 18 - 효능 태그 정의
|
|
||||||
'herb_item_tags', # 22 - 약재-태그 매핑
|
|
||||||
'survey_templates', # 56 - 설문 템플릿
|
|
||||||
]
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 삭제할 운영 데이터 테이블 (FK 순서 고려 — 자식 먼저)
|
|
||||||
# ============================================================
|
|
||||||
CLEAR_TABLES = [
|
|
||||||
# 조제/판매 하위
|
|
||||||
'compound_consumptions',
|
|
||||||
'compound_ingredients',
|
|
||||||
'sales_status_history',
|
|
||||||
'sales_transactions',
|
|
||||||
'mileage_transactions',
|
|
||||||
|
|
||||||
# 조제 마스터
|
|
||||||
'compounds',
|
|
||||||
|
|
||||||
# 재고 하위
|
|
||||||
'stock_ledger',
|
|
||||||
'stock_adjustment_details',
|
|
||||||
'stock_adjustments',
|
|
||||||
'lot_variants',
|
|
||||||
'inventory_lots',
|
|
||||||
'inventory_lots_v2',
|
|
||||||
|
|
||||||
# 입고 하위
|
|
||||||
'purchase_receipt_lines',
|
|
||||||
'purchase_receipts',
|
|
||||||
|
|
||||||
# 처방
|
|
||||||
'formula_ingredients',
|
|
||||||
'formula_ingredients_backup',
|
|
||||||
'formulas',
|
|
||||||
'price_policies',
|
|
||||||
|
|
||||||
# 환자/설문
|
|
||||||
'survey_responses',
|
|
||||||
'survey_progress',
|
|
||||||
'patient_surveys',
|
|
||||||
'patients',
|
|
||||||
|
|
||||||
# 도매상/약재
|
|
||||||
'supplier_product_catalog',
|
|
||||||
'suppliers',
|
|
||||||
'herb_items',
|
|
||||||
|
|
||||||
# 규칙/로그 (재정비)
|
|
||||||
'prescription_rules',
|
|
||||||
'data_update_logs',
|
|
||||||
'disease_herb_mapping',
|
|
||||||
'herb_research_papers',
|
|
||||||
'herb_safety_info',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def reset_db():
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
conn.execute("PRAGMA foreign_keys = OFF")
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
print(f"DB: {DB_PATH}")
|
|
||||||
print(f"시각: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 1. 마스터 테이블 행 수 확인 (보존 확인)
|
|
||||||
print("=" * 50)
|
|
||||||
print("보존 대상 마스터 테이블")
|
|
||||||
print("=" * 50)
|
|
||||||
for table in MASTER_TABLES:
|
|
||||||
try:
|
|
||||||
cursor.execute(f"SELECT COUNT(*) FROM [{table}]")
|
|
||||||
cnt = cursor.fetchone()[0]
|
|
||||||
print(f" ✓ {table}: {cnt}행 (보존)")
|
|
||||||
except:
|
|
||||||
print(f" - {table}: 테이블 없음 (skip)")
|
|
||||||
|
|
||||||
# 2. 운영 테이블 삭제
|
|
||||||
print()
|
|
||||||
print("=" * 50)
|
|
||||||
print("초기화 대상 운영 테이블")
|
|
||||||
print("=" * 50)
|
|
||||||
for table in CLEAR_TABLES:
|
|
||||||
try:
|
|
||||||
cursor.execute(f"SELECT COUNT(*) FROM [{table}]")
|
|
||||||
before = cursor.fetchone()[0]
|
|
||||||
cursor.execute(f"DELETE FROM [{table}]")
|
|
||||||
# AUTOINCREMENT 리셋
|
|
||||||
cursor.execute(f"DELETE FROM sqlite_sequence WHERE name = ?", (table,))
|
|
||||||
print(f" ✗ {table}: {before}행 → 0행")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" - {table}: {e}")
|
|
||||||
|
|
||||||
# 3. prescription_rules 중복 제거 후 재삽입
|
|
||||||
print()
|
|
||||||
print("=" * 50)
|
|
||||||
print("prescription_rules 정리 (중복 제거)")
|
|
||||||
print("=" * 50)
|
|
||||||
rules = [
|
|
||||||
(298, 438, '상수', '두 약재가 함께 사용되면 보기 효과가 증강됨 (인삼+황기)', 0, 0),
|
|
||||||
(73, 358, '상수', '혈액순환 개선 효과가 증강됨 (당귀+천궁)', 0, 0),
|
|
||||||
(123, 193, '상사', '생강이 반하의 독성을 감소시킴', 0, 0),
|
|
||||||
(7, 6, '상반', '십팔반(十八反) - 함께 사용 금기', 5, 1),
|
|
||||||
(298, 252, '상반', '십구외(十九畏) - 함께 사용 주의', 4, 0),
|
|
||||||
]
|
|
||||||
for r in rules:
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO prescription_rules (herb1_id, herb2_id, relationship_type, description, severity_level, is_absolute)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
""", r)
|
|
||||||
print(f" ✓ {len(rules)}개 규칙 재삽입 (중복 제거)")
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.execute("PRAGMA foreign_keys = ON")
|
|
||||||
|
|
||||||
# 4. VACUUM
|
|
||||||
conn.execute("VACUUM")
|
|
||||||
print()
|
|
||||||
print("✓ VACUUM 완료")
|
|
||||||
|
|
||||||
# 5. 최종 확인
|
|
||||||
print()
|
|
||||||
print("=" * 50)
|
|
||||||
print("최종 상태")
|
|
||||||
print("=" * 50)
|
|
||||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
table = row[0]
|
|
||||||
cursor.execute(f"SELECT COUNT(*) FROM [{table}]")
|
|
||||||
cnt = cursor.fetchone()[0]
|
|
||||||
marker = "★" if cnt > 0 else " "
|
|
||||||
print(f" {marker} {table}: {cnt}행")
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
print()
|
|
||||||
print("초기화 완료!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
confirm = input("운영 데이터를 모두 초기화합니다. 계속하시겠습니까? (yes/no): ")
|
|
||||||
if confirm.strip().lower() == 'yes':
|
|
||||||
reset_db()
|
|
||||||
else:
|
|
||||||
print("취소되었습니다.")
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
백업 DB 복원 스크립트
|
|
||||||
- 백업 파일에서 운영 DB로 복원
|
|
||||||
|
|
||||||
실행: python3 scripts/restore_backup.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import glob
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
DB_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database')
|
|
||||||
DB_PATH = os.path.join(DB_DIR, 'kdrug.db')
|
|
||||||
|
|
||||||
|
|
||||||
def list_backups():
|
|
||||||
"""사용 가능한 백업 파일 목록"""
|
|
||||||
pattern = os.path.join(DB_DIR, 'kdrug_backup*.db')
|
|
||||||
backups = sorted(glob.glob(pattern), key=os.path.getmtime, reverse=True)
|
|
||||||
return backups
|
|
||||||
|
|
||||||
|
|
||||||
def restore():
|
|
||||||
backups = list_backups()
|
|
||||||
|
|
||||||
if not backups:
|
|
||||||
print("사용 가능한 백업 파일이 없습니다.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("=" * 50)
|
|
||||||
print("사용 가능한 백업 파일")
|
|
||||||
print("=" * 50)
|
|
||||||
for i, path in enumerate(backups):
|
|
||||||
size_mb = os.path.getsize(path) / (1024 * 1024)
|
|
||||||
mtime = datetime.fromtimestamp(os.path.getmtime(path)).strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
name = os.path.basename(path)
|
|
||||||
print(f" [{i + 1}] {name} ({size_mb:.1f}MB, {mtime})")
|
|
||||||
|
|
||||||
print()
|
|
||||||
choice = input(f"복원할 백업 번호를 선택하세요 (1-{len(backups)}): ").strip()
|
|
||||||
|
|
||||||
try:
|
|
||||||
idx = int(choice) - 1
|
|
||||||
if idx < 0 or idx >= len(backups):
|
|
||||||
print("잘못된 번호입니다.")
|
|
||||||
return
|
|
||||||
except ValueError:
|
|
||||||
print("숫자를 입력하세요.")
|
|
||||||
return
|
|
||||||
|
|
||||||
selected = backups[idx]
|
|
||||||
print()
|
|
||||||
print(f"선택: {os.path.basename(selected)}")
|
|
||||||
confirm = input("현재 DB를 덮어쓰고 복원합니다. 계속하시겠습니까? (yes/no): ").strip().lower()
|
|
||||||
|
|
||||||
if confirm != 'yes':
|
|
||||||
print("취소되었습니다.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 현재 DB를 복원 전 백업
|
|
||||||
pre_restore = os.path.join(DB_DIR, f"kdrug_pre_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db")
|
|
||||||
shutil.copy2(DB_PATH, pre_restore)
|
|
||||||
print(f" 복원 전 현재 DB 백업 → {os.path.basename(pre_restore)}")
|
|
||||||
|
|
||||||
# 복원
|
|
||||||
shutil.copy2(selected, DB_PATH)
|
|
||||||
print(f" {os.path.basename(selected)} → kdrug.db 복원 완료")
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("복원 완료! 앱을 재시작하세요.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
restore()
|
|
||||||
@ -590,18 +590,11 @@ $(document).ready(function() {
|
|||||||
tbody.empty();
|
tbody.empty();
|
||||||
|
|
||||||
response.data.forEach(formula => {
|
response.data.forEach(formula => {
|
||||||
// 100처방 매칭: 1차 official_formula_id FK, 2차 이름 매칭 (원방명이 내 처방명에 포함)
|
// 100처방 매칭: 정확 매칭 우선, 없으면 내 처방명이 100처방명으로 시작하는지 확인
|
||||||
let officialNum = null;
|
let officialNum = officialNames.get(formula.formula_name);
|
||||||
if (formula.official_formula_id) {
|
|
||||||
// FK로 연결된 경우 — official_name은 API에서 JOIN으로 내려옴
|
|
||||||
officialNum = officialNames.get(formula.official_name);
|
|
||||||
}
|
|
||||||
if (officialNum == null) {
|
|
||||||
officialNum = officialNames.get(formula.formula_name);
|
|
||||||
}
|
|
||||||
if (officialNum == null) {
|
if (officialNum == null) {
|
||||||
for (const [name, num] of officialNames) {
|
for (const [name, num] of officialNames) {
|
||||||
if (formula.formula_name.includes(name)) {
|
if (formula.formula_name.startsWith(name)) {
|
||||||
officialNum = num;
|
officialNum = num;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -704,6 +697,13 @@ $(document).ready(function() {
|
|||||||
function loadOfficialFormulas(search) {
|
function loadOfficialFormulas(search) {
|
||||||
const params = search ? `?search=${encodeURIComponent(search)}` : '';
|
const params = search ? `?search=${encodeURIComponent(search)}` : '';
|
||||||
|
|
||||||
|
// 내 처방 이름 목록을 API에서 가져와서 비교
|
||||||
|
$.get('/api/formulas', function(formulasRes) {
|
||||||
|
const myFormulaNames = new Set();
|
||||||
|
if (formulasRes.success) {
|
||||||
|
formulasRes.data.forEach(f => myFormulaNames.add(f.formula_name));
|
||||||
|
}
|
||||||
|
|
||||||
$.get(`/api/official-formulas${params}`, function(response) {
|
$.get(`/api/official-formulas${params}`, function(response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
const tbody = $('#officialFormulasList');
|
const tbody = $('#officialFormulasList');
|
||||||
@ -712,24 +712,15 @@ $(document).ready(function() {
|
|||||||
$('#officialFormulaCount').text(response.data.length);
|
$('#officialFormulaCount').text(response.data.length);
|
||||||
|
|
||||||
response.data.forEach(formula => {
|
response.data.forEach(formula => {
|
||||||
// 등록 여부: 백엔드에서 판정 (official_formula_id FK + 이름 fallback)
|
// 등록 여부: 정확 매칭 또는 내 처방명이 100처방명으로 시작
|
||||||
const isRegistered = formula.is_registered;
|
const isRegistered = myFormulaNames.has(formula.formula_name)
|
||||||
let statusBadge;
|
|| [...myFormulaNames].some(name => name.startsWith(formula.formula_name));
|
||||||
if (isRegistered && formula.registered_names && formula.registered_names.length > 0) {
|
const statusBadge = isRegistered
|
||||||
const names = formula.registered_names.map(n => n.length > 12 ? n.substring(0, 12) + '…' : n).join(', ');
|
? '<span class="badge bg-success">등록됨</span>'
|
||||||
statusBadge = `<span class="badge bg-success" title="${formula.registered_names.join(', ')}">${formula.registered_names.length > 1 ? formula.registered_names.length + '개 등록' : '등록됨'}</span>`;
|
: '<span class="badge bg-outline-secondary text-muted">미등록</span>';
|
||||||
} else {
|
|
||||||
statusBadge = '<span class="badge bg-outline-secondary text-muted">미등록</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasNotes = formula.reference_notes ? '<i class="bi bi-journal-text text-info ms-1" title="참고자료 있음"></i>' : '';
|
const hasNotes = formula.reference_notes ? '<i class="bi bi-journal-text text-info ms-1" title="참고자료 있음"></i>' : '';
|
||||||
|
|
||||||
// 구성 약재 수 표시
|
|
||||||
const ingCount = formula.ingredient_count || 0;
|
|
||||||
const ingBadge = ingCount > 0
|
|
||||||
? `<span class="badge bg-success bg-opacity-75">${ingCount}종</span>`
|
|
||||||
: `<span class="badge bg-light text-muted">미입력</span>`;
|
|
||||||
|
|
||||||
tbody.append(`
|
tbody.append(`
|
||||||
<tr class="official-formula-row" style="cursor:pointer"
|
<tr class="official-formula-row" style="cursor:pointer"
|
||||||
data-id="${formula.official_formula_id}"
|
data-id="${formula.official_formula_id}"
|
||||||
@ -743,17 +734,17 @@ $(document).ready(function() {
|
|||||||
<td><strong>${formula.formula_name}</strong>${hasNotes}</td>
|
<td><strong>${formula.formula_name}</strong>${hasNotes}</td>
|
||||||
<td class="text-muted">${formula.formula_name_hanja || '-'}</td>
|
<td class="text-muted">${formula.formula_name_hanja || '-'}</td>
|
||||||
<td>${formula.source_text || '-'}</td>
|
<td>${formula.source_text || '-'}</td>
|
||||||
<td class="text-center">${ingBadge}</td>
|
|
||||||
<td class="text-center">${statusBadge}</td>
|
<td class="text-center">${statusBadge}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.length === 0) {
|
if (response.data.length === 0) {
|
||||||
tbody.html('<tr><td colspan="6" class="text-center text-muted">검색 결과가 없습니다.</td></tr>');
|
tbody.html('<tr><td colspan="5" class="text-center text-muted">검색 결과가 없습니다.</td></tr>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}); // /api/formulas 콜백 닫기
|
||||||
}
|
}
|
||||||
|
|
||||||
// 100처방 검색 이벤트
|
// 100처방 검색 이벤트
|
||||||
|
|||||||
@ -403,7 +403,6 @@
|
|||||||
<th>처방명</th>
|
<th>처방명</th>
|
||||||
<th>한자명</th>
|
<th>한자명</th>
|
||||||
<th>출전</th>
|
<th>출전</th>
|
||||||
<th width="80">구성약재</th>
|
|
||||||
<th width="80">상태</th>
|
<th width="80">상태</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user