feat: 소수 환자 약품 뱃지 표시

- 1년간 3명 이하 환자만 사용하는 약품에 환자 이름 뱃지 표시
- 조회 기간 내 사용한 환자는 핑크색으로 강조
- 매출액 컬럼명 변경 (약가 → 매출액)
- SUM(DRUPRICE)로 매출액 계산
This commit is contained in:
thug0bin 2026-03-07 00:43:02 +09:00
parent 846883cbfa
commit ee300f80ca
11 changed files with 456 additions and 11 deletions

View File

@ -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

View File

@ -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)

48
backend/check_lasix.py Normal file
View File

@ -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:,}")

View File

@ -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 테이블 없음!')

View File

@ -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}")

View File

@ -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}")

View File

@ -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()

View File

@ -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 @@
}
<div class="product-info">
<span class="product-name">${escapeHtml(item.product_name)}</span>
<span class="product-code">${item.drug_code}${item.supplier ? ` · ${escapeHtml(item.supplier)}` : ''}${item.location ? ` <span class="location-badge">📍${escapeHtml(item.location)}</span>` : ''}</span>
<span class="product-code">${item.drug_code}${item.supplier ? ` · ${escapeHtml(item.supplier)}` : ''}${item.location ? ` <span class="location-badge">📍${escapeHtml(item.location)}</span>` : ''}${item.patient_names ? ` <span class="patient-badge ${item.today_patients ? 'has-today' : ''}" title="${item.patient_count}명 사용${item.today_patients ? ' (오늘: ' + item.today_patients + ')' : ''}">👤${formatPatientNames(item.patient_names, item.today_patients)}</span>` : ''}</span>
</div>
</div>
</td>
@ -1861,6 +1883,23 @@
return str.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 `<strong class="today-patient">${escapeHtml(trimmed)}</strong>`;
}
return escapeHtml(trimmed);
}).join(', ');
}
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;

View File

@ -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}")

View File

@ -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}')

40
backend/test_today_rx.py Normal file
View File

@ -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}')