From a42af23038d8620125c17527b50796f6f9f6e266 Mon Sep 17 00:00:00 2001 From: thug0bin Date: Sat, 28 Feb 2026 12:19:34 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8F=84=EB=A7=A4=EC=83=81=20=EC=9E=AC?= =?UTF-8?q?=EA=B3=A0=20=ED=91=9C=EC=8B=9C=20=EC=B6=94=EA=B0=80=20(?= =?UTF-8?q?=EC=95=BD=EA=B5=AD=20N=20/=20=EB=8F=84=EB=A7=A4=20M)=20+=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 28 +++++++-- backend/templates/admin_products.html | 12 ++-- docs/DATABASE_STRUCTURE.md | 90 +++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9a9edad..477d449 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2796,26 +2796,43 @@ def _get_animal_drugs(): 'barcode': barcode, 'apc': apc, 'stock': int(r.Stock) if r.Stock else 0, + 'wholesaler_stock': 0, # PostgreSQL에서 가져옴 'image_url': None # PostgreSQL에서 가져옴 }) - # PostgreSQL에서 이미지 URL 가져오기 + # PostgreSQL에서 이미지 URL + 도매상 재고 가져오기 if apc_list: try: from sqlalchemy import create_engine pg_engine = create_engine('postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master') with pg_engine.connect() as conn: placeholders = ','.join([f"'{a}'" for a in apc_list]) + # 이미지 URL 조회 img_result = conn.execute(text(f""" SELECT apc, image_url1 FROM apc WHERE apc IN ({placeholders}) """)) img_map = {row.apc: row.image_url1 for row in img_result} + # 도매상 재고 조회 (SUM) + stock_result = conn.execute(text(f""" + SELECT A.apc, COALESCE(SUM(I.quantity), 0) as wholesaler_stock + FROM apc A + LEFT JOIN inventory I ON I.apdb_id = A.idx + WHERE A.apc IN ({placeholders}) + GROUP BY A.apc + """)) + stock_map = {row.apc: int(row.wholesaler_stock) for row in stock_result} + for item in result: - if item['apc'] and item['apc'] in img_map: - item['image_url'] = img_map[item['apc']] + if item['apc']: + if item['apc'] in img_map: + item['image_url'] = img_map[item['apc']] + if item['apc'] in stock_map: + item['wholesaler_stock'] = stock_map[item['apc']] + else: + item['wholesaler_stock'] = 0 except Exception as e: - logging.warning(f"PostgreSQL 이미지 URL 조회 실패: {e}") + logging.warning(f"PostgreSQL 이미지/재고 조회 실패: {e}") return result except Exception as e: @@ -2973,7 +2990,8 @@ def api_animal_chat(): 'price': drug['price'], 'code': drug['code'], 'image_url': drug.get('image_url'), # APC 이미지 URL - 'stock': drug.get('stock', 0) # 재고 + 'stock': drug.get('stock', 0), # 약국 재고 + 'wholesaler_stock': drug.get('wholesaler_stock', 0) # 도매상 재고 }) return jsonify({ diff --git a/backend/templates/admin_products.html b/backend/templates/admin_products.html index 0caef94..6a6cdc1 100644 --- a/backend/templates/admin_products.html +++ b/backend/templates/admin_products.html @@ -987,11 +987,15 @@ imgContainer.innerHTML = '
💊
'; } - // 텍스트 + // 텍스트 (약국/도매 재고) const textDiv = document.createElement('div'); - const stockColor = (p.stock > 0) ? '#10b981' : '#ef4444'; - const stockText = (p.stock > 0) ? `재고 ${p.stock}` : '품절'; - textDiv.innerHTML = `
${p.name}
${formatPrice(p.price)} ${stockText}
`; + const pharmacyStock = p.stock || 0; + const wholesalerStock = p.wholesaler_stock || 0; + const stockColor = (pharmacyStock > 0) ? '#10b981' : '#ef4444'; + const pharmacyText = (pharmacyStock > 0) ? `약국 ${pharmacyStock}` : '품절'; + const wholesalerText = (wholesalerStock > 0) ? `도매 ${wholesalerStock}` : ''; + const stockDisplay = wholesalerText ? `${pharmacyText} / ${wholesalerText}` : pharmacyText; + textDiv.innerHTML = `
${p.name}
${formatPrice(p.price)} ${stockDisplay}
`; card.appendChild(imgContainer); card.appendChild(textDiv); diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index 590d63b..a5db0a4 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -212,8 +212,98 @@ PostgreSQL에서 일부 제품은 APC 대신 **바코드**로 등록됨: --- +## 재고 시스템 (2025-06-30) + +### 이중 재고 구조 + +| 위치 | 테이블 | 용도 | 조회 방식 | +|------|--------|------|-----------| +| **MSSQL (PM_DRUG)** | `IM_total` | 약국 재고 | `IM_QT_sale_debit` | +| **PostgreSQL** | `inventory` | 도매상 재고 | `SUM(quantity)` | + +### 약국 재고 (MSSQL) + +```sql +-- IM_total 테이블 +SELECT DrugCode, IM_QT_sale_debit as stock +FROM IM_total +WHERE DrugCode = 'LB000003157'; +-- → 8 (현재 약국 보유 수량) +``` + +### 도매상 재고 (PostgreSQL) + +도매상 재고는 **입출고 이력**으로 관리됩니다. + +```sql +-- inventory 테이블 (입출고 이력) +-- quantity: +입고(INBOUND), -출고(OUTBOUND) + +SELECT A.apc, A.product_name, SUM(I.quantity) as wholesaler_stock +FROM inventory I +JOIN apc A ON I.apdb_id = A.idx +WHERE A.for_pets = true +GROUP BY A.apc, A.product_name +HAVING SUM(I.quantity) > 0; + +-- 안텔민뽀삐: 38개 +-- 복합개시딘: 6개 +-- 세레니아16mg: 4개 +``` + +### inventory 테이블 주요 컬럼 + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| apdb_id | integer | apc.idx FK | +| quantity | integer | 수량 (+입고/-출고) | +| transaction_type | varchar | INBOUND/OUTBOUND | +| transaction_date | timestamp | 거래일시 | +| wholesaler_price | numeric | 도매가 | +| retail_price | numeric | 소매가 | +| expiration_date | date | 유효기간 | + +### API 응답 예시 + +```json +{ + "name": "안텔민뽀삐(5kg이하)", + "price": 5000, + "stock": 8, // 약국 재고 + "wholesaler_stock": 38 // 도매상 재고 +} +``` + +### 프론트엔드 표시 + +``` +┌────────────────────────────────┐ +│ 💊 안텔민뽀삐(5kg이하) │ +│ ₩5,000 약국 8 / 도매 38 │ +└────────────────────────────────┘ +``` + +- **약국 재고 있음**: 초록색 `약국 8` +- **약국 품절**: 빨간색 `품절` +- **도매상 재고**: 파란색 `도매 38` (발주 가능) + +--- + +## 향후 계획: 연관 제품 추천 + +약국에 없지만 도매상에 있는 제품 추천 로직: + +1. **카테고리 기반**: 같은 efficacy_effect (심장사상충, 외부기생충 등) +2. **신제품**: PostgreSQL `created_at` 최신순 +3. **인기 제품**: 도매상 출고량 기준 (`transaction_type = 'OUTBOUND'` 집계) + +→ 클릭 시 발주 연결 (미구현) + +--- + ## 관련 파일 - `backend/app.py`: `_get_animal_drugs()`, `_get_animal_drug_rag()` - `backend/scripts/insert_apc_*.py`: APC INSERT 스크립트 +- `backend/scripts/check_pgsql_stock_sum.py`: 도매상 재고 확인 - `docs/APC_MAPPING_PLAN.md`: APC 매핑 기획