feat: 도매상 잔고 모달에 월간 매출 추가

- 백제/지오영/수인 월간매출 API 라우트 추가
- 모달 UI: 잔고 + 월간 매출 동시 표시
- 총 주문액 / 총 미수금 요약 표시
This commit is contained in:
thug0bin 2026-03-06 18:01:37 +09:00
parent 4b2d934839
commit 5519f5ae62
9 changed files with 562 additions and 33 deletions

View File

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

View File

@ -447,6 +447,37 @@ def api_get_balance():
}), 501 }), 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
# ========== 하위 호환성 ========== # ========== 하위 호환성 ==========
# 기존 코드에서 직접 클래스 참조하는 경우를 위해 # 기존 코드에서 직접 클래스 참조하는 경우를 위해

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

View File

@ -134,6 +134,55 @@ def api_sooin_balance():
}), 500 }), 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']) @sooin_bp.route('/cart', methods=['GET'])
def api_sooin_cart(): def api_sooin_cart():
"""장바구니 조회 API""" """장바구니 조회 API"""

View File

@ -2256,26 +2256,43 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.balance-total { .balance-summary {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(6, 182, 212, 0.1)); background: linear-gradient(135deg, rgba(168, 85, 247, 0.1), rgba(6, 182, 212, 0.1));
border-radius: 12px; border-radius: 12px;
padding: 20px; padding: 20px;
text-align: center; display: flex;
margin-top: 8px; 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; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);
margin-bottom: 8px; margin-bottom: 8px;
} }
.balance-total-value { .summary-value {
font-size: 28px; font-size: 24px;
font-weight: 700; font-weight: 700;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
}
.summary-value.sales {
color: #10b981;
}
.summary-value.balance {
background: linear-gradient(135deg, #a855f7, #06b6d4); background: linear-gradient(135deg, #a855f7, #06b6d4);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
.summary-divider {
width: 1px;
height: 50px;
background: var(--border);
}
.balance-updated { .balance-updated {
text-align: center; text-align: center;
font-size: 11px; font-size: 11px;
@ -2300,17 +2317,23 @@
baekje: { baekje: {
id: 'baekje', name: '백제약품', icon: '💉', id: 'baekje', name: '백제약품', icon: '💉',
logo: '/static/img/logo_baekje.svg', logo: '/static/img/logo_baekje.svg',
color: '#f59e0b', api: '/api/baekje/balance' color: '#f59e0b',
balanceApi: '/api/baekje/balance',
salesApi: '/api/baekje/monthly-sales'
}, },
geoyoung: { geoyoung: {
id: 'geoyoung', name: '지오영', icon: '🏭', id: 'geoyoung', name: '지오영', icon: '🏭',
logo: '/static/img/logo_geoyoung.ico', logo: '/static/img/logo_geoyoung.ico',
color: '#06b6d4', api: '/api/geoyoung/balance' color: '#06b6d4',
balanceApi: '/api/geoyoung/balance',
salesApi: '/api/geoyoung/monthly-sales'
}, },
sooin: { sooin: {
id: 'sooin', name: '수인약품', icon: '💊', id: 'sooin', name: '수인약품', icon: '💊',
logo: '/static/img/logo_sooin.svg', 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']; const WHOLESALER_ORDER = ['baekje', 'geoyoung', 'sooin'];
@ -2320,36 +2343,47 @@
content.innerHTML = ` content.innerHTML = `
<div class="loading-state"> <div class="loading-state">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<div>잔고 조회 중...</div> <div>잔고 및 매출 조회 중...</div>
</div>`; </div>`;
const wholesalers = WHOLESALER_ORDER.map(id => WHOLESALER_CONFIG[id]); 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 totalBalance = 0;
let totalSales = 0;
// 병렬로 조회 // 병렬로 잔고 + 월간매출 조회
await Promise.all(wholesalers.map(async (ws) => { await Promise.all(wholesalers.flatMap(ws => [
try { // 잔고 조회
const resp = await fetch(ws.api, { timeout: 30000 }); fetch(ws.balanceApi).then(r => r.json()).then(data => {
const data = await resp.json(); balanceResults[ws.id] = data;
results[ws.id] = data; if (data.success && data.balance) totalBalance += data.balance;
if (data.success && data.balance) { }).catch(err => {
totalBalance += data.balance; balanceResults[ws.id] = { success: false, error: err.message };
} }),
} catch (err) { // 월간 매출 조회
results[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 = '<div class="balance-grid">'; let html = '<div class="balance-grid">';
wholesalers.forEach(ws => { wholesalers.forEach(ws => {
const data = results[ws.id]; const balData = balanceResults[ws.id] || {};
const isError = !data.success; const salesData = salesResults[ws.id] || {};
const balance = data.balance || 0; const isError = !balData.success;
const prevBalance = data.prev_balance || data.prev_month_balance || 0; const balance = balData.balance || 0;
const monthlySales = salesData.total_amount || 0;
const monthlyPaid = salesData.total_paid || 0;
html += ` html += `
<div class="balance-card ${isError ? 'error' : ''}"> <div class="balance-card ${isError ? 'error' : ''}">
@ -2362,8 +2396,9 @@
<div class="balance-name">${ws.name}</div> <div class="balance-name">${ws.name}</div>
<div class="balance-detail"> <div class="balance-detail">
${isError ${isError
? `❌ ${data.error || '조회 실패'}` ? `❌ ${balData.error || '조회 실패'}`
: `전월/전일: ${prevBalance.toLocaleString()}원`} : `${month}월 매출: <span style="color:${ws.color};font-weight:600;">${monthlySales.toLocaleString()}원</span>
${monthlyPaid > 0 ? ` · 입금: ${monthlyPaid.toLocaleString()}원` : ''}`}
</div> </div>
</div> </div>
<div class="balance-amount"> <div class="balance-amount">
@ -2376,9 +2411,16 @@
}); });
html += ` html += `
<div class="balance-total"> <div class="balance-summary">
<div class="balance-total-label">총 미수금</div> <div class="summary-item">
<div class="balance-total-value">${totalBalance.toLocaleString()}원</div> <div class="summary-label">📊 ${month}월 총 주문</div>
<div class="summary-value sales">${totalSales.toLocaleString()}원</div>
</div>
<div class="summary-divider"></div>
<div class="summary-item">
<div class="summary-label">💰 총 미수금</div>
<div class="summary-value balance">${totalBalance.toLocaleString()}원</div>
</div>
</div> </div>
<div class="balance-updated">🕐 ${new Date().toLocaleString('ko-KR')}</div> <div class="balance-updated">🕐 ${new Date().toLocaleString('ko-KR')}</div>
</div>`; </div>`;

View File

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

View File

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

View File

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