fix: rx-usage 쿼리에 PS_Type!=9 조건 추가 (실제 조제된 약만 집계)

- patient_query: 대체조제 원본 처방 제외
- rx_query: 대체조제 원본 처방 제외
- PS_Type=9는 대체조제시 원래 처방된 약(조제 안됨)
- 기타 배치 스크립트 및 문서 추가
This commit is contained in:
thug0bin
2026-03-09 21:54:32 +09:00
parent f92abf94c8
commit e470deaefc
10 changed files with 1909 additions and 121 deletions

View File

@@ -0,0 +1,260 @@
# API 개발 가이드 및 트러블슈팅
## 📋 목차
1. [도매상 주문 API 응답 형식](#도매상-주문-api-응답-형식)
2. [동원약품 API 버그 수정](#동원약품-api-버그-수정)
---
## 도매상 주문 API 응답 형식
### `/api/order/quick-submit` 응답 표준
모든 도매상(지오영, 수인, 백제, 동원)의 주문 응답은 **동일한 형식**을 따라야 합니다:
```json
{
"success": true,
"dry_run": true,
"cart_only": false,
"order_id": 123,
"order_no": "ORD-20260308-001",
"wholesaler": "dongwon",
"total_items": 1,
"success_count": 1,
"failed_count": 0,
"results": [
{
"item_id": 456,
"drug_code": "643900470",
"product_name": "부루펜정200mg",
"specification": "500정(병)",
"order_qty": 1,
"status": "success",
"result_code": "OK",
"result_message": "[DRY RUN] 주문 가능: 재고 9, 단가 17,000원",
"price": 17000
}
],
"note": "장바구니에 담김. 도매상 사이트에서 최종 확정 필요."
}
```
### ⚠️ 필수 필드
| 필드 | 설명 | 비고 |
|------|------|------|
| `wholesaler` | 도매상 ID | 프론트엔드에서 결과 모달 표시에 사용 |
| `success_count` | 성공 개수 | 최상위 레벨에 있어야 함 (summary 안에만 있으면 안됨) |
| `failed_count` | 실패 개수 | 최상위 레벨에 있어야 함 |
| `order_no` | 주문번호 | 프론트엔드 결과 모달에 표시 |
---
## 동원약품 API 버그 수정
### 📅 수정일: 2026-03-08
### 🐛 문제
**증상:**
- 동원약품으로 주문하면 결과 모달에 "**지오영 주문 결과**"로 표시됨
- 성공/실패 개수가 "**undefined**"로 표시됨
**원인:**
`submit_dongwon_order()` 함수의 응답에 다음 필드가 누락됨:
1. `wholesaler` 필드 없음
2. `success_count`, `failed_count``summary` 객체 안에만 있음 (최상위에 없음)
3. `order_no` 필드 없음
### 🔧 수정 내용
**파일:** `backend/order_api.py`
**수정 전 (dry_run 응답):**
```python
return {
'success': True,
'dry_run': True,
'results': results,
'summary': {
'total': len(items),
'success': success_count,
'failed': failed_count
}
}
```
**수정 후:**
```python
return {
'success': True,
'dry_run': dry_run,
'cart_only': cart_only,
'order_id': order_id,
'order_no': order['order_no'],
'wholesaler': 'dongwon',
'total_items': len(items),
'success_count': success_count,
'failed_count': failed_count,
'results': results
}
```
### ✅ 검증
테스트 절차:
1. `http://localhost:7001/admin/rx-usage` 접속
2. 테이블에서 약품 더블클릭 → 도매상 재고 모달 열기
3. 동원약품 섹션에서 "담기" 버튼 클릭
4. 장바구니에서 "주문서 생성하기" 클릭
5. "🧪 테스트" 버튼 클릭
6. 결과 모달에서 확인:
- 제목: "🏥 **동원약품** 주문 결과"
- 성공: "1개"
- 실패: "0개"
---
## 프론트엔드 장바구니 구조
### `addToCartFromWholesale()` 함수
동원약품에서 "담기" 버튼 클릭 시 장바구니에 추가되는 아이템 구조:
```javascript
const cartItem = {
drug_code: '643900470',
product_name: '부루펜정200mg',
supplier: '동원약품',
qty: 1,
specification: '500정(병)',
wholesaler: 'dongwon', // ← 필터링에 사용
internal_code: '16045',
dongwon_code: '16045', // ← 동원 API 호출에 사용
unit_price: 17000
};
```
### 도매상 필터링 로직
```javascript
const WHOLESALERS = {
dongwon: {
filterFn: (item) => item.supplier === '동원약품' || item.wholesaler === 'dongwon'
}
};
```
---
## 📝 개발 시 체크리스트
새로운 도매상 API 추가 시:
- [ ] `submit_xxx_order()` 함수 응답에 `wholesaler` 필드 포함
- [ ] `success_count`, `failed_count` 최상위 레벨에 포함
- [ ] `order_no` 필드 포함
- [ ] 프론트엔드 `WHOLESALERS` 객체에 도매상 추가
- [ ] `filterFn` 함수 정의
- [ ] E2E 테스트 수행
---
## 주문량 조회 API (summary-by-kd)
### 📅 추가일: 2025-07-14
### 📋 개요
전문의약품 사용량 페이지(`/admin/rx-usage`)의 "주문량" 컬럼은 도매상별 주문량을 KD 코드 기준으로 합산하여 표시합니다.
### ⚠️ 필수 구현: `/orders/summary-by-kd` 엔드포인트
**새로운 도매상 추가 시 반드시 구현해야 합니다!**
#### 요청
```
GET /api/{wholesaler}/orders/summary-by-kd?start_date=2025-07-01&end_date=2025-07-14
```
#### 응답 형식 (표준)
```json
{
"success": true,
"order_count": 4,
"period": {
"start": "2025-07-01",
"end": "2025-07-14"
},
"by_kd_code": {
"670400830": {
"product_name": "레바미피드정100mg",
"spec": "100T",
"boxes": 2,
"units": 200
},
"643900470": {
"product_name": "부루펜정200mg",
"spec": "500정(병)",
"boxes": 1,
"units": 500
}
},
"total_products": 2
}
```
### 현재 구현 상태
| 도매상 | 엔드포인트 | KD 코드 집계 | 비고 |
|--------|------------|--------------|------|
| 지오영 | `/api/geoyoung/orders/summary-by-kd` | ✅ | 정상 작동 |
| 수인 | `/api/sooin/orders/summary-by-kd` | ✅ | 정상 작동 |
| 백제 | `/api/baekje/orders/summary-by-kd` | ✅ | 정상 작동 |
| 동원 | `/api/dongwon/orders/summary-by-kd` | ⚠️ | 주문 건수만 제공, 품목별 집계 불가 |
### 동원약품 한계
동원약품 API(`onLineOrderListAX`)는 주문 목록만 반환하고, 각 주문의 상세 품목(items)을 제공하지 않습니다.
**향후 개선 필요:**
- 동원 주문 상세 조회 API 탐색 필요
- 또는 주문 상세 페이지 크롤링 구현
### 프론트엔드 연동
`admin_rx_usage.html``loadOrderData()` 함수:
```javascript
// 4사 병렬 조회
const [geoRes, sooinRes, baekjeRes, dongwonRes] = await Promise.all([
fetch(`/api/geoyoung/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`),
fetch(`/api/sooin/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`),
fetch(`/api/baekje/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`),
fetch(`/api/dongwon/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`)
]);
// 각 도매상 데이터를 KD 코드 기준으로 합산
if (dongwonRes.success && dongwonRes.by_kd_code) {
for (const [kd, data] of Object.entries(dongwonRes.by_kd_code)) {
orderDataByKd[kd].boxes += data.boxes || 0;
orderDataByKd[kd].units += data.units || 0;
orderDataByKd[kd].sources.push('동원');
}
}
```
### 📝 새 도매상 추가 시 체크리스트
- [ ] `{wholesaler}_api.py``/orders/summary-by-kd` 엔드포인트 구현
- [ ] 응답 형식 표준 준수 (`by_kd_code`, `order_count` 등)
- [ ] `admin_rx_usage.html``loadOrderData()`에 새 도매상 추가
- [ ] 합산 로직에 새 도매상 데이터 추가
- [ ] API 테스트 수행
---
*마지막 업데이트: 2025-07-14*

View File

@@ -0,0 +1,129 @@
# 동원약품 rx-usage 프론트엔드 연동 트러블슈팅
**작성일**: 2025-07-14
**수정 파일**: `backend/templates/admin_rx_usage.html`
## 발견된 문제점 3가지
### 문제 1: 재고 모달에서 KD코드가 아닌 내부코드 표시
**증상**: 동원약품만 재고 모달에서 내부코드(예: 16045, A4394)가 표시됨. 다른 도매상(지오영, 수인, 백제)은 KD코드(보험코드)가 정상 표시됨.
**원인**: `renderWholesaleResults()` 함수의 동원 섹션에서 `item.internal_code`를 표시함
```javascript
// 잘못된 코드
<span class="geo-code">${item.internal_code || ''} · ${item.manufacturer || ''}</span>
```
**해결**: 동원 API는 `code`에 KD코드(보험코드)를, `internal_code`에 내부코드를 반환함. 표시용은 `code` 사용.
```javascript
// 수정된 코드
const displayCode = item.code || item.internal_code || '';
<span class="geo-code">${displayCode} · ${item.manufacturer || ''}</span>
```
### 문제 2: 장바구니 "주문서 생성하기"에 동원 미포함
**증상**: 장바구니에 동원 상품을 담아도 "주문서 생성하기" 모달에 동원이 나오지 않음.
**원인**: `WHOLESALERS` 객체에 동원 설정 누락
```javascript
// 기존 코드 - 동원 없음
const WHOLESALERS = {
geoyoung: {...},
sooin: {...},
baekje: {...}
};
```
**해결**: `WHOLESALERS`에 동원 추가
```javascript
dongwon: {
id: 'dongwon',
name: '동원약품',
icon: '🏥',
logo: '/static/img/logo_dongwon.png',
color: '#22c55e',
gradient: 'linear-gradient(135deg, #16a34a, #22c55e)',
filterFn: (item) => item.supplier === '동원약품' || item.wholesaler === 'dongwon',
getCode: (item) => item.dongwon_code || item.internal_code || item.drug_code
}
```
### 문제 3: 장바구니에서 "dongwon"으로 표시
**증상**: 동원 상품이 장바구니에 담기면 "동원약품" 대신 "dongwon"으로 표시됨.
**원인**: `addToCartFromWholesale()` 함수의 `supplierNames` 객체에 동원 누락
```javascript
// 기존
const supplierNames = { geoyoung: '지오영', sooin: '수인약품', baekje: '백제약품' };
```
**해결**: 동원 추가
```javascript
const supplierNames = {
geoyoung: '지오영',
sooin: '수인약품',
baekje: '백제약품',
dongwon: '동원약품'
};
```
## 추가 수정 사항
### 1. 장바구니 아이템에 dongwon_code 필드 추가
```javascript
const cartItem = {
...
dongwon_code: wholesaler === 'dongwon' ? item.internal_code : null,
...
};
```
동원은 장바구니 담기/주문 시 `internal_code`를 사용해야 함.
### 2. CSS 스타일 - 다중 도매상 모달 카드 색상
```css
.multi-ws-card.dongwon {
border-left: 3px solid #22c55e;
}
```
## 동원약품 API 필드 매핑
| API 필드 | 의미 | 용도 |
|----------|------|------|
| `code` | KD코드 (보험코드) | 화면 표시용 |
| `internal_code` | 동원 내부코드 | 장바구니 담기/주문 시 사용 |
| `name` | 제품명 | 표시용 |
| `manufacturer` | 제조사 | 표시용 |
| `spec` | 규격 | 표시용 |
| `price` | 단가 | 표시용 |
| `stock` | 재고 | 표시용 |
## 관련 파일
- **프론트엔드**: `backend/templates/admin_rx_usage.html`
- **동원 API**: `backend/dongwon_api.py`
- **동원 세션**: `pharmacy-wholesale-api/wholesale/dongwon.py`
## 테스트 방법
1. 전문의약품 사용량 페이지 접속: http://localhost:7001/admin/rx-usage
2. 약품 행 더블클릭하여 재고 모달 열기
3. 동원약품 섹션에서:
- KD코드(9자리 숫자)가 표시되는지 확인
- "담기" 버튼 클릭하여 장바구니 추가
4. 장바구니 열어서:
- "동원약품"으로 표시되는지 확인
5. "주문서 생성하기" 클릭하여:
- 동원약품이 도매상 목록에 나타나는지 확인

278
docs/postgresql-apdb.md Normal file
View File

@@ -0,0 +1,278 @@
# PostgreSQL APDB (apdb_master) 데이터베이스 문서
## 접속 정보
| 항목 | 값 |
|------|-----|
| Host | 192.168.0.87 |
| Port | 5432 |
| Database | apdb_master |
| User | admin |
| Password | trajet6640 |
| Connection String | `postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master` |
```python
from sqlalchemy import create_engine
engine = create_engine('postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master')
```
---
## 핵심 테이블
### apc — 동물약품 마스터 (16,326건)
APC(Animal Product Code) 기반 동물약품 정보. 모든 동물약의 기준 테이블.
| 컬럼 | 타입 | 설명 |
|------|------|------|
| idx | INTEGER PK | 일련번호 |
| apc | VARCHAR(100) | APC 코드 (13자리, '023'으로 시작) |
| item_seq | VARCHAR(100) | 품목기준코드 |
| item_code | VARCHAR(100) | 품목코드 (APC 앞 8자리 = item_code) |
| product_name | VARCHAR(200) | 제품명 (한글) |
| product_english_name | VARCHAR(200) | 제품 영문명 |
| company_name | VARCHAR(100) | 제조/수입사명 |
| approval_number | VARCHAR(100) | 허가번호 |
| ac | VARCHAR(100) | AC 코드 |
| dosage_code | VARCHAR(100) | 제형코드 |
| packaging_code | VARCHAR(100) | 포장코드 |
| pc | VARCHAR(100) | PC 코드 |
| dosage | VARCHAR(100) | 제형 (정, 액, 캡슐 등) |
| packaging | VARCHAR(100) | 포장단위 |
| approval_date | VARCHAR(100) | 허가일자 |
| product_type | VARCHAR(500) | 제품유형 |
| main_ingredient | VARCHAR(500) | 주성분 |
| finished_material | VARCHAR(500) | 완제원료 |
| manufacture_import | VARCHAR(100) | 제조/수입 구분 |
| country_of_manufacture | VARCHAR(100) | 제조국 |
| basic_info | TEXT | 기본정보 |
| raw_material | TEXT | 원료약품 |
| efficacy_effect | TEXT | 효능효과 |
| dosage_instructions | TEXT | 용법용량 |
| precautions | TEXT | 주의사항 |
| component_code | VARCHAR(100) | 성분코드 |
| component_name_ko | VARCHAR(200) | 성분명(한글) |
| component_name_en | VARCHAR(200) | 성분명(영문) |
| dosage_factor | VARCHAR(100) | 용량계수 |
| llm_pharm | JSONB | LLM 생성 약사용 정보 (투여량, 주의사항 등) |
| llm_user | VARCHAR(500) | LLM 생성 사용자용 설명 |
| image_url1~3 | VARCHAR(500) | 제품 이미지 URL |
| list_price | NUMERIC(10,2) | 정가 |
| weight_min_kg | DOUBLE PRECISION | 체중 하한 (kg) |
| weight_max_kg | DOUBLE PRECISION | 체중 상한 (kg) |
| pet_size_label | VARCHAR(100) | 체중 라벨 (소형견용, 대형견용 등) |
| pet_size_code | VARCHAR(10) | 체중 코드 |
| for_pets | BOOLEAN | 반려동물용 여부 |
| prescription_target | BOOLEAN | 처방대상 여부 |
| is_not_medicine | BOOLEAN | 비의약품 여부 |
| usage_guide | JSONB | 사용 가이드 (구조화) |
| godoimage_url_f/b/d | VARCHAR(500) | 고도몰 이미지 URL |
| pill_color | VARCHAR(100) | 알약 색상 |
| updated_at | TIMESTAMP | 수정일시 |
| parent_item_id | INTEGER | 부모 품목 ID |
**APC 코드 구조**: `023XXXXXYYZZZ`
- 앞 8자리 (`023XXXXX`) = item_code (품목코드, 대표 APC)
- 나머지 = 포장단위별 구분
---
### component_code — 성분 정보 (1,105건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| idx | INTEGER PK | 일련번호 |
| code | VARCHAR(500) | 성분코드 |
| component_name_ko | VARCHAR(500) | 성분명(한글) |
| component_name_en | VARCHAR(500) | 성분명(영문) |
| description | VARCHAR(500) | 설명 |
| efficacy | TEXT | 효능 |
| target_animals | JSONB | 대상 동물 |
| precautions | TEXT | 주의사항 |
| additional_precautions | TEXT | 추가 주의사항 |
| prohibited_breeds | VARCHAR(500) | 금기 품종 |
| offlabel | TEXT | 오프라벨 사용 |
### component_guide — 성분별 투여 가이드 (1건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| component_code | VARCHAR(50) PK | 성분코드 |
| component_name_ko/en | VARCHAR(200) | 성분명 |
| dosing_interval_adult | VARCHAR(200) | 성체 투여간격 |
| dosing_interval_high_risk | VARCHAR(200) | 고위험군 투여간격 |
| dosing_interval_puppy | VARCHAR(200) | 유아 투여간격 |
| dosing_interval_source | VARCHAR(500) | 출처 |
| withdrawal_period | VARCHAR(200) | 휴약기간 |
| contraindication | VARCHAR(500) | 금기사항 |
| companion_drugs | VARCHAR(500) | 병용약물 |
### dosage_info — 용량 정보 (152건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | INTEGER PK | 일련번호 |
| apdb_idx | INTEGER | apc 테이블 idx 참조 |
| component_code | VARCHAR(100) | 성분코드 |
| dose_per_kg | DOUBLE PRECISION | kg당 용량 |
| dose_per_kg_min/max | DOUBLE PRECISION | kg당 용량 범위 |
| dose_unit | VARCHAR(20) | 용량 단위 |
| unit_dose | DOUBLE PRECISION | 단위 용량 |
| unit_type | VARCHAR(20) | 단위 타입 |
| frequency | VARCHAR(50) | 투여 빈도 |
| route | VARCHAR(30) | 투여 경로 |
| weight_min/max_kg | DOUBLE PRECISION | 적용 체중 범위 |
| animal_type | VARCHAR(10) | 동물 종류 |
| source | VARCHAR(20) | 출처 |
| verified | BOOLEAN | 검증 여부 |
| raw_text | TEXT | 원문 |
### symptoms — 증상 코드 (51건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| idx | INTEGER PK | 일련번호 |
| prefix | VARCHAR(1) | 카테고리 접두사 |
| prefix_description | VARCHAR(50) | 카테고리 설명 |
| symptom_code | VARCHAR(10) | 증상 코드 |
| symptom_description | VARCHAR(255) | 증상 설명 |
| disease_description | VARCHAR(255) | 질병 설명 |
### symptom_component_mapping — 증상-성분 매핑 (111건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| symptom_code | VARCHAR(10) | 증상 코드 |
| component_code | VARCHAR(500) | 성분 코드 |
---
## 재고/유통 테이블
### inventory — 재고 (656건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | INTEGER PK | 일련번호 |
| apdb_id | INTEGER | apc.idx 참조 |
| supplier_cost | NUMERIC(12,2) | 공급가 |
| wholesaler_price | NUMERIC(12,2) | 도매가 |
| retail_price | NUMERIC(12,2) | 소매가 |
| quantity | INTEGER | 수량 |
| transaction_type | VARCHAR(20) | 거래유형 |
| order_no | VARCHAR(100) | 주문번호 |
| serial_number | VARCHAR(100) | 시리얼번호 |
| expiration_date | DATE | 유효기간 |
| receipt_id | INTEGER | 입고전표 ID |
| entity_id | VARCHAR(50) | 거래처 ID |
| entity_type | VARCHAR(20) | 거래처 유형 |
| location_id | INTEGER | 보관위치 ID |
| goods_no | INTEGER | 고도몰 상품번호 |
### receipt — 입고전표 (21건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| idx | INTEGER PK | 일련번호 |
| receipt_number | VARCHAR(100) | 전표번호 |
| receipt_date | TIMESTAMP | 입고일 |
| total_quantity | INTEGER | 총수량 |
| total_amount | NUMERIC(10,2) | 총금액 |
| entity_id | VARCHAR(50) | 거래처 ID |
| entity_type | VARCHAR(20) | 거래처 유형 |
### vendor — 거래처 (3건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| idx | INTEGER PK | 일련번호 |
| vendor_code | VARCHAR(50) | 거래처 코드 |
| name | VARCHAR(200) | 거래처명 |
| business_reg_no | VARCHAR(50) | 사업자번호 |
---
## 약국/회원 테이블
### animal_pharmacies — 동물약국 목록 (18,955건)
전국 동물약국 데이터 (공공데이터 기반).
| 컬럼 | 타입 | 설명 |
|------|------|------|
| id | INTEGER PK | 일련번호 |
| management_number | VARCHAR(50) | 관리번호 |
| name | VARCHAR(200) | 약국명 |
| phone | VARCHAR(20) | 전화번호 |
| address_old/new | VARCHAR(500) | 주소 |
| latitude/longitude | NUMERIC | 위경도 |
| business_status | VARCHAR(10) | 영업상태 |
### p_member — 약국 회원 (31건)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| idx | INTEGER PK | 일련번호 |
| memno | INTEGER | 회원번호 |
| pharmacyname | VARCHAR(100) | 약국명 |
| businessregno | VARCHAR(20) | 사업자번호 |
| kioskusage | BOOLEAN | 키오스크 사용 |
| mem_nm | VARCHAR(100) | 회원명 |
---
## 기타 테이블
| 테이블 | 행수 | 설명 |
|--------|------|------|
| apc_subnames | 0 | APC 별칭 (미사용) |
| cs_memo | 13 | CS 메모 |
| excluded_pharmacies | 15 | 제외 약국 |
| evidence_reference | 0 | 근거 문헌 참조 |
| recommendation_log | 3 | 추천 로그 |
| supplementary_product | 5 | 보조제품 |
| optimal_stock | 3 | 적정재고 설정 |
| sync_status | 168 | 동기화 상태 |
| system_log | 438 | 시스템 로그 |
| location | 4 | 보관 위치 |
| region / subregion | 3/8 | 지역 구분 |
| member_group_change_logs | 4 | 회원그룹 변경 이력 |
---
## 주요 쿼리 예시
```sql
-- APC로 제품 조회
SELECT * FROM apc WHERE apc = '0230338510101';
-- 제품명 검색 (띄어쓰기 무시)
SELECT apc, product_name
FROM apc
WHERE REGEXP_REPLACE(LOWER(product_name), '[\s\-\.]+', '', 'g')
LIKE '%파라캅%';
-- 체중별 제품 검색
SELECT apc, product_name, weight_min_kg, weight_max_kg
FROM apc
WHERE weight_min_kg IS NOT NULL
ORDER BY product_name;
-- 대표 APC → 포장단위 APC 조회 (앞 8자리 기준)
SELECT apc, product_name, packaging
FROM apc
WHERE LEFT(apc, 8) = '02303385';
-- 성분별 제품 검색
SELECT a.apc, a.product_name, a.component_name_ko
FROM apc a
WHERE a.component_code = 'P001';
-- 증상 → 성분 → 제품 검색
SELECT s.symptom_description, cc.component_name_ko, a.product_name
FROM symptoms s
JOIN symptom_component_mapping scm ON s.symptom_code = scm.symptom_code
JOIN component_code cc ON cc.code = scm.component_code
JOIN apc a ON a.component_code = cc.code;
```