fix: 날짜 표시 UTC → KST 변환 (admin 페이지들)
This commit is contained in:
parent
c1fae04344
commit
d842c776c9
40
backend/check_paai_status.py
Normal file
40
backend/check_paai_status.py
Normal file
@ -0,0 +1,40 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
db_path = 'db/paai_logs.db'
|
||||
print(f"DB 경로: {os.path.abspath(db_path)}")
|
||||
print(f"파일 존재: {os.path.exists(db_path)}")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 모든 테이블 확인
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
all_tables = [t[0] for t in cursor.fetchall()]
|
||||
print(f"전체 테이블: {all_tables}")
|
||||
|
||||
for table in all_tables:
|
||||
cursor.execute(f"SELECT COUNT(*) FROM {table}")
|
||||
count = cursor.fetchone()[0]
|
||||
print(f"\n=== {table}: {count}건 ===")
|
||||
|
||||
# 컬럼 정보
|
||||
cursor.execute(f"PRAGMA table_info({table})")
|
||||
cols = [c[1] for c in cursor.fetchall()]
|
||||
print(f"컬럼: {cols}")
|
||||
|
||||
# status 컬럼이 있으면 상태별 카운트
|
||||
if 'status' in cols:
|
||||
cursor.execute(f"SELECT status, COUNT(*) FROM {table} GROUP BY status")
|
||||
for row in cursor.fetchall():
|
||||
print(f" {row[0]}: {row[1]}건")
|
||||
|
||||
# 최근 5건
|
||||
cursor.execute(f"SELECT * FROM {table} ORDER BY rowid DESC LIMIT 3")
|
||||
rows = cursor.fetchall()
|
||||
if rows:
|
||||
print(f"최근 3건:")
|
||||
for row in rows:
|
||||
print(f" {row}")
|
||||
|
||||
conn.close()
|
||||
6
backend/check_schema.py
Normal file
6
backend/check_schema.py
Normal file
@ -0,0 +1,6 @@
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('db/paai_logs.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='paai_logs'")
|
||||
print(cursor.fetchone()[0])
|
||||
conn.close()
|
||||
@ -437,3 +437,236 @@ def api_sooin_order_batch():
|
||||
'failed_count': failed_count,
|
||||
'results': results
|
||||
})
|
||||
|
||||
|
||||
# ========== 주문 조회 API ==========
|
||||
|
||||
@sooin_bp.route('/orders', methods=['GET'])
|
||||
def api_sooin_orders():
|
||||
"""
|
||||
수인약품 주문 목록 조회 API
|
||||
|
||||
GET /api/sooin/orders?start_date=2026-03-01&end_date=2026-03-07
|
||||
|
||||
파라미터:
|
||||
start_date: 시작일 (YYYY-MM-DD), 기본값: 오늘
|
||||
end_date: 종료일 (YYYY-MM-DD), 기본값: 오늘
|
||||
|
||||
Returns:
|
||||
{
|
||||
"success": true,
|
||||
"orders": [
|
||||
{
|
||||
"order_num": "202603095091177",
|
||||
"order_date": "2026-03-09",
|
||||
"order_time": "14:30:25",
|
||||
"total_amount": 125000,
|
||||
"item_count": 5,
|
||||
"status": "완료"
|
||||
}
|
||||
],
|
||||
"total_count": 10
|
||||
}
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
start_date = flask_request.args.get('start_date', today).strip()
|
||||
end_date = flask_request.args.get('end_date', today).strip()
|
||||
|
||||
try:
|
||||
session = get_sooin_session()
|
||||
result = session.get_order_list(start_date, end_date)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
logger.error(f"수인약품 주문 목록 조회 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'ORDERS_ERROR',
|
||||
'message': str(e),
|
||||
'orders': [],
|
||||
'total_count': 0
|
||||
}), 500
|
||||
|
||||
|
||||
@sooin_bp.route('/orders/today-summary', methods=['GET'])
|
||||
def api_sooin_today_summary():
|
||||
"""
|
||||
수인약품 오늘 주문 집계 API
|
||||
|
||||
GET /api/sooin/orders/today-summary
|
||||
|
||||
Returns:
|
||||
{
|
||||
"success": true,
|
||||
"date": "2026-03-09",
|
||||
"summary": [
|
||||
{
|
||||
"kd_code": "073100220",
|
||||
"product_name": "코자정50mg",
|
||||
"total_quantity": 10,
|
||||
"total_amount": 150000,
|
||||
"order_count": 3
|
||||
}
|
||||
],
|
||||
"grand_total_amount": 500000,
|
||||
"grand_total_items": 25,
|
||||
"order_count": 5
|
||||
}
|
||||
"""
|
||||
try:
|
||||
session = get_sooin_session()
|
||||
result = session.get_today_order_summary()
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
logger.error(f"수인약품 오늘 주문 집계 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'TODAY_SUMMARY_ERROR',
|
||||
'message': str(e),
|
||||
'summary': [],
|
||||
'grand_total_amount': 0,
|
||||
'grand_total_items': 0,
|
||||
'order_count': 0
|
||||
}), 500
|
||||
|
||||
|
||||
@sooin_bp.route('/orders/<order_num>', methods=['GET'])
|
||||
def api_sooin_order_detail(order_num):
|
||||
"""
|
||||
수인약품 주문 상세 조회 API
|
||||
|
||||
GET /api/sooin/orders/202603095091177
|
||||
|
||||
Returns:
|
||||
{
|
||||
"success": true,
|
||||
"order_num": "202603095091177",
|
||||
"order_date": "2026-03-09",
|
||||
"items": [
|
||||
{
|
||||
"product_code": "32495",
|
||||
"kd_code": "073100220",
|
||||
"product_name": "코자정50mg",
|
||||
"spec": "30T",
|
||||
"quantity": 2,
|
||||
"unit_price": 15000,
|
||||
"amount": 30000
|
||||
}
|
||||
],
|
||||
"total_amount": 125000,
|
||||
"item_count": 5
|
||||
}
|
||||
"""
|
||||
if not order_num or not order_num.isdigit():
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'INVALID_ORDER_NUM',
|
||||
'message': '유효한 주문번호를 입력하세요'
|
||||
}), 400
|
||||
|
||||
try:
|
||||
session = get_sooin_session()
|
||||
result = session.get_order_detail(order_num)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
logger.error(f"수인약품 주문 상세 조회 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'ORDER_DETAIL_ERROR',
|
||||
'message': str(e),
|
||||
'order_num': order_num,
|
||||
'items': [],
|
||||
'total_amount': 0
|
||||
}), 500
|
||||
|
||||
|
||||
@sooin_bp.route('/orders/summary-by-kd', methods=['GET'])
|
||||
def api_sooin_orders_by_kd():
|
||||
"""
|
||||
수인약품 주문량 KD코드별 집계 API (병렬 처리)
|
||||
|
||||
GET /api/sooin/orders/summary-by-kd?start_date=2026-03-01&end_date=2026-03-07
|
||||
"""
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
start_date = flask_request.args.get('start_date', today).strip()
|
||||
end_date = flask_request.args.get('end_date', today).strip()
|
||||
|
||||
def parse_spec(spec: str) -> int:
|
||||
if not spec:
|
||||
return 1
|
||||
match = re.search(r'(\d+)', spec)
|
||||
return int(match.group(1)) if match else 1
|
||||
|
||||
try:
|
||||
session = get_sooin_session()
|
||||
|
||||
# 주문 목록 조회
|
||||
orders_result = session.get_order_list(start_date, end_date)
|
||||
|
||||
if not orders_result.get('success'):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': orders_result.get('error', 'ORDERS_FETCH_FAILED'),
|
||||
'by_kd_code': {}
|
||||
})
|
||||
|
||||
orders = orders_result.get('orders', [])
|
||||
order_nums = [o.get('order_num') for o in orders if o.get('order_num')]
|
||||
|
||||
# 순차 처리 + 캐시 (캐시 효과 극대화)
|
||||
all_details = []
|
||||
|
||||
for order_num in order_nums:
|
||||
try:
|
||||
detail = session.get_order_detail(order_num)
|
||||
if detail.get('success'):
|
||||
all_details.append(detail)
|
||||
except Exception as e:
|
||||
logger.warning(f"주문 상세 조회 실패: {e}")
|
||||
|
||||
# KD코드별 집계
|
||||
kd_summary = {}
|
||||
|
||||
for detail in all_details:
|
||||
for item in detail.get('items', []):
|
||||
kd_code = item.get('kd_code', '')
|
||||
if not kd_code:
|
||||
continue
|
||||
|
||||
product_name = item.get('product_name', '')
|
||||
spec = item.get('spec', '')
|
||||
quantity = item.get('quantity', 0)
|
||||
per_unit = parse_spec(spec)
|
||||
total_units = quantity * per_unit
|
||||
|
||||
if kd_code not in kd_summary:
|
||||
kd_summary[kd_code] = {
|
||||
'product_name': product_name,
|
||||
'spec': spec,
|
||||
'boxes': 0,
|
||||
'units': 0
|
||||
}
|
||||
|
||||
kd_summary[kd_code]['boxes'] += quantity
|
||||
kd_summary[kd_code]['units'] += total_units
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'order_count': len(order_nums),
|
||||
'period': {'start': start_date, 'end': end_date},
|
||||
'by_kd_code': kd_summary,
|
||||
'total_products': len(kd_summary)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"수인약품 KD코드별 집계 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'SUMMARY_ERROR',
|
||||
'message': str(e),
|
||||
'by_kd_code': {}
|
||||
}), 500
|
||||
|
||||
@ -924,7 +924,8 @@
|
||||
const txs = detailData.mileage.transactions;
|
||||
container.innerHTML = txs.map(tx => {
|
||||
const isPositive = tx.points > 0;
|
||||
const date = tx.created_at ? new Date(tx.created_at).toLocaleString('ko-KR', {
|
||||
// DB는 UTC로 저장 → 'Z' 붙여서 UTC로 해석 → KST로 표시
|
||||
const date = tx.created_at ? new Date(tx.created_at + 'Z').toLocaleString('ko-KR', {
|
||||
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||
}) : '';
|
||||
|
||||
@ -1075,8 +1076,8 @@
|
||||
}
|
||||
|
||||
container.innerHTML = detailData.interests.map(item => {
|
||||
// 날짜 포맷
|
||||
const date = item.created_at ? new Date(item.created_at).toLocaleString('ko-KR', {
|
||||
// 날짜 포맷 (DB는 UTC → KST 변환)
|
||||
const date = item.created_at ? new Date(item.created_at + 'Z').toLocaleString('ko-KR', {
|
||||
month: 'short', day: 'numeric'
|
||||
}) : '';
|
||||
|
||||
|
||||
@ -358,7 +358,8 @@
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.logs.map(log => {
|
||||
const time = new Date(log.created_at).toLocaleString('ko-KR', {
|
||||
// DB는 UTC로 저장 → 'Z' 붙여서 UTC로 해석 → KST로 표시
|
||||
const time = new Date(log.created_at + 'Z').toLocaleString('ko-KR', {
|
||||
month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
|
||||
@ -37,6 +37,17 @@
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ══════════════════ 주문량 로딩 ══════════════════ */
|
||||
.order-loading {
|
||||
display: inline-block;
|
||||
color: var(--accent-cyan);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ══════════════════ 헤더 ══════════════════ */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #0891b2 0%, #06b6d4 50%, #22d3ee 100%);
|
||||
@ -825,17 +836,18 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="check-col"><input type="checkbox" class="custom-check" id="checkAll" onchange="toggleCheckAll()"></th>
|
||||
<th style="width:32%">약품</th>
|
||||
<th style="width:28%">약품</th>
|
||||
<th class="center">현재고</th>
|
||||
<th class="center">처방횟수</th>
|
||||
<th class="center">투약량</th>
|
||||
<th class="center" style="color:var(--accent-cyan);">주문량</th>
|
||||
<th class="right">매출액</th>
|
||||
<th class="center" style="width:90px">주문수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usageTableBody">
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>데이터 로딩 중...</div>
|
||||
@ -882,6 +894,7 @@
|
||||
<script>
|
||||
let usageData = [];
|
||||
let cart = [];
|
||||
let orderDataByKd = {}; // 수인약품 주문량 (KD코드별)
|
||||
|
||||
// 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@ -891,8 +904,42 @@
|
||||
document.getElementById('endDate').value = todayStr;
|
||||
|
||||
loadUsageData();
|
||||
loadOrderData(); // 수인약품 주문량 로드
|
||||
});
|
||||
|
||||
// ──────────────── 수인약품 주문량 조회 ────────────────
|
||||
async function loadOrderData() {
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
|
||||
orderDataLoading = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/sooin/orders/summary-by-kd?start_date=${startDate}&end_date=${endDate}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
orderDataByKd = data.by_kd_code || {};
|
||||
console.log('📦 수인약품 주문량:', Object.keys(orderDataByKd).length, '품목,', data.order_count, '건 주문');
|
||||
}
|
||||
} catch(err) {
|
||||
console.warn('주문량 조회 실패:', err);
|
||||
orderDataByKd = {};
|
||||
} finally {
|
||||
orderDataLoading = false;
|
||||
renderTable(); // 로딩 완료 후 테이블 갱신
|
||||
}
|
||||
}
|
||||
|
||||
// KD코드로 주문량 조회
|
||||
let orderDataLoading = true; // 로딩 상태
|
||||
|
||||
function getOrderedQty(kdCode) {
|
||||
if (orderDataLoading) return '<span class="order-loading">···</span>';
|
||||
const order = orderDataByKd[kdCode];
|
||||
if (!order) return '-';
|
||||
return order.units.toLocaleString();
|
||||
}
|
||||
|
||||
// ──────────────── 데이터 로드 ────────────────
|
||||
function loadUsageData() {
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
@ -901,7 +948,7 @@
|
||||
const sort = document.getElementById('sortSelect').value;
|
||||
|
||||
document.getElementById('usageTableBody').innerHTML = `
|
||||
<tr><td colspan="7">
|
||||
<tr><td colspan="8">
|
||||
<div class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>데이터 로딩 중...</div>
|
||||
@ -928,7 +975,7 @@
|
||||
renderTable();
|
||||
} else {
|
||||
document.getElementById('usageTableBody').innerHTML = `
|
||||
<tr><td colspan="7">
|
||||
<tr><td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">⚠️</div>
|
||||
<div>오류: ${data.error}</div>
|
||||
@ -938,7 +985,7 @@
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('usageTableBody').innerHTML = `
|
||||
<tr><td colspan="7">
|
||||
<tr><td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">❌</div>
|
||||
<div>데이터 로드 실패</div>
|
||||
@ -953,7 +1000,7 @@
|
||||
|
||||
if (usageData.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr><td colspan="7">
|
||||
<tr><td colspan="8">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💊</div>
|
||||
<div>해당 기간 처방 내역이 없습니다</div>
|
||||
@ -988,6 +1035,7 @@
|
||||
</td>
|
||||
<td class="qty-cell" style="color:var(--text-secondary);">${item.prescription_count}건</td>
|
||||
<td class="qty-cell ${qtyClass}">${item.total_dose}</td>
|
||||
<td class="qty-cell" style="color:var(--accent-cyan);">${getOrderedQty(item.drug_code)}</td>
|
||||
<td style="text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;">
|
||||
${formatPrice(item.total_amount)}원
|
||||
</td>
|
||||
|
||||
@ -651,7 +651,8 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
${logs.map(log => {
|
||||
const date = new Date(log.created_at);
|
||||
// DB는 UTC로 저장 → 'Z' 붙여서 UTC로 해석 → KST로 표시
|
||||
const date = new Date(log.created_at + 'Z');
|
||||
const dateStr = date.toLocaleString('ko-KR', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
|
||||
60
backend/test_query_perf.py
Normal file
60
backend/test_query_perf.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pyodbc
|
||||
import time
|
||||
|
||||
conn_str = (
|
||||
'DRIVER={ODBC Driver 17 for SQL Server};'
|
||||
'SERVER=192.168.0.4\\PM2014;'
|
||||
'DATABASE=PM_PRES;'
|
||||
'UID=sa;'
|
||||
'PWD=tmddls214!%(;'
|
||||
'TrustServerCertificate=yes;'
|
||||
'Connection Timeout=10'
|
||||
)
|
||||
|
||||
conn = pyodbc.connect(conn_str, timeout=10)
|
||||
cur = conn.cursor()
|
||||
|
||||
# 현재 쿼리 성능 측정
|
||||
start = time.time()
|
||||
|
||||
query = """
|
||||
WITH PatientUsage AS (
|
||||
SELECT DISTINCT
|
||||
P.DrugCode,
|
||||
M.Paname,
|
||||
MAX(CASE WHEN M.Indate >= '20260306' AND M.Indate <= '20260306' THEN 1 ELSE 0 END) as used_in_period
|
||||
FROM PS_sub_pharm P
|
||||
JOIN PS_main M ON P.PreSerial = M.PreSerial
|
||||
WHERE M.Indate >= CONVERT(VARCHAR, DATEADD(YEAR, -1, GETDATE()), 112)
|
||||
GROUP BY P.DrugCode, M.Paname
|
||||
)
|
||||
SELECT
|
||||
PU.DrugCode as drug_code,
|
||||
COUNT(*) as patient_count,
|
||||
STUFF((
|
||||
SELECT ', ' + PU2.Paname
|
||||
FROM PatientUsage PU2
|
||||
WHERE PU2.DrugCode = PU.DrugCode
|
||||
ORDER BY PU2.Paname
|
||||
FOR XML PATH(''), TYPE
|
||||
).value('.', 'NVARCHAR(MAX)'), 1, 2, '') as patient_names,
|
||||
STUFF((
|
||||
SELECT ', ' + PU3.Paname
|
||||
FROM PatientUsage PU3
|
||||
WHERE PU3.DrugCode = PU.DrugCode AND PU3.used_in_period = 1
|
||||
ORDER BY PU3.Paname
|
||||
FOR XML PATH(''), TYPE
|
||||
).value('.', 'NVARCHAR(MAX)'), 1, 2, '') as today_patients
|
||||
FROM PatientUsage PU
|
||||
GROUP BY PU.DrugCode
|
||||
HAVING COUNT(*) <= 3
|
||||
"""
|
||||
|
||||
cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
elapsed = time.time() - start
|
||||
|
||||
print(f"결과: {len(rows)}개 약품")
|
||||
print(f"실행 시간: {elapsed:.2f}초")
|
||||
print(f"약품당: {elapsed/len(rows)*1000:.2f}ms" if rows else "")
|
||||
241
docs/SOOIN_API.md
Normal file
241
docs/SOOIN_API.md
Normal file
@ -0,0 +1,241 @@
|
||||
# 수인약품 (Sooin) API 문서
|
||||
|
||||
## 개요
|
||||
- **회사명**: 수인약품
|
||||
- **웹사이트**: http://sooinpharm.co.kr
|
||||
- **인증방식**: Playwright 로그인 → requests 쿠키 재사용 (세션 30분 유효)
|
||||
|
||||
## 인증 정보 (환경변수)
|
||||
```
|
||||
SOOIN_USER_ID=thug0bin
|
||||
SOOIN_PASSWORD=@Trajet6640
|
||||
SOOIN_VENDOR_CODE=50911
|
||||
SOOIN_VENDOR_NAME=청춘약국
|
||||
```
|
||||
|
||||
## 핵심 URL 구조
|
||||
|
||||
### 기존 구현된 URL
|
||||
| URL | 용도 |
|
||||
|-----|------|
|
||||
| `/Homepage/intro.asp` | 로그인 페이지 |
|
||||
| `/Service/Order/Order.asp` | 제품 검색 |
|
||||
| `/Service/Order/BagOrder.asp` | 장바구니 추가 |
|
||||
| `/Service/Order/Bag.asp` | 장바구니 조회 |
|
||||
| `/Service/Order/ControlBag.asp` | 장바구니 항목 제어 (취소/복원) |
|
||||
| `/Service/Order/OrderEnd.asp` | 주문 전송 (확정) |
|
||||
| `/Service/Order/PhysicInfo.asp` | 제품 상세 정보 |
|
||||
| `/Service/SalesLedger/SalesLedger.asp` | 잔고/매출 조회 |
|
||||
|
||||
### 🆕 신규 구현 필요 URL (주문 조회)
|
||||
| URL | 용도 |
|
||||
|-----|------|
|
||||
| `/Service/Report/Report.asp` | **주문 내역 목록 조회** |
|
||||
| `/Service/Report/Report.asp?f=view&orderNum=...` | **주문 상세 조회** |
|
||||
|
||||
## 주문 조회 URL 분석
|
||||
|
||||
### 주문 목록 URL 예시
|
||||
```
|
||||
http://sooinpharm.co.kr/Service/Report/Report.asp?
|
||||
sDate=2026-03-01 # 시작일
|
||||
&eDate=2026-03-07 # 종료일
|
||||
&tx_ven=50911 # 거래처 코드 (SOOIN_VENDOR_CODE)
|
||||
&currVenNm=청춘약국 # 거래처명 (URL 인코딩됨)
|
||||
&orderStatus=0 # 주문상태 (0=전체?)
|
||||
&ListOrder=0 # 정렬 (0=기본)
|
||||
&PhysicNm= # 제품명 필터 (선택)
|
||||
&sg= # 미확인
|
||||
&page=1 # 페이지 번호
|
||||
```
|
||||
|
||||
### 주문 상세 URL 예시
|
||||
```
|
||||
http://sooinpharm.co.kr/Service/Report/Report.asp?
|
||||
f=view # view 모드 (상세 조회)
|
||||
&orderNum=202603095091177 # 주문번호 (YYYYMMDD + 거래처코드 + 순번?)
|
||||
&Ifflag= # 미확인
|
||||
&sDate=2026-03-01
|
||||
&eDate=2026-03-07
|
||||
&PhysicNm=
|
||||
&ListOrder=0
|
||||
&tx_ven=50911
|
||||
&currVenNm=청춘약국
|
||||
&orderStatus=0
|
||||
&sg=
|
||||
&page=1
|
||||
```
|
||||
|
||||
### 주문번호 구조 분석
|
||||
- `202603095091177` → `20260309` (날짜) + `50911` (거래처코드) + `77` (순번?)
|
||||
- 형식: `YYYYMMDD` + `VENDOR_CODE` + `SEQ`
|
||||
|
||||
## API 엔드포인트
|
||||
|
||||
### Flask Blueprint: `/api/sooin/*`
|
||||
|
||||
| Method | Endpoint | 설명 |
|
||||
|--------|----------|------|
|
||||
| GET | `/api/sooin/stock` | 재고 조회 |
|
||||
| GET | `/api/sooin/session-status` | 세션 상태 확인 |
|
||||
| GET | `/api/sooin/balance` | 잔고(미수금) 조회 |
|
||||
| GET | `/api/sooin/monthly-sales` | 월간 매출 조회 |
|
||||
| GET | `/api/sooin/cart` | 장바구니 조회 |
|
||||
| POST | `/api/sooin/cart/clear` | 장바구니 비우기 |
|
||||
| POST | `/api/sooin/cart/cancel` | 장바구니 항목 취소 |
|
||||
| POST | `/api/sooin/cart/restore` | 취소 항목 복원 |
|
||||
| POST | `/api/sooin/order` | 주문 (장바구니 추가) |
|
||||
| POST | `/api/sooin/confirm` | 주문 확정 |
|
||||
| POST | `/api/sooin/full-order` | 전체 주문 (검색→담기→확정) |
|
||||
| POST | `/api/sooin/order-batch` | 일괄 주문 |
|
||||
| **GET** | **`/api/sooin/orders`** | **✅ 주문 목록 조회** |
|
||||
| **GET** | **`/api/sooin/orders/<order_num>`** | **✅ 주문 상세 조회** |
|
||||
| **GET** | **`/api/sooin/orders/today-summary`** | **✅ 오늘 주문 집계** |
|
||||
|
||||
## 🆕 신규 구현 목표: 주문 조회 API
|
||||
|
||||
### 목적
|
||||
1. **오늘 주문한 약 목록 조회** - 주문이 정상적으로 들어갔는지 확인
|
||||
2. **제품별 주문량 집계** - 오늘의 총 주문량을 제품별로 파악
|
||||
|
||||
### 구현할 API 엔드포인트
|
||||
|
||||
#### 1. 주문 목록 조회
|
||||
```
|
||||
GET /api/sooin/orders?start_date=2026-03-01&end_date=2026-03-07
|
||||
```
|
||||
**응답 예시:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"orders": [
|
||||
{
|
||||
"order_num": "202603095091177",
|
||||
"order_date": "2026-03-09",
|
||||
"order_time": "14:30:25",
|
||||
"total_amount": 125000,
|
||||
"item_count": 5,
|
||||
"status": "완료"
|
||||
}
|
||||
],
|
||||
"total_count": 10
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 주문 상세 조회
|
||||
```
|
||||
GET /api/sooin/orders/<order_num>
|
||||
```
|
||||
**응답 예시:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"order_num": "202603095091177",
|
||||
"order_date": "2026-03-09",
|
||||
"items": [
|
||||
{
|
||||
"product_code": "32495", // 수인 내부코드
|
||||
"kd_code": "073100220", // KD코드 (있으면)
|
||||
"product_name": "코자정50mg",
|
||||
"spec": "30T",
|
||||
"quantity": 2,
|
||||
"unit_price": 15000,
|
||||
"amount": 30000
|
||||
}
|
||||
],
|
||||
"total_amount": 125000
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 오늘 주문 집계 (제품별)
|
||||
```
|
||||
GET /api/sooin/orders/today-summary
|
||||
```
|
||||
**응답 예시:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"date": "2026-03-09",
|
||||
"summary": [
|
||||
{
|
||||
"kd_code": "073100220",
|
||||
"product_name": "코자정50mg",
|
||||
"total_quantity": 10,
|
||||
"total_amount": 150000,
|
||||
"order_count": 3
|
||||
}
|
||||
],
|
||||
"grand_total_amount": 500000,
|
||||
"grand_total_items": 25
|
||||
}
|
||||
```
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
pharmacy-wholesale-api/
|
||||
├── wholesale/
|
||||
│ ├── __init__.py
|
||||
│ ├── base.py # 공통 베이스 클래스
|
||||
│ ├── sooin.py # 수인약품 핵심 로직 ← 여기에 주문 조회 메서드 추가
|
||||
│ ├── geoyoung.py
|
||||
│ └── baekje.py
|
||||
└── .env # 인증 정보
|
||||
|
||||
pharmacy-pos-qr-system/
|
||||
├── backend/
|
||||
│ ├── app.py # 메인 Flask 앱
|
||||
│ ├── sooin_api.py # 수인약품 Flask Blueprint ← 여기에 API 엔드포인트 추가
|
||||
│ ├── wholesale_path.py # wholesale 패키지 경로 설정
|
||||
│ └── templates/
|
||||
│ └── admin_rx_usage.html # 프론트엔드 (주문 조회 UI 추가 가능)
|
||||
└── docs/
|
||||
└── SOOIN_API.md # 이 문서
|
||||
```
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
### 1단계: SooinSession에 메서드 추가 (`wholesale/sooin.py`)
|
||||
|
||||
```python
|
||||
def get_order_list(self, start_date: str, end_date: str) -> dict:
|
||||
"""주문 목록 조회"""
|
||||
# /Service/Report/Report.asp 파싱
|
||||
pass
|
||||
|
||||
def get_order_detail(self, order_num: str) -> dict:
|
||||
"""주문 상세 조회"""
|
||||
# /Service/Report/Report.asp?f=view&orderNum=... 파싱
|
||||
pass
|
||||
|
||||
def get_today_order_summary(self) -> dict:
|
||||
"""오늘 주문 제품별 집계"""
|
||||
# get_order_list + get_order_detail 조합
|
||||
pass
|
||||
```
|
||||
|
||||
### 2단계: Flask API 엔드포인트 추가 (`sooin_api.py`)
|
||||
|
||||
```python
|
||||
@sooin_bp.route('/orders', methods=['GET'])
|
||||
def api_sooin_orders():
|
||||
"""주문 목록 조회"""
|
||||
pass
|
||||
|
||||
@sooin_bp.route('/orders/<order_num>', methods=['GET'])
|
||||
def api_sooin_order_detail(order_num):
|
||||
"""주문 상세 조회"""
|
||||
pass
|
||||
|
||||
@sooin_bp.route('/orders/today-summary', methods=['GET'])
|
||||
def api_sooin_today_summary():
|
||||
"""오늘 주문 집계"""
|
||||
pass
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **인코딩**: 수인약품 사이트는 `euc-kr` 인코딩 사용
|
||||
2. **세션 유지**: 30분 세션 타임아웃, 자동 재로그인 필요
|
||||
3. **HTML 파싱**: BeautifulSoup으로 테이블 구조 파싱
|
||||
4. **에러 처리**: 로그인 실패, 네트워크 오류 등 처리 필요
|
||||
Loading…
Reference in New Issue
Block a user