diff --git a/backend/analyze_baekje_ledger.py b/backend/analyze_baekje_ledger.py new file mode 100644 index 0000000..9429023 --- /dev/null +++ b/backend/analyze_baekje_ledger.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +"""백제약품 주문 원장 페이지 분석""" + +import asyncio +import json +import os +from dotenv import load_dotenv + +load_dotenv() + +async def analyze_order_ledger(): + from playwright.async_api import async_playwright + + username = os.getenv('BAEKJE_USER_ID') + password = os.getenv('BAEKJE_PASSWORD') + + print(f'Username: {username}') + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=False) + context = await browser.new_context() + page = await context.new_page() + + # 로그인 페이지 + await page.goto('https://ibjp.co.kr/dist/login', timeout=15000) + await page.wait_for_load_state('networkidle', timeout=10000) + + # 로그인 폼 입력 + inputs = await page.locator('input[type="text"], input[type="password"]').all() + if len(inputs) >= 2: + await inputs[0].fill(username) + await inputs[1].fill(password) + + # 로그인 버튼 클릭 + buttons = await page.locator('button').all() + for btn in buttons: + text = await btn.text_content() + if '로그인' in (text or ''): + await btn.click() + break + + # 로그인 완료 대기 + try: + await page.wait_for_url('**/comOrd**', timeout=15000) + print('Login successful, redirected to comOrd') + except Exception as e: + print(f'URL wait failed: {e}') + await asyncio.sleep(3) + + print(f'Current URL: {page.url}') + + # 주문 원장 페이지로 이동 + await page.goto('https://ibjp.co.kr/dist/ordLedger', timeout=15000) + await page.wait_for_load_state('networkidle', timeout=15000) + + print(f'Order Ledger URL: {page.url}') + + # 페이지 HTML 저장 + html = await page.content() + with open('ordLedger_page.html', 'w', encoding='utf-8') as f: + f.write(html) + print('Page HTML saved to ordLedger_page.html') + + # 스크린샷 저장 + await page.screenshot(path='ordLedger_screenshot.png', full_page=True) + print('Screenshot saved') + + # 테이블 데이터 분석 + tables = await page.locator('table').all() + print(f'Found {len(tables)} tables') + + for i, table in enumerate(tables): + headers = await table.locator('th').all() + header_texts = [await h.text_content() for h in headers] + print(f'Table {i} headers: {header_texts}') + + # 페이지 텍스트 출력 (분석용) + body_text = await page.locator('body').text_content() + print('\n=== Page Text Preview ===') + print(body_text[:3000] if body_text else 'No text') + + await asyncio.sleep(30) # 페이지 확인 시간 + await browser.close() + +if __name__ == '__main__': + asyncio.run(analyze_order_ledger()) diff --git a/backend/geoyoung_api.py b/backend/geoyoung_api.py index 6f26860..93f2be4 100644 --- a/backend/geoyoung_api.py +++ b/backend/geoyoung_api.py @@ -447,6 +447,37 @@ def api_get_balance(): }), 501 +@geoyoung_bp.route('/monthly-sales', methods=['GET']) +def api_get_monthly_sales(): + """ + 월간 매출 조회 + + GET /api/geoyoung/monthly-sales?year=2026&month=3 + """ + from datetime import datetime + + year = request.args.get('year', type=int) + month = request.args.get('month', type=int) + + # 기본값: 현재 월 + if not year or not month: + now = datetime.now() + year = year or now.year + month = month or now.month + + session = get_geo_session() + + if hasattr(session, 'get_monthly_sales'): + result = session.get_monthly_sales(year, month) + return jsonify(result) + else: + return jsonify({ + 'success': False, + 'error': 'NOT_IMPLEMENTED', + 'message': '지오영 월간 매출 조회 미구현' + }), 501 + + # ========== 하위 호환성 ========== # 기존 코드에서 직접 클래스 참조하는 경우를 위해 diff --git a/backend/ordLedger_page.html b/backend/ordLedger_page.html new file mode 100644 index 0000000..b83f598 --- /dev/null +++ b/backend/ordLedger_page.html @@ -0,0 +1,10 @@ +백제약품 + +
백제약품 주식회사
웹 주문 사이트

LOGIN

© BAEKJE PHARMACEUTICAL Co., Ltd. All rights reserved.

\ No newline at end of file diff --git a/backend/ordLedger_screenshot.png b/backend/ordLedger_screenshot.png new file mode 100644 index 0000000..1c05694 Binary files /dev/null and b/backend/ordLedger_screenshot.png differ diff --git a/backend/sooin_api.py b/backend/sooin_api.py index 1cbdaa6..780c859 100644 --- a/backend/sooin_api.py +++ b/backend/sooin_api.py @@ -134,6 +134,55 @@ def api_sooin_balance(): }), 500 +@sooin_bp.route('/monthly-sales', methods=['GET']) +def api_sooin_monthly_sales(): + """ + 수인약품 월간 매출 조회 API + + GET /api/sooin/monthly-sales?year=2026&month=3 + + Returns: + { + "success": true, + "total_amount": 3700239, // 월간 매출 합계 + "total_paid": 0, // 월간 입금 합계 + "ending_balance": 14293001, // 월말 잔액 + "opening_balance": 10592762, // 전일(기초) 잔액 + "from_date": "2026-03-01", + "to_date": "2026-03-31" + } + """ + from datetime import datetime + + year = flask_request.args.get('year', type=int) + month = flask_request.args.get('month', type=int) + + # 기본값: 현재 월 + if not year or not month: + now = datetime.now() + year = year or now.year + month = month or now.month + + try: + session = get_sooin_session() + if hasattr(session, 'get_monthly_sales'): + result = session.get_monthly_sales(year, month) + return jsonify(result) + else: + return jsonify({ + 'success': False, + 'error': 'NOT_IMPLEMENTED', + 'message': '수인약품 월간 매출 조회 미구현' + }), 501 + except Exception as e: + logger.error(f"수인약품 월간 매출 조회 오류: {e}") + return jsonify({ + 'success': False, + 'error': 'MONTHLY_SALES_ERROR', + 'message': str(e) + }), 500 + + @sooin_bp.route('/cart', methods=['GET']) def api_sooin_cart(): """장바구니 조회 API""" diff --git a/backend/templates/admin_rx_usage.html b/backend/templates/admin_rx_usage.html index 4b1d42e..d8a58dd 100644 --- a/backend/templates/admin_rx_usage.html +++ b/backend/templates/admin_rx_usage.html @@ -2256,26 +2256,43 @@ text-transform: uppercase; letter-spacing: 0.5px; } - .balance-total { + .balance-summary { background: linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(6, 182, 212, 0.1)); border-radius: 12px; padding: 20px; - text-align: center; - margin-top: 8px; + display: flex; + justify-content: space-around; + align-items: center; + margin-top: 12px; + gap: 16px; } - .balance-total-label { + .summary-item { + text-align: center; + flex: 1; + } + .summary-label { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; } - .balance-total-value { - font-size: 28px; + .summary-value { + font-size: 24px; font-weight: 700; font-family: 'JetBrains Mono', monospace; + } + .summary-value.sales { + color: #10b981; + } + .summary-value.balance { background: linear-gradient(135deg, #a855f7, #06b6d4); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } + .summary-divider { + width: 1px; + height: 50px; + background: var(--border); + } .balance-updated { text-align: center; font-size: 11px; @@ -2300,17 +2317,23 @@ baekje: { id: 'baekje', name: '백제약품', icon: '💉', logo: '/static/img/logo_baekje.svg', - color: '#f59e0b', api: '/api/baekje/balance' + color: '#f59e0b', + balanceApi: '/api/baekje/balance', + salesApi: '/api/baekje/monthly-sales' }, geoyoung: { id: 'geoyoung', name: '지오영', icon: '🏭', logo: '/static/img/logo_geoyoung.ico', - color: '#06b6d4', api: '/api/geoyoung/balance' + color: '#06b6d4', + balanceApi: '/api/geoyoung/balance', + salesApi: '/api/geoyoung/monthly-sales' }, sooin: { id: 'sooin', name: '수인약품', icon: '💊', logo: '/static/img/logo_sooin.svg', - color: '#a855f7', api: '/api/sooin/balance' + color: '#a855f7', + balanceApi: '/api/sooin/balance', + salesApi: '/api/sooin/monthly-sales' } }; const WHOLESALER_ORDER = ['baekje', 'geoyoung', 'sooin']; @@ -2320,36 +2343,47 @@ content.innerHTML = `
-
잔고 조회 중...
+
잔고 및 매출 조회 중...
`; const wholesalers = WHOLESALER_ORDER.map(id => WHOLESALER_CONFIG[id]); + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth() + 1; - const results = {}; + const balanceResults = {}; + const salesResults = {}; let totalBalance = 0; + let totalSales = 0; - // 병렬로 조회 - await Promise.all(wholesalers.map(async (ws) => { - try { - const resp = await fetch(ws.api, { timeout: 30000 }); - const data = await resp.json(); - results[ws.id] = data; - if (data.success && data.balance) { - totalBalance += data.balance; - } - } catch (err) { - results[ws.id] = { success: false, error: err.message }; - } - })); + // 병렬로 잔고 + 월간매출 조회 + await Promise.all(wholesalers.flatMap(ws => [ + // 잔고 조회 + fetch(ws.balanceApi).then(r => r.json()).then(data => { + balanceResults[ws.id] = data; + if (data.success && data.balance) totalBalance += data.balance; + }).catch(err => { + balanceResults[ws.id] = { success: false, error: err.message }; + }), + // 월간 매출 조회 + fetch(`${ws.salesApi}?year=${year}&month=${month}`).then(r => r.json()).then(data => { + salesResults[ws.id] = data; + if (data.success && data.total_amount) totalSales += data.total_amount; + }).catch(err => { + salesResults[ws.id] = { success: false, error: err.message }; + }) + ])); // 결과 렌더링 let html = '
'; wholesalers.forEach(ws => { - const data = results[ws.id]; - const isError = !data.success; - const balance = data.balance || 0; - const prevBalance = data.prev_balance || data.prev_month_balance || 0; + const balData = balanceResults[ws.id] || {}; + const salesData = salesResults[ws.id] || {}; + const isError = !balData.success; + const balance = balData.balance || 0; + const monthlySales = salesData.total_amount || 0; + const monthlyPaid = salesData.total_paid || 0; html += `
@@ -2362,8 +2396,9 @@
${ws.name}
${isError - ? `❌ ${data.error || '조회 실패'}` - : `전월/전일: ${prevBalance.toLocaleString()}원`} + ? `❌ ${balData.error || '조회 실패'}` + : `${month}월 매출: ${monthlySales.toLocaleString()}원 + ${monthlyPaid > 0 ? ` · 입금: ${monthlyPaid.toLocaleString()}원` : ''}`}
@@ -2376,9 +2411,16 @@ }); html += ` -
-
총 미수금
-
${totalBalance.toLocaleString()}원
+
+
+
📊 ${month}월 총 주문
+
${totalSales.toLocaleString()}원
+
+
+
+
💰 총 미수금
+
${totalBalance.toLocaleString()}원
+
🕐 ${new Date().toLocaleString('ko-KR')}
`; diff --git a/backend/test_baekje_ledger_api.py b/backend/test_baekje_ledger_api.py new file mode 100644 index 0000000..b229c87 --- /dev/null +++ b/backend/test_baekje_ledger_api.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +"""백제약품 주문 원장 API 분석""" + +import json +import requests +from datetime import datetime, timedelta +import calendar + +# 저장된 토큰 로드 +TOKEN_FILE = r'c:\Users\청춘약국\source\pharmacy-wholesale-api\.baekje_token.json' +with open(TOKEN_FILE, 'r', encoding='utf-8') as f: + token_data = json.load(f) + +token = token_data['token'] +cust_cd = token_data['cust_cd'] + +print(f"Token expires: {datetime.fromtimestamp(token_data['expires'])}") +print(f"Customer code: {cust_cd}") + +# API 세션 설정 +session = requests.Session() +session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7', + 'Origin': 'https://ibjp.co.kr', + 'Referer': 'https://ibjp.co.kr/', + 'Authorization': f'Bearer {token}' +}) + +API_URL = "https://www.ibjp.co.kr" + +# 1. 주문 원장 API 시도 - 다양한 엔드포인트 +endpoints = [ + '/ordLedger/listSearch', + '/ordLedger/list', + '/ord/ledgerList', + '/ord/ledgerSearch', + '/cust/ordLedger', + '/custOrd/ledgerList', + '/ordHist/listSearch', + '/ordHist/list', +] + +# 날짜 설정 (이번 달) +today = datetime.now() +year = today.year +month = today.month +_, last_day = calendar.monthrange(year, month) +from_date = f"{year}{month:02d}01" +to_date = f"{year}{month:02d}{last_day:02d}" + +print(f"\n조회 기간: {from_date} ~ {to_date}") +print("\n=== API 엔드포인트 탐색 ===\n") + +params = { + 'custCd': cust_cd, + 'startDt': from_date, + 'endDt': to_date, + 'stDate': from_date, + 'edDate': to_date, + 'year': str(year), + 'month': f"{month:02d}", +} + +for endpoint in endpoints: + try: + # GET 시도 + resp = session.get(f"{API_URL}{endpoint}", params=params, timeout=10) + print(f"GET {endpoint}: {resp.status_code}") + if resp.status_code == 200: + try: + data = resp.json() + print(f" -> JSON Response (first 500 chars): {str(data)[:500]}") + except: + print(f" -> Text (first 200 chars): {resp.text[:200]}") + except Exception as e: + print(f"GET {endpoint}: Error - {e}") + + try: + # POST 시도 + resp = session.post(f"{API_URL}{endpoint}", json=params, timeout=10) + print(f"POST {endpoint}: {resp.status_code}") + if resp.status_code == 200: + try: + data = resp.json() + print(f" -> JSON Response (first 500 chars): {str(data)[:500]}") + except: + print(f" -> Text (first 200 chars): {resp.text[:200]}") + except Exception as e: + print(f"POST {endpoint}: Error - {e}") + +# 2. 이미 알려진 API로 데이터 확인 +print("\n=== 알려진 API 테스트 ===\n") + +# 월간 잔고 조회 (이미 있는 함수에서 사용) +resp = session.get(f"{API_URL}/custMonth/listSearch", params={'custCd': cust_cd, 'year': str(year), 'endDt': to_date}, timeout=10) +print(f"custMonth/listSearch: {resp.status_code}") +if resp.status_code == 200: + data = resp.json() + print(f" -> {json.dumps(data, ensure_ascii=False, indent=2)[:1500]}") diff --git a/backend/test_baekje_ledger_api2.py b/backend/test_baekje_ledger_api2.py new file mode 100644 index 0000000..eae0740 --- /dev/null +++ b/backend/test_baekje_ledger_api2.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +"""백제약품 주문 원장 API 분석 - 상세 탐색""" + +import json +import requests +from datetime import datetime +import calendar + +# 저장된 토큰 로드 +TOKEN_FILE = r'c:\Users\청춘약국\source\pharmacy-wholesale-api\.baekje_token.json' +with open(TOKEN_FILE, 'r', encoding='utf-8') as f: + token_data = json.load(f) + +token = token_data['token'] +cust_cd = token_data['cust_cd'] + +# API 세션 설정 +session = requests.Session() +session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7', + 'Origin': 'https://ibjp.co.kr', + 'Referer': 'https://ibjp.co.kr/', + 'Authorization': f'Bearer {token}' +}) + +API_URL = "https://www.ibjp.co.kr" + +today = datetime.now() +year = today.year +month = today.month +_, last_day = calendar.monthrange(year, month) + +print("=== 주문 원장 API 탐색 (다양한 파라미터) ===\n") + +# 날짜 형식 변형 +date_formats = [ + {'startDt': f'{year}{month:02d}01', 'endDt': f'{year}{month:02d}{last_day:02d}'}, + {'stDt': f'{year}{month:02d}01', 'edDt': f'{year}{month:02d}{last_day:02d}'}, + {'fromDate': f'{year}-{month:02d}-01', 'toDate': f'{year}-{month:02d}-{last_day:02d}'}, + {'strDt': f'{year}{month:02d}01', 'endDt': f'{year}{month:02d}{last_day:02d}'}, + {'ordDt': f'{year}{month:02d}'}, +] + +endpoints = [ + '/ordLedger/listSearch', + '/ordLedger/search', + '/ordLedger/ledgerList', + '/cust/ordLedgerList', + '/cust/ledger', + '/ord/histList', + '/ord/history', + '/ord/list', +] + +for endpoint in endpoints: + for params in date_formats: + full_params = {**params, 'custCd': cust_cd} + try: + resp = session.get(f"{API_URL}{endpoint}", params=full_params, timeout=10) + if resp.status_code == 200: + print(f"✓ GET {endpoint} {params}: {resp.status_code}") + try: + data = resp.json() + print(f" -> {str(data)[:300]}") + except: + print(f" -> {resp.text[:200]}") + except Exception as e: + pass + + try: + resp = session.post(f"{API_URL}{endpoint}", json=full_params, timeout=10) + if resp.status_code == 200: + print(f"✓ POST {endpoint} {params}: {resp.status_code}") + try: + data = resp.json() + print(f" -> {str(data)[:300]}") + except: + print(f" -> {resp.text[:200]}") + except Exception as e: + pass + +print("\n=== 주문 이력 관련 API ===\n") + +# 주문 이력 조회 시도 +order_endpoints = [ + '/ord/ordList', + '/ord/orderHistory', + '/ordReg/list', + '/ordReg/history', + '/order/list', + '/order/history', +] + +for endpoint in order_endpoints: + try: + params = {'custCd': cust_cd, 'startDt': f'{year}{month:02d}01', 'endDt': f'{year}{month:02d}{last_day:02d}'} + resp = session.get(f"{API_URL}{endpoint}", params=params, timeout=10) + print(f"GET {endpoint}: {resp.status_code}") + if resp.status_code == 200: + try: + data = resp.json() + print(f" -> {str(data)[:500]}") + except: + print(f" -> {resp.text[:200]}") + except: + pass + +print("\n=== custMonth/listSearch 상세 데이터 분석 ===\n") + +# 이미 작동하는 API의 데이터 상세 분석 +resp = session.get(f"{API_URL}/custMonth/listSearch", params={'custCd': cust_cd, 'year': str(year), 'endDt': f'{year}{month:02d}{last_day:02d}'}, timeout=10) +if resp.status_code == 200: + data = resp.json() + print("월간 데이터 구조:") + for item in data: + print(f"\n월: {item.get('BALANCE_YM')}") + print(f" 매출액(SALE_AMT): {item.get('SALE_AMT'):,}") + print(f" 반품액(BACK_AMT): {item.get('BACK_AMT'):,}") + print(f" 순반품(PURE_BACK_AMT): {item.get('PURE_BACK_AMT'):,}") + print(f" 순매출(TOTAL_AMT): {item.get('TOTAL_AMT'):,}") + print(f" 입금액(PAY_CASH_AMT): {item.get('PAY_CASH_AMT'):,}") + print(f" 전월이월(PRE_TOTAL_AMT): {item.get('PRE_TOTAL_AMT'):,}") + print(f" 월말잔고(BALANCE_A_AMT): {item.get('BALANCE_A_AMT'):,}") + print(f" 회전일수(ROTATE_DAY): {item.get('ROTATE_DAY')}") diff --git a/backend/test_baekje_monthly_sales.py b/backend/test_baekje_monthly_sales.py new file mode 100644 index 0000000..83718e1 --- /dev/null +++ b/backend/test_baekje_monthly_sales.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""백제약품 get_monthly_sales() 테스트""" + +import os +import sys + +# wholesale 패키지 경로 추가 +sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-wholesale-api') +os.chdir(r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend') + +from dotenv import load_dotenv +load_dotenv() + +from wholesale import BaekjeSession + +def test_monthly_sales(): + print("=" * 60) + print("백제약품 월간 매출 조회 테스트") + print("=" * 60) + + session = BaekjeSession() + + # 현재 월 조회 + from datetime import datetime + now = datetime.now() + year = now.year + month = now.month + + print(f"\n1. 현재 월 ({year}-{month:02d}) 조회:") + result = session.get_monthly_sales(year, month) + print(f" Success: {result.get('success')}") + if result.get('success'): + print(f" 월간 매출: {result.get('total_amount'):,}원") + print(f" 월간 반품: {result.get('total_returns'):,}원") + print(f" 순매출: {result.get('net_amount'):,}원") + print(f" 월간 입금: {result.get('total_paid'):,}원") + print(f" 월말 잔고: {result.get('ending_balance'):,}원") + print(f" 전월이월: {result.get('prev_balance'):,}원") + print(f" 회전일수: {result.get('rotate_days')}") + print(f" 조회기간: {result.get('from_date')} ~ {result.get('to_date')}") + else: + print(f" Error: {result.get('error')}") + + # 전월 조회 + prev_month = month - 1 if month > 1 else 12 + prev_year = year if month > 1 else year - 1 + + print(f"\n2. 전월 ({prev_year}-{prev_month:02d}) 조회:") + result = session.get_monthly_sales(prev_year, prev_month) + print(f" Success: {result.get('success')}") + if result.get('success'): + print(f" 월간 매출: {result.get('total_amount'):,}원") + print(f" 월간 반품: {result.get('total_returns'):,}원") + print(f" 순매출: {result.get('net_amount'):,}원") + print(f" 월간 입금: {result.get('total_paid'):,}원") + print(f" 월말 잔고: {result.get('ending_balance'):,}원") + print(f" 전월이월: {result.get('prev_balance'):,}원") + print(f" 회전일수: {result.get('rotate_days')}") + print(f" 조회기간: {result.get('from_date')} ~ {result.get('to_date')}") + else: + print(f" Error: {result.get('error')}") + + # 2달 전 조회 + prev_month2 = prev_month - 1 if prev_month > 1 else 12 + prev_year2 = prev_year if prev_month > 1 else prev_year - 1 + + print(f"\n3. 2달 전 ({prev_year2}-{prev_month2:02d}) 조회:") + result = session.get_monthly_sales(prev_year2, prev_month2) + print(f" Success: {result.get('success')}") + if result.get('success'): + print(f" 월간 매출: {result.get('total_amount'):,}원") + print(f" 월간 반품: {result.get('total_returns'):,}원") + print(f" 순매출: {result.get('net_amount'):,}원") + print(f" 월간 입금: {result.get('total_paid'):,}원") + print(f" 월말 잔고: {result.get('ending_balance'):,}원") + else: + print(f" Error: {result.get('error')}") + + print("\n" + "=" * 60) + print("테스트 완료!") + print("=" * 60) + +if __name__ == '__main__': + test_monthly_sales()