Compare commits
No commits in common. "2a14af59c322a3447b10037f55c9531d7fc1cea2" and "40be340a63f7fff87e9823b212084c0324abd60a" have entirely different histories.
2a14af59c3
...
40be340a63
@ -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가 이미 생성되어 있음:
|
||||
- 기존 프로세스 자동 종료
|
||||
- 새 프로세스 시작
|
||||
- 단일 프로세스만 실행 보장
|
||||
@ -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
|
||||
@ -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. ⏳ 조제 이력에 원산지 기록
|
||||
@ -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()
|
||||
310
app.py
310
app.py
@ -125,36 +125,18 @@ def create_patient():
|
||||
|
||||
@app.route('/api/herbs', methods=['GET'])
|
||||
def get_herbs():
|
||||
"""약재 목록 조회 (효능 태그 포함)"""
|
||||
"""약재 목록 조회"""
|
||||
try:
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
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
|
||||
SELECT h.*, COALESCE(s.total_quantity, 0) as current_stock
|
||||
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 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
|
||||
LEFT JOIN v_current_stock s ON h.herb_item_id = s.herb_item_id
|
||||
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
|
||||
""")
|
||||
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})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
@ -245,53 +227,6 @@ def get_formula_ingredients(formula_id):
|
||||
except Exception as e:
|
||||
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 ====================
|
||||
|
||||
@app.route('/api/upload/purchase', methods=['POST'])
|
||||
@ -308,11 +243,6 @@ def upload_purchase_excel():
|
||||
if not allowed_file(file.filename):
|
||||
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)
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
@ -351,16 +281,21 @@ def upload_purchase_excel():
|
||||
processed_rows = 0
|
||||
processed_items = set()
|
||||
|
||||
# 도매상 정보 확인
|
||||
cursor.execute("SELECT name FROM suppliers WHERE supplier_id = ?", (supplier_id,))
|
||||
supplier_info = cursor.fetchone()
|
||||
if not supplier_info:
|
||||
return jsonify({'success': False, 'error': '유효하지 않은 도매상입니다'}), 400
|
||||
# 날짜별, 업체별로 그룹화
|
||||
grouped = df.groupby(['receipt_date', 'supplier_name'])
|
||||
|
||||
# 날짜별로 그룹화 (도매상은 이미 선택됨)
|
||||
grouped = df.groupby(['receipt_date'])
|
||||
for (receipt_date, supplier_name), group in grouped:
|
||||
# 공급업체 확인/생성
|
||||
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()
|
||||
@ -464,13 +399,13 @@ def get_purchase_receipts():
|
||||
pr.receipt_id,
|
||||
pr.receipt_date,
|
||||
pr.receipt_no,
|
||||
pr.total_amount,
|
||||
pr.source_file,
|
||||
pr.created_at,
|
||||
s.name as supplier_name,
|
||||
s.supplier_id,
|
||||
COUNT(prl.line_id) as line_count,
|
||||
SUM(prl.quantity_g) as total_quantity,
|
||||
SUM(prl.line_total) as total_amount
|
||||
SUM(prl.quantity_g) as total_quantity
|
||||
FROM purchase_receipts pr
|
||||
JOIN suppliers s ON pr.supplier_id = s.supplier_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이 이미 사용되어 삭제할 수 없습니다'
|
||||
}), 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("""
|
||||
DELETE FROM inventory_lots
|
||||
WHERE receipt_line_id IN (
|
||||
@ -710,10 +633,16 @@ def delete_purchase_receipt(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,))
|
||||
|
||||
# 4. 입고장 헤더 삭제
|
||||
# 입고장 헤더 삭제
|
||||
cursor.execute("DELETE FROM purchase_receipts WHERE receipt_id = ?", (receipt_id,))
|
||||
|
||||
return jsonify({'success': True, 'message': '입고장이 삭제되었습니다'})
|
||||
@ -757,7 +686,6 @@ def create_compound():
|
||||
for ingredient in data['ingredients']:
|
||||
herb_item_id = ingredient['herb_item_id']
|
||||
total_grams = ingredient['total_grams']
|
||||
origin_country = ingredient.get('origin_country') # 원산지 선택 정보
|
||||
|
||||
# 조제 약재 구성 기록
|
||||
cursor.execute("""
|
||||
@ -767,25 +695,13 @@ def create_compound():
|
||||
""", (compound_id, herb_item_id,
|
||||
ingredient['grams_per_cheop'], total_grams))
|
||||
|
||||
# 재고 차감 (FIFO 방식 - 원산지 지정 시 해당 원산지만)
|
||||
# 재고 차감 (FIFO 방식)
|
||||
remaining_qty = total_grams
|
||||
|
||||
# 원산지가 지정된 경우 해당 원산지만, 아니면 전체에서 FIFO
|
||||
if origin_country and origin_country != 'auto':
|
||||
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
|
||||
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
|
||||
ORDER BY received_date, lot_id
|
||||
""", (herb_item_id,))
|
||||
|
||||
lots = cursor.fetchall()
|
||||
@ -845,90 +761,11 @@ def create_compound():
|
||||
except Exception as e:
|
||||
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 ====================
|
||||
|
||||
@app.route('/api/inventory/summary', methods=['GET'])
|
||||
def get_inventory_summary():
|
||||
"""재고 현황 요약 - 원산지별 구분 표시"""
|
||||
"""재고 현황 요약"""
|
||||
try:
|
||||
with get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
@ -939,29 +776,16 @@ def get_inventory_summary():
|
||||
h.herb_name,
|
||||
COALESCE(SUM(il.quantity_onhand), 0) as total_quantity,
|
||||
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,
|
||||
MIN(il.unit_price_per_g) as min_price,
|
||||
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
|
||||
COALESCE(SUM(il.quantity_onhand * il.unit_price_per_g), 0) as total_value
|
||||
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 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
|
||||
HAVING total_quantity > 0
|
||||
ORDER BY h.herb_name
|
||||
""")
|
||||
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_items = len(inventory)
|
||||
@ -977,78 +801,6 @@ def get_inventory_summary():
|
||||
except Exception as e:
|
||||
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__':
|
||||
# 데이터베이스 초기화
|
||||
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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
|
||||
342
static/app.js
342
static/app.js
@ -32,7 +32,6 @@ $(document).ready(function() {
|
||||
break;
|
||||
case 'purchase':
|
||||
loadPurchaseReceipts();
|
||||
loadSuppliersForSelect();
|
||||
break;
|
||||
case 'formulas':
|
||||
loadFormulas();
|
||||
@ -288,15 +287,6 @@ $(document).ready(function() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 직접조제인 경우
|
||||
if (formulaId === 'custom') {
|
||||
$('#compoundIngredients').empty();
|
||||
// 빈 행 하나 추가
|
||||
addEmptyIngredientRow();
|
||||
return;
|
||||
}
|
||||
|
||||
// 등록된 처방인 경우
|
||||
$.get(`/api/formulas/${formulaId}/ingredients`, function(response) {
|
||||
if (response.success) {
|
||||
$('#compoundIngredients').empty();
|
||||
@ -313,11 +303,6 @@ $(document).ready(function() {
|
||||
value="${ing.grams_per_cheop}" min="0.1" step="0.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>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
|
||||
@ -326,9 +311,6 @@ $(document).ready(function() {
|
||||
</td>
|
||||
</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() {
|
||||
const newRow = $(`
|
||||
<tr>
|
||||
@ -480,7 +391,6 @@ $(document).ready(function() {
|
||||
updateIngredientTotals();
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
// 조제 실행
|
||||
$('#compoundEntryForm').on('submit', function(e) {
|
||||
@ -491,14 +401,12 @@ $(document).ready(function() {
|
||||
const herbId = $(this).data('herb-id');
|
||||
const gramsPerCheop = parseFloat($(this).find('.grams-per-cheop').val());
|
||||
const totalGrams = parseFloat($(this).find('.total-grams').text());
|
||||
const originCountry = $(this).find('.origin-select').val();
|
||||
|
||||
if (herbId && gramsPerCheop) {
|
||||
ingredients.push({
|
||||
herb_item_id: parseInt(herbId),
|
||||
grams_per_cheop: gramsPerCheop,
|
||||
total_grams: totalGrams,
|
||||
origin_country: originCountry || null // 원산지 선택 정보 추가
|
||||
total_grams: totalGrams
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -544,140 +452,17 @@ $(document).ready(function() {
|
||||
tbody.empty();
|
||||
|
||||
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(`
|
||||
<tr class="inventory-row" data-herb-id="${item.herb_item_id}" style="cursor: pointer;">
|
||||
<tr>
|
||||
<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.lot_count}</td>
|
||||
<td>${priceDisplay}</td>
|
||||
<td>${item.avg_price ? formatCurrency(item.avg_price) : '-'}</td>
|
||||
<td>${formatCurrency(item.total_value)}</td>
|
||||
</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.supplier_name}</td>
|
||||
<td>${receipt.line_count}개</td>
|
||||
<td class="fw-bold text-primary">${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'}</td>
|
||||
<td class="text-muted small">${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'}</td>
|
||||
<td>${receipt.total_quantity ? receipt.total_quantity.toLocaleString() + 'g' : '-'}</td>
|
||||
<td>${receipt.total_amount ? formatCurrency(receipt.total_amount) : '-'}</td>
|
||||
<td>${receipt.source_file || '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-info view-receipt" data-id="${receipt.receipt_id}">
|
||||
@ -853,71 +638,10 @@ $(document).ready(function() {
|
||||
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) {
|
||||
e.preventDefault();
|
||||
|
||||
const supplierId = $('#uploadSupplier').val();
|
||||
if (!supplierId) {
|
||||
alert('도매상을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
const fileInput = $('#purchaseFile')[0];
|
||||
|
||||
@ -927,7 +651,6 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
formData.append('file', fileInput.files[0]);
|
||||
formData.append('supplier_id', supplierId);
|
||||
|
||||
$('#uploadResult').html('<div class="alert alert-info">업로드 중...</div>');
|
||||
|
||||
@ -1008,17 +731,9 @@ $(document).ready(function() {
|
||||
const select = $('#compoundFormula');
|
||||
select.empty().append('<option value="">처방을 선택하세요</option>');
|
||||
|
||||
// 직접조제 옵션 추가
|
||||
select.append('<option value="custom">직접조제</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) {
|
||||
if (amount === null || amount === undefined) return '0원';
|
||||
return new Intl.NumberFormat('ko-KR', {
|
||||
|
||||
@ -257,8 +257,8 @@
|
||||
<th>입고일</th>
|
||||
<th>공급업체</th>
|
||||
<th>품목 수</th>
|
||||
<th>총 금액</th>
|
||||
<th>총 수량</th>
|
||||
<th>총 금액</th>
|
||||
<th>파일명</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
@ -272,31 +272,19 @@
|
||||
|
||||
<!-- Excel 업로드 -->
|
||||
<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>
|
||||
<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 class="card-body">
|
||||
<form id="purchaseUploadForm" enctype="multipart/form-data">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="uploadSupplier" class="form-label">도매상 선택 *</label>
|
||||
<select class="form-control" id="uploadSupplier" required>
|
||||
<option value="">도매상을 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="purchaseFile" class="form-label">Excel 파일 선택 *</label>
|
||||
<div class="mb-3">
|
||||
<label for="purchaseFile" class="form-label">입고 Excel 파일 선택</label>
|
||||
<input type="file" class="form-control" id="purchaseFile" accept=".xlsx,.xls" required>
|
||||
<div class="form-text">
|
||||
지원 형식: 한의사랑, 한의정보 (자동 감지)
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text mt-2">
|
||||
<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">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-upload"></i> 업로드 및 처리
|
||||
</button>
|
||||
</form>
|
||||
@ -382,7 +370,6 @@
|
||||
<th>약재명</th>
|
||||
<th>1첩당 용량(g)</th>
|
||||
<th>총 용량(g)</th>
|
||||
<th>원산지 선택</th>
|
||||
<th>재고</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
@ -612,46 +599,6 @@
|
||||
</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 -->
|
||||
<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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user