diff --git a/backend/app.py b/backend/app.py index 18a9bbd..69689f7 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4088,6 +4088,50 @@ def api_rx_usage(): mssql_session = db_manager.get_session('PM_PRES') + # 1년간 사용 환자 3명 이하 약품의 환자 목록 조회 + 조회 기간 내 사용 여부 + patient_query = text(""" + WITH PatientUsage AS ( + SELECT DISTINCT + P.DrugCode, + M.Paname, + MAX(CASE WHEN M.Indate >= :start_date AND M.Indate <= :end_date 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 + """) + + patient_rows = mssql_session.execute(patient_query, { + 'start_date': start_date_fmt, + 'end_date': end_date_fmt + }).fetchall() + patient_map = {row.drug_code: { + 'count': row.patient_count, + 'names': row.patient_names, + 'today': row.today_patients # 오늘 사용한 환자 + } for row in patient_rows} + # 전문의약품 품목별 사용량 집계 쿼리 (현재고: IM_total.IM_QT_sale_debit, 위치: CD_item_position.CD_NM_sale) rx_query = text(""" SELECT @@ -4138,6 +4182,9 @@ def api_rx_usage(): amount = float(row.total_amount or 0) rx_count = int(row.prescription_count or 0) + # 소수 환자 약품인지 확인 (1년간 3명 이하) + patient_info = patient_map.get(drug_code) + items.append({ 'drug_code': drug_code, 'product_name': product_name, @@ -4149,7 +4196,10 @@ def api_rx_usage(): 'prescription_count': rx_count, 'current_stock': int(row.current_stock or 0), # 현재고 'location': row.location or '', # 약국 내 위치 - 'thumbnail': None + 'thumbnail': None, + 'patient_count': patient_info['count'] if patient_info else None, # 1년간 사용 환자 수 (3명 이하만) + 'patient_names': patient_info['names'] if patient_info else None, # 환자 이름 (3명 이하만) + 'today_patients': patient_info['today'] if patient_info else None # 오늘 사용한 환자 }) total_qty += qty diff --git a/backend/check_db.py b/backend/check_db.py index d39cee3..9d6de43 100644 --- a/backend/check_db.py +++ b/backend/check_db.py @@ -1,11 +1,25 @@ import sqlite3 -conn = sqlite3.connect('db/orders.db') -cursor = conn.cursor() -cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") -tables = [r[0] for r in cursor.fetchall()] -print('Tables:', tables) +conn = sqlite3.connect('db/mileage.db') +cur = conn.cursor() + +# 테이블 목록 +cur.execute("SELECT name FROM sqlite_master WHERE type='table'") +tables = cur.fetchall() +print('=== Tables ===') for t in tables: - cursor.execute(f"PRAGMA table_info({t})") - cols = [r[1] for r in cursor.fetchall()] - print(f" {t}: {cols}") -conn.close() + print(t[0]) + +# wholesaler/limit/setting 관련 테이블 스키마 확인 +for t in tables: + tname = t[0].lower() + if 'wholesal' in tname or 'limit' in tname or 'setting' in tname or 'config' in tname: + print(f'\n=== {t[0]} schema ===') + cur.execute(f'PRAGMA table_info({t[0]})') + for col in cur.fetchall(): + print(col) + cur.execute(f'SELECT * FROM {t[0]} LIMIT 5') + rows = cur.fetchall() + if rows: + print('Sample data:') + for r in rows: + print(r) diff --git a/backend/check_lasix.py b/backend/check_lasix.py new file mode 100644 index 0000000..b116669 --- /dev/null +++ b/backend/check_lasix.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import pyodbc + +conn_str = ( + 'DRIVER={ODBC Driver 17 for SQL Server};' + 'SERVER=192.168.0.4\\PM2014;' + 'DATABASE=PM_DRUG;' + 'UID=sa;' + 'PWD=tmddls214!%(;' + 'TrustServerCertificate=yes;' + 'Connection Timeout=10' +) + +conn = pyodbc.connect(conn_str, timeout=10) +cur = conn.cursor() + +# 라식스 약품 정보 조회 (전체 컬럼) +cur.execute(""" + SELECT TOP 1 * + FROM CD_GOODS + WHERE DrugCode = '652100200' +""") + +row = cur.fetchone() +if row: + columns = [desc[0] for desc in cur.description] + print("=== 라식스 약품 정보 ===") + for i, col in enumerate(columns): + if 'price' in col.lower() or 'cost' in col.lower() or 'amount' in col.lower(): + print(f"{col}: {row[i]}") + +# 처방전에서 라식스 DRUPRICE 확인 +conn2 = pyodbc.connect(conn_str.replace('PM_DRUG', 'PM_PRES'), timeout=10) +cur2 = conn2.cursor() + +cur2.execute(""" + SELECT TOP 5 DrugCode, QUAN, Days, DRUPRICE + FROM PS_sub_pharm + WHERE DrugCode = '652100200' + ORDER BY Indate DESC +""") + +print("\n=== 최근 처방 라식스 DRUPRICE ===") +for row in cur2.fetchall(): + print(f"DrugCode: {row.DrugCode}, QUAN: {row.QUAN}, Days: {row.Days}, DRUPRICE: {row.DRUPRICE}") + dose = row.QUAN * row.Days + amount = row.DRUPRICE * row.QUAN * row.Days + print(f" → 투약량: {dose}, 매출액: {amount:,}") diff --git a/backend/check_orders_db.py b/backend/check_orders_db.py new file mode 100644 index 0000000..0eea8a9 --- /dev/null +++ b/backend/check_orders_db.py @@ -0,0 +1,32 @@ +import sqlite3 +conn = sqlite3.connect('db/orders.db') +cur = conn.cursor() + +# 테이블 목록 +cur.execute("SELECT name FROM sqlite_master WHERE type='table'") +tables = cur.fetchall() +print('=== Tables in orders.db ===') +for t in tables: + print(t[0]) + +# wholesaler_limits 확인 +cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='wholesaler_limits'") +if cur.fetchone(): + print('\n=== wholesaler_limits schema ===') + cur.execute('PRAGMA table_info(wholesaler_limits)') + for col in cur.fetchall(): + print(col) + cur.execute('SELECT * FROM wholesaler_limits') + rows = cur.fetchall() + print('\n=== Data ===') + for r in rows: + print(r) +else: + print('\n❌ wholesaler_limits 테이블 없음!') + +# delivery_schedules 확인 +cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='delivery_schedules'") +if cur.fetchone(): + print('\n=== delivery_schedules 있음 ===') +else: + print('\n❌ delivery_schedules 테이블 없음!') diff --git a/backend/check_patient_columns.py b/backend/check_patient_columns.py new file mode 100644 index 0000000..4c111b8 --- /dev/null +++ b/backend/check_patient_columns.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import pyodbc + +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() + +# PS_main 테이블 컬럼 확인 +cur.execute("SELECT TOP 1 * FROM PS_main") +row = cur.fetchone() +columns = [desc[0] for desc in cur.description] +print("=== PS_main 컬럼 ===") +for col in columns: + print(col) + +print("\n=== 샘플 데이터 (환자 관련) ===") +cur.execute("SELECT TOP 3 PreSerial, Paname, Indate FROM PS_main ORDER BY Indate DESC") +for row in cur.fetchall(): + print(f"PreSerial: {row.PreSerial}, 환자명: {row.Paname}, 날짜: {row.Indate}") diff --git a/backend/check_price_columns.py b/backend/check_price_columns.py new file mode 100644 index 0000000..8fc3691 --- /dev/null +++ b/backend/check_price_columns.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import pyodbc + +conn_str = ( + 'DRIVER={ODBC Driver 17 for SQL Server};' + 'SERVER=192.168.0.4\\PM2014;' + 'DATABASE=PM_DRUG;' + 'UID=sa;' + 'PWD=tmddls214!%(;' + 'TrustServerCertificate=yes;' + 'Connection Timeout=10' +) + +conn = pyodbc.connect(conn_str, timeout=10) +cur = conn.cursor() + +# CD_GOODS 테이블 전체 컬럼 조회 (라식스) +cur.execute(""" + SELECT * + FROM CD_GOODS + WHERE DrugCode = '652100200' +""") + +row = cur.fetchone() +if row: + columns = [desc[0] for desc in cur.description] + print("=== CD_GOODS 라식스 전체 컬럼 ===") + for i, col in enumerate(columns): + val = row[i] + if val is not None and val != '' and val != 0: + print(f"{col}: {val}") diff --git a/backend/create_limits_table.py b/backend/create_limits_table.py new file mode 100644 index 0000000..179ce7e --- /dev/null +++ b/backend/create_limits_table.py @@ -0,0 +1,50 @@ +import sqlite3 + +conn = sqlite3.connect('db/orders.db') +cur = conn.cursor() + +# wholesaler_limits 테이블 생성 +cur.execute(''' +CREATE TABLE IF NOT EXISTS wholesaler_limits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + wholesaler_id TEXT NOT NULL UNIQUE, + + -- 한도 설정 + monthly_limit INTEGER DEFAULT 0, -- 월 한도 (원) + warning_threshold REAL DEFAULT 0.9, -- 경고 임계값 (90%) + + -- 우선순위 + priority INTEGER DEFAULT 1, -- 1이 최우선 + + -- 상태 + is_active INTEGER DEFAULT 1, + + -- 메타 + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP +) +''') + +# 기본 데이터 삽입 (각 2000만원) +wholesalers = [ + ('geoyoung', 20000000, 0.9, 1), + ('sooin', 20000000, 0.9, 2), + ('baekje', 20000000, 0.9, 3), +] + +for ws_id, limit, threshold, priority in wholesalers: + cur.execute(''' + INSERT OR REPLACE INTO wholesaler_limits + (wholesaler_id, monthly_limit, warning_threshold, priority) + VALUES (?, ?, ?, ?) + ''', (ws_id, limit, threshold, priority)) + +conn.commit() + +# 확인 +cur.execute('SELECT * FROM wholesaler_limits') +print('=== wholesaler_limits 생성 완료 ===') +for row in cur.fetchall(): + print(row) + +conn.close() diff --git a/backend/templates/admin_rx_usage.html b/backend/templates/admin_rx_usage.html index 66b0bc8..569163f 100644 --- a/backend/templates/admin_rx_usage.html +++ b/backend/templates/admin_rx_usage.html @@ -391,6 +391,28 @@ font-family: inherit; } + .patient-badge { + display: inline-block; + background: rgba(156, 163, 175, 0.15); + color: #9ca3af; + font-size: 10px; + font-weight: 500; + padding: 2px 6px; + border-radius: 4px; + margin-left: 4px; + font-family: inherit; + } + + .patient-badge.has-today { + background: rgba(236, 72, 153, 0.2); + color: #ec4899; + } + + .today-patient { + color: #ec4899; + font-weight: 700; + } + /* 수량 관련 */ .qty-cell { text-align: center; @@ -957,7 +979,7 @@ }
${escapeHtml(item.product_name)} - ${item.drug_code}${item.supplier ? ` · ${escapeHtml(item.supplier)}` : ''}${item.location ? ` 📍${escapeHtml(item.location)}` : ''} + ${item.drug_code}${item.supplier ? ` · ${escapeHtml(item.supplier)}` : ''}${item.location ? ` 📍${escapeHtml(item.location)}` : ''}${item.patient_names ? ` 👤${formatPatientNames(item.patient_names, item.today_patients)}` : ''}
@@ -1861,6 +1883,23 @@ return str.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } + // 환자 이름 포맷 (오늘 사용 환자 강조) + function formatPatientNames(allNames, todayNames) { + if (!allNames) return ''; + if (!todayNames) return escapeHtml(allNames); + + const todaySet = new Set(todayNames.split(', ').map(n => n.trim())); + const names = allNames.split(', '); + + return names.map(name => { + const trimmed = name.trim(); + if (todaySet.has(trimmed)) { + return `${escapeHtml(trimmed)}`; + } + return escapeHtml(trimmed); + }).join(', '); + } + function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; diff --git a/backend/test_patient_query.py b/backend/test_patient_query.py new file mode 100644 index 0000000..fb238da --- /dev/null +++ b/backend/test_patient_query.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import pyodbc + +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() + +# 최근 1년간 약품별 사용 환자 수 + 3명 이하면 이름 표시 +query = """ +WITH PatientUsage AS ( + SELECT DISTINCT + P.DrugCode, + M.Paname + FROM PS_sub_pharm P + JOIN PS_main M ON P.PreSerial = M.PreSerial + WHERE M.Indate >= CONVERT(VARCHAR, DATEADD(YEAR, -1, GETDATE()), 112) +) +SELECT + PU.DrugCode, + 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 +FROM PatientUsage PU +GROUP BY PU.DrugCode +HAVING COUNT(*) <= 3 +ORDER BY COUNT(*), PU.DrugCode +""" + +cur.execute(query) +rows = cur.fetchall() + +print(f"=== 최근 1년 사용 환자 3명 이하 약품 ({len(rows)}개) ===\n") +for row in rows[:20]: # 상위 20개만 + print(f"[{row.DrugCode}] {row.patient_count}명: {row.patient_names}") diff --git a/backend/test_today_patients.py b/backend/test_today_patients.py new file mode 100644 index 0000000..ef9a955 --- /dev/null +++ b/backend/test_today_patients.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import pyodbc + +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() + +# 오늘 날짜 확인 +cur.execute('SELECT CONVERT(VARCHAR, GETDATE(), 112)') +today = cur.fetchone()[0] +print(f'오늘 날짜: {today}') + +# 오늘 처방된 약품 중 3명 이하 환자 약품 테스트 +query = """ +WITH PatientUsage AS ( + SELECT DISTINCT + P.DrugCode, + M.Paname, + MAX(CASE WHEN M.Indate = CONVERT(VARCHAR, GETDATE(), 112) THEN 1 ELSE 0 END) as used_today + 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 TOP 20 + PU.DrugCode, + 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_today = 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 +ORDER BY + CASE WHEN MAX(PU.used_today) = 1 THEN 0 ELSE 1 END, -- 오늘 사용한 것 먼저 + PU.DrugCode +""" + +cur.execute(query) +print('\n=== 3명 이하 환자 약품 (오늘 사용 우선) ===') +for row in cur.fetchall(): + today_mark = ' ⭐오늘' if row.today_patients else '' + print(f'[{row.DrugCode}] {row.patient_count}명: {row.patient_names}{today_mark}') + if row.today_patients: + print(f' → 오늘 사용: {row.today_patients}') diff --git a/backend/test_today_rx.py b/backend/test_today_rx.py new file mode 100644 index 0000000..716aa79 --- /dev/null +++ b/backend/test_today_rx.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import pyodbc + +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() + +# 오늘 처방 있는지 확인 +cur.execute(""" + SELECT COUNT(*) as cnt, MAX(Indate) as last_date + FROM PS_main + WHERE Indate = CONVERT(VARCHAR, GETDATE(), 112) +""") +row = cur.fetchone() +print(f'오늘(20260307) 처방 수: {row.cnt}') + +# 최근 처방일 +cur.execute("SELECT MAX(Indate) FROM PS_main") +print(f'최근 처방일: {cur.fetchone()[0]}') + +# 어제 처방 약품 중 3명 이하 확인 (테스트용) +cur.execute(""" + SELECT TOP 5 P.DrugCode, M.Paname, M.Indate + FROM PS_sub_pharm P + JOIN PS_main M ON P.PreSerial = M.PreSerial + WHERE M.Indate = '20260306' + ORDER BY P.DrugCode +""") +print('\n=== 3/6 처방 샘플 ===') +for row in cur.fetchall(): + print(f'{row.DrugCode}: {row.Paname}')