perf: OTC API 최적화 및 뱃지 안정화

- N+1 쿼리 → JOIN 한방 쿼리로 개선
  - 21번 쿼리 → 1번 쿼리
  - 2.4초 → 37ms (20건 조회 기준)
- DB warmup 추가 (앱 시작 시 연결 미리 생성)
- OTC 뱃지 insertBefore 방식으로 변경 (레이아웃 안정화)
- 중복 뱃지 방지 로직 추가
This commit is contained in:
thug0bin 2026-03-05 20:32:00 +09:00
parent 69b75d6724
commit 636fd66f9e
2 changed files with 76 additions and 63 deletions

View File

@ -38,6 +38,21 @@ def get_mssql_connection(database='PM_PRES'):
return pyodbc.connect(conn_str, timeout=10)
def warmup_db_connection():
"""앱 시작 시 DB 연결 미리 생성 (첫 요청 속도 개선)"""
try:
conn = get_mssql_connection('PM_PRES')
conn.cursor().execute("SELECT 1")
conn.close()
logging.info("[PMR] DB 연결 warmup 완료")
except Exception as e:
logging.warning(f"[PMR] DB warmup 실패: {e}")
# 앱 로드 시 warmup 실행
warmup_db_connection()
# ─────────────────────────────────────────────────────────────
# 조제관리 페이지
# ─────────────────────────────────────────────────────────────
@ -726,36 +741,69 @@ def get_patient_otc_history(cus_code):
conn = get_mssql_connection('PM_PRES')
cursor = conn.cursor()
# OTC 거래 목록 조회 (PRESERIAL = 'V' = OTC 판매)
# ✅ 최적화: 한번의 쿼리로 거래 + 품목 모두 조회 (JOIN)
cursor.execute("""
SELECT
m.SL_NO_order,
m.SL_DT_appl,
m.InsertTime,
m.SL_MY_sale,
m.SL_NM_custom
FROM SALE_MAIN m
WHERE m.SL_CD_custom = ?
AND m.PRESERIAL = 'V'
ORDER BY m.InsertTime DESC
""", (cus_code,))
m.SL_NM_custom,
s.DrugCode,
g.GoodsName,
s.SL_NM_item,
s.SL_TOTAL_PRICE,
mc.PRINT_TYPE
FROM (
SELECT TOP (?) *
FROM SALE_MAIN
WHERE SL_CD_custom = ? AND PRESERIAL = 'V'
ORDER BY InsertTime DESC
) m
LEFT JOIN SALE_SUB s ON m.SL_NO_order = s.SL_NO_order
LEFT JOIN PM_DRUG.dbo.CD_GOODS g ON s.DrugCode = g.DrugCode
LEFT JOIN PM_DRUG.dbo.CD_MC mc ON s.DrugCode = mc.DRUGCODE
ORDER BY m.InsertTime DESC, s.DrugCode
""", (limit, cus_code))
# 결과를 order_no별로 그룹핑
orders_dict = {}
all_drug_codes = []
# 먼저 거래 목록 수집
orders = []
for row in cursor.fetchall():
orders.append({
'order_no': row.SL_NO_order,
'date': row.SL_DT_appl,
'datetime': row.InsertTime.strftime('%Y-%m-%d %H:%M') if row.InsertTime else '',
'amount': int(row.SL_MY_sale or 0),
'customer_name': row.SL_NM_custom or ''
})
order_no = row.SL_NO_order
if order_no not in orders_dict:
orders_dict[order_no] = {
'order_no': order_no,
'date': row.SL_DT_appl,
'datetime': row.InsertTime.strftime('%Y-%m-%d %H:%M') if row.InsertTime else '',
'amount': int(row.SL_MY_sale or 0),
'customer_name': row.SL_NM_custom or '',
'items': []
}
# 품목 추가 (DrugCode가 있는 경우만)
if row.DrugCode:
drug_code = row.DrugCode
all_drug_codes.append(drug_code)
orders_dict[order_no]['items'].append({
'drug_code': drug_code,
'name': row.GoodsName or drug_code,
'quantity': int(row.SL_NM_item or 0),
'price': int(row.SL_TOTAL_PRICE or 0),
'category': row.PRINT_TYPE or '',
'image': None
})
# 최근 limit개만
orders = orders[:limit]
conn.close()
if not orders:
conn.close()
# dict → list 변환
purchases = list(orders_dict.values())
for p in purchases:
p['item_count'] = len(p['items'])
if not purchases:
return jsonify({
'success': True,
'cus_code': cus_code,
@ -763,46 +811,6 @@ def get_patient_otc_history(cus_code):
'purchases': []
})
# 각 거래의 품목 조회
purchases = []
all_drug_codes = []
for order in orders:
cursor.execute("""
SELECT
s.DrugCode,
g.GoodsName,
s.SL_NM_item,
s.SL_TOTAL_PRICE,
mc.PRINT_TYPE
FROM SALE_SUB s
LEFT JOIN PM_DRUG.dbo.CD_GOODS g ON s.DrugCode = g.DrugCode
LEFT JOIN PM_DRUG.dbo.CD_MC mc ON s.DrugCode = mc.DRUGCODE
WHERE s.SL_NO_order = ?
ORDER BY s.DrugCode
""", (order['order_no'],))
items = []
for item_row in cursor.fetchall():
drug_code = item_row.DrugCode or ''
all_drug_codes.append(drug_code)
items.append({
'drug_code': drug_code,
'name': item_row.GoodsName or drug_code,
'quantity': int(item_row.SL_NM_item or 0),
'price': int(item_row.SL_TOTAL_PRICE or 0),
'category': item_row.PRINT_TYPE or '',
'image': None
})
purchases.append({
**order,
'items': items,
'item_count': len(items)
})
conn.close()
# 제품 이미지 조회 (product_images.db)
image_map = {}
try:

View File

@ -1880,10 +1880,15 @@
if (data.success && data.count > 0) {
otcData = data;
// OTC 뱃지 추가 (질병 뱃지 앞에)
// OTC 뱃지 추가 ( 앞에)
const rxInfo = document.getElementById('rxInfo');
const otcBadge = `<span class="otc-badge" onclick="showOtcModal()">💊 OTC ${data.count}건</span>`;
rxInfo.innerHTML = otcBadge + rxInfo.innerHTML;
if (rxInfo && !rxInfo.querySelector('.otc-badge')) {
const otcBadge = document.createElement('span');
otcBadge.className = 'otc-badge';
otcBadge.onclick = showOtcModal;
otcBadge.innerHTML = `💊 OTC ${data.count}건`;
rxInfo.insertBefore(otcBadge, rxInfo.firstChild);
}
} else {
otcData = null;
}