diff --git a/backend/app.py b/backend/app.py index b2cf378..191f40a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3639,6 +3639,69 @@ def api_products(): return jsonify({'success': False, 'error': str(e)}), 500 +# ==================== 입고이력 API ==================== + +@app.route('/api/drugs//purchase-history') +def api_drug_purchase_history(drug_code): + """ + 약품 입고이력 조회 API + - WH_sub: 입고 상세 (약품코드, 수량, 단가) + - WH_main: 입고 마스터 (입고일, 도매상코드) + - PM_BASE.CD_custom: 도매상명 + """ + try: + drug_session = db_manager.get_session('PM_DRUG') + + # 입고이력 조회 (최근 100건) + result = drug_session.execute(text(""" + SELECT TOP 100 + m.WH_DT_appl as purchase_date, + COALESCE(c.CD_NM_custom, m.WH_BUSINAME, '미확인') as supplier_name, + CAST(COALESCE(s.WH_NM_item_a, 0) AS INT) as quantity, + CAST(COALESCE(s.WH_MY_unit_a, 0) AS INT) as unit_price, + c.CD_TEL_charge1 as supplier_tel + FROM WH_sub s + JOIN WH_main m ON m.WH_NO_stock = s.WH_SR_stock AND m.WH_DT_appl = s.WH_DT_appl + LEFT JOIN PM_BASE.dbo.CD_custom c ON m.WH_CD_cust_sale = c.CD_CD_custom + WHERE s.DrugCode = :drug_code + ORDER BY m.WH_DT_appl DESC + """), {'drug_code': drug_code}) + + history = [] + for row in result.fetchall(): + # 날짜 포맷팅 (YYYYMMDD -> YYYY-MM-DD) + date_str = row.purchase_date or '' + if len(date_str) == 8: + date_str = f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}" + + history.append({ + 'date': date_str, + 'supplier': row.supplier_name or '미확인', + 'quantity': row.quantity or 0, + 'unit_price': row.unit_price or 0, + 'supplier_tel': row.supplier_tel or '' + }) + + # 약품명 조회 + name_result = drug_session.execute(text(""" + SELECT GoodsName FROM CD_GOODS WHERE DrugCode = :drug_code + """), {'drug_code': drug_code}) + name_row = name_result.fetchone() + drug_name = name_row[0] if name_row else drug_code + + return jsonify({ + 'success': True, + 'drug_code': drug_code, + 'drug_name': drug_name, + 'history': history, + 'count': len(history) + }) + + except Exception as e: + logging.error(f"입고이력 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + # ==================== 위치 정보 API ==================== @app.route('/api/locations') @@ -5245,7 +5308,9 @@ def admin_return_management(): @app.route('/api/return-candidates') def api_return_candidates(): - """반품 후보 목록 조회 API""" + """반품 후보 목록 조회 API (가격 정보 포함)""" + import pyodbc + status = request.args.get('status', '') urgency = request.args.get('urgency', '') search = request.args.get('search', '') @@ -5292,13 +5357,81 @@ def api_return_candidates(): cursor.execute(query, params) rows = cursor.fetchall() + # 약품코드 목록 추출 + drug_codes = [row['drug_code'] for row in rows] + + # MSSQL에서 가격 정보 조회 (한 번에) + price_map = {} + if drug_codes: + try: + mssql_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=5' + ) + mssql_conn = pyodbc.connect(mssql_conn_str, timeout=5) + mssql_cursor = mssql_conn.cursor() + + # IN 절 생성 (SQL Injection 방지를 위해 파라미터화) + # Price가 없으면 Saleprice, Topprice 순으로 fallback + placeholders = ','.join(['?' for _ in drug_codes]) + mssql_cursor.execute(f""" + SELECT DrugCode, + COALESCE(NULLIF(Price, 0), NULLIF(Saleprice, 0), NULLIF(Topprice, 0), 0) as BestPrice + FROM CD_GOODS + WHERE DrugCode IN ({placeholders}) + """, drug_codes) + + for row in mssql_cursor.fetchall(): + price_map[row[0]] = float(row[1]) if row[1] else 0 + + mssql_conn.close() + except Exception as e: + logging.warning(f"MSSQL 가격 조회 실패: {e}") + + # 전체 데이터 조회 (통계용) + cursor.execute("SELECT drug_code, current_stock, months_since_use, months_since_purchase FROM return_candidates") + all_rows = cursor.fetchall() + + # 긴급도별 금액 합계 계산 + total_amount = 0 + critical_amount = 0 + warning_amount = 0 + + for row in all_rows: + code = row['drug_code'] + stock = row['current_stock'] or 0 + price = price_map.get(code, 0) + amount = stock * price + + months_use = row['months_since_use'] or 0 + months_purchase = row['months_since_purchase'] or 0 + max_months = max(months_use, months_purchase) + + total_amount += amount + if max_months >= 36: + critical_amount += amount + elif max_months >= 24: + warning_amount += amount + items = [] for row in rows: + code = row['drug_code'] + stock = row['current_stock'] or 0 + price = price_map.get(code, 0) + recoverable = stock * price + items.append({ 'id': row['id'], - 'drug_code': row['drug_code'], + 'drug_code': code, 'drug_name': row['drug_name'], - 'current_stock': row['current_stock'], + 'current_stock': stock, + 'unit_price': price, + 'recoverable_amount': recoverable, 'last_prescription_date': row['last_prescription_date'], 'months_since_use': row['months_since_use'], 'last_purchase_date': row['last_purchase_date'], @@ -5337,7 +5470,10 @@ def api_return_candidates(): 'warning': warning, 'pending': pending, 'processed': processed, - 'total': total + 'total': total, + 'total_amount': total_amount, + 'critical_amount': critical_amount, + 'warning_amount': warning_amount } }) diff --git a/backend/check_barcodes.py b/backend/check_barcodes.py deleted file mode 100644 index e7af70c..0000000 --- a/backend/check_barcodes.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -바코드가 있는 제품 샘플 조회 -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(__file__)) - -from db.dbsetup import DatabaseManager -from sqlalchemy import text - -def check_barcode_samples(): - """바코드가 있는 제품 샘플 조회""" - db_manager = DatabaseManager() - - try: - session = db_manager.get_session('PM_PRES') - - # 바코드가 있는 제품 샘플 조회 - query = text(""" - SELECT TOP 10 - S.DrugCode, - S.BARCODE, - G.GoodsName, - S.SL_NM_cost_a as price - FROM SALE_SUB S - LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode - WHERE S.BARCODE IS NOT NULL AND S.BARCODE != '' - ORDER BY S.SL_NO_order DESC - """) - - results = session.execute(query).fetchall() - - print('=' * 100) - print('바코드가 있는 제품 샘플 (최근 10개)') - print('=' * 100) - for r in results: - barcode = r.BARCODE if r.BARCODE else '(없음)' - goods_name = r.GoodsName if r.GoodsName else '(약품명 없음)' - print(f'DrugCode: {r.DrugCode:20} | BARCODE: {barcode:20} | 제품명: {goods_name}') - print('=' * 100) - - # 바코드 통계 - stats_query = text(""" - SELECT - COUNT(DISTINCT DrugCode) as total_drugs, - COUNT(DISTINCT BARCODE) as total_barcodes, - SUM(CASE WHEN BARCODE IS NOT NULL AND BARCODE != '' THEN 1 ELSE 0 END) as with_barcode, - COUNT(*) as total_sales - FROM SALE_SUB - """) - - stats = session.execute(stats_query).fetchone() - - print('\n바코드 통계') - print('=' * 100) - print(f'전체 제품 수 (DrugCode): {stats.total_drugs:,}') - print(f'바코드 종류 수: {stats.total_barcodes:,}') - print(f'바코드가 있는 판매 건수: {stats.with_barcode:,}') - print(f'전체 판매 건수: {stats.total_sales:,}') - print(f'바코드 보유율: {stats.with_barcode / stats.total_sales * 100:.2f}%') - print('=' * 100) - - except Exception as e: - print(f"오류 발생: {e}") - finally: - db_manager.close_all() - -if __name__ == '__main__': - check_barcode_samples() diff --git a/backend/check_basen.py b/backend/check_basen.py deleted file mode 100644 index aa82b1c..0000000 --- a/backend/check_basen.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -print("=== 어제 베이슨 주문 (지오영 + 수인) ===\n") - -# 지오영 -geo = requests.get('http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date=2026-03-06&end_date=2026-03-06', timeout=120).json() -print("【지오영】") -found = False -for kd, info in geo.get('by_kd_code', {}).items(): - if '베이슨' in info['product_name']: - print(f" KD: {kd}") - print(f" 제품명: {info['product_name']}") - print(f" spec: {info['spec']}") - print(f" boxes: {info['boxes']}") - print(f" units: {info['units']}") - found = True -if not found: - print(" (없음)") - -print() - -# 수인 -sooin = requests.get('http://localhost:7001/api/sooin/orders/summary-by-kd?start_date=2026-03-06&end_date=2026-03-06', timeout=120).json() -print("【수인약품】") -found = False -for kd, info in sooin.get('by_kd_code', {}).items(): - if '베이슨' in info['product_name']: - print(f" KD: {kd}") - print(f" 제품명: {info['product_name']}") - print(f" spec: {info['spec']}") - print(f" boxes: {info['boxes']}") - print(f" units: {info['units']}") - found = True -if not found: - print(" (없음)") diff --git a/backend/check_basen_detail.py b/backend/check_basen_detail.py deleted file mode 100644 index 5e356d0..0000000 --- a/backend/check_basen_detail.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -import requests -import sys -sys.path.insert(0, 'c:/Users/청춘약국/source/pharmacy-wholesale-api') -from dotenv import load_dotenv -load_dotenv('c:/Users/청춘약국/source/pharmacy-wholesale-api/.env') -from wholesale import GeoYoungSession - -# 지오영 베이슨 검색 -print("=== 지오영 베이슨 검색 결과 ===") -res = requests.get('http://localhost:7001/api/geoyoung/stock?keyword=베이슨', timeout=30).json() -for item in res.get('items', []): - internal = item.get('internal_code', '') - spec = item.get('specification', '') - name = item.get('product_name', '') - print(f" 내부: {internal} | spec: {spec:8} | {name}") - -print() - -# 어제 주문 상세 -print("=== 어제 베이슨 주문 상세 ===") -session = GeoYoungSession() -session.login() -result = session.get_order_list('2026-03-06', '2026-03-06') - -if result.get('success'): - for order in result.get('orders', []): - for item in order.get('items', []): - name = item.get('product_name', '') - if '베이슨' in name: - internal = item.get('product_code', '') - qty = item.get('quantity', 0) - status = item.get('status', '') - print(f" 주문번호: {order.get('order_num')}") - print(f" 제품명: {name}") - print(f" 내부코드: {internal}") - print(f" 수량: {qty}박스") - print(f" 상태: {status}") - print() diff --git a/backend/check_basen_html.py b/backend/check_basen_html.py deleted file mode 100644 index d972621..0000000 --- a/backend/check_basen_html.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -sys.path.insert(0, 'c:/Users/청춘약국/source/pharmacy-wholesale-api') -from dotenv import load_dotenv -load_dotenv('c:/Users/청춘약국/source/pharmacy-wholesale-api/.env') -from wholesale import GeoYoungSession -from bs4 import BeautifulSoup - -session = GeoYoungSession() -session.login() - -# MyPage HTML 직접 확인 -resp = session.session.get( - f"{session.BASE_URL}/MyPage", - params={'dtpFrom': '2026-03-06', 'dtpTo': '2026-03-06'}, - timeout=30 -) - -soup = BeautifulSoup(resp.text, 'html.parser') -table = soup.find('table') -tbody = table.find('tbody') or table -rows = tbody.find_all('tr') - -print("=== 베이슨 행 HTML 분석 ===\n") -for row in rows: - cells = row.find_all('td') - if not cells or len(cells) < 10: - continue - - name = cells[1].get_text(strip=True) - if '베이슨' not in name: - continue - - status = cells[9].get_text(strip=True) if len(cells) > 9 else '' - print(f"제품명: {name}") - print(f"상태: {status}") - - # 모든 버튼의 onclick 확인 - print("버튼들:") - for cell in cells: - for btn in cell.find_all('button'): - onclick = btn.get('onclick', '') - btn_text = btn.get_text(strip=True) - print(f" [{btn_text}] onclick: {onclick[:80]}...") - - print() diff --git a/backend/check_cart.py b/backend/check_cart.py deleted file mode 100644 index bd0aa60..0000000 --- a/backend/check_cart.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -import sys; sys.path.insert(0, '.'); import wholesale_path -from wholesale import SooinSession - -s = SooinSession() -s.login() -cart = s.get_cart() -print(f"장바구니: {cart['total_items']}개, {cart['total_amount']:,}원") diff --git a/backend/check_db.py b/backend/check_db.py deleted file mode 100644 index 9d6de43..0000000 --- a/backend/check_db.py +++ /dev/null @@ -1,25 +0,0 @@ -import sqlite3 -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: - 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 deleted file mode 100644 index 4cf0298..0000000 --- a/backend/check_lasix.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -sys.path.insert(0, 'c:/Users/청춘약국/source/pharmacy-wholesale-api') -from dotenv import load_dotenv -load_dotenv('c:/Users/청춘약국/source/pharmacy-wholesale-api/.env') -from wholesale import GeoYoungSession - -session = GeoYoungSession() -session.login() - -# 어제 (3월 6일) 주문 조회 -result = session.get_order_list('2026-03-06', '2026-03-06') - -if result.get('success'): - # KD코드 매핑 - for order in result.get('orders', []): - items = order.get('items', []) - if items: - session._enrich_kd_codes(items) - - print("=== 어제 라식스 주문 상세 ===") - total_boxes = 0 - for order in result.get('orders', []): - for item in order.get('items', []): - name = item.get('product_name', '') - if '라식스' in name: - status = item.get('status', '') - qty = item.get('quantity', 0) - spec = item.get('spec', '') - internal = item.get('product_code', '') - kd = item.get('kd_code', '') - order_num = order.get('order_num', '') - print(f" 주문번호: {order_num}") - print(f" 제품명: {name}") - print(f" 내부코드: {internal}") - print(f" KD코드: {kd}") - print(f" spec: {spec}") - print(f" 수량: {qty}박스") - print(f" 상태: {status}") - print() - - if '취소' not in status and '삭제' not in status: - total_boxes += qty - - print(f"=== 합계 (취소 제외): {total_boxes}박스 ===") -else: - print(f"오류: {result.get('error')}") diff --git a/backend/check_lasix_spec.py b/backend/check_lasix_spec.py deleted file mode 100644 index c6f32a7..0000000 --- a/backend/check_lasix_spec.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -res = requests.get('http://localhost:7001/api/geoyoung/stock?keyword=라식스', timeout=30).json() -print("=== 지오영 라식스 검색 ===") -for item in res.get('items', []): - internal = item.get('internal_code', '') - spec = item.get('specification', '') - name = item.get('product_name', '') - print(f" 내부: {internal} | spec: {spec} | {name}") diff --git a/backend/check_megace_code.py b/backend/check_megace_code.py deleted file mode 100644 index aa5c79f..0000000 --- a/backend/check_megace_code.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -# 메게이스 검색 -res = requests.get('http://localhost:7001/api/geoyoung/stock?keyword=메게이스', timeout=30).json() - -print("=== 지오영 메게이스 검색 결과 ===") -for item in res.get('items', []): - internal = item.get('internal_code', '') - spec = item.get('specification', '') - name = item.get('product_name', '')[:35] - kd = item.get('insurance_code', '') - print(f" 내부코드: {internal:8} | KD: {kd} | spec: {spec:6} | {name}") - -print() -print("찾는 내부코드: 043735") diff --git a/backend/check_megace_fixed.py b/backend/check_megace_fixed.py deleted file mode 100644 index c3cd7ac..0000000 --- a/backend/check_megace_fixed.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -res = requests.get('http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date=2026-03-07&end_date=2026-03-07', timeout=120).json() - -print("=== 메게이스 주문 확인 ===") -for kd, info in res.get('by_kd_code', {}).items(): - if '메게이스' in info['product_name']: - print(f"KD: {kd}") - print(f"제품명: {info['product_name']}") - print(f"spec: {info['spec']}") - print(f"boxes: {info['boxes']}") - print(f"units: {info['units']}") - print() diff --git a/backend/check_order.py b/backend/check_order.py deleted file mode 100644 index 82eb654..0000000 --- a/backend/check_order.py +++ /dev/null @@ -1,6 +0,0 @@ -import requests -res = requests.get('http://localhost:7001/api/geoyoung/order-detail/DA2603-0255533', timeout=120) -data = res.json() -print('items:', len(data.get('items', []))) -for item in data.get('items', []): - print(f" {item.get('product_name')[:20]}: qty={item.get('quantity')}, order_qty={item.get('order_qty')}, amount={item.get('amount')}") diff --git a/backend/check_order_db.py b/backend/check_order_db.py deleted file mode 100644 index d0d3b77..0000000 --- a/backend/check_order_db.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -import sqlite3 - -conn = sqlite3.connect('db/orders.db') - -# 테이블 목록 -tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() -print('=== orders.db 테이블 ===') -for t in tables: - count = conn.execute(f'SELECT COUNT(*) FROM {t[0]}').fetchone()[0] - print(f' {t[0]}: {count}개 레코드') - -conn.close() diff --git a/backend/check_orders_db.py b/backend/check_orders_db.py deleted file mode 100644 index 0eea8a9..0000000 --- a/backend/check_orders_db.py +++ /dev/null @@ -1,32 +0,0 @@ -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_paai_db.py b/backend/check_paai_db.py deleted file mode 100644 index aee2a6c..0000000 --- a/backend/check_paai_db.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -import sqlite3 - -conn = sqlite3.connect('db/paai_logs.db') - -# 테이블 목록 -cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'") -tables = cursor.fetchall() -print('테이블 목록:', [t[0] for t in tables]) - -# 로그 개수 -count = conn.execute('SELECT COUNT(*) FROM paai_logs').fetchone()[0] -print(f'PAAI 로그 수: {count}개') - -# 최근 로그 -print('\n최근 로그 3개:') -recent = conn.execute('SELECT id, created_at, patient_name, status FROM paai_logs ORDER BY id DESC LIMIT 3').fetchall() -for r in recent: - print(f' #{r[0]} | {r[1]} | {r[2]} | {r[3]}') - -# 피드백 통계 -feedback = conn.execute('SELECT feedback_useful, COUNT(*) FROM paai_logs GROUP BY feedback_useful').fetchall() -print('\n피드백 통계:') -for f in feedback: - label = '유용' if f[0] == 1 else ('아님' if f[0] == 0 else '미응답') - print(f' {label}: {f[1]}건') - -conn.close() diff --git a/backend/check_paai_status.py b/backend/check_paai_status.py deleted file mode 100644 index 1e76109..0000000 --- a/backend/check_paai_status.py +++ /dev/null @@ -1,40 +0,0 @@ -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() diff --git a/backend/check_patient_columns.py b/backend/check_patient_columns.py deleted file mode 100644 index 4c111b8..0000000 --- a/backend/check_patient_columns.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- 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_pets.py b/backend/check_pets.py deleted file mode 100644 index efb9fa4..0000000 --- a/backend/check_pets.py +++ /dev/null @@ -1,23 +0,0 @@ -import sqlite3 - -conn = sqlite3.connect('db/mileage.db') -c = conn.cursor() - -# 테이블 구조 -c.execute("SELECT sql FROM sqlite_master WHERE name='pets'") -print("=== PETS TABLE SCHEMA ===") -print(c.fetchone()) - -# 샘플 데이터 -c.execute("SELECT * FROM pets LIMIT 5") -print("\n=== SAMPLE DATA ===") -for row in c.fetchall(): - print(row) - -# 컬럼명 -c.execute("PRAGMA table_info(pets)") -print("\n=== COLUMNS ===") -for col in c.fetchall(): - print(col) - -conn.close() diff --git a/backend/check_price_columns.py b/backend/check_price_columns.py deleted file mode 100644 index 8fc3691..0000000 --- a/backend/check_price_columns.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- 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/check_raninex.py b/backend/check_raninex.py deleted file mode 100644 index e1442af..0000000 --- a/backend/check_raninex.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -# 지오영 확인 -geo = requests.get('http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date=2026-03-07&end_date=2026-03-07', timeout=120).json() -print("=== 지오영 ===") -for kd, info in geo.get('by_kd_code', {}).items(): - if '라니넥스' in info['product_name'] or '나잘' in info['product_name']: - print(f" KD: {kd}") - print(f" product_name: {info['product_name']}") - print(f" spec: {info['spec']}") - print(f" boxes: {info['boxes']}") - print(f" units: {info['units']}") - print() - -# 수인 확인 -sooin = requests.get('http://localhost:7001/api/sooin/orders/summary-by-kd?start_date=2026-03-07&end_date=2026-03-07', timeout=120).json() -print("=== 수인약품 ===") -found = False -for kd, info in sooin.get('by_kd_code', {}).items(): - if '라니넥스' in info['product_name'] or '나잘' in info['product_name']: - print(f" KD: {kd}") - print(f" product_name: {info['product_name']}") - print(f" spec: {info['spec']}") - print(f" boxes: {info['boxes']}") - print(f" units: {info['units']}") - found = True - print() - -if not found: - print(" (없음)") diff --git a/backend/check_sale_data.py b/backend/check_sale_data.py deleted file mode 100644 index 9a53af3..0000000 --- a/backend/check_sale_data.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -특정 거래의 SALE_SUB 데이터 확인 -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(__file__)) - -from db.dbsetup import DatabaseManager -from sqlalchemy import text - -def check_sale_sub_data(transaction_id): - """특정 거래의 판매 상세 데이터 확인""" - db_manager = DatabaseManager() - - try: - session = db_manager.get_session('PM_PRES') - - # SALE_SUB 모든 컬럼 조회 - query = text(""" - SELECT * - FROM SALE_SUB - WHERE SL_NO_order = :transaction_id - """) - - result = session.execute(query, {'transaction_id': transaction_id}).fetchone() - - if result: - print("=" * 80) - print(f"거래번호 {transaction_id}의 SALE_SUB 데이터") - print("=" * 80) - - # 모든 컬럼 출력 - for key in result._mapping.keys(): - value = result._mapping[key] - print(f"{key:30} = {value}") - - print("=" * 80) - else: - print(f"거래번호 {transaction_id}를 찾을 수 없습니다.") - - except Exception as e: - print(f"오류 발생: {e}") - finally: - db_manager.close_all() - -def check_sale_main_data(transaction_id): - """특정 거래의 SALE_MAIN 데이터 확인""" - db_manager = DatabaseManager() - - try: - session = db_manager.get_session('PM_PRES') - - query = text(""" - SELECT * - FROM SALE_MAIN - WHERE SL_NO_order = :transaction_id - """) - - result = session.execute(query, {'transaction_id': transaction_id}).fetchone() - - if result: - print("\n" + "=" * 80) - print(f"거래번호 {transaction_id}의 SALE_MAIN 데이터") - print("=" * 80) - - for key in result._mapping.keys(): - value = result._mapping[key] - print(f"{key:30} = {value}") - - print("=" * 80) - else: - print(f"거래번호 {transaction_id}를 찾을 수 없습니다.") - - except Exception as e: - print(f"오류 발생: {e}") - finally: - db_manager.close_all() - -if __name__ == '__main__': - # 스크린샷의 거래번호 - check_sale_sub_data('20260123000261') - check_sale_main_data('20260123000261') diff --git a/backend/check_schema.py b/backend/check_schema.py deleted file mode 100644 index b923f97..0000000 --- a/backend/check_schema.py +++ /dev/null @@ -1,6 +0,0 @@ -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() diff --git a/backend/check_search_spec.py b/backend/check_search_spec.py deleted file mode 100644 index d6c980c..0000000 --- a/backend/check_search_spec.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -# 스틸녹스 검색해서 spec 필드 확인 -res = requests.get('http://localhost:7001/api/geoyoung/stock?keyword=스틸녹스', timeout=30) -data = res.json() - -print("=== 지오영 제품 검색 결과 ===") -for item in data.get('items', [])[:3]: - print(f" product_name: {item.get('product_name')}") - print(f" specification: {item.get('specification')}") - print(f" stock: {item.get('stock')}") - print() - -# 수인도 확인 -res2 = requests.get('http://localhost:7001/api/sooin/stock?keyword=스틸녹스', timeout=30) -data2 = res2.json() - -print("=== 수인 제품 검색 결과 ===") -for item in data2.get('items', [])[:3]: - print(f" name: {item.get('name')}") - print(f" spec: {item.get('spec')}") - print(f" stock: {item.get('stock')}") - print() diff --git a/backend/check_sooin_cart.py b/backend/check_sooin_cart.py deleted file mode 100644 index 576bf11..0000000 --- a/backend/check_sooin_cart.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -import sys; sys.path.insert(0, '.'); import wholesale_path -from wholesale import SooinSession - -s = SooinSession() -s.login() - -cart = s.get_cart() -print(f'성공: {cart["success"]}') -print(f'품목 수: {cart["total_items"]}') -print(f'총액: {cart["total_amount"]:,}원') -print() - -if cart['items']: - print('=== 장바구니 품목 ===') - for item in cart['items']: - status = '✅' if item.get('active') else '❌취소' - name = item['product_name'][:30] - print(f"{status} {name:30} x{item['quantity']} = {item['amount']:,}원") -else: - print('🛒 장바구니 비어있음') diff --git a/backend/check_spec.py b/backend/check_spec.py deleted file mode 100644 index 37596b5..0000000 --- a/backend/check_spec.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -geo = requests.get('http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date=2026-03-07&end_date=2026-03-07', timeout=120).json() - -print("품목별 spec 필드 확인:") -for kd, info in geo.get('by_kd_code', {}).items(): - print(f" spec='{info['spec']}' | {info['product_name'][:30]}") diff --git a/backend/check_status.py b/backend/check_status.py deleted file mode 100644 index 313716a..0000000 --- a/backend/check_status.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -sys.path.insert(0, 'c:/Users/청춘약국/source/pharmacy-wholesale-api') -from dotenv import load_dotenv -load_dotenv('c:/Users/청춘약국/source/pharmacy-wholesale-api/.env') -from wholesale import GeoYoungSession - -session = GeoYoungSession() -session.login() - -result = session.get_order_list('2026-03-01', '2026-03-07') - -if result.get('success'): - status_set = set() - for order in result.get('orders', []): - for item in order.get('items', []): - status = item.get('status', '').strip() - if status: - status_set.add(status) - - print("=== 발견된 상태값들 ===") - for s in sorted(status_set): - print(f" '{s}'") -else: - print(f"오류: {result.get('error')}") diff --git a/backend/check_stilnox.py b/backend/check_stilnox.py deleted file mode 100644 index 77d36ca..0000000 --- a/backend/check_stilnox.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -import requests - -# 지오영 확인 -geo = requests.get('http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date=2026-03-07&end_date=2026-03-07', timeout=120).json() -print("=== 지오영 ===") -for kd, info in geo.get('by_kd_code', {}).items(): - if '스틸' in info['product_name'] or '녹스' in info['product_name']: - print(f" {info['product_name']}: {info['boxes']}박스, {info['units']}개") - -# 수인 확인 -sooin = requests.get('http://localhost:7001/api/sooin/orders/summary-by-kd?start_date=2026-03-07&end_date=2026-03-07', timeout=120).json() -print("\n=== 수인약품 ===") -for kd, info in sooin.get('by_kd_code', {}).items(): - if '스틸' in info['product_name'] or '녹스' in info['product_name']: - print(f" {info['product_name']}: {info['boxes']}박스, {info['units']}개") - -if not any('스틸' in info['product_name'] for info in sooin.get('by_kd_code', {}).values()): - print(" (없음)") diff --git a/backend/check_summary.py b/backend/check_summary.py deleted file mode 100644 index ee65763..0000000 --- a/backend/check_summary.py +++ /dev/null @@ -1,9 +0,0 @@ -import requests -res = requests.get('http://localhost:7001/api/geoyoung/orders/summary-by-kd?start_date=2026-03-07&end_date=2026-03-07', timeout=120) -data = res.json() -print('success:', data.get('success')) -print('order_count:', data.get('order_count')) -print('total_products:', data.get('total_products')) -print() -for kd, info in list(data.get('by_kd_code', {}).items()): - print(f"{kd}: boxes={info['boxes']}, units={info['units']}, {info['product_name'][:20]}") diff --git a/backend/check_table_schema.py b/backend/check_table_schema.py deleted file mode 100644 index 71432a0..0000000 --- a/backend/check_table_schema.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -SALE_MAIN 테이블 컬럼 확인 스크립트 -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(__file__)) - -from db.dbsetup import DatabaseManager -from sqlalchemy import text - -def check_sale_table_columns(table_name): - """테이블의 모든 컬럼 확인""" - db_manager = DatabaseManager() - - try: - session = db_manager.get_session('PM_PRES') - - # SQL Server에서 테이블 컬럼 정보 조회 - query = text(f""" - SELECT - COLUMN_NAME, - DATA_TYPE, - CHARACTER_MAXIMUM_LENGTH, - IS_NULLABLE - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_NAME = '{table_name}' - ORDER BY ORDINAL_POSITION - """) - - columns = session.execute(query).fetchall() - - print("=" * 80) - print(f"{table_name} 테이블 컬럼 목록") - print("=" * 80) - - for col in columns: - nullable = "NULL" if col.IS_NULLABLE == 'YES' else "NOT NULL" - max_len = f"({col.CHARACTER_MAXIMUM_LENGTH})" if col.CHARACTER_MAXIMUM_LENGTH else "" - print(f"{col.COLUMN_NAME:30} {col.DATA_TYPE}{max_len:20} {nullable}") - - print("=" * 80) - print(f"총 {len(columns)}개 컬럼") - print("=" * 80) - - except Exception as e: - print(f"오류 발생: {e}") - finally: - db_manager.close_all() - -if __name__ == '__main__': - check_sale_table_columns('SALE_MAIN') - print("\n\n") - check_sale_table_columns('SALE_SUB') diff --git a/backend/templates/admin_products.html b/backend/templates/admin_products.html index b405240..2ec0981 100644 --- a/backend/templates/admin_products.html +++ b/backend/templates/admin_products.html @@ -590,6 +590,135 @@ .location-modal-btn.primary { background: #f59e0b; color: #fff; } .location-modal-btn.primary:hover { background: #d97706; } + /* ── 입고이력 모달 ── */ + .purchase-modal { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 2000; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + } + .purchase-modal.show { display: flex; } + .purchase-modal-content { + background: #fff; + border-radius: 16px; + padding: 0; + max-width: 600px; + width: 95%; + max-height: 80vh; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + animation: modalSlideIn 0.2s ease; + overflow: hidden; + display: flex; + flex-direction: column; + } + .purchase-modal-header { + padding: 20px 24px; + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + color: #fff; + } + .purchase-modal-header h3 { + margin: 0 0 6px 0; + font-size: 18px; + display: flex; + align-items: center; + gap: 8px; + } + .purchase-modal-header .drug-name { + font-size: 14px; + opacity: 0.9; + } + .purchase-modal-body { + padding: 16px 24px 24px; + overflow-y: auto; + flex: 1; + } + .purchase-history-table { + width: 100%; + border-collapse: collapse; + } + .purchase-history-table th { + background: #f8fafc; + padding: 12px 10px; + font-size: 12px; + font-weight: 600; + color: #64748b; + text-align: left; + border-bottom: 2px solid #e2e8f0; + position: sticky; + top: 0; + } + .purchase-history-table td { + padding: 14px 10px; + font-size: 14px; + border-bottom: 1px solid #f1f5f9; + } + .purchase-history-table tr:hover td { + background: #f8fafc; + } + .supplier-name { + font-weight: 600; + color: #1e293b; + } + .supplier-tel { + font-size: 12px; + color: #3b82f6; + cursor: pointer; + } + .supplier-tel:hover { + text-decoration: underline; + } + .purchase-date { + color: #64748b; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + } + .purchase-qty { + font-weight: 600; + color: #10b981; + } + .purchase-price { + color: #6b7280; + } + .purchase-empty { + text-align: center; + padding: 40px 20px; + color: #94a3b8; + } + .purchase-empty .icon { + font-size: 40px; + margin-bottom: 12px; + } + .purchase-modal-footer { + padding: 16px 24px; + border-top: 1px solid #e2e8f0; + display: flex; + justify-content: flex-end; + } + .purchase-modal-btn { + padding: 10px 24px; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + font-size: 14px; + background: #f1f5f9; + color: #64748b; + transition: all 0.15s; + } + .purchase-modal-btn:hover { + background: #e2e8f0; + } + tbody tr { + cursor: pointer; + } + tbody tr:active { + background: #ede9fe; + } + /* ── 가격 ── */ .price { font-weight: 600; @@ -916,6 +1045,7 @@
@@ -993,6 +1123,34 @@
+ +
+
+
+

📦 입고 이력

+
-
+
+
+ + + + + + + + + + + + +
도매상입고일수량단가
📭

로딩 중...

+
+ +
+
+ diff --git a/backend/templates/admin_return_management.html b/backend/templates/admin_return_management.html index da9e7d6..39baeec 100644 --- a/backend/templates/admin_return_management.html +++ b/backend/templates/admin_return_management.html @@ -25,6 +25,7 @@ --accent-rose: #f43f5e; --accent-orange: #f97316; --accent-cyan: #06b6d4; + --accent-gold: #eab308; } * { margin: 0; padding: 0; box-sizing: border-box; } @@ -47,7 +48,7 @@ box-shadow: 0 4px 20px rgba(0,0,0,0.3); } .header-inner { - max-width: 1600px; + max-width: 1800px; margin: 0 auto; display: flex; justify-content: space-between; @@ -90,22 +91,25 @@ /* ══════════════════ 컨텐츠 ══════════════════ */ .content { - max-width: 1600px; + max-width: 1800px; margin: 0 auto; padding: 24px; } - /* ══════════════════ 통계 카드 ══════════════════ */ - .stats-grid { - display: grid; - grid-template-columns: repeat(5, 1fr); + /* ══════════════════ 통계 카드 (2줄) ══════════════════ */ + .stats-row { + display: flex; gap: 16px; + margin-bottom: 16px; + } + .stats-row.amount-row { margin-bottom: 24px; } .stat-card { + flex: 1; background: var(--bg-card); border-radius: 14px; - padding: 20px; + padding: 18px 20px; border: 1px solid var(--border); position: relative; overflow: hidden; @@ -123,22 +127,27 @@ .stat-card.cyan::before { background: var(--accent-cyan); } .stat-card.emerald::before { background: var(--accent-emerald); } .stat-card.purple::before { background: var(--accent-purple); } + .stat-card.gold::before { background: var(--accent-gold); } .stat-icon { - font-size: 24px; - margin-bottom: 12px; + font-size: 20px; + margin-bottom: 8px; } .stat-value { - font-size: 26px; + font-size: 24px; font-weight: 700; letter-spacing: -1px; - margin-bottom: 4px; + margin-bottom: 2px; + } + .stat-value.small { + font-size: 18px; } .stat-card.rose .stat-value { color: var(--accent-rose); } .stat-card.amber .stat-value { color: var(--accent-amber); } .stat-card.cyan .stat-value { color: var(--accent-cyan); } .stat-card.emerald .stat-value { color: var(--accent-emerald); } .stat-card.purple .stat-value { color: var(--accent-purple); } + .stat-card.gold .stat-value { color: var(--accent-gold); } .stat-label { font-size: 11px; @@ -148,9 +157,9 @@ letter-spacing: 0.5px; } .stat-sub { - font-size: 11px; + font-size: 10px; color: var(--text-muted); - margin-top: 4px; + margin-top: 2px; } /* ══════════════════ 필터 바 ══════════════════ */ @@ -259,7 +268,7 @@ border-collapse: collapse; } .data-table th { - padding: 14px 16px; + padding: 14px 12px; font-size: 11px; font-weight: 600; color: var(--text-muted); @@ -270,11 +279,12 @@ border-bottom: 1px solid var(--border); position: sticky; top: 0; + white-space: nowrap; } .data-table th.center { text-align: center; } .data-table th.right { text-align: right; } .data-table td { - padding: 14px 16px; + padding: 12px; font-size: 13px; border-bottom: 1px solid rgba(255,255,255,0.05); vertical-align: middle; @@ -293,15 +303,16 @@ .drug-cell { display: flex; flex-direction: column; - gap: 4px; + gap: 2px; } .drug-name { font-weight: 600; color: var(--text-primary); + font-size: 12px; } .drug-code { font-family: 'JetBrains Mono', monospace; - font-size: 11px; + font-size: 10px; color: var(--text-muted); } @@ -310,9 +321,9 @@ display: inline-flex; align-items: center; gap: 4px; - padding: 4px 10px; + padding: 4px 8px; border-radius: 6px; - font-size: 11px; + font-size: 10px; font-weight: 600; } .urgency-badge.critical { @@ -336,49 +347,44 @@ font-size: 11px; font-weight: 600; } - .status-badge.pending { - background: rgba(249, 115, 22, 0.2); - color: var(--accent-orange); - } - .status-badge.reviewed { - background: rgba(59, 130, 246, 0.2); - color: var(--accent-blue); - } - .status-badge.returned { - background: rgba(16, 185, 129, 0.2); - color: var(--accent-emerald); - } - .status-badge.keep { - background: rgba(168, 85, 247, 0.2); - color: var(--accent-purple); - } - .status-badge.disposed { - background: rgba(244, 63, 94, 0.2); - color: var(--accent-rose); - } - .status-badge.resolved { - background: rgba(100, 116, 139, 0.2); - color: var(--text-muted); - } + .status-badge.pending { background: rgba(249, 115, 22, 0.2); color: var(--accent-orange); } + .status-badge.reviewed { background: rgba(59, 130, 246, 0.2); color: var(--accent-blue); } + .status-badge.returned { background: rgba(16, 185, 129, 0.2); color: var(--accent-emerald); } + .status-badge.keep { background: rgba(168, 85, 247, 0.2); color: var(--accent-purple); } + .status-badge.disposed { background: rgba(244, 63, 94, 0.2); color: var(--accent-rose); } + .status-badge.resolved { background: rgba(100, 116, 139, 0.2); color: var(--text-muted); } - /* 수량/날짜 셀 */ + /* 수량/날짜/금액 셀 */ .qty-cell { font-family: 'JetBrains Mono', monospace; font-weight: 600; text-align: center; + font-size: 12px; } .date-cell { - font-size: 12px; + font-size: 11px; color: var(--text-secondary); } .months-cell { font-family: 'JetBrains Mono', monospace; font-weight: 700; + font-size: 12px; } .months-cell.critical { color: var(--accent-rose); } .months-cell.warning { color: var(--accent-orange); } .months-cell.normal { color: var(--text-muted); } + .amount-cell { + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + text-align: right; + font-size: 12px; + color: var(--accent-gold); + } + .amount-cell.zero { + color: var(--text-muted); + } + /* 액션 버튼 */ .action-btn { padding: 6px 12px; @@ -397,14 +403,6 @@ transform: translateY(-1px); box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4); } - .action-btn.secondary { - background: var(--bg-secondary); - border: 1px solid var(--border); - color: var(--text-secondary); - } - .action-btn.secondary:hover { - border-color: var(--text-muted); - } /* ══════════════════ 페이지네이션 ══════════════════ */ .pagination { @@ -425,23 +423,10 @@ cursor: pointer; transition: all 0.2s; } - .page-btn:hover { - border-color: var(--accent-orange); - color: var(--accent-orange); - } - .page-btn.active { - background: linear-gradient(135deg, var(--accent-orange), #ea580c); - border-color: transparent; - color: #fff; - } - .page-btn:disabled { - opacity: 0.3; - cursor: not-allowed; - } - .page-info { - color: var(--text-muted); - font-size: 13px; - } + .page-btn:hover { border-color: var(--accent-orange); color: var(--accent-orange); } + .page-btn.active { background: linear-gradient(135deg, var(--accent-orange), #ea580c); border-color: transparent; color: #fff; } + .page-btn:disabled { opacity: 0.3; cursor: not-allowed; } + .page-info { color: var(--text-muted); font-size: 13px; } /* ══════════════════ 모달 ══════════════════ */ .modal-overlay { @@ -453,9 +438,7 @@ justify-content: center; z-index: 1000; } - .modal-overlay.open { - display: flex; - } + .modal-overlay.open { display: flex; } .modal { background: var(--bg-card); border-radius: 20px; @@ -471,31 +454,12 @@ align-items: center; margin-bottom: 24px; } - .modal-title { - font-size: 18px; - font-weight: 700; - } - .modal-close { - background: none; - border: none; - color: var(--text-muted); - font-size: 24px; - cursor: pointer; - } - .modal-body { - margin-bottom: 24px; - } - .form-group { - margin-bottom: 16px; - } - .form-group label { - display: block; - font-size: 12px; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 8px; - } - .form-group select, .form-group input, .form-group textarea { + .modal-title { font-size: 18px; font-weight: 700; } + .modal-close { background: none; border: none; color: var(--text-muted); font-size: 24px; cursor: pointer; } + .modal-body { margin-bottom: 24px; } + .form-group { margin-bottom: 16px; } + .form-group label { display: block; font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; } + .form-group select, .form-group textarea { width: 100%; padding: 12px 16px; background: var(--bg-primary); @@ -505,19 +469,9 @@ font-family: inherit; color: var(--text-primary); } - .form-group textarea { - min-height: 100px; - resize: vertical; - } - .form-group select:focus, .form-group input:focus, .form-group textarea:focus { - outline: none; - border-color: var(--accent-orange); - } - .modal-footer { - display: flex; - justify-content: flex-end; - gap: 12px; - } + .form-group textarea { min-height: 100px; resize: vertical; } + .form-group select:focus, .form-group textarea:focus { outline: none; border-color: var(--accent-orange); } + .modal-footer { display: flex; justify-content: flex-end; gap: 12px; } .modal-btn { padding: 12px 24px; border-radius: 10px; @@ -526,73 +480,34 @@ cursor: pointer; transition: all 0.2s; } - .modal-btn.cancel { - background: var(--bg-secondary); - border: 1px solid var(--border); - color: var(--text-secondary); - } - .modal-btn.submit { - background: linear-gradient(135deg, var(--accent-orange), #ea580c); - border: none; - color: #fff; - } - .modal-btn.submit:hover { - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4); - } + .modal-btn.cancel { background: var(--bg-secondary); border: 1px solid var(--border); color: var(--text-secondary); } + .modal-btn.submit { background: linear-gradient(135deg, var(--accent-orange), #ea580c); border: none; color: #fff; } + .modal-btn.submit:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4); } - /* 약품 상세 정보 */ .drug-detail { background: var(--bg-secondary); border-radius: 12px; padding: 16px; margin-bottom: 20px; } - .drug-detail-name { - font-size: 16px; - font-weight: 700; - margin-bottom: 8px; - } - .drug-detail-info { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; - font-size: 13px; - } - .drug-detail-item { - display: flex; - justify-content: space-between; - } - .drug-detail-label { - color: var(--text-muted); - } - .drug-detail-value { - font-weight: 600; - font-family: 'JetBrains Mono', monospace; - } + .drug-detail-name { font-size: 16px; font-weight: 700; margin-bottom: 8px; } + .drug-detail-info { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px; } + .drug-detail-item { display: flex; justify-content: space-between; } + .drug-detail-label { color: var(--text-muted); } + .drug-detail-value { font-weight: 600; font-family: 'JetBrains Mono', monospace; } /* ══════════════════ 로딩/빈 상태 ══════════════════ */ - .loading-state, .empty-state { - text-align: center; - padding: 60px 20px; - color: var(--text-muted); - } + .loading-state, .empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); } .loading-spinner { - width: 40px; - height: 40px; + width: 40px; height: 40px; border: 3px solid var(--border); border-top-color: var(--accent-orange); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 16px; } - @keyframes spin { - to { transform: rotate(360deg); } - } - .empty-icon { - font-size: 48px; - margin-bottom: 16px; - } + @keyframes spin { to { transform: rotate(360deg); } } + .empty-icon { font-size: 48px; margin-bottom: 16px; } /* ══════════════════ 토스트 ══════════════════ */ .toast { @@ -612,19 +527,17 @@ transition: all 0.3s; z-index: 300; } - .toast.show { - opacity: 1; - transform: translateX(-50%) translateY(0); - } + .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .toast.success { border-color: var(--accent-emerald); } .toast.error { border-color: var(--accent-rose); } /* ══════════════════ 반응형 ══════════════════ */ @media (max-width: 1200px) { - .stats-grid { grid-template-columns: repeat(3, 1fr); } + .stats-row { flex-wrap: wrap; } + .stat-card { min-width: calc(33% - 12px); } } @media (max-width: 768px) { - .stats-grid { grid-template-columns: repeat(2, 1fr); } + .stat-card { min-width: calc(50% - 8px); } .header-nav { display: none; } .filter-bar { flex-direction: column; } .filter-group { width: 100%; } @@ -649,8 +562,8 @@
- -
+ +
🔴
-
@@ -673,7 +586,6 @@
-
처리완료
-
반품/보류/폐기
📊
@@ -682,6 +594,28 @@
+ +
+
+
💰
+
-
+
총 회수가능 금액
+
전체 반품 대상
+
+
+
🔴💵
+
-
+
3년+ 금액
+
긴급 회수 대상
+
+
+
🟠💵
+
-
+
2년+ 금액
+
주의 대상
+
+
+
@@ -743,19 +677,21 @@ - - - - - - - - + + + + + + + + + + - `; + tbody.innerHTML = ``; const status = document.getElementById('filterStatus').value; const urgency = document.getElementById('filterUrgency').value; @@ -855,35 +781,39 @@ currentPage = 1; renderTable(); } else { - tbody.innerHTML = ` - `; + tbody.innerHTML = ``; } } catch (err) { - tbody.innerHTML = ` - `; + tbody.innerHTML = ``; } } - // 통계 업데이트 function updateStats(stats) { document.getElementById('statCritical').textContent = stats.critical || 0; document.getElementById('statWarning').textContent = stats.warning || 0; document.getElementById('statPending').textContent = stats.pending || 0; document.getElementById('statProcessed').textContent = stats.processed || 0; document.getElementById('statTotal').textContent = stats.total || 0; + + // 금액 표시 + document.getElementById('statTotalAmount').textContent = formatAmount(stats.total_amount || 0); + document.getElementById('statCriticalAmount').textContent = formatAmount(stats.critical_amount || 0); + document.getElementById('statWarningAmount').textContent = formatAmount(stats.warning_amount || 0); + } + + function formatAmount(amount) { + if (!amount || amount === 0) return '₩0'; + if (amount >= 1000000) { + return '₩' + (amount / 10000).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + '만'; + } + return '₩' + Math.round(amount).toLocaleString(); + } + + function formatPrice(price) { + if (!price || price === 0) return '-'; + return '₩' + Math.round(price).toLocaleString(); } - // 탭 카운트 업데이트 function updateTabs() { const counts = { all: allData.length, critical: 0, warning: 0, normal: 0 }; allData.forEach(item => { @@ -897,7 +827,6 @@ document.getElementById('tabNormal').textContent = counts.normal; } - // 긴급도 레벨 계산 function getUrgencyLevel(item) { const months = Math.max(item.months_since_use || 0, item.months_since_purchase || 0); if (months >= 36) return 'critical'; @@ -905,7 +834,6 @@ return 'normal'; } - // 긴급도 탭 클릭 function setUrgencyTab(urgency) { document.querySelectorAll('.urgency-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.urgency === urgency); @@ -915,30 +843,21 @@ renderTable(); } - // 테이블 렌더링 function renderTable() { const tbody = document.getElementById('dataTableBody'); const urgencyFilter = document.getElementById('filterUrgency').value; - // 필터링 let filteredData = allData; if (urgencyFilter) { filteredData = allData.filter(item => getUrgencyLevel(item) === urgencyFilter); } if (filteredData.length === 0) { - tbody.innerHTML = ` - `; + tbody.innerHTML = ``; document.getElementById('pagination').innerHTML = ''; return; } - // 페이지네이션 계산 const totalPages = Math.ceil(filteredData.length / pageSize); const start = (currentPage - 1) * pageSize; const end = start + pageSize; @@ -947,7 +866,7 @@ tbody.innerHTML = pageData.map(item => { const urgency = getUrgencyLevel(item); const urgencyClass = urgency === 'critical' ? 'urgent-critical' : urgency === 'warning' ? 'urgent-warning' : ''; - const monthsMax = Math.max(item.months_since_use || 0, item.months_since_purchase || 0); + const hasAmount = item.recoverable_amount && item.recoverable_amount > 0; return ` @@ -963,6 +882,8 @@ + + `; }).join(''); - // 페이지네이션 렌더링 renderPagination(totalPages, filteredData.length); } - // 페이지네이션 function renderPagination(totalPages, totalItems) { const pagination = document.getElementById('pagination'); @@ -997,10 +914,7 @@ const maxVisible = 5; let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); let endPage = Math.min(totalPages, startPage + maxVisible - 1); - - if (endPage - startPage < maxVisible - 1) { - startPage = Math.max(1, endPage - maxVisible + 1); - } + if (endPage - startPage < maxVisible - 1) startPage = Math.max(1, endPage - maxVisible + 1); for (let i = startPage; i <= endPage; i++) { html += ``; @@ -1019,12 +933,12 @@ window.scrollTo({ top: 0, behavior: 'smooth' }); } - // 상태 변경 모달 function openStatusModal(itemId) { currentItemId = itemId; const item = allData.find(i => i.id === itemId); if (!item) return; + const hasAmount = item.recoverable_amount && item.recoverable_amount > 0; document.getElementById('modalDrugDetail').innerHTML = `
${escapeHtml(item.drug_name)}
@@ -1036,6 +950,14 @@ 현재고 ${item.current_stock || 0}
+
+ 단가 + ${formatPrice(item.unit_price)} +
+
+ 회수가능금액 + ${hasAmount ? '₩' + Math.round(item.recoverable_amount).toLocaleString() : '-'} +
마지막 처방 ${formatDate(item.last_prescription_date) || '-'} @@ -1063,7 +985,6 @@ const status = document.getElementById('modalStatus').value; const reason = document.getElementById('modalReason').value; - // 보류 선택 시 사유 필수 if (status === 'keep' && !reason.trim()) { showToast('보류 상태에서는 사유 입력이 필수입니다', 'error'); return; @@ -1090,7 +1011,6 @@ } } - // 유틸리티 함수 function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); @@ -1100,7 +1020,6 @@ function formatDate(dateStr) { if (!dateStr) return null; - // YYYYMMDD 형식 처리 if (dateStr.length === 8 && !dateStr.includes('-')) { return `${dateStr.slice(0,4)}-${dateStr.slice(4,6)}-${dateStr.slice(6,8)}`; }
긴급약품현재고마지막 처방미사용마지막 입고상태액션긴급약품현재고단가회수가능금액마지막 처방미사용마지막 입고상태액션
+
데이터 로딩 중...
@@ -812,26 +748,16 @@ const pageSize = 30; let currentItemId = null; - // 초기 로드 document.addEventListener('DOMContentLoaded', function() { loadData(); - - // 엔터키로 검색 document.getElementById('searchInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') loadData(); }); }); - // 데이터 로드 async function loadData() { const tbody = document.getElementById('dataTableBody'); - tbody.innerHTML = ` -
-
-
-
데이터 로딩 중...
-
-
데이터 로딩 중...
-
-
⚠️
-
오류: ${data.error}
-
-
⚠️
오류: ${data.error}
-
-
-
데이터 로드 실패
-
-
데이터 로드 실패
-
-
📦
-
해당 조건의 반품 후보가 없습니다
-
-
📦
해당 조건의 반품 후보가 없습니다
${item.current_stock || 0}${formatPrice(item.unit_price)}${hasAmount ? '₩' + Math.round(item.recoverable_amount).toLocaleString() : '-'} ${formatDate(item.last_prescription_date) || '-'} ${item.months_since_use ? item.months_since_use + '개월' : '-'} @@ -970,19 +891,15 @@ ${formatDate(item.last_purchase_date) || '-'} ${getStatusLabel(item.status)} - +