Compare commits

..

No commits in common. "2a14af59c322a3447b10037f55c9531d7fc1cea2" and "40be340a63f7fff87e9823b212084c0324abd60a" have entirely different histories.

10 changed files with 53 additions and 1336 deletions

View File

@ -1,48 +0,0 @@
# Flask 프로세스 관리 가이드
## 🚨 문제 상황
- Flask Debug 모드로 실행 시 코드 변경 시마다 자동 재시작
- 여러 개의 백그라운드 프로세스가 누적되어 실행됨
- 동일한 포트(5001)를 여러 프로세스가 점유하려고 시도
## ✅ 해결 방법
### 1. 즉시 해결 명령어
```bash
# 한 줄로 모든 Flask 프로세스 종료 및 재시작
lsof -ti:5001 | xargs -r kill -9 && sleep 2 && source venv/bin/activate && python app.py
```
### 2. run_server.sh 스크립트 사용 (이미 생성됨)
```bash
./run_server.sh
```
### 3. 근본적 해결책
```python
# app.py 수정 - Debug 모드 끄기
app.run(debug=False, host='0.0.0.0', port=5001)
```
## 📝 프로세스 확인 명령어
```bash
# 포트 5001 사용 중인 프로세스 확인
lsof -i:5001
# 모든 Python 프로세스 확인
ps aux | grep python
# 특정 포트 프로세스만 종료
lsof -ti:5001 | xargs -r kill -9
```
## ⚠️ 주의사항
- Debug 모드는 개발 중에만 사용
- Production에서는 반드시 debug=False로 설정
- 코드 변경이 빈번할 때는 수동으로 재시작 권장
## 🔄 자동화 스크립트
run_server.sh가 이미 생성되어 있음:
- 기존 프로세스 자동 종료
- 새 프로세스 시작
- 단일 프로세스만 실행 보장

View File

@ -1,76 +0,0 @@
# Git 사용 가이드라인
## ⚠️ 중요한 주의사항
### 1. Git 초기화 금지
- **절대로 `git init`를 실행하지 말 것**
- 이 프로젝트는 이미 Gitea 서버에 연결되어 있음
- 원격 저장소: origin (Gitea 서버)
### 2. 커밋 전 확인 사항
- 항상 `git status`로 현재 상태 확인
- `git log --oneline -n 5`로 최근 커밋 이력 확인
- `git remote -v`로 원격 저장소 확인
### 3. 커밋 시 규칙
```bash
# 상태 확인
git status
# 변경사항 확인
git diff
# 논리적 단위로 나누어서 커밋
git add [파일명]
git commit -m "커밋 메시지"
# 원격 저장소에 푸시
git push origin main
```
### 4. 커밋 메시지 작성 규칙
- **기능 추가**: `feat: 효능 태그 시스템 추가`
- **버그 수정**: `fix: 총금액 표시 오류 수정`
- **문서 작성**: `docs: README 업데이트`
- **리팩토링**: `refactor: 코드 구조 개선`
- **스타일**: `style: 코드 포맷팅`
- **테스트**: `test: 단위 테스트 추가`
- **기타**: `chore: 빌드 스크립트 수정`
### 5. 파일 관리
- 테스트 파일들은 `.gitignore`에 추가 고려
- 업로드 폴더는 커밋하지 않기 (uploads/)
- 데이터베이스 파일은 주의해서 관리
### 6. 브랜치 전략
- 현재 main 브랜치 사용 중
- 큰 기능은 별도 브랜치 생성 고려
```bash
# 브랜치 생성 및 체크아웃
git checkout -b feature/기능명
# 작업 후 main으로 머지
git checkout main
git merge feature/기능명
```
### 7. 실수 방지
- `git push --force`는 절대 사용 금지
- `git reset --hard`는 신중하게 사용
- 작업 전 `git pull` 실행 습관화
## 현재 프로젝트 상태
- Repository: kdrug (한약재 재고관리 시스템)
- Branch: main
- Remote: origin (Gitea 서버)
## 체크리스트
- [ ] git status 확인
- [ ] 논리적 단위로 파일 그룹화
- [ ] 의미있는 커밋 메시지 작성
- [ ] 불필요한 파일 제외 확인
- [ ] push 전 최종 검토
---
작성일: 2026-02-15
작성자: Claude Assistant

View File

@ -1,82 +0,0 @@
# 원산지별 재고 관리 및 조제 시 선택 기능
## 🎯 완료된 기능 (2026-02-15)
### 1. 원산지별 재고 구분 표시
- **재고 현황 API 개선** (`/api/inventory/summary`)
- `origin_count`: 원산지 개수
- `min_price`, `max_price`: 가격 범위
- 평균 가격 표시
### 2. 재고 상세 API 추가 (`/api/inventory/detail/<herb_id>`)
- 원산지별로 그룹화된 재고 정보
- 각 원산지별 로트 목록
- 평균 단가, 재고 수량, 재고 가치
### 3. UI 개선
- 재고 목록에서 원산지 개수 배지 표시
- 가격 범위 표시 (여러 원산지일 경우)
- 클릭 시 상세 모달 표시
- 원산지별 재고 카드형 UI
## 📌 실제 사례: 건강(乾薑) 약재
```
페루산: 5,000g @ 12.4원/g (총 62,000원)
한국산: 1,500g @ 51.4원/g (총 77,100원)
```
## 🔄 조제 시 원산지 선택 기능 (설계)
### 1. 데이터베이스 스키마 변경
```sql
-- compound_details 테이블에 로트 정보 추가
ALTER TABLE compound_details ADD COLUMN lot_id INTEGER;
ALTER TABLE compound_details ADD FOREIGN KEY (lot_id) REFERENCES inventory_lots(lot_id);
```
### 2. 조제 프로세스 개선
1. **처방 선택 후 약재 목록 표시**
- 각 약재에 원산지 선택 옵션 제공
- 기본값: 가장 저렴한 원산지
- 선택 가능: 고품질(비싼) 원산지
2. **원산지 선택 UI**
```javascript
// 예시: 건강 30g 조제 시
{
herb: "건강",
required: 30,
options: [
{origin: "페루", price: 12.4, available: 5000},
{origin: "한국", price: 51.4, available: 1500}
]
}
```
3. **가격 차이 실시간 표시**
- 기본 선택: 페루산 30g = 372원
- 프리미엄 선택: 한국산 30g = 1,542원
- 차이: +1,170원
### 3. API 엔드포인트 추가
- `GET /api/herbs/<id>/lots` - 약재별 가용 로트 목록
- `POST /api/compounds/calculate` - 원산지별 가격 계산
- `POST /api/compounds/dispense` - 원산지 지정 조제
### 4. FIFO 변형 전략
- **기본**: 저렴한 원산지 우선 (Cost-Optimized FIFO)
- **선택**: 특정 원산지 지정 가능
- **혼합**: 일부는 저렴, 일부는 고품질
## 💡 비즈니스 가치
1. **환자 선택권**: 가격 vs 품질 선택 가능
2. **투명성**: 원산지별 가격 명시
3. **재고 관리**: 원산지별 재고 추적
4. **수익성**: 프리미엄 옵션 제공
## 🚀 구현 우선순위
1. ✅ 원산지별 재고 표시 (완료)
2. ✅ 재고 상세 모달 (완료)
3. ⏳ 조제 시 원산지 선택 UI
4. ⏳ 원산지별 가격 계산
5. ⏳ 조제 이력에 원산지 기록

View File

@ -1,135 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
누락된 약재 추가 스크립트
"""
import sqlite3
def add_missing_herbs():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 누락된 약재 추가
missing_herbs = [
('A001100', '당귀', '보혈'), # 보혈약
('A001200', '백작약', '보혈'), # 보혈약
('A001300', '인삼', '대보원기'), # 대보원기약
('A001400', '생강', '온중'), # 온중약
]
print("=== 누락된 약재 추가 ===")
for code, name, efficacy in missing_herbs:
# 약재가 이미 있는지 확인
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (name,))
if cursor.fetchone():
print(f" ⚠️ {name} - 이미 존재함")
continue
# 약재 추가
cursor.execute("""
INSERT INTO herb_items (insurance_code, herb_name, specification, default_unit, is_active)
VALUES (?, ?, ?, 'g', 1)
""", (code, name, ''))
herb_id = cursor.lastrowid
print(f"{name} (ID: {herb_id}) 추가 완료")
# 효능 태그 연결
cursor.execute("SELECT tag_id FROM herb_efficacy_tags WHERE tag_name = ?", (efficacy,))
tag_result = cursor.fetchone()
if tag_result:
tag_id = tag_result[0]
cursor.execute("""
INSERT OR IGNORE INTO herb_item_tags (herb_item_id, tag_id)
VALUES (?, ?)
""", (herb_id, tag_id))
print(f" → 효능 '{efficacy}' 연결")
conn.commit()
# 쌍화탕 처방 재구성 시도
print("\n=== 쌍화탕 처방 재구성 ===")
# 쌍화탕 처방 ID 가져오기
cursor.execute("SELECT formula_id FROM formulas WHERE formula_name = '쌍화탕'")
formula_result = cursor.fetchone()
if formula_result:
formula_id = formula_result[0]
# 현재 없는 약재들 다시 추가 시도
missing_ingredients = [
('당귀', 6.0, '보혈'),
('백작약', 6.0, '보혈'),
('인삼', 4.0, '대보원기'),
('생강', 5.0, '온중'),
]
for herb_name, grams, notes in missing_ingredients:
# 이미 등록되었는지 확인
cursor.execute("""
SELECT fi.ingredient_id
FROM formula_ingredients fi
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
WHERE fi.formula_id = ? AND h.herb_name = ?
""", (formula_id, herb_name))
if cursor.fetchone():
print(f" ⚠️ {herb_name} - 이미 처방에 포함됨")
continue
# 약재 ID 찾기
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (herb_name,))
herb_result = cursor.fetchone()
if herb_result:
herb_id = herb_result[0]
cursor.execute("""
INSERT INTO formula_ingredients (
formula_id, herb_item_id, grams_per_cheop, notes
) VALUES (?, ?, ?, ?)
""", (formula_id, herb_id, grams, notes))
print(f"{herb_name}: {grams}g ({notes}) 추가")
conn.commit()
# 최종 쌍화탕 구성 확인
print("\n=== 최종 쌍화탕 구성 확인 ===")
cursor.execute("""
SELECT
h.herb_name,
fi.grams_per_cheop,
fi.notes,
GROUP_CONCAT(et.tag_name) as efficacy_tags
FROM formula_ingredients fi
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
LEFT JOIN herb_item_tags hit ON h.herb_item_id = hit.herb_item_id
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
WHERE fi.formula_id = (SELECT formula_id FROM formulas WHERE formula_name = '쌍화탕')
GROUP BY h.herb_name, fi.grams_per_cheop, fi.notes
ORDER BY fi.grams_per_cheop DESC
""")
total_grams = 0
for herb_name, grams, notes, tags in cursor.fetchall():
total_grams += grams
tags_display = f" [{tags}]" if tags else ""
print(f" - {herb_name}: {grams}g ({notes}){tags_display}")
print(f"\n 📊 총 {cursor.rowcount}종 약재")
print(f" 💊 1첩 총량: {total_grams}g")
print(f" 📦 20첩 총량: {total_grams * 20}g")
print("\n✅ 약재 추가 및 처방 재구성 완료!")
except Exception as e:
print(f"❌ 오류 발생: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == "__main__":
add_missing_herbs()

320
app.py
View File

@ -125,36 +125,18 @@ def create_patient():
@app.route('/api/herbs', methods=['GET']) @app.route('/api/herbs', methods=['GET'])
def get_herbs(): def get_herbs():
"""약재 목록 조회 (효능 태그 포함)""" """약재 목록 조회"""
try: try:
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
SELECT SELECT h.*, COALESCE(s.total_quantity, 0) as current_stock
h.herb_item_id,
h.insurance_code,
h.herb_name,
h.is_active,
COALESCE(SUM(il.quantity_onhand), 0) as current_stock,
GROUP_CONCAT(DISTINCT et.tag_name) as efficacy_tags
FROM herb_items h FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id LEFT JOIN v_current_stock s ON h.herb_item_id = s.herb_item_id
AND il.is_depleted = 0
LEFT JOIN herb_item_tags hit ON h.herb_item_id = hit.herb_item_id
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
WHERE h.is_active = 1 WHERE h.is_active = 1
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name, h.is_active
ORDER BY h.herb_name ORDER BY h.herb_name
""") """)
herbs = [dict(row) for row in cursor.fetchall()] herbs = [dict(row) for row in cursor.fetchall()]
# 태그를 리스트로 변환
for herb in herbs:
if herb['efficacy_tags']:
herb['efficacy_tags'] = herb['efficacy_tags'].split(',')
else:
herb['efficacy_tags'] = []
return jsonify({'success': True, 'data': herbs}) return jsonify({'success': True, 'data': herbs})
except Exception as e: except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@ -245,53 +227,6 @@ def get_formula_ingredients(formula_id):
except Exception as e: except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 도매상 관리 API ====================
@app.route('/api/suppliers', methods=['GET'])
def get_suppliers():
"""도매상 목록 조회"""
try:
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT supplier_id, name, business_no, phone, address, is_active
FROM suppliers
WHERE is_active = 1
ORDER BY name
""")
suppliers = [dict(row) for row in cursor.fetchall()]
return jsonify({'success': True, 'data': suppliers})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/suppliers', methods=['POST'])
def create_supplier():
"""도매상 등록"""
try:
data = request.json
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO suppliers (name, business_no, contact_person, phone, address)
VALUES (?, ?, ?, ?, ?)
""", (
data['name'],
data.get('business_no'),
data.get('contact_person'),
data.get('phone'),
data.get('address')
))
supplier_id = cursor.lastrowid
return jsonify({
'success': True,
'message': '도매상이 등록되었습니다',
'supplier_id': supplier_id
})
except sqlite3.IntegrityError:
return jsonify({'success': False, 'error': '이미 등록된 도매상입니다'}), 400
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 입고 관리 API ==================== # ==================== 입고 관리 API ====================
@app.route('/api/upload/purchase', methods=['POST']) @app.route('/api/upload/purchase', methods=['POST'])
@ -308,11 +243,6 @@ def upload_purchase_excel():
if not allowed_file(file.filename): if not allowed_file(file.filename):
return jsonify({'success': False, 'error': '허용되지 않는 파일 형식입니다'}), 400 return jsonify({'success': False, 'error': '허용되지 않는 파일 형식입니다'}), 400
# 도매상 ID 가져오기 (폼 데이터에서)
supplier_id = request.form.get('supplier_id')
if not supplier_id:
return jsonify({'success': False, 'error': '도매상을 선택해주세요'}), 400
# 파일 저장 # 파일 저장
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
@ -351,16 +281,21 @@ def upload_purchase_excel():
processed_rows = 0 processed_rows = 0
processed_items = set() processed_items = set()
# 도매상 정보 확인 # 날짜별, 업체별로 그룹화
cursor.execute("SELECT name FROM suppliers WHERE supplier_id = ?", (supplier_id,)) grouped = df.groupby(['receipt_date', 'supplier_name'])
supplier_info = cursor.fetchone()
if not supplier_info:
return jsonify({'success': False, 'error': '유효하지 않은 도매상입니다'}), 400
# 날짜별로 그룹화 (도매상은 이미 선택됨) for (receipt_date, supplier_name), group in grouped:
grouped = df.groupby(['receipt_date']) # 공급업체 확인/생성
cursor.execute("SELECT supplier_id FROM suppliers WHERE name = ?", (supplier_name,))
supplier = cursor.fetchone()
for receipt_date, group in grouped: if not supplier:
cursor.execute("""
INSERT INTO suppliers (name) VALUES (?)
""", (supplier_name,))
supplier_id = cursor.lastrowid
else:
supplier_id = supplier[0]
# 입고장 헤더 생성 # 입고장 헤더 생성
total_amount = group['total_amount'].sum() total_amount = group['total_amount'].sum()
@ -464,13 +399,13 @@ def get_purchase_receipts():
pr.receipt_id, pr.receipt_id,
pr.receipt_date, pr.receipt_date,
pr.receipt_no, pr.receipt_no,
pr.total_amount,
pr.source_file, pr.source_file,
pr.created_at, pr.created_at,
s.name as supplier_name, s.name as supplier_name,
s.supplier_id, s.supplier_id,
COUNT(prl.line_id) as line_count, COUNT(prl.line_id) as line_count,
SUM(prl.quantity_g) as total_quantity, SUM(prl.quantity_g) as total_quantity
SUM(prl.line_total) as total_amount
FROM purchase_receipts pr FROM purchase_receipts pr
JOIN suppliers s ON pr.supplier_id = s.supplier_id JOIN suppliers s ON pr.supplier_id = s.supplier_id
LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id LEFT JOIN purchase_receipt_lines prl ON pr.receipt_id = prl.receipt_id
@ -690,19 +625,7 @@ def delete_purchase_receipt(receipt_id):
'error': f'{usage["used_count"]}개 품목에서 {usage["used_quantity"]}g이 이미 사용되어 삭제할 수 없습니다' 'error': f'{usage["used_count"]}개 품목에서 {usage["used_quantity"]}g이 이미 사용되어 삭제할 수 없습니다'
}), 400 }), 400
# 삭제 순서 중요: 참조하는 테이블부터 삭제 # 재고 로트 삭제
# 1. 재고 원장 기록 삭제 (lot_id를 참조)
cursor.execute("""
DELETE FROM stock_ledger
WHERE lot_id IN (
SELECT lot_id FROM inventory_lots
WHERE receipt_line_id IN (
SELECT line_id FROM purchase_receipt_lines WHERE receipt_id = ?
)
)
""", (receipt_id,))
# 2. 재고 로트 삭제 (receipt_line_id를 참조)
cursor.execute(""" cursor.execute("""
DELETE FROM inventory_lots DELETE FROM inventory_lots
WHERE receipt_line_id IN ( WHERE receipt_line_id IN (
@ -710,10 +633,16 @@ def delete_purchase_receipt(receipt_id):
) )
""", (receipt_id,)) """, (receipt_id,))
# 3. 입고장 라인 삭제 (receipt_id를 참조) # 재고 원장 기록
cursor.execute("""
DELETE FROM stock_ledger
WHERE reference_table = 'purchase_receipts' AND reference_id = ?
""", (receipt_id,))
# 입고장 라인 삭제
cursor.execute("DELETE FROM purchase_receipt_lines WHERE receipt_id = ?", (receipt_id,)) cursor.execute("DELETE FROM purchase_receipt_lines WHERE receipt_id = ?", (receipt_id,))
# 4. 입고장 헤더 삭제 # 입고장 헤더 삭제
cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,)) cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,))
return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'}) return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'})
@ -757,7 +686,6 @@ def create_compound():
for ingredient in data['ingredients']: for ingredient in data['ingredients']:
herb_item_id = ingredient['herb_item_id'] herb_item_id = ingredient['herb_item_id']
total_grams = ingredient['total_grams'] total_grams = ingredient['total_grams']
origin_country = ingredient.get('origin_country') # 원산지 선택 정보
# 조제 약재 구성 기록 # 조제 약재 구성 기록
cursor.execute(""" cursor.execute("""
@ -767,26 +695,14 @@ def create_compound():
""", (compound_id, herb_item_id, """, (compound_id, herb_item_id,
ingredient['grams_per_cheop'], total_grams)) ingredient['grams_per_cheop'], total_grams))
# 재고 차감 (FIFO 방식 - 원산지 지정 시 해당 원산지만) # 재고 차감 (FIFO 방식)
remaining_qty = total_grams remaining_qty = total_grams
cursor.execute("""
# 원산지가 지정된 경우 해당 원산지만, 아니면 전체에서 FIFO SELECT lot_id, quantity_onhand, unit_price_per_g
if origin_country and origin_country != 'auto': FROM inventory_lots
cursor.execute(""" WHERE herb_item_id = ? AND is_depleted = 0 AND quantity_onhand > 0
SELECT lot_id, quantity_onhand, unit_price_per_g ORDER BY received_date, lot_id
FROM inventory_lots """, (herb_item_id,))
WHERE herb_item_id = ? AND is_depleted = 0 AND quantity_onhand > 0
AND origin_country = ?
ORDER BY unit_price_per_g, received_date, lot_id
""", (herb_item_id, origin_country))
else:
# 자동 선택: 가격이 저렴한 것부터
cursor.execute("""
SELECT lot_id, quantity_onhand, unit_price_per_g
FROM inventory_lots
WHERE herb_item_id = ? AND is_depleted = 0 AND quantity_onhand > 0
ORDER BY unit_price_per_g, received_date, lot_id
""", (herb_item_id,))
lots = cursor.fetchall() lots = cursor.fetchall()
@ -845,90 +761,11 @@ def create_compound():
except Exception as e: except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 조제용 재고 조회 API ====================
@app.route('/api/herbs/<int:herb_item_id>/available-lots', methods=['GET'])
def get_available_lots(herb_item_id):
"""조제용 가용 로트 목록 - 원산지별로 그룹화"""
try:
with get_db() as conn:
cursor = conn.cursor()
# 약재 정보
cursor.execute("""
SELECT herb_name, insurance_code
FROM herb_items
WHERE herb_item_id = ?
""", (herb_item_id,))
herb = cursor.fetchone()
if not herb:
return jsonify({'success': False, 'error': '약재를 찾을 수 없습니다'}), 404
# 가용 로트 목록 (소진되지 않은 재고)
cursor.execute("""
SELECT
lot_id,
origin_country,
quantity_onhand,
unit_price_per_g,
received_date,
supplier_id
FROM inventory_lots
WHERE herb_item_id = ?
AND is_depleted = 0
AND quantity_onhand > 0
ORDER BY origin_country, unit_price_per_g, received_date
""", (herb_item_id,))
lots = []
for row in cursor.fetchall():
lots.append({
'lot_id': row[0],
'origin_country': row[1] or '미지정',
'quantity_onhand': row[2],
'unit_price_per_g': row[3],
'received_date': row[4],
'supplier_id': row[5]
})
# 원산지별 요약
origin_summary = {}
for lot in lots:
origin = lot['origin_country']
if origin not in origin_summary:
origin_summary[origin] = {
'origin_country': origin,
'total_quantity': 0,
'min_price': float('inf'),
'max_price': 0,
'lot_count': 0,
'lots': []
}
origin_summary[origin]['total_quantity'] += lot['quantity_onhand']
origin_summary[origin]['min_price'] = min(origin_summary[origin]['min_price'], lot['unit_price_per_g'])
origin_summary[origin]['max_price'] = max(origin_summary[origin]['max_price'], lot['unit_price_per_g'])
origin_summary[origin]['lot_count'] += 1
origin_summary[origin]['lots'].append(lot)
return jsonify({
'success': True,
'data': {
'herb_name': herb[0],
'insurance_code': herb[1],
'origins': list(origin_summary.values()),
'total_quantity': sum(lot['quantity_onhand'] for lot in lots)
}
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 재고 현황 API ==================== # ==================== 재고 현황 API ====================
@app.route('/api/inventory/summary', methods=['GET']) @app.route('/api/inventory/summary', methods=['GET'])
def get_inventory_summary(): def get_inventory_summary():
"""재고 현황 요약 - 원산지별 구분 표시""" """재고 현황 요약"""
try: try:
with get_db() as conn: with get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
@ -939,29 +776,16 @@ def get_inventory_summary():
h.herb_name, h.herb_name,
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity, COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
COUNT(DISTINCT il.lot_id) as lot_count, COUNT(DISTINCT il.lot_id) as lot_count,
COUNT(DISTINCT il.origin_country) as origin_count,
AVG(il.unit_price_per_g) as avg_price, AVG(il.unit_price_per_g) as avg_price,
MIN(il.unit_price_per_g) as min_price, COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
MAX(il.unit_price_per_g) as max_price,
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value,
GROUP_CONCAT(DISTINCT et.tag_name) as efficacy_tags
FROM herb_items h FROM herb_items h
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0 LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
LEFT JOIN herb_item_tags hit ON h.herb_item_id = hit.herb_item_id
LEFT JOIN herb_efficacy_tags et ON hit.tag_id = et.tag_id
GROUP BY h.herb_item_id, h.insurance_code, h.herb_name GROUP BY h.herb_item_id, h.insurance_code, h.herb_name
HAVING total_quantity > 0 HAVING total_quantity > 0
ORDER BY h.herb_name ORDER BY h.herb_name
""") """)
inventory = [dict(row) for row in cursor.fetchall()] inventory = [dict(row) for row in cursor.fetchall()]
# 태그를 리스트로 변환
for item in inventory:
if item['efficacy_tags']:
item['efficacy_tags'] = item['efficacy_tags'].split(',')
else:
item['efficacy_tags'] = []
# 전체 요약 # 전체 요약
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)
@ -977,78 +801,6 @@ def get_inventory_summary():
except Exception as e: except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/inventory/detail/<int:herb_item_id>', methods=['GET'])
def get_inventory_detail(herb_item_id):
"""약재별 재고 상세 - 원산지별로 구분"""
try:
with get_db() as conn:
cursor = conn.cursor()
# 약재 기본 정보
cursor.execute("""
SELECT herb_item_id, insurance_code, herb_name
FROM herb_items
WHERE herb_item_id = ?
""", (herb_item_id,))
herb = cursor.fetchone()
if not herb:
return jsonify({'success': False, 'error': '약재를 찾을 수 없습니다'}), 404
herb_data = dict(herb)
# 원산지별 재고 정보
cursor.execute("""
SELECT
il.lot_id,
il.origin_country,
il.quantity_onhand,
il.unit_price_per_g,
il.received_date,
il.supplier_id,
s.name as supplier_name,
il.quantity_onhand * il.unit_price_per_g as lot_value
FROM inventory_lots il
LEFT JOIN suppliers s ON il.supplier_id = s.supplier_id
WHERE il.herb_item_id = ? AND il.is_depleted = 0
ORDER BY il.origin_country, il.unit_price_per_g, il.received_date
""", (herb_item_id,))
lots = [dict(row) for row in cursor.fetchall()]
# 원산지별 그룹화
by_origin = {}
for lot in lots:
origin = lot['origin_country'] or '미지정'
if origin not in by_origin:
by_origin[origin] = {
'origin_country': origin,
'lots': [],
'total_quantity': 0,
'total_value': 0,
'min_price': float('inf'),
'max_price': 0,
'avg_price': 0
}
by_origin[origin]['lots'].append(lot)
by_origin[origin]['total_quantity'] += lot['quantity_onhand']
by_origin[origin]['total_value'] += lot['lot_value']
by_origin[origin]['min_price'] = min(by_origin[origin]['min_price'], lot['unit_price_per_g'])
by_origin[origin]['max_price'] = max(by_origin[origin]['max_price'], lot['unit_price_per_g'])
# 평균 단가 계산
for origin_data in by_origin.values():
if origin_data['total_quantity'] > 0:
origin_data['avg_price'] = origin_data['total_value'] / origin_data['total_quantity']
herb_data['origins'] = list(by_origin.values())
herb_data['total_origins'] = len(by_origin)
return jsonify({'success': True, 'data': herb_data})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
# 서버 실행 # 서버 실행
if __name__ == '__main__': if __name__ == '__main__':
# 데이터베이스 초기화 # 데이터베이스 초기화

View File

@ -1,158 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
약재 효능 태그 시스템 추가 스크립트
"""
import sqlite3
def check_and_create_efficacy_system():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 1. 현재 테이블 확인
cursor.execute("""
SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name
""")
print("=== 현재 테이블 목록 ===")
for table in cursor.fetchall():
print(f" - {table[0]}")
# 2. herb_items 테이블 스키마 확인
print("\n=== herb_items 테이블 구조 ===")
cursor.execute("PRAGMA table_info(herb_items)")
columns = cursor.fetchall()
for col in columns:
print(f" {col[1]} ({col[2]})")
# 3. 효능 태그 테이블 생성
print("\n=== 효능 태그 시스템 생성 ===")
# 효능 마스터 테이블
cursor.execute("""
CREATE TABLE IF NOT EXISTS herb_efficacy_tags (
tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
tag_name VARCHAR(50) NOT NULL UNIQUE,
tag_category VARCHAR(50), -- (), (), (), ()
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
print("✅ herb_efficacy_tags 테이블 생성")
# 약재-효능 연결 테이블 (다대다)
cursor.execute("""
CREATE TABLE IF NOT EXISTS herb_item_tags (
herb_item_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (herb_item_id, tag_id),
FOREIGN KEY (herb_item_id) REFERENCES herb_items(herb_item_id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES herb_efficacy_tags(tag_id) ON DELETE CASCADE
)
""")
print("✅ herb_item_tags 테이블 생성")
# 4. 기본 효능 태그 추가
basic_tags = [
('보혈', '', '혈을 보하는 효능'),
('보기', '', '기를 보하는 효능'),
('보양', '', '양기를 보하는 효능'),
('보음', '', '음액을 보하는 효능'),
('활혈', '', '혈액순환을 활발하게 하는 효능'),
('거담', '', '가래를 제거하는 효능'),
('온중', '', '속을 따뜻하게 하는 효능'),
('온양', '', '양기를 따뜻하게 하는 효능'),
('청열', '', '열을 내리는 효능'),
('해표', '', '표증을 해소하는 효능'),
('소화', '', '소화를 돕는 효능'),
('이수', '', '수분대사를 돕는 효능'),
('안신', '', '정신을 안정시키는 효능'),
('지혈', '', '출혈을 멈추는 효능'),
('조화제약', '조화', '여러 약재를 조화롭게 하는 효능'),
('대보원기', '대보', '원기를 크게 보하는 효능'),
('보기건비', '', '기를 보하고 비장을 건강하게 하는 효능'),
('보중익기', '', '중초를 보하고 기를 증진시키는 효능'),
]
for tag_name, category, description in basic_tags:
cursor.execute("""
INSERT OR IGNORE INTO herb_efficacy_tags (tag_name, tag_category, description)
VALUES (?, ?, ?)
""", (tag_name, category, description))
print(f"{len(basic_tags)}개 기본 효능 태그 추가")
# 5. 쌍화탕 약재들에 효능 태그 연결
ssanghwa_herbs = [
('숙지황', '보혈'),
('당귀', '보혈'),
('백작약', '보혈'),
('천궁', '활혈'),
('황기', '보기'),
('인삼', '대보원기'),
('백출', '보기건비'),
('감초', '조화제약'),
('생강', '온중'),
('대추', '보중익기'),
('육계', '온양'),
('건강', '온중'),
]
print("\n=== 약재별 효능 태그 연결 ===")
for herb_name, tag_name in ssanghwa_herbs:
# 약재 ID 찾기
cursor.execute("SELECT herb_item_id FROM herb_items WHERE herb_name = ?", (herb_name,))
herb_result = cursor.fetchone()
# 태그 ID 찾기
cursor.execute("SELECT tag_id FROM herb_efficacy_tags WHERE tag_name = ?", (tag_name,))
tag_result = cursor.fetchone()
if herb_result and tag_result:
herb_id = herb_result[0]
tag_id = tag_result[0]
cursor.execute("""
INSERT OR IGNORE INTO herb_item_tags (herb_item_id, tag_id)
VALUES (?, ?)
""", (herb_id, tag_id))
print(f"{herb_name}{tag_name}")
else:
if not herb_result:
print(f" ⚠️ {herb_name} 약재 없음")
if not tag_result:
print(f" ⚠️ {tag_name} 태그 없음")
conn.commit()
# 6. 결과 확인 - 약재별 태그 조회
print("\n=== 약재별 효능 태그 확인 ===")
cursor.execute("""
SELECT
h.herb_name,
GROUP_CONCAT(t.tag_name, ', ') as tags
FROM herb_items h
LEFT JOIN herb_item_tags ht ON h.herb_item_id = ht.herb_item_id
LEFT JOIN herb_efficacy_tags t ON ht.tag_id = t.tag_id
WHERE ht.tag_id IS NOT NULL
GROUP BY h.herb_item_id
ORDER BY h.herb_name
""")
results = cursor.fetchall()
for herb_name, tags in results:
print(f" {herb_name}: {tags}")
print("\n✅ 효능 태그 시스템 구축 완료!")
except Exception as e:
print(f"❌ 오류 발생: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == "__main__":
check_and_create_efficacy_system()

View File

@ -1,109 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
쌍화탕 처방 등록 스크립트
"""
import sqlite3
from datetime import datetime
def insert_ssanghwa_formula():
conn = sqlite3.connect('database/kdrug.db')
cursor = conn.cursor()
try:
# 1. 쌍화탕 처방 등록
cursor.execute("""
INSERT INTO formulas (
formula_code,
formula_name,
formula_type,
base_cheop,
base_pouches,
description
) VALUES (?, ?, ?, ?, ?, ?)
""", (
'SST001',
'쌍화탕',
'CUSTOM',
20,
30,
'기혈을 보하고 원기를 회복시키는 대표적인 보약 처방'
))
formula_id = cursor.lastrowid
print(f"✅ 쌍화탕 처방 등록 완료 (ID: {formula_id})")
# 2. 쌍화탕 구성 약재 등록
# 전형적인 쌍화탕 구성 (1첩 기준 용량)
ingredients = [
('숙지황', 8.0, '보혈'),
('당귀', 6.0, '보혈'),
('백작약', 6.0, '보혈'),
('천궁', 4.0, '활혈'),
('황기', 6.0, '보기'),
('인삼', 4.0, '대보원기'),
('백출', 4.0, '보기건비'),
('감초', 3.0, '조화제약'),
('생강', 5.0, '온중'),
('대추', 3.0, '보중익기'),
('육계', 2.0, '온양'),
('건강', 2.0, '온중') # 건강 포함!
]
for herb_name, grams, notes in ingredients:
# 약재 ID 찾기
cursor.execute("""
SELECT herb_item_id FROM herb_items
WHERE herb_name = ?
""", (herb_name,))
result = cursor.fetchone()
if result:
herb_id = result[0]
cursor.execute("""
INSERT INTO formula_ingredients (
formula_id,
herb_item_id,
grams_per_cheop,
notes
) VALUES (?, ?, ?, ?)
""", (formula_id, herb_id, grams, notes))
print(f" - {herb_name}: {grams}g ({notes})")
else:
print(f" ⚠️ {herb_name} 약재를 찾을 수 없음")
conn.commit()
print("\n✅ 쌍화탕 처방 구성 완료!")
# 3. 등록 확인
cursor.execute("""
SELECT
f.formula_name,
COUNT(fi.ingredient_id) as ingredient_count,
SUM(fi.grams_per_cheop) as total_grams_per_cheop
FROM formulas f
LEFT JOIN formula_ingredients fi ON f.formula_id = fi.formula_id
WHERE f.formula_id = ?
GROUP BY f.formula_id
""", (formula_id,))
result = cursor.fetchone()
if result:
print(f"\n📋 등록 결과:")
print(f" 처방명: {result[0]}")
print(f" 구성 약재: {result[1]}")
print(f" 1첩 총량: {result[2]}g")
print(f" 20첩 총량: {result[2] * 20}g")
except sqlite3.IntegrityError as e:
print(f"⚠️ 이미 등록된 처방이거나 오류 발생: {e}")
conn.rollback()
except Exception as e:
print(f"❌ 오류 발생: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == "__main__":
insert_ssanghwa_formula()

View File

@ -1,44 +0,0 @@
#!/bin/bash
# Flask 서버 실행 스크립트 - 강화 버전
echo "=== Flask 서버 관리 스크립트 (강화 버전) ==="
# 1. 모든 Python Flask 프로세스 종료
echo "1. 모든 Flask 프로세스 종료 중..."
pkill -9 -f "python app.py" 2>/dev/null
sleep 1
# 2. 포트 5001 강제 해제
echo "2. 포트 5001 강제 해제..."
lsof -ti:5001 | xargs -r kill -9 2>/dev/null
sleep 1
# 3. 잔여 Python 프로세스 확인
echo "3. 잔여 프로세스 확인..."
REMAINING=$(lsof -ti:5001)
if [ ! -z "$REMAINING" ]; then
echo " 경고: 아직 포트를 사용 중인 프로세스 발견. 강제 종료..."
kill -9 $REMAINING 2>/dev/null
sleep 1
fi
# 4. 포트 상태 최종 확인
if lsof -i:5001 > /dev/null 2>&1; then
echo " ⚠️ 포트 5001이 여전히 사용 중입니다. 잠시 후 다시 시도하세요."
exit 1
else
echo " ✅ 포트 5001이 비어있습니다."
fi
# 5. Flask 서버 시작
echo "4. Flask 서버 시작..."
cd /root/kdrug
source venv/bin/activate
# Debug 모드 비활성화 옵션 (더 안정적)
# export FLASK_DEBUG=0
echo " 서버 시작 중... (http://localhost:5001)"
echo " 종료: Ctrl+C"
echo "======================================="
python app.py

View File

@ -32,7 +32,6 @@ $(document).ready(function() {
break; break;
case 'purchase': case 'purchase':
loadPurchaseReceipts(); loadPurchaseReceipts();
loadSuppliersForSelect();
break; break;
case 'formulas': case 'formulas':
loadFormulas(); loadFormulas();
@ -288,15 +287,6 @@ $(document).ready(function() {
return; return;
} }
// 직접조제인 경우
if (formulaId === 'custom') {
$('#compoundIngredients').empty();
// 빈 행 하나 추가
addEmptyIngredientRow();
return;
}
// 등록된 처방인 경우
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) { $.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
if (response.success) { if (response.success) {
$('#compoundIngredients').empty(); $('#compoundIngredients').empty();
@ -313,11 +303,6 @@ $(document).ready(function() {
value="${ing.grams_per_cheop}" min="0.1" step="0.1"> value="${ing.grams_per_cheop}" min="0.1" step="0.1">
</td> </td>
<td class="total-grams">${totalGrams.toFixed(1)}</td> <td class="total-grams">${totalGrams.toFixed(1)}</td>
<td class="origin-select-cell">
<select class="form-control form-control-sm origin-select" disabled>
<option value="">로딩중...</option>
</select>
</td>
<td class="stock-status">확인중...</td> <td class="stock-status">확인중...</td>
<td> <td>
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient"> <button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
@ -326,9 +311,6 @@ $(document).ready(function() {
</td> </td>
</tr> </tr>
`); `);
// 각 약재별로 원산지별 재고 확인
loadOriginOptions(ing.herb_item_id, totalGrams);
}); });
// 재고 확인 // 재고 확인
@ -371,77 +353,6 @@ $(document).ready(function() {
} }
// 조제 약재 추가 // 조제 약재 추가
// 빈 약재 행 추가 함수
function addEmptyIngredientRow() {
const newRow = $(`
<tr>
<td>
<select class="form-control form-control-sm herb-select-compound">
<option value="">약재 선택</option>
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm grams-per-cheop"
min="0.1" step="0.1" placeholder="0.0">
</td>
<td class="total-grams">0.0</td>
<td class="origin-select-cell">
<select class="form-control form-control-sm origin-select" disabled>
<option value="">약재 선택 표시</option>
</select>
</td>
<td class="stock-status">-</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
$('#compoundIngredients').append(newRow);
// 약재 목록 로드
loadHerbsForSelect(newRow.find('.herb-select-compound'));
// 약재 선택 시 원산지 옵션 로드
newRow.find('.herb-select-compound').on('change', function() {
const herbId = $(this).val();
if (herbId) {
const row = $(this).closest('tr');
row.attr('data-herb-id', herbId);
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0;
const totalGrams = gramsPerCheop * cheopTotal;
loadOriginOptions(herbId, totalGrams);
}
});
// 이벤트 바인딩
newRow.find('.grams-per-cheop').on('input', function() {
updateIngredientTotals();
// 원산지 옵션 다시 로드
const herbId = $(this).closest('tr').attr('data-herb-id');
if (herbId) {
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
const gramsPerCheop = parseFloat($(this).val()) || 0;
const totalGrams = gramsPerCheop * cheopTotal;
loadOriginOptions(herbId, totalGrams);
}
});
newRow.find('.remove-compound-ingredient').on('click', function() {
$(this).closest('tr').remove();
updateIngredientTotals();
});
}
$('#addIngredientBtn').on('click', function() {
addEmptyIngredientRow();
});
// 기존 약재 추가 버튼 (기존 코드 삭제)
/*
$('#addIngredientBtn').on('click', function() { $('#addIngredientBtn').on('click', function() {
const newRow = $(` const newRow = $(`
<tr> <tr>
@ -480,7 +391,6 @@ $(document).ready(function() {
updateIngredientTotals(); updateIngredientTotals();
}); });
}); });
*/
// 조제 실행 // 조제 실행
$('#compoundEntryForm').on('submit', function(e) { $('#compoundEntryForm').on('submit', function(e) {
@ -491,14 +401,12 @@ $(document).ready(function() {
const herbId = $(this).data('herb-id'); const herbId = $(this).data('herb-id');
const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val()); const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val());
const totalGrams = parseFloat($(this).find('.total-grams').text()); const totalGrams = parseFloat($(this).find('.total-grams').text());
const originCountry = $(this).find('.origin-select').val();
if (herbId && gramsPerCheop) { if (herbId && gramsPerCheop) {
ingredients.push({ ingredients.push({
herb_item_id: parseInt(herbId), herb_item_id: parseInt(herbId),
grams_per_cheop: gramsPerCheop, grams_per_cheop: gramsPerCheop,
total_grams: totalGrams, total_grams: totalGrams
origin_country: originCountry || null // 원산지 선택 정보 추가
}); });
} }
}); });
@ -544,140 +452,17 @@ $(document).ready(function() {
tbody.empty(); tbody.empty();
response.data.forEach(item => { response.data.forEach(item => {
// 원산지가 여러 개인 경우 표시
const originBadge = item.origin_count > 1
? `<span class="badge bg-info ms-2">${item.origin_count}개 원산지</span>`
: '';
// 효능 태그 표시
let efficacyTags = '';
if (item.efficacy_tags && item.efficacy_tags.length > 0) {
efficacyTags = item.efficacy_tags.map(tag =>
`<span class="badge bg-success ms-1">${tag}</span>`
).join('');
}
// 가격 범위 표시 (원산지가 여러 개이고 가격차가 있는 경우)
let priceDisplay = item.avg_price ? formatCurrency(item.avg_price) : '-';
if (item.origin_count > 1 && item.min_price && item.max_price && item.min_price !== item.max_price) {
priceDisplay = `${formatCurrency(item.min_price)} ~ ${formatCurrency(item.max_price)}`;
}
tbody.append(` tbody.append(`
<tr class="inventory-row" data-herb-id="${item.herb_item_id}" style="cursor: pointer;"> <tr>
<td>${item.insurance_code || '-'}</td> <td>${item.insurance_code || '-'}</td>
<td>${item.herb_name}${originBadge}${efficacyTags}</td> <td>${item.herb_name}</td>
<td>${item.total_quantity.toFixed(1)}</td> <td>${item.total_quantity.toFixed(1)}</td>
<td>${item.lot_count}</td> <td>${item.lot_count}</td>
<td>${priceDisplay}</td> <td>${item.avg_price ? formatCurrency(item.avg_price) : '-'}</td>
<td>${formatCurrency(item.total_value)}</td> <td>${formatCurrency(item.total_value)}</td>
</tr> </tr>
`); `);
}); });
// 클릭 이벤트 바인딩
$('.inventory-row').on('click', function() {
const herbId = $(this).data('herb-id');
showInventoryDetail(herbId);
});
}
});
}
// 재고 상세 모달 표시
function showInventoryDetail(herbId) {
$.get(`/api/inventory/detail/${herbId}`, function(response) {
if (response.success) {
const data = response.data;
// 원산지별 재고 정보 HTML 생성
let originsHtml = '';
data.origins.forEach(origin => {
originsHtml += `
<div class="card mb-3">
<div class="card-header bg-light">
<h6 class="mb-0">
<i class="bi bi-geo-alt"></i> ${origin.origin_country}
<span class="badge bg-primary float-end">${origin.total_quantity.toFixed(1)}g</span>
</h6>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-6">
<small class="text-muted">평균 단가:</small><br>
<strong>${formatCurrency(origin.avg_price)}/g</strong>
</div>
<div class="col-6">
<small class="text-muted">재고 가치:</small><br>
<strong>${formatCurrency(origin.total_value)}</strong>
</div>
</div>
<table class="table table-sm">
<thead>
<tr>
<th>로트ID</th>
<th>수량</th>
<th>단가</th>
<th>입고일</th>
<th>도매상</th>
</tr>
</thead>
<tbody>`;
origin.lots.forEach(lot => {
originsHtml += `
<tr>
<td>#${lot.lot_id}</td>
<td>${lot.quantity_onhand.toFixed(1)}g</td>
<td>${formatCurrency(lot.unit_price_per_g)}</td>
<td>${lot.received_date}</td>
<td>${lot.supplier_name || '-'}</td>
</tr>`;
});
originsHtml += `
</tbody>
</table>
</div>
</div>`;
});
// 모달 생성 및 표시
const modalHtml = `
<div class="modal fade" id="inventoryDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
${data.herb_name} 재고 상세
<small class="text-muted">(${data.insurance_code})</small>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
${data.total_origins > 1
? `<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
약재는 ${data.total_origins} 원산지의 재고가 있습니다.
조제 원산지를 선택할 있습니다.
</div>`
: ''}
${originsHtml}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
</div>
</div>
</div>
</div>`;
// 기존 모달 제거
$('#inventoryDetailModal').remove();
$('body').append(modalHtml);
// 모달 표시
const modal = new bootstrap.Modal(document.getElementById('inventoryDetailModal'));
modal.show();
} }
}); });
} }
@ -735,8 +520,8 @@ $(document).ready(function() {
<td>${receipt.receipt_date}</td> <td>${receipt.receipt_date}</td>
<td>${receipt.supplier_name}</td> <td>${receipt.supplier_name}</td>
<td>${receipt.line_count}</td> <td>${receipt.line_count}</td>
<td class="fw-bold text-primary">${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'}</td> <td>${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'}</td>
<td class="text-muted small">${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'}</td> <td>${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'}</td>
<td>${receipt.source_file || '-'}</td> <td>${receipt.source_file || '-'}</td>
<td> <td>
<button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}"> <button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}">
@ -853,71 +638,10 @@ $(document).ready(function() {
loadPurchaseReceipts(); loadPurchaseReceipts();
}); });
// 도매상 목록 로드 (셀렉트 박스용)
function loadSuppliersForSelect() {
$.get('/api/suppliers', function(response) {
if (response.success) {
const select = $('#uploadSupplier');
select.empty().append('<option value="">도매상을 선택하세요</option>');
response.data.forEach(supplier => {
select.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
});
// 필터용 셀렉트 박스도 업데이트
const filterSelect = $('#purchaseSupplier');
filterSelect.empty().append('<option value="">전체</option>');
response.data.forEach(supplier => {
filterSelect.append(`<option value="${supplier.supplier_id}">${supplier.name}</option>`);
});
}
});
}
// 도매상 등록
$('#saveSupplierBtn').on('click', function() {
const supplierData = {
name: $('#supplierName').val(),
business_no: $('#supplierBusinessNo').val(),
contact_person: $('#supplierContactPerson').val(),
phone: $('#supplierPhone').val(),
address: $('#supplierAddress').val()
};
if (!supplierData.name) {
alert('도매상명은 필수입니다.');
return;
}
$.ajax({
url: '/api/suppliers',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(supplierData),
success: function(response) {
if (response.success) {
alert('도매상이 등록되었습니다.');
$('#supplierModal').modal('hide');
$('#supplierForm')[0].reset();
loadSuppliersForSelect();
}
},
error: function(xhr) {
alert('오류: ' + xhr.responseJSON.error);
}
});
});
// 입고장 업로드 // 입고장 업로드
$('#purchaseUploadForm').on('submit', function(e) { $('#purchaseUploadForm').on('submit', function(e) {
e.preventDefault(); e.preventDefault();
const supplierId = $('#uploadSupplier').val();
if (!supplierId) {
alert('도매상을 선택해주세요.');
return;
}
const formData = new FormData(); const formData = new FormData();
const fileInput = $('#purchaseFile')[0]; const fileInput = $('#purchaseFile')[0];
@ -927,7 +651,6 @@ $(document).ready(function() {
} }
formData.append('file', fileInput.files[0]); formData.append('file', fileInput.files[0]);
formData.append('supplier_id', supplierId);
$('#uploadResult').html('<div class="alert alert-info">업로드 중...</div>'); $('#uploadResult').html('<div class="alert alert-info">업로드 중...</div>');
@ -1008,17 +731,9 @@ $(document).ready(function() {
const select = $('#compoundFormula'); const select = $('#compoundFormula');
select.empty().append('<option value="">처방을 선택하세요</option>'); select.empty().append('<option value="">처방을 선택하세요</option>');
// 직접조제 옵션 추가 response.data.forEach(formula => {
select.append('<option value="custom">직접조제</option>'); select.append(`<option value="${formula.formula_id}">${formula.formula_name}</option>`);
});
// 등록된 처방 추가
if (response.data.length > 0) {
select.append('<optgroup label="등록된 처방">');
response.data.forEach(formula => {
select.append(`<option value="${formula.formula_id}">${formula.formula_name}</option>`);
});
select.append('</optgroup>');
}
} }
}); });
} }
@ -1035,51 +750,6 @@ $(document).ready(function() {
}); });
} }
// 원산지별 재고 옵션 로드
function loadOriginOptions(herbId, requiredQty) {
$.get(`/api/herbs/${herbId}/available-lots`, function(response) {
if (response.success) {
const selectElement = $(`tr[data-herb-id="${herbId}"] .origin-select`);
selectElement.empty();
const origins = response.data.origins;
if (origins.length === 0) {
selectElement.append('<option value="">재고 없음</option>');
selectElement.prop('disabled', true);
$(`tr[data-herb-id="${herbId}"] .stock-status`)
.html('<span class="text-danger">재고 없음</span>');
} else {
selectElement.append('<option value="auto">자동 선택 (저렴한 것부터)</option>');
origins.forEach(origin => {
const stockStatus = origin.total_quantity >= requiredQty ? '' : ' (재고 부족)';
const priceInfo = `${formatCurrency(origin.min_price)}/g`;
const option = `<option value="${origin.origin_country}"
data-price="${origin.min_price}"
data-available="${origin.total_quantity}"
${origin.total_quantity < requiredQty ? 'disabled' : ''}>
${origin.origin_country} - ${priceInfo} (재고: ${origin.total_quantity.toFixed(1)}g)${stockStatus}
</option>`;
selectElement.append(option);
});
selectElement.prop('disabled', false);
// 재고 상태 업데이트
const totalAvailable = response.data.total_quantity;
const statusElement = $(`tr[data-herb-id="${herbId}"] .stock-status`);
if (totalAvailable >= requiredQty) {
statusElement.html(`<span class="text-success">충분 (${totalAvailable.toFixed(1)}g)</span>`);
} else {
statusElement.html(`<span class="text-danger">부족 (${totalAvailable.toFixed(1)}g)</span>`);
}
}
}
});
}
function formatCurrency(amount) { function formatCurrency(amount) {
if (amount === null || amount === undefined) return '0원'; if (amount === null || amount === undefined) return '0원';
return new Intl.NumberFormat('ko-KR', { return new Intl.NumberFormat('ko-KR', {

View File

@ -257,8 +257,8 @@
<th>입고일</th> <th>입고일</th>
<th>공급업체</th> <th>공급업체</th>
<th>품목 수</th> <th>품목 수</th>
<th>총 금액</th>
<th>총 수량</th> <th>총 수량</th>
<th>총 금액</th>
<th>파일명</th> <th>파일명</th>
<th>작업</th> <th>작업</th>
</tr> </tr>
@ -272,31 +272,19 @@
<!-- Excel 업로드 --> <!-- Excel 업로드 -->
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header">
<h5 class="mb-0">새 입고 등록 (Excel 업로드)</h5> <h5 class="mb-0">새 입고 등록 (Excel 업로드)</h5>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#supplierModal">
<i class="bi bi-plus"></i> 도매상 등록
</button>
</div> </div>
<div class="card-body"> <div class="card-body">
<form id="purchaseUploadForm" enctype="multipart/form-data"> <form id="purchaseUploadForm" enctype="multipart/form-data">
<div class="row"> <div class="mb-3">
<div class="col-md-6"> <label for="purchaseFile" class="form-label">입고 Excel 파일 선택</label>
<label for="uploadSupplier" class="form-label">도매상 선택 *</label> <input type="file" class="form-control" id="purchaseFile" accept=".xlsx,.xls" required>
<select class="form-control" id="uploadSupplier" required> <div class="form-text">
<option value="">도매상을 선택하세요</option> 지원 형식: 한의사랑, 한의정보 (자동 감지)
</select>
</div>
<div class="col-md-6">
<label for="purchaseFile" class="form-label">Excel 파일 선택 *</label>
<input type="file" class="form-control" id="purchaseFile" accept=".xlsx,.xls" required>
</div> </div>
</div> </div>
<div class="form-text mt-2"> <button type="submit" class="btn btn-primary">
<i class="bi bi-info-circle"></i> Excel 형식: 한의사랑, 한의정보 (자동 감지)<br>
<i class="bi bi-info-circle"></i> Excel 내 업체명은 제조사(제약사)로 저장됩니다
</div>
<button type="submit" class="btn btn-primary mt-3">
<i class="bi bi-upload"></i> 업로드 및 처리 <i class="bi bi-upload"></i> 업로드 및 처리
</button> </button>
</form> </form>
@ -382,7 +370,6 @@
<th>약재명</th> <th>약재명</th>
<th>1첩당 용량(g)</th> <th>1첩당 용량(g)</th>
<th>총 용량(g)</th> <th>총 용량(g)</th>
<th>원산지 선택</th>
<th>재고</th> <th>재고</th>
<th>작업</th> <th>작업</th>
</tr> </tr>
@ -612,46 +599,6 @@
</div> </div>
</div> </div>
<!-- Supplier Modal -->
<div class="modal fade" id="supplierModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">도매상 등록</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="supplierForm">
<div class="mb-3">
<label class="form-label">도매상명 *</label>
<input type="text" class="form-control" id="supplierName" required>
</div>
<div class="mb-3">
<label class="form-label">사업자번호</label>
<input type="text" class="form-control" id="supplierBusinessNo" placeholder="000-00-00000">
</div>
<div class="mb-3">
<label class="form-label">담당자</label>
<input type="text" class="form-control" id="supplierContactPerson">
</div>
<div class="mb-3">
<label class="form-label">전화번호</label>
<input type="tel" class="form-control" id="supplierPhone">
</div>
<div class="mb-3">
<label class="form-label">주소</label>
<input type="text" class="form-control" id="supplierAddress">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button type="button" class="btn btn-primary" id="saveSupplierBtn">저장</button>
</div>
</div>
</div>
</div>
<!-- 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>