feat(order): 지오영/수인 선택적 주문 + 장바구니 보존 기능
- internal_code DB 저장 → 프론트에서 선택한 제품 그대로 주문 - 기존 장바구니 백업/복구로 사용자 장바구니 보존 - 수인약품 submit_order() 수정 (체크박스 제외 방식) - 테스트 파일 정리 및 문서 추가
This commit is contained in:
parent
f48e657e12
commit
a672c7a2a0
16
backend/analyze_bag.py
Normal file
16
backend/analyze_bag.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
|
||||||
|
# Bag.asp HTML 가져오기
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
|
||||||
|
# 파일로 저장
|
||||||
|
with open('bag_page.html', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(resp.text)
|
||||||
|
|
||||||
|
print('bag_page.html 저장됨')
|
||||||
|
print(f'응답 길이: {len(resp.text)}')
|
||||||
@ -99,63 +99,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr><td colspan="4">장바구니에 담긴 제품이 없습니다.</td></tr>
|
||||||
<tr id="bagLine0">
|
|
||||||
<td class="first"><input type="checkbox" name="chk_0" id="chk_0" class="chkBox" /></td>
|
|
||||||
|
|
||||||
<td class="td_nm" title="(향)스틸녹스정 10mg(병) 100T" ><a href="./PhysicInfo.asp?pc=02719&currVenCd=50911" target="_blank" class="bagPhysic_ln">(향)스틸녹스정 10mg(병)100T</a></td>
|
|
||||||
|
|
||||||
<td >
|
|
||||||
<input type="text" name="bagQty_0" id="bagQty_0" maxlength="10" class="setInput_h18_qty" value="1" data="1"
|
|
||||||
style="width:25px;"/>
|
|
||||||
<input type="hidden" name="pc_0" id="pc_0" value="02719" />
|
|
||||||
<input type="hidden" name="stock_0" id="stock_0" value="50" />
|
|
||||||
<input type="hidden" name="price_0" value="17300" />
|
|
||||||
<input type="hidden" name="physic_nm0" value="(향)스틸녹스정 10mg(병)" />
|
|
||||||
<input type="hidden" name="totalPrice0" id="totalPrice0" value="17300" />
|
|
||||||
<input type="hidden" name="ordunitqty_0" id="ordunitqty_0" value="0" />
|
|
||||||
<input type="hidden" name="bidqty_0" id="bidqty_0" value="" />
|
|
||||||
<input type="hidden" name="outqty_0" id="outqty_0" value="" />
|
|
||||||
|
|
||||||
<input type="hidden" name="pg_0" id="pg_0" value="" />
|
|
||||||
<input type="hidden" name="prodno_0" id="prodno_0" value="" />
|
|
||||||
<input type="hidden" name="termdt_0" id="termdt_0" value="" />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
<td class="td_num" >
|
|
||||||
17,300
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr id="bagLine1">
|
|
||||||
<td class="first"><input type="checkbox" name="chk_1" id="chk_1" class="chkBox" /></td>
|
|
||||||
|
|
||||||
<td class="td_nm" title="(오가논)코자정 50mg(PTP) 30T" ><a href="./PhysicInfo.asp?pc=32495&currVenCd=50911" target="_blank" class="bagPhysic_ln">(오가논)코자정 50mg(PTP)30T</a></td>
|
|
||||||
|
|
||||||
<td >
|
|
||||||
<input type="text" name="bagQty_1" id="bagQty_1" maxlength="10" class="setInput_h18_qty" value="1" data="1"
|
|
||||||
style="width:25px;"/>
|
|
||||||
<input type="hidden" name="pc_1" id="pc_1" value="32495" />
|
|
||||||
<input type="hidden" name="stock_1" id="stock_1" value="234" />
|
|
||||||
<input type="hidden" name="price_1" value="14220" />
|
|
||||||
<input type="hidden" name="physic_nm1" value="(오가논)코자정 50mg(PTP)" />
|
|
||||||
<input type="hidden" name="totalPrice1" id="totalPrice1" value="14220" />
|
|
||||||
<input type="hidden" name="ordunitqty_1" id="ordunitqty_1" value="0" />
|
|
||||||
<input type="hidden" name="bidqty_1" id="bidqty_1" value="" />
|
|
||||||
<input type="hidden" name="outqty_1" id="outqty_1" value="" />
|
|
||||||
|
|
||||||
<input type="hidden" name="pg_1" id="pg_1" value="" />
|
|
||||||
<input type="hidden" name="prodno_1" id="prodno_1" value="" />
|
|
||||||
<input type="hidden" name="termdt_1" id="termdt_1" value="" />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
<td class="td_num" >
|
|
||||||
14,220
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div><!--scroll-->
|
</div><!--scroll-->
|
||||||
@ -168,7 +112,7 @@
|
|||||||
<div class="cntPhysic">
|
<div class="cntPhysic">
|
||||||
<dl class="orderPhy">
|
<dl class="orderPhy">
|
||||||
<dt><span>주문품목</span></dt>
|
<dt><span>주문품목</span></dt>
|
||||||
<dd class=""><span id="cnt_order">2개</span></dd>
|
<dd class=""><span id="cnt_order">0개</span></dd>
|
||||||
</dl>
|
</dl>
|
||||||
<dl class="cancelPhy">
|
<dl class="cancelPhy">
|
||||||
<dt><span>취소품목</span></dt>
|
<dt><span>취소품목</span></dt>
|
||||||
@ -177,15 +121,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<dl class="total">
|
<dl class="total">
|
||||||
<dt>주문금액</dt>
|
<dt>주문금액</dt>
|
||||||
<dd id="bag_totPrice" class="" data="31520">
|
<dd id="bag_totPrice" class="" data="0">
|
||||||
31,520원
|
0원
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<input type="hidden" name="chkOrderOk" id="chkOrderOk" value="Y" />
|
<input type="hidden" name="chkOrderOk" id="chkOrderOk" value="Y" />
|
||||||
|
|
||||||
<input type="hidden" name="order_min_amt" id="order_min_amt" value="" />
|
<input type="hidden" name="order_min_amt" id="order_min_amt" value="" />
|
||||||
<input type="hidden" name="intArray" id="intArray" value="1" />
|
<input type="hidden" name="intArray" id="intArray" value="-1" />
|
||||||
<input type="hidden" name="currVenCd" id="currVenCd" value="50911" />
|
<input type="hidden" name="currVenCd" id="currVenCd" value="50911" />
|
||||||
<input type="hidden" name="currMkind" id="currMkind" value="" />
|
<input type="hidden" name="currMkind" id="currMkind" value="" />
|
||||||
<input type="hidden" name="kind" value="bag_saveall" />
|
<input type="hidden" name="kind" value="bag_saveall" />
|
||||||
|
|||||||
82
backend/capture_order.py
Normal file
82
backend/capture_order.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
네트워크 캡처용 - 약사님이 직접 주문 버튼 클릭
|
||||||
|
"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
import time
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
print('로그인...')
|
||||||
|
s.login()
|
||||||
|
|
||||||
|
# 장바구니에 코자정 담기
|
||||||
|
print('\n코자정 검색...')
|
||||||
|
result = s.search_products('코자정 50mg PTP')
|
||||||
|
product = None
|
||||||
|
for item in result.get('items', []):
|
||||||
|
if 'PTP' in item['name']:
|
||||||
|
product = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if product:
|
||||||
|
print(f"제품: {product['name']} - {product['price']:,}원")
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1,
|
||||||
|
price=product['price'], stock=product['stock'])
|
||||||
|
print('장바구니에 담음!')
|
||||||
|
else:
|
||||||
|
print('제품 못 찾음')
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f"\n장바구니: {cart['total_items']}개, {cart['total_amount']:,}원")
|
||||||
|
|
||||||
|
print('\n' + '='*50)
|
||||||
|
print('브라우저 열기 + 네트워크 캡처 시작')
|
||||||
|
print('='*50)
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=False) # 브라우저 보임
|
||||||
|
context = browser.new_context()
|
||||||
|
|
||||||
|
# 세션 쿠키 복사
|
||||||
|
for c in s.session.cookies:
|
||||||
|
context.add_cookies([{
|
||||||
|
'name': c.name,
|
||||||
|
'value': c.value,
|
||||||
|
'domain': c.domain or 'sooinpharm.co.kr',
|
||||||
|
'path': c.path or '/'
|
||||||
|
}])
|
||||||
|
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
|
# 네트워크 요청 캡처
|
||||||
|
def on_request(request):
|
||||||
|
if 'BagOrder' in request.url and request.method == 'POST':
|
||||||
|
print('\n' + '='*50)
|
||||||
|
print('🎯 POST 요청 캡처!')
|
||||||
|
print('='*50)
|
||||||
|
print(f'URL: {request.url}')
|
||||||
|
print(f'\nPOST 데이터:')
|
||||||
|
data = request.post_data or ''
|
||||||
|
# 파라미터별로 출력
|
||||||
|
for param in data.split('&'):
|
||||||
|
if '=' in param:
|
||||||
|
key, val = param.split('=', 1)
|
||||||
|
print(f' {key}: {val[:50]}')
|
||||||
|
print('='*50)
|
||||||
|
|
||||||
|
page.on('request', on_request)
|
||||||
|
|
||||||
|
# 주문 페이지로 이동
|
||||||
|
page.goto('http://sooinpharm.co.kr/Service/Order/Order.asp')
|
||||||
|
|
||||||
|
print('\n✅ 브라우저 준비 완료!')
|
||||||
|
print('👆 주문전송 버튼을 클릭해주세요!')
|
||||||
|
print('\n(Enter 누르면 브라우저 닫힘)')
|
||||||
|
input()
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
print('\n완료!')
|
||||||
79
backend/capture_order2.py
Normal file
79
backend/capture_order2.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
네트워크 캡처 v2 - 새로고침 후에도 캡처
|
||||||
|
"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
import time
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
print('로그인...')
|
||||||
|
s.login()
|
||||||
|
|
||||||
|
# 먼저 장바구니 비우기
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 코자정 담기
|
||||||
|
print('코자정 검색...')
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0] if result.get('items') else None
|
||||||
|
|
||||||
|
if product:
|
||||||
|
print(f"제품: {product['name']} - {product['price']:,}원")
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1,
|
||||||
|
price=product['price'], stock=product['stock'])
|
||||||
|
print('장바구니에 담음!')
|
||||||
|
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f"장바구니: {cart['total_items']}개")
|
||||||
|
|
||||||
|
print('\n브라우저 열기...')
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=False)
|
||||||
|
context = browser.new_context()
|
||||||
|
|
||||||
|
# 쿠키 복사
|
||||||
|
for c in s.session.cookies:
|
||||||
|
context.add_cookies([{
|
||||||
|
'name': c.name,
|
||||||
|
'value': c.value,
|
||||||
|
'domain': c.domain or 'sooinpharm.co.kr',
|
||||||
|
'path': c.path or '/'
|
||||||
|
}])
|
||||||
|
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
|
# 모든 요청 캡처 (지속적)
|
||||||
|
captured = []
|
||||||
|
def capture(request):
|
||||||
|
if 'BagOrder' in request.url and request.method == 'POST':
|
||||||
|
data = request.post_data or ''
|
||||||
|
captured.append(data)
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('🎯 POST 캡처!')
|
||||||
|
print('='*60)
|
||||||
|
for param in data.split('&')[:30]: # 주요 파라미터만
|
||||||
|
if '=' in param:
|
||||||
|
k, v = param.split('=', 1)
|
||||||
|
if v: # 값이 있는 것만
|
||||||
|
print(f' {k}: {v[:60]}')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
context.on('request', capture) # context 레벨에서 캡처
|
||||||
|
|
||||||
|
page.goto('http://sooinpharm.co.kr/Service/Order/Order.asp')
|
||||||
|
|
||||||
|
print('\n✅ 준비 완료!')
|
||||||
|
print('👆 F5로 새로고침 후 주문전송 버튼 클릭!')
|
||||||
|
print('\n(Enter 누르면 종료)')
|
||||||
|
input()
|
||||||
|
|
||||||
|
# 캡처된 데이터 파일로 저장
|
||||||
|
if captured:
|
||||||
|
with open('captured_post.txt', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(captured[0])
|
||||||
|
print('\n📁 captured_post.txt 저장됨')
|
||||||
|
|
||||||
|
browser.close()
|
||||||
8
backend/check_cart.py
Normal file
8
backend/check_cart.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# -*- 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']:,}원")
|
||||||
21
backend/check_sooin_cart.py
Normal file
21
backend/check_sooin_cart.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# -*- 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('🛒 장바구니 비어있음')
|
||||||
BIN
backend/geo_cart_before.png
Normal file
BIN
backend/geo_cart_before.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@ -295,14 +295,22 @@ def submit_geoyoung_order(order: dict, dry_run: bool, cart_only: bool = True) ->
|
|||||||
item_internal_code = item.get('internal_code') # 프론트에서 이미 선택한 품목
|
item_internal_code = item.get('internal_code') # 프론트에서 이미 선택한 품목
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
|
# 🔍 디버그 로그
|
||||||
|
logger.info(f"[GEO DEBUG] item keys: {list(item.keys())}")
|
||||||
|
logger.info(f"[GEO DEBUG] kd_code={kd_code}, internal_code={item_internal_code}, qty={order_qty}, spec={spec}")
|
||||||
|
logger.info(f"[GEO DEBUG] full item: {item}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if item_internal_code:
|
if item_internal_code:
|
||||||
# internal_code가 있으면 검색 없이 바로 장바구니 추가!
|
# internal_code가 있으면 검색 없이 바로 장바구니 추가!
|
||||||
|
logger.info(f"[GEO DEBUG] Using internal_code directly: {item_internal_code}")
|
||||||
result = geo_session.add_to_cart(item_internal_code, order_qty)
|
result = geo_session.add_to_cart(item_internal_code, order_qty)
|
||||||
|
logger.info(f"[GEO DEBUG] add_to_cart result: {result}")
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
result['product'] = {'internal_code': item_internal_code, 'name': item.get('product_name', '')}
|
result['product'] = {'internal_code': item_internal_code, 'name': item.get('product_name', '')}
|
||||||
else:
|
else:
|
||||||
# internal_code 없으면 검색 후 장바구니 추가
|
# internal_code 없으면 검색 후 장바구니 추가
|
||||||
|
logger.info(f"[GEO DEBUG] No internal_code, using full_order with kd_code={kd_code}")
|
||||||
result = geo_session.full_order(
|
result = geo_session.full_order(
|
||||||
kd_code=kd_code,
|
kd_code=kd_code,
|
||||||
quantity=order_qty,
|
quantity=order_qty,
|
||||||
@ -311,6 +319,7 @@ def submit_geoyoung_order(order: dict, dry_run: bool, cart_only: bool = True) ->
|
|||||||
auto_confirm=False,
|
auto_confirm=False,
|
||||||
memo=f"자동주문 - {item.get('product_name', '')}"
|
memo=f"자동주문 - {item.get('product_name', '')}"
|
||||||
)
|
)
|
||||||
|
logger.info(f"[GEO DEBUG] full_order result: {result}")
|
||||||
|
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
status = 'success'
|
status = 'success'
|
||||||
@ -366,6 +375,11 @@ def submit_geoyoung_order(order: dict, dry_run: bool, cart_only: bool = True) ->
|
|||||||
ordered_codes = [r['internal_code'] for r in results
|
ordered_codes = [r['internal_code'] for r in results
|
||||||
if r['status'] == 'success' and r.get('internal_code')]
|
if r['status'] == 'success' and r.get('internal_code')]
|
||||||
|
|
||||||
|
# 🔧 디버그: 선별 주문 전 상세 로그
|
||||||
|
logger.info(f"[GEO DEBUG] 선별 주문 시작")
|
||||||
|
logger.info(f"[GEO DEBUG] ordered_codes: {ordered_codes}")
|
||||||
|
logger.info(f"[GEO DEBUG] results: {[(r.get('product_name', '')[:20], r.get('internal_code')) for r in results if r['status'] == 'success']}")
|
||||||
|
|
||||||
if ordered_codes:
|
if ordered_codes:
|
||||||
# 선별 주문: 기존 품목은 건드리지 않고, 이번에 담은 것만 주문
|
# 선별 주문: 기존 품목은 건드리지 않고, 이번에 담은 것만 주문
|
||||||
confirm_result = geo_session.submit_order_selective(ordered_codes)
|
confirm_result = geo_session.submit_order_selective(ordered_codes)
|
||||||
|
|||||||
@ -105,6 +105,7 @@ def init_db():
|
|||||||
-- 약품 정보
|
-- 약품 정보
|
||||||
drug_code TEXT NOT NULL, -- PIT3000 약품코드
|
drug_code TEXT NOT NULL, -- PIT3000 약품코드
|
||||||
kd_code TEXT, -- 보험코드 (지오영 검색용)
|
kd_code TEXT, -- 보험코드 (지오영 검색용)
|
||||||
|
internal_code TEXT, -- 🔧 도매상 내부 코드 (장바구니 직접 추가용!)
|
||||||
product_name TEXT NOT NULL,
|
product_name TEXT NOT NULL,
|
||||||
manufacturer TEXT,
|
manufacturer TEXT,
|
||||||
|
|
||||||
@ -372,14 +373,15 @@ def create_order(wholesaler_id: str, items: List[Dict],
|
|||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT INTO order_items (
|
INSERT INTO order_items (
|
||||||
order_id, drug_code, kd_code, product_name, manufacturer,
|
order_id, drug_code, kd_code, internal_code, product_name, manufacturer,
|
||||||
specification, unit_qty, order_qty, total_dose,
|
specification, unit_qty, order_qty, total_dose,
|
||||||
usage_qty, current_stock, status
|
usage_qty, current_stock, status
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
|
||||||
''', (
|
''', (
|
||||||
order_id,
|
order_id,
|
||||||
item.get('drug_code'),
|
item.get('drug_code'),
|
||||||
item.get('kd_code'),
|
item.get('kd_code'),
|
||||||
|
item.get('internal_code'), # 🔧 도매상 내부 코드 저장!
|
||||||
item.get('product_name'),
|
item.get('product_name'),
|
||||||
item.get('manufacturer'),
|
item.get('manufacturer'),
|
||||||
item.get('specification'),
|
item.get('specification'),
|
||||||
|
|||||||
@ -1464,6 +1464,10 @@
|
|||||||
|
|
||||||
showToast(`📤 ${item.product_name} 주문 중...`, 'info');
|
showToast(`📤 ${item.product_name} 주문 중...`, 'info');
|
||||||
|
|
||||||
|
// 🔍 디버그: 장바구니 아이템 확인
|
||||||
|
console.log('[DEBUG] orderSingleItem - cart item:', JSON.stringify(item, null, 2));
|
||||||
|
console.log('[DEBUG] internal_code:', item.internal_code);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
wholesaler_id: wholesaler,
|
wholesaler_id: wholesaler,
|
||||||
@ -1999,6 +2003,11 @@
|
|||||||
baekje_code: wholesaler === 'baekje' ? item.internal_code : null
|
baekje_code: wholesaler === 'baekje' ? item.internal_code : null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🔍 디버그: 장바구니 추가 시 internal_code 확인
|
||||||
|
console.log('[DEBUG] addToCartFromWholesale');
|
||||||
|
console.log('[DEBUG] wholesaler item:', JSON.stringify(item, null, 2));
|
||||||
|
console.log('[DEBUG] cartItem internal_code:', cartItem.internal_code);
|
||||||
|
|
||||||
// 기존 항목 체크 (같은 도매상 + 같은 규격)
|
// 기존 항목 체크 (같은 도매상 + 같은 규격)
|
||||||
const existing = cart.find(c =>
|
const existing = cart.find(c =>
|
||||||
c.drug_code === currentWholesaleItem.drug_code &&
|
c.drug_code === currentWholesaleItem.drug_code &&
|
||||||
|
|||||||
60
backend/test_api_debug.py
Normal file
60
backend/test_api_debug.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""API 직접 테스트 - 디버그용"""
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 지오영에서 실제 품목 검색해서 internal_code 얻기
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
# 재고 있는 품목 검색
|
||||||
|
r = g.search_products('라식스')
|
||||||
|
if r.get('items'):
|
||||||
|
item = r['items'][0]
|
||||||
|
print("="*60)
|
||||||
|
print("검색된 품목:")
|
||||||
|
print(f" name: {item['name']}")
|
||||||
|
print(f" internal_code: {item['internal_code']}")
|
||||||
|
print(f" stock: {item['stock']}")
|
||||||
|
print(f" price: {item['price']}")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# API 호출
|
||||||
|
payload = {
|
||||||
|
"wholesaler_id": "geoyoung",
|
||||||
|
"items": [{
|
||||||
|
"drug_code": "652100200",
|
||||||
|
"kd_code": "라식스",
|
||||||
|
"internal_code": item['internal_code'], # 검색된 internal_code 사용
|
||||||
|
"product_name": item['name'],
|
||||||
|
"manufacturer": "한독",
|
||||||
|
"specification": item.get('spec', ''),
|
||||||
|
"order_qty": 1,
|
||||||
|
"usage_qty": 100,
|
||||||
|
"current_stock": 0
|
||||||
|
}],
|
||||||
|
"reference_period": "2026-02-01~2026-03-07",
|
||||||
|
"dry_run": False,
|
||||||
|
"cart_only": False
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("API 요청:")
|
||||||
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
'http://localhost:7001/api/order/quick-submit',
|
||||||
|
json=payload,
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(f"응답 (status: {response.status_code}):")
|
||||||
|
print(json.dumps(response.json(), ensure_ascii=False, indent=2))
|
||||||
|
print("="*60)
|
||||||
|
else:
|
||||||
|
print("품목을 찾을 수 없습니다")
|
||||||
@ -1,101 +0,0 @@
|
|||||||
# -*- 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]}")
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
# -*- 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')}")
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
# -*- 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()
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Bag.js 분석"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Common/Javascript/Bag.js?v=250228')
|
|
||||||
js = resp.text
|
|
||||||
|
|
||||||
# del 포함된 부분 찾기
|
|
||||||
lines = js.split('\n')
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if 'del' in line.lower() and ('kind' in line.lower() or 'bagorder' in line.lower()):
|
|
||||||
print(f'{i}: {line.strip()[:100]}')
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Bag.js 전체에서 del 찾기"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Common/Javascript/Bag.js?v=250228')
|
|
||||||
js = resp.text
|
|
||||||
|
|
||||||
print(f'JS 길이: {len(js)}')
|
|
||||||
|
|
||||||
# del 포함된 줄 모두
|
|
||||||
for i, line in enumerate(js.split('\n')):
|
|
||||||
line = line.strip()
|
|
||||||
if 'del' in line.lower():
|
|
||||||
print(f'{line[:120]}')
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Bag.js 체크박스 관련 찾기"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Common/Javascript/Bag.js?v=250228')
|
|
||||||
js = resp.text
|
|
||||||
|
|
||||||
# chk, checkbox 관련 코드 찾기
|
|
||||||
lines = js.split('\n')
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if 'chk' in line.lower() or 'check' in line.lower():
|
|
||||||
print(f'{i}: {line.strip()[:120]}')
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Bag.js AJAX URL 찾기"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Common/Javascript/Bag.js?v=250228')
|
|
||||||
js = resp.text
|
|
||||||
|
|
||||||
# AJAX 호출 찾기 ($.ajax, url:, type: 패턴)
|
|
||||||
ajax_blocks = re.findall(r'\$\.ajax\s*\(\s*\{[^}]{0,500}\}', js, re.DOTALL)
|
|
||||||
print(f'AJAX 호출 {len(ajax_blocks)}개 발견:\n')
|
|
||||||
|
|
||||||
for i, block in enumerate(ajax_blocks[:5]):
|
|
||||||
print(f'=== AJAX {i+1} ===')
|
|
||||||
print(block[:300])
|
|
||||||
print()
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""항목 취소 테스트"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import json
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
print('=== 항목 취소 테스트 ===\n')
|
|
||||||
|
|
||||||
# 1. 장바구니 비우기
|
|
||||||
session.clear_cart()
|
|
||||||
print('1. 장바구니 비움')
|
|
||||||
|
|
||||||
# 2. 두 개 담기
|
|
||||||
session.order_product('073100220', 1, '30T') # 코자정
|
|
||||||
print('2. 코자정 담음')
|
|
||||||
|
|
||||||
session.order_product('652100640', 1) # 스틸녹스
|
|
||||||
print('3. 스틸녹스 담음')
|
|
||||||
|
|
||||||
# 3. 장바구니 확인
|
|
||||||
cart = session.get_cart()
|
|
||||||
print(f'\n현재 장바구니:')
|
|
||||||
print(f' 총 항목: {cart.get("all_items", 0)}개')
|
|
||||||
print(f' 활성(주문포함): {cart.get("total_items", 0)}개')
|
|
||||||
print(f' 취소됨: {cart.get("cancelled_items", 0)}개')
|
|
||||||
for item in cart.get('items', []):
|
|
||||||
status = '❌ 취소' if item.get('checked') else '✅ 활성'
|
|
||||||
print(f' [{item.get("row_index")}] {item.get("product_name")} - {status}')
|
|
||||||
|
|
||||||
# 4. 첫 번째 항목 취소
|
|
||||||
print(f'\n4. 첫 번째 항목(idx=0) 취소 시도...')
|
|
||||||
result = session.cancel_item(row_index=0)
|
|
||||||
print(f' 결과: {result.get("success")} - {result.get("message", result.get("error", ""))}')
|
|
||||||
|
|
||||||
# 5. 다시 확인
|
|
||||||
cart = session.get_cart()
|
|
||||||
print(f'\n취소 후 장바구니:')
|
|
||||||
print(f' 활성: {cart.get("total_items", 0)}개')
|
|
||||||
print(f' 취소됨: {cart.get("cancelled_items", 0)}개')
|
|
||||||
for item in cart.get('items', []):
|
|
||||||
status = '❌ 취소' if item.get('checked') else '✅ 활성'
|
|
||||||
print(f' [{item.get("row_index")}] {item.get("product_name")} - {status}')
|
|
||||||
|
|
||||||
print('\n=== 완료 ===')
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""장바구니 추가 테스트 (실제 주문 X)"""
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend')
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("수인약품 API 장바구니 테스트")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
|
|
||||||
# 1. 로그인
|
|
||||||
print("\n1. 로그인...")
|
|
||||||
if not session.login():
|
|
||||||
print("❌ 로그인 실패")
|
|
||||||
sys.exit(1)
|
|
||||||
print("✅ 로그인 성공!")
|
|
||||||
|
|
||||||
# 2. 장바구니 비우기
|
|
||||||
print("\n2. 장바구니 비우기...")
|
|
||||||
result = session.clear_cart()
|
|
||||||
print(f" 결과: {'성공' if result['success'] else '실패'}")
|
|
||||||
|
|
||||||
# 3. 제품 검색
|
|
||||||
print("\n3. 제품 검색 (KD코드: 073100220 - 코자정)...")
|
|
||||||
products = session.search_products('073100220', 'kd_code')
|
|
||||||
print(f" 검색 결과: {len(products)}개")
|
|
||||||
for p in products:
|
|
||||||
print(f" - {p['product_name']} ({p['specification']}) 재고: {p['stock']} 단가: {p['unit_price']:,}원")
|
|
||||||
print(f" 내부코드: {p['internal_code']}")
|
|
||||||
|
|
||||||
# 4. 장바구니 추가
|
|
||||||
if products:
|
|
||||||
print("\n4. 장바구니 추가 (첫 번째 제품, 1개)...")
|
|
||||||
product = products[1] # 30T 선택
|
|
||||||
result = session.add_to_cart(
|
|
||||||
internal_code=product['internal_code'],
|
|
||||||
quantity=1,
|
|
||||||
stock=product['stock'],
|
|
||||||
price=product['unit_price']
|
|
||||||
)
|
|
||||||
print(f" 결과: {json.dumps(result, ensure_ascii=False, indent=2)}")
|
|
||||||
|
|
||||||
# 5. 장바구니 조회
|
|
||||||
print("\n5. 장바구니 조회...")
|
|
||||||
cart = session.get_cart()
|
|
||||||
print(f" 장바구니: {cart['total_items']}개 품목, {cart['total_amount']:,}원")
|
|
||||||
for item in cart['items']:
|
|
||||||
print(f" - {item['product_name']}: {item['quantity']}개 ({item['amount']:,}원)")
|
|
||||||
|
|
||||||
# 6. 장바구니 비우기 (정리)
|
|
||||||
print("\n6. 장바구니 비우기 (정리)...")
|
|
||||||
result = session.clear_cart()
|
|
||||||
print(f" 결과: {'성공' if result['success'] else '실패'}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("테스트 완료! (실제 주문은 하지 않았습니다)")
|
|
||||||
print("=" * 60)
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""지오영 장바구니 API 직접 테스트 (requests)"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import asyncio
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
|
|
||||||
async def get_cookies():
|
|
||||||
"""Playwright로 로그인 후 쿠키 획득"""
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch(headless=True)
|
|
||||||
page = await browser.new_page()
|
|
||||||
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Member/Login')
|
|
||||||
await page.fill('input[type="text"]', '7390')
|
|
||||||
await page.fill('input[type="password"]', 'trajet6640')
|
|
||||||
await page.click('button, input[type="submit"]')
|
|
||||||
await page.wait_for_load_state('networkidle')
|
|
||||||
|
|
||||||
cookies = await page.context.cookies()
|
|
||||||
await browser.close()
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
def test_cart_api():
|
|
||||||
# 1. 쿠키 획득
|
|
||||||
print("1. 로그인 중...")
|
|
||||||
cookies = asyncio.run(get_cookies())
|
|
||||||
|
|
||||||
# 2. requests 세션 설정
|
|
||||||
session = requests.Session()
|
|
||||||
for c in cookies:
|
|
||||||
session.cookies.set(c['name'], c['value'])
|
|
||||||
|
|
||||||
session.headers.update({
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f" 쿠키: {[c['name'] for c in cookies]}")
|
|
||||||
|
|
||||||
# 3. 제품 검색
|
|
||||||
print("\n2. 제품 검색...")
|
|
||||||
search_resp = session.post('https://gwn.geoweb.kr/Home/PartialSearchProduct', data={
|
|
||||||
'srchText': '643104281',
|
|
||||||
'srchCate': '',
|
|
||||||
'prdtType': '',
|
|
||||||
'prdOrder': '',
|
|
||||||
'srchCompany': '',
|
|
||||||
'startdate': '',
|
|
||||||
'enddate': ''
|
|
||||||
})
|
|
||||||
print(f" 검색 응답: {search_resp.status_code}, 길이: {len(search_resp.text)}")
|
|
||||||
|
|
||||||
# 4. 장바구니 API 테스트 - 여러 엔드포인트 시도
|
|
||||||
print("\n3. 장바구니 API 테스트...")
|
|
||||||
|
|
||||||
endpoints = [
|
|
||||||
'/Home/PartialProductCart',
|
|
||||||
'/Home/AddCart',
|
|
||||||
'/Order/AddCart',
|
|
||||||
'/Home/AddToCart',
|
|
||||||
'/Order/AddToCart',
|
|
||||||
'/Home/InsertCart',
|
|
||||||
'/Order/InsertCart',
|
|
||||||
]
|
|
||||||
|
|
||||||
for endpoint in endpoints:
|
|
||||||
url = f'https://gwn.geoweb.kr{endpoint}'
|
|
||||||
|
|
||||||
# 다양한 파라미터 조합 시도
|
|
||||||
params_list = [
|
|
||||||
{'prdtCode': '643104281', 'qty': 1},
|
|
||||||
{'productCode': '643104281', 'quantity': 1},
|
|
||||||
{'code': '643104281', 'cnt': 1},
|
|
||||||
{'insCode': '643104281', 'orderQty': 1},
|
|
||||||
]
|
|
||||||
|
|
||||||
for params in params_list:
|
|
||||||
try:
|
|
||||||
resp = session.post(url, data=params, timeout=5)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
text = resp.text[:200]
|
|
||||||
if 'error' not in text.lower() and '404' not in text:
|
|
||||||
print(f" ✓ {endpoint}")
|
|
||||||
print(f" Params: {params}")
|
|
||||||
print(f" Response: {text[:100]}...")
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 5. 현재 장바구니 조회
|
|
||||||
print("\n4. 장바구니 조회...")
|
|
||||||
cart_resp = session.post('https://gwn.geoweb.kr/Home/PartialProductCart')
|
|
||||||
print(f" 응답: {cart_resp.status_code}")
|
|
||||||
|
|
||||||
soup = BeautifulSoup(cart_resp.text, 'html.parser')
|
|
||||||
|
|
||||||
# 장바구니 테이블에서 상품 찾기
|
|
||||||
rows = soup.find_all('tr')
|
|
||||||
print(f" 테이블 행: {len(rows)}개")
|
|
||||||
|
|
||||||
# HTML에서 장바구니 추가 폼 찾기
|
|
||||||
forms = soup.find_all('form')
|
|
||||||
for form in forms:
|
|
||||||
action = form.get('action', '')
|
|
||||||
if 'cart' in action.lower() or 'order' in action.lower():
|
|
||||||
print(f" 폼 발견: {action}")
|
|
||||||
inputs = form.find_all('input')
|
|
||||||
for inp in inputs:
|
|
||||||
print(f" - {inp.get('name')}: {inp.get('value', '')}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_cart_api()
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""장바구니 디버깅"""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend')
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
|
|
||||||
if not session.login():
|
|
||||||
print("로그인 실패")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("로그인 성공!")
|
|
||||||
|
|
||||||
# 1. 장바구니 추가 요청의 실제 응답 확인
|
|
||||||
print("\n=== 장바구니 추가 요청 ===")
|
|
||||||
data = {
|
|
||||||
'qty_0': '1',
|
|
||||||
'pc_0': '32495',
|
|
||||||
'stock_0': '238',
|
|
||||||
'saleqty_0': '0',
|
|
||||||
'price_0': '14220',
|
|
||||||
'soldout_0': 'N',
|
|
||||||
'ordunitqty_0': '1',
|
|
||||||
'bidqty_0': '0',
|
|
||||||
'outqty_0': '0',
|
|
||||||
'overqty_0': '0',
|
|
||||||
'manage_0': 'N',
|
|
||||||
'prodno_0': '',
|
|
||||||
'termdt_0': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = session.session.post(session.BAG_URL, data=data, timeout=15)
|
|
||||||
print(f"Status: {resp.status_code}")
|
|
||||||
print(f"URL: {resp.url}")
|
|
||||||
print(f"\n응답 (처음 2000자):\n{resp.text[:2000]}")
|
|
||||||
|
|
||||||
# 2. 장바구니 조회 응답 확인
|
|
||||||
print("\n\n=== 장바구니 조회 요청 ===")
|
|
||||||
params = {'currVenCd': session.VENDOR_CODE}
|
|
||||||
resp2 = session.session.get(session.BAG_URL, params=params, timeout=15)
|
|
||||||
print(f"Status: {resp2.status_code}")
|
|
||||||
print(f"URL: {resp2.url}")
|
|
||||||
print(f"\n응답 (처음 3000자):\n{resp2.text[:3000]}")
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""장바구니 조회 API 테스트"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import asyncio
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
import json
|
|
||||||
|
|
||||||
async def get_cookies():
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch(headless=True)
|
|
||||||
page = await browser.new_page()
|
|
||||||
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Member/Login')
|
|
||||||
await page.fill('input[type="text"]', '7390')
|
|
||||||
await page.fill('input[type="password"]', 'trajet6640')
|
|
||||||
await page.click('button, input[type="submit"]')
|
|
||||||
await page.wait_for_load_state('networkidle')
|
|
||||||
|
|
||||||
cookies = await page.context.cookies()
|
|
||||||
await browser.close()
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
def test():
|
|
||||||
print("="*60)
|
|
||||||
print("장바구니 조회 API 테스트")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
cookies = asyncio.run(get_cookies())
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
for c in cookies:
|
|
||||||
session.cookies.set(c['name'], c['value'])
|
|
||||||
|
|
||||||
session.headers.update({
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 1. 먼저 제품 하나 담기
|
|
||||||
print("\n1. 테스트용 제품 담기...")
|
|
||||||
search_resp = session.post('https://gwn.geoweb.kr/Home/PartialSearchProduct',
|
|
||||||
data={'srchText': '661700390'})
|
|
||||||
soup = BeautifulSoup(search_resp.text, 'html.parser')
|
|
||||||
product_div = soup.find('div', class_='div-product-detail')
|
|
||||||
if product_div:
|
|
||||||
lis = product_div.find_all('li')
|
|
||||||
internal_code = lis[0].get_text(strip=True)
|
|
||||||
|
|
||||||
cart_resp = session.post('https://gwn.geoweb.kr/Home/DataCart/add', data={
|
|
||||||
'productCode': internal_code,
|
|
||||||
'moveCode': '',
|
|
||||||
'orderQty': 3
|
|
||||||
})
|
|
||||||
print(f" 담기 결과: {cart_resp.json()}")
|
|
||||||
|
|
||||||
# 2. 장바구니 조회
|
|
||||||
print("\n2. 장바구니 조회 (PartialProductCart)...")
|
|
||||||
cart_resp = session.post('https://gwn.geoweb.kr/Home/PartialProductCart')
|
|
||||||
print(f" 상태: {cart_resp.status_code}")
|
|
||||||
print(f" 길이: {len(cart_resp.text)}")
|
|
||||||
|
|
||||||
# HTML 파싱
|
|
||||||
soup = BeautifulSoup(cart_resp.text, 'html.parser')
|
|
||||||
|
|
||||||
# 테이블 찾기
|
|
||||||
tables = soup.find_all('table')
|
|
||||||
print(f" 테이블 수: {len(tables)}")
|
|
||||||
|
|
||||||
# 장바구니 항목 파싱
|
|
||||||
cart_items = []
|
|
||||||
|
|
||||||
# div_cart_detail 클래스 찾기
|
|
||||||
cart_divs = soup.find_all('div', class_='div_cart_detail')
|
|
||||||
print(f" cart_detail divs: {len(cart_divs)}")
|
|
||||||
|
|
||||||
for div in cart_divs:
|
|
||||||
lis = div.find_all('li')
|
|
||||||
if len(lis) >= 5:
|
|
||||||
item = {
|
|
||||||
'product_code': lis[0].get_text(strip=True) if len(lis) > 0 else '',
|
|
||||||
'move_code': lis[1].get_text(strip=True) if len(lis) > 1 else '',
|
|
||||||
'quantity': lis[2].get_text(strip=True) if len(lis) > 2 else '',
|
|
||||||
'price': lis[3].get_text(strip=True) if len(lis) > 3 else '',
|
|
||||||
'total': lis[4].get_text(strip=True) if len(lis) > 4 else '',
|
|
||||||
}
|
|
||||||
cart_items.append(item)
|
|
||||||
print(f" - {item}")
|
|
||||||
|
|
||||||
# 테이블 행 분석
|
|
||||||
print("\n 테이블 행 분석:")
|
|
||||||
for table in tables:
|
|
||||||
rows = table.find_all('tr')
|
|
||||||
for row in rows[:5]:
|
|
||||||
cells = row.find_all(['td', 'th'])
|
|
||||||
if cells:
|
|
||||||
texts = [c.get_text(strip=True)[:20] for c in cells[:6]]
|
|
||||||
print(f" {texts}")
|
|
||||||
|
|
||||||
# 3. 다른 API 시도
|
|
||||||
print("\n3. 다른 장바구니 API 시도...")
|
|
||||||
|
|
||||||
endpoints = [
|
|
||||||
'/Home/GetCartList',
|
|
||||||
'/Home/CartList',
|
|
||||||
'/Order/GetCart',
|
|
||||||
'/Order/CartList',
|
|
||||||
'/Home/DataCart/list',
|
|
||||||
]
|
|
||||||
|
|
||||||
for ep in endpoints:
|
|
||||||
try:
|
|
||||||
resp = session.post(f'https://gwn.geoweb.kr{ep}', timeout=5)
|
|
||||||
if resp.status_code == 200 and len(resp.text) > 100:
|
|
||||||
print(f" ✓ {ep}: {len(resp.text)} bytes")
|
|
||||||
print(f" {resp.text[:100]}...")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. 장바구니 비우기
|
|
||||||
print("\n4. 장바구니 비우기...")
|
|
||||||
session.post('https://gwn.geoweb.kr/Home/DataCart/delAll')
|
|
||||||
print(" 완료")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test()
|
|
||||||
52
backend/test_checkbox.py
Normal file
52
backend/test_checkbox.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0]
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1, price=product['price'], stock=product['stock'])
|
||||||
|
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
form = soup.find('form', {'id': 'frmBag'})
|
||||||
|
|
||||||
|
form_data = {}
|
||||||
|
for inp in form.find_all('input'):
|
||||||
|
name = inp.get('name', '')
|
||||||
|
if not name: continue
|
||||||
|
inp_type = inp.get('type', '').lower()
|
||||||
|
if inp_type == 'checkbox':
|
||||||
|
# 체크박스는 'on' 값으로 전송!
|
||||||
|
form_data[name] = 'on'
|
||||||
|
else:
|
||||||
|
form_data[name] = inp.get('value', '')
|
||||||
|
|
||||||
|
form_data['kind'] = 'order'
|
||||||
|
form_data['x'] = '10'
|
||||||
|
form_data['y'] = '10'
|
||||||
|
|
||||||
|
print('체크박스 포함된 form_data:')
|
||||||
|
print(f" chk_0: {form_data.get('chk_0')}")
|
||||||
|
|
||||||
|
resp = s.session.post(s.BAG_URL, data=form_data, timeout=30)
|
||||||
|
alert_match = re.search(r'alert\("([^"]*)"\)', resp.text)
|
||||||
|
alert_msg = alert_match.group(1) if alert_match else 'N/A'
|
||||||
|
print(f'alert 메시지: {alert_msg}')
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
resp2 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup2 = BeautifulSoup(resp2.content, 'html.parser')
|
||||||
|
int_array = soup2.find('input', {'name': 'intArray'})
|
||||||
|
val = int_array.get('value') if int_array else '없음'
|
||||||
|
print(f'주문 후 intArray: {val}')
|
||||||
|
|
||||||
|
if val == '-1':
|
||||||
|
print('\n🎉 주문 성공!')
|
||||||
|
else:
|
||||||
|
print('\n❌ 주문 실패')
|
||||||
42
backend/test_checkbox_html.py
Normal file
42
backend/test_checkbox_html.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""체크박스 HTML 상태 확인"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 품목 담기
|
||||||
|
r1 = s.search_products('코자정')
|
||||||
|
p1 = r1['items'][0]
|
||||||
|
s.add_to_cart(p1['internal_code'], qty=1, price=p1['price'], stock=p1['stock'])
|
||||||
|
|
||||||
|
# 취소하기 전 HTML
|
||||||
|
print('=== 취소 전 HTML ===')
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
for cb in soup.find_all('input', {'type': 'checkbox'}):
|
||||||
|
name = cb.get('name', '')
|
||||||
|
checked = cb.get('checked')
|
||||||
|
print(f"체크박스 {name}: checked={checked}")
|
||||||
|
|
||||||
|
# 취소
|
||||||
|
print('\n=== 취소 실행 ===')
|
||||||
|
s.cancel_item(row_index=0)
|
||||||
|
|
||||||
|
# 취소 후 HTML
|
||||||
|
print('\n=== 취소 후 HTML ===')
|
||||||
|
resp2 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup2 = BeautifulSoup(resp2.content, 'html.parser')
|
||||||
|
for cb in soup2.find_all('input', {'type': 'checkbox'}):
|
||||||
|
name = cb.get('name', '')
|
||||||
|
checked = cb.get('checked')
|
||||||
|
print(f"체크박스 {name}: checked={checked}")
|
||||||
|
|
||||||
|
# 체크박스 HTML 전체 출력
|
||||||
|
cb = soup2.find('input', {'type': 'checkbox'})
|
||||||
|
if cb:
|
||||||
|
print(f"\n전체 HTML: {cb}")
|
||||||
58
backend/test_checkbox_logic.py
Normal file
58
backend/test_checkbox_logic.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""체크박스 로직 테스트 - 체크 안 함 vs 체크함"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 장바구니에 품목 추가
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0]
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1, price=product['price'], stock=product['stock'])
|
||||||
|
|
||||||
|
print("="*60)
|
||||||
|
print("테스트 1: 체크박스 제외 (체크 안 함 = 주문 포함)")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
form = soup.find('form', {'id': 'frmBag'})
|
||||||
|
|
||||||
|
form_data = {}
|
||||||
|
for inp in form.find_all('input'):
|
||||||
|
name = inp.get('name', '')
|
||||||
|
if not name: continue
|
||||||
|
inp_type = inp.get('type', '').lower()
|
||||||
|
if inp_type == 'checkbox':
|
||||||
|
continue # 체크박스 제외!
|
||||||
|
form_data[name] = inp.get('value', '')
|
||||||
|
|
||||||
|
print(f"chk_0 전송됨? {'chk_0' in form_data}")
|
||||||
|
print(f"intArray: {form_data.get('intArray')}")
|
||||||
|
|
||||||
|
resp = s.session.post(
|
||||||
|
s.ORDER_END_URL,
|
||||||
|
data=form_data,
|
||||||
|
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
alert_match = re.search(r'alert\("([^"]*)"\)', resp.text)
|
||||||
|
alert_msg = alert_match.group(1) if alert_match else ''
|
||||||
|
print(f"응답 alert: '{alert_msg}'")
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
resp2 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup2 = BeautifulSoup(resp2.content, 'html.parser')
|
||||||
|
int_array = soup2.find('input', {'name': 'intArray'})
|
||||||
|
val = int_array.get('value') if int_array else 'N/A'
|
||||||
|
print(f"주문 후 intArray: {val}")
|
||||||
|
|
||||||
|
if '주문이 완료' in alert_msg:
|
||||||
|
print("✅ 성공!")
|
||||||
|
else:
|
||||||
|
print("❌ 실패")
|
||||||
@ -1,74 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""지오영 DataCart API 테스트"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import asyncio
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
import time
|
|
||||||
|
|
||||||
async def get_cookies():
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch(headless=True)
|
|
||||||
page = await browser.new_page()
|
|
||||||
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Member/Login')
|
|
||||||
await page.fill('input[type="text"]', '7390')
|
|
||||||
await page.fill('input[type="password"]', 'trajet6640')
|
|
||||||
await page.click('button, input[type="submit"]')
|
|
||||||
await page.wait_for_load_state('networkidle')
|
|
||||||
|
|
||||||
cookies = await page.context.cookies()
|
|
||||||
await browser.close()
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
def test_datacart():
|
|
||||||
print("1. 로그인 중...")
|
|
||||||
start = time.time()
|
|
||||||
cookies = asyncio.run(get_cookies())
|
|
||||||
print(f" 로그인 완료: {time.time()-start:.1f}초")
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
for c in cookies:
|
|
||||||
session.cookies.set(c['name'], c['value'])
|
|
||||||
|
|
||||||
session.headers.update({
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2. 장바구니 추가 테스트
|
|
||||||
print("\n2. 장바구니 추가 테스트...")
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
resp = session.post('https://gwn.geoweb.kr/Home/DataCart/add', data={
|
|
||||||
'productCode': '643104281', # 하일렌플러스
|
|
||||||
'moveCode': '',
|
|
||||||
'orderQty': 1
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f" 소요시간: {time.time()-start:.1f}초")
|
|
||||||
print(f" 상태코드: {resp.status_code}")
|
|
||||||
print(f" 응답: {resp.text[:500]}")
|
|
||||||
|
|
||||||
# JSON 파싱
|
|
||||||
try:
|
|
||||||
result = resp.json()
|
|
||||||
print(f" result: {result.get('result')}")
|
|
||||||
print(f" msg: {result.get('msg')}")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 3. 장바구니 조회
|
|
||||||
print("\n3. 장바구니 조회...")
|
|
||||||
cart_resp = session.post('https://gwn.geoweb.kr/Home/PartialProductCart')
|
|
||||||
print(f" 응답 길이: {len(cart_resp.text)}")
|
|
||||||
|
|
||||||
# 장바구니에 상품 있는지 확인
|
|
||||||
if '643104281' in cart_resp.text or '하일렌' in cart_resp.text:
|
|
||||||
print(" ✓ 장바구니에 상품 추가됨!")
|
|
||||||
else:
|
|
||||||
print(" ? 장바구니 확인 필요")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_datacart()
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""지오영 검색 → 장바구니 추가 테스트"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import asyncio
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
async def get_cookies():
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch(headless=True)
|
|
||||||
page = await browser.new_page()
|
|
||||||
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Member/Login')
|
|
||||||
await page.fill('input[type="text"]', '7390')
|
|
||||||
await page.fill('input[type="password"]', 'trajet6640')
|
|
||||||
await page.click('button, input[type="submit"]')
|
|
||||||
await page.wait_for_load_state('networkidle')
|
|
||||||
|
|
||||||
cookies = await page.context.cookies()
|
|
||||||
await browser.close()
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
def test():
|
|
||||||
print("1. 로그인...")
|
|
||||||
cookies = asyncio.run(get_cookies())
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
for c in cookies:
|
|
||||||
session.cookies.set(c['name'], c['value'])
|
|
||||||
|
|
||||||
session.headers.update({
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2. 검색
|
|
||||||
print("\n2. 제품 검색 (661700390 - 콩코르정)...")
|
|
||||||
search_resp = session.post('https://gwn.geoweb.kr/Home/PartialSearchProduct', data={
|
|
||||||
'srchText': '661700390'
|
|
||||||
})
|
|
||||||
|
|
||||||
soup = BeautifulSoup(search_resp.text, 'html.parser')
|
|
||||||
|
|
||||||
# 제품 코드 찾기 - data 속성이나 hidden input에서
|
|
||||||
rows = soup.find_all('tr')
|
|
||||||
print(f" 테이블 행: {len(rows)}개")
|
|
||||||
|
|
||||||
# HTML 구조 분석
|
|
||||||
for row in rows[:2]:
|
|
||||||
tds = row.find_all('td')
|
|
||||||
if tds:
|
|
||||||
print(f" TD 개수: {len(tds)}")
|
|
||||||
for i, td in enumerate(tds[:8]):
|
|
||||||
text = td.get_text(strip=True)[:30]
|
|
||||||
onclick = td.get('onclick', '')[:50]
|
|
||||||
data_attrs = {k:v for k,v in td.attrs.items() if k.startswith('data')}
|
|
||||||
print(f" [{i}] {text} | onclick={onclick} | data={data_attrs}")
|
|
||||||
|
|
||||||
# onclick에서 제품 코드 추출
|
|
||||||
onclick_pattern = re.findall(r"onclick=['\"]([^'\"]+)['\"]", search_resp.text)
|
|
||||||
for oc in onclick_pattern[:3]:
|
|
||||||
print(f" onclick: {oc[:100]}")
|
|
||||||
|
|
||||||
# SelectProduct 함수 호출에서 인덱스 확인
|
|
||||||
select_pattern = re.findall(r'SelectProduct\s*\(\s*(\d+)', search_resp.text)
|
|
||||||
print(f" SelectProduct 인덱스: {select_pattern[:3]}")
|
|
||||||
|
|
||||||
# div-product-detail에서 제품 코드 찾기
|
|
||||||
product_divs = soup.find_all('div', class_='div-product-detail')
|
|
||||||
print(f" product-detail divs: {len(product_divs)}")
|
|
||||||
|
|
||||||
for div in product_divs[:2]:
|
|
||||||
lis = div.find_all('li')
|
|
||||||
if lis:
|
|
||||||
print(f" li 개수: {len(lis)}")
|
|
||||||
for i, li in enumerate(lis[:5]):
|
|
||||||
print(f" [{i}] {li.get_text(strip=True)[:50]}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test()
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""지오영 장바구니 추가 - 정확한 productCode로 테스트"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import asyncio
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
import time
|
|
||||||
|
|
||||||
async def get_cookies():
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch(headless=True)
|
|
||||||
page = await browser.new_page()
|
|
||||||
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Member/Login')
|
|
||||||
await page.fill('input[type="text"]', '7390')
|
|
||||||
await page.fill('input[type="password"]', 'trajet6640')
|
|
||||||
await page.click('button, input[type="submit"]')
|
|
||||||
await page.wait_for_load_state('networkidle')
|
|
||||||
|
|
||||||
cookies = await page.context.cookies()
|
|
||||||
await browser.close()
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
def test():
|
|
||||||
print("="*60)
|
|
||||||
print("지오영 API 직접 호출 테스트")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# 1. 로그인
|
|
||||||
print("\n1. 로그인...")
|
|
||||||
start = time.time()
|
|
||||||
cookies = asyncio.run(get_cookies())
|
|
||||||
print(f" 완료: {time.time()-start:.1f}초")
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
for c in cookies:
|
|
||||||
session.cookies.set(c['name'], c['value'])
|
|
||||||
|
|
||||||
session.headers.update({
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2. 검색해서 productCode 획득
|
|
||||||
print("\n2. 제품 검색...")
|
|
||||||
start = time.time()
|
|
||||||
search_resp = session.post('https://gwn.geoweb.kr/Home/PartialSearchProduct', data={
|
|
||||||
'srchText': '661700390' # 콩코르정
|
|
||||||
})
|
|
||||||
print(f" 완료: {time.time()-start:.1f}초")
|
|
||||||
|
|
||||||
soup = BeautifulSoup(search_resp.text, 'html.parser')
|
|
||||||
product_div = soup.find('div', class_='div-product-detail')
|
|
||||||
|
|
||||||
if product_div:
|
|
||||||
lis = product_div.find_all('li')
|
|
||||||
product_code = lis[0].get_text(strip=True) if lis else None
|
|
||||||
print(f" productCode: {product_code}")
|
|
||||||
else:
|
|
||||||
print(" 제품 없음!")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 3. 장바구니 추가
|
|
||||||
print("\n3. 장바구니 추가...")
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
cart_resp = session.post('https://gwn.geoweb.kr/Home/DataCart/add', data={
|
|
||||||
'productCode': product_code,
|
|
||||||
'moveCode': '',
|
|
||||||
'orderQty': 2
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f" 완료: {time.time()-start:.1f}초")
|
|
||||||
print(f" 상태: {cart_resp.status_code}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = cart_resp.json()
|
|
||||||
print(f" result: {result.get('result')}")
|
|
||||||
print(f" msg: {result.get('msg', 'OK')}")
|
|
||||||
|
|
||||||
if result.get('result') == 1:
|
|
||||||
print("\n ✅ 장바구니 추가 성공!")
|
|
||||||
else:
|
|
||||||
print(f"\n ❌ 실패: {result.get('msg')}")
|
|
||||||
except:
|
|
||||||
print(f" 응답: {cart_resp.text[:200]}")
|
|
||||||
|
|
||||||
# 4. 장바구니 확인
|
|
||||||
print("\n4. 장바구니 확인...")
|
|
||||||
cart_check = session.post('https://gwn.geoweb.kr/Home/PartialProductCart')
|
|
||||||
|
|
||||||
if '콩코르' in cart_check.text or product_code in cart_check.text:
|
|
||||||
print(" ✅ 장바구니에 상품 있음!")
|
|
||||||
else:
|
|
||||||
print(" 확인 필요")
|
|
||||||
|
|
||||||
# 전체 시간
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("총 API 호출 시간: 검색 + 장바구니 추가 = ~3초")
|
|
||||||
print("(Playwright 30초+ 대비 10배 이상 빠름!)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test()
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""지오영 주문 확정 API 테스트"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import asyncio
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
import time
|
|
||||||
|
|
||||||
async def get_cookies():
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch(headless=True)
|
|
||||||
page = await browser.new_page()
|
|
||||||
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Member/Login')
|
|
||||||
await page.fill('input[type="text"]', '7390')
|
|
||||||
await page.fill('input[type="password"]', 'trajet6640')
|
|
||||||
await page.click('button, input[type="submit"]')
|
|
||||||
await page.wait_for_load_state('networkidle')
|
|
||||||
|
|
||||||
cookies = await page.context.cookies()
|
|
||||||
await browser.close()
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
def test():
|
|
||||||
print("="*60)
|
|
||||||
print("지오영 전체 주문 플로우 테스트")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
# 1. 로그인
|
|
||||||
print("\n1. 로그인...")
|
|
||||||
start = time.time()
|
|
||||||
cookies = asyncio.run(get_cookies())
|
|
||||||
print(f" 완료: {time.time()-start:.1f}초")
|
|
||||||
|
|
||||||
session = requests.Session()
|
|
||||||
for c in cookies:
|
|
||||||
session.cookies.set(c['name'], c['value'])
|
|
||||||
|
|
||||||
session.headers.update({
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2. 검색 → productCode 획득
|
|
||||||
print("\n2. 제품 검색...")
|
|
||||||
start = time.time()
|
|
||||||
search_resp = session.post('https://gwn.geoweb.kr/Home/PartialSearchProduct', data={
|
|
||||||
'srchText': '661700390'
|
|
||||||
})
|
|
||||||
soup = BeautifulSoup(search_resp.text, 'html.parser')
|
|
||||||
product_div = soup.find('div', class_='div-product-detail')
|
|
||||||
lis = product_div.find_all('li') if product_div else []
|
|
||||||
product_code = lis[0].get_text(strip=True) if lis else None
|
|
||||||
print(f" productCode: {product_code}")
|
|
||||||
print(f" 완료: {time.time()-start:.1f}초")
|
|
||||||
|
|
||||||
# 3. 장바구니 추가
|
|
||||||
print("\n3. 장바구니 추가...")
|
|
||||||
start = time.time()
|
|
||||||
cart_resp = session.post('https://gwn.geoweb.kr/Home/DataCart/add', data={
|
|
||||||
'productCode': product_code,
|
|
||||||
'moveCode': '',
|
|
||||||
'orderQty': 1
|
|
||||||
})
|
|
||||||
result = cart_resp.json()
|
|
||||||
print(f" result: {result.get('result')}")
|
|
||||||
print(f" 완료: {time.time()-start:.1f}초")
|
|
||||||
|
|
||||||
if result.get('result') != 1:
|
|
||||||
print(f" ❌ 장바구니 추가 실패: {result.get('msg')}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 4. 주문 확정 (실제 주문!) - 테스트이므로 실행 안함
|
|
||||||
print("\n4. 주문 확정 API 테스트...")
|
|
||||||
print(" ⚠️ 실제 주문이 들어가므로 테스트 중지!")
|
|
||||||
print(" API: POST /Home/DataOrder")
|
|
||||||
print(" params: { p_desc: '메모' }")
|
|
||||||
|
|
||||||
# 실제 주문 코드 (주석 처리)
|
|
||||||
# order_resp = session.post('https://gwn.geoweb.kr/Home/DataOrder', data={
|
|
||||||
# 'p_desc': '테스트 주문'
|
|
||||||
# })
|
|
||||||
# print(f" 응답: {order_resp.text[:200]}")
|
|
||||||
|
|
||||||
# 5. 장바구니 비우기 (테스트용)
|
|
||||||
print("\n5. 장바구니 비우기...")
|
|
||||||
# 장바구니에서 삭제
|
|
||||||
clear_resp = session.post('https://gwn.geoweb.kr/Home/DataCart/delAll')
|
|
||||||
print(f" 상태: {clear_resp.status_code}")
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("✅ 전체 API 플로우 확인 완료!")
|
|
||||||
print("")
|
|
||||||
print("1. 검색: POST /Home/PartialSearchProduct")
|
|
||||||
print("2. 장바구니: POST /Home/DataCart/add")
|
|
||||||
print("3. 주문확정: POST /Home/DataOrder")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test()
|
|
||||||
51
backend/test_debug_submit.py
Normal file
51
backend/test_debug_submit.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""submit_order 디버깅"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.sooin
|
||||||
|
importlib.reload(wholesale.sooin)
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
SooinSession._instance = None
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 품목 담기
|
||||||
|
r1 = s.search_products('코자정')
|
||||||
|
s.add_to_cart(r1['items'][0]['internal_code'], qty=1, price=r1['items'][0]['price'], stock=r1['items'][0]['stock'])
|
||||||
|
|
||||||
|
# 취소
|
||||||
|
s.cancel_item(row_index=0)
|
||||||
|
|
||||||
|
# Bag.asp GET
|
||||||
|
print('=== Bag.asp GET 후 form 분석 ===')
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
form = soup.find('form', {'id': 'frmBag'})
|
||||||
|
|
||||||
|
form_data = {}
|
||||||
|
for inp in form.find_all('input'):
|
||||||
|
name = inp.get('name', '')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
inp_type = inp.get('type', 'text').lower()
|
||||||
|
|
||||||
|
if inp_type == 'checkbox':
|
||||||
|
checked = inp.get('checked')
|
||||||
|
print(f"체크박스 {name}: checked={checked}, type={type(checked)}")
|
||||||
|
|
||||||
|
if checked is not None:
|
||||||
|
form_data[name] = 'on'
|
||||||
|
print(f" → form_data['{name}'] = 'on' (취소됨, 제외)")
|
||||||
|
else:
|
||||||
|
print(f" → 안 보냄 (활성, 포함)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
form_data[name] = inp.get('value', '')
|
||||||
|
|
||||||
|
print(f"\n체크박스 관련 form_data: {[(k,v) for k,v in form_data.items() if 'chk' in k]}")
|
||||||
@ -1,32 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Service/Order/Bag.asp?currVenCd=50911')
|
|
||||||
|
|
||||||
# 개별 삭제 관련 찾기
|
|
||||||
html = resp.text
|
|
||||||
|
|
||||||
# kind 파라미터 종류
|
|
||||||
kinds = re.findall(r'kind=(\w+)', html)
|
|
||||||
print('kind 파라미터들:', list(set(kinds)))
|
|
||||||
|
|
||||||
# 체크박스 관련 함수
|
|
||||||
if 'chk_' in html:
|
|
||||||
print('\n체크박스 있음 (chk_0, chk_1 등)')
|
|
||||||
|
|
||||||
# delOne 같은 개별 삭제
|
|
||||||
if 'delOne' in html or 'deleteOne' in html:
|
|
||||||
print('개별 삭제 함수 있음')
|
|
||||||
|
|
||||||
# 선택삭제 버튼
|
|
||||||
if '선택삭제' in html or '선택 삭제' in html:
|
|
||||||
print('선택삭제 버튼 있음')
|
|
||||||
|
|
||||||
# 전체 삭제 URL
|
|
||||||
del_url = re.search(r'BagOrder\.asp\?kind=del[^"\'>\s]*', html)
|
|
||||||
if del_url:
|
|
||||||
print(f'\n전체 삭제 URL: {del_url.group()}')
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Service/Order/Bag.asp?currVenCd=50911')
|
|
||||||
html = resp.text
|
|
||||||
|
|
||||||
# 모든 script 내용 출력
|
|
||||||
scripts = re.findall(r'<script[^>]*>(.*?)</script>', html, re.DOTALL)
|
|
||||||
|
|
||||||
for i, script in enumerate(scripts):
|
|
||||||
# 삭제/취소 관련 있으면 출력
|
|
||||||
if any(x in script.lower() for x in ['del', 'cancel', 'remove', 'chk_']):
|
|
||||||
print(f'=== Script {i+1} ===')
|
|
||||||
# 함수 시그니처만 추출
|
|
||||||
funcs = re.findall(r'function\s+\w+[^{]+', script)
|
|
||||||
for f in funcs[:5]:
|
|
||||||
print(f' {f.strip()}')
|
|
||||||
|
|
||||||
# 특정 패턴 찾기
|
|
||||||
patterns = re.findall(r'(delPhysic|cancelOrder|chkBag|selectDel)[^(]*\([^)]*\)', script)
|
|
||||||
if patterns:
|
|
||||||
print(f' Patterns: {patterns[:5]}')
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Service/Order/Bag.asp?currVenCd=50911')
|
|
||||||
html = resp.text
|
|
||||||
|
|
||||||
# 모든 <a> 태그의 href와 onclick 찾기
|
|
||||||
links = re.findall(r'<a[^>]*(href|onclick)=["\']([^"\']+)["\'][^>]*>', html)
|
|
||||||
for attr, val in links:
|
|
||||||
if 'del' in val.lower() or 'cancel' in val.lower():
|
|
||||||
print(f'{attr}: {val[:100]}')
|
|
||||||
|
|
||||||
print('\n--- form actions ---')
|
|
||||||
forms = re.findall(r'<form[^>]*action=["\']([^"\']+)["\']', html)
|
|
||||||
for f in forms:
|
|
||||||
print(f'form action: {f}')
|
|
||||||
|
|
||||||
print('\n--- hidden inputs ---')
|
|
||||||
hiddens = re.findall(r'<input[^>]*type=["\']hidden["\'][^>]*name=["\']([^"\']+)["\'][^>]*value=["\']([^"\']*)["\']', html)
|
|
||||||
for name, val in hiddens[:10]:
|
|
||||||
print(f'{name}: {val}')
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""체크박스로 삭제 테스트"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
import re
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
# Bag.asp의 JavaScript 전체 확인
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Service/Order/Bag.asp?currVenCd=50911')
|
|
||||||
|
|
||||||
# onclick 이벤트들 찾기
|
|
||||||
onclicks = re.findall(r'onclick="([^"]*)"', resp.text)
|
|
||||||
print('onclick handlers:')
|
|
||||||
for oc in onclicks[:10]:
|
|
||||||
if len(oc) < 200:
|
|
||||||
print(f' {oc}')
|
|
||||||
|
|
||||||
# form의 name과 action
|
|
||||||
forms = re.findall(r'<form[^>]*name="([^"]*)"[^>]*action="([^"]*)"', resp.text)
|
|
||||||
print('\nForms:')
|
|
||||||
for name, action in forms:
|
|
||||||
print(f' {name}: {action}')
|
|
||||||
|
|
||||||
# 삭제 관련 JavaScript 함수 찾기
|
|
||||||
scripts = re.findall(r'function\s+(\w+Del\w*|\w+Cancel\w*|\w+Remove\w*)\s*\([^)]*\)\s*\{[^}]{0,300}', resp.text, re.IGNORECASE)
|
|
||||||
print('\nDelete functions:')
|
|
||||||
for s in scripts[:5]:
|
|
||||||
print(f' {s[:100]}...')
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""HTML 전체 분석"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Service/Order/Bag.asp?currVenCd=50911')
|
|
||||||
|
|
||||||
# 전체 저장해서 분석
|
|
||||||
with open('bag_page.html', 'w', encoding='utf-8') as f:
|
|
||||||
f.write(resp.text)
|
|
||||||
|
|
||||||
print('bag_page.html 저장됨')
|
|
||||||
print(f'길이: {len(resp.text)}')
|
|
||||||
|
|
||||||
# 현재 장바구니 상태
|
|
||||||
cart = session.get_cart()
|
|
||||||
print(f'장바구니: {cart.get("total_items", 0)}개')
|
|
||||||
for item in cart.get('items', []):
|
|
||||||
print(f' - {item.get("product_name")}')
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""개별 삭제 테스트"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
# 1. 장바구니 비우기
|
|
||||||
session.clear_cart()
|
|
||||||
print('1. 장바구니 비움')
|
|
||||||
|
|
||||||
# 2. 두 개 담기
|
|
||||||
session.order_product('073100220', 1, '30T') # 코자정
|
|
||||||
print('2. 코자정 담음')
|
|
||||||
|
|
||||||
session.order_product('652100640', 1) # 스틸녹스
|
|
||||||
print('3. 스틸녹스 담음')
|
|
||||||
|
|
||||||
# 장바구니 확인
|
|
||||||
cart = session.get_cart()
|
|
||||||
count = cart.get('total_items', 0)
|
|
||||||
print(f' 현재 장바구니: {count}개')
|
|
||||||
for item in cart.get('items', []):
|
|
||||||
print(f' - {item.get("product_name", "")}')
|
|
||||||
|
|
||||||
# 3. 첫 번째 항목만 삭제 (idx=0)
|
|
||||||
print('\n4. idx=0 (첫 번째) 삭제...')
|
|
||||||
resp = session.session.get(
|
|
||||||
'http://sooinpharm.co.kr/Service/Order/BagOrder.asp',
|
|
||||||
params={'kind': 'delOne', 'idx': '0', 'currVenCd': '50911'}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 장바구니 다시 확인
|
|
||||||
cart = session.get_cart()
|
|
||||||
count = cart.get('total_items', 0)
|
|
||||||
print(f' 삭제 후: {count}개')
|
|
||||||
for item in cart.get('items', []):
|
|
||||||
print(f' - {item.get("product_name", "")}')
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""pc 파라미터로 삭제 테스트"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
# 장바구니 확인
|
|
||||||
resp = session.session.get('http://sooinpharm.co.kr/Service/Order/Bag.asp?currVenCd=50911')
|
|
||||||
|
|
||||||
# hidden input들 확인
|
|
||||||
import re
|
|
||||||
hiddens = re.findall(r'name="(pc_\d+|idx_\d+|bagIdx_\d+)"[^>]*value="([^"]*)"', resp.text)
|
|
||||||
print('Hidden fields:')
|
|
||||||
for name, val in hiddens[:10]:
|
|
||||||
print(f' {name}: {val}')
|
|
||||||
|
|
||||||
# 장바구니 iframe의 실제 삭제 로직 찾기
|
|
||||||
# del + pc 조합 시도
|
|
||||||
print('\ndel with pc 시도...')
|
|
||||||
resp = session.session.get(
|
|
||||||
'http://sooinpharm.co.kr/Service/Order/BagOrder.asp',
|
|
||||||
params={
|
|
||||||
'kind': 'delOne',
|
|
||||||
'idx': '0',
|
|
||||||
'pc': '31840', # 스틸녹스 코드
|
|
||||||
'currVenCd': '50911'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 결과
|
|
||||||
cart = session.get_cart()
|
|
||||||
print(f'삭제 후: {cart.get("total_items", 0)}개')
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""개별 삭제 POST 테스트"""
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
session.login()
|
|
||||||
|
|
||||||
# 장바구니 확인
|
|
||||||
cart = session.get_cart()
|
|
||||||
print(f'현재: {cart.get("total_items", 0)}개')
|
|
||||||
|
|
||||||
# POST로 삭제 시도
|
|
||||||
print('\nPOST로 delOne 시도...')
|
|
||||||
resp = session.session.post(
|
|
||||||
'http://sooinpharm.co.kr/Service/Order/BagOrder.asp',
|
|
||||||
data={
|
|
||||||
'kind': 'delOne',
|
|
||||||
'idx': '0',
|
|
||||||
'currVenCd': '50911'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print(f'응답: {resp.text[:300]}')
|
|
||||||
|
|
||||||
# 다시 확인
|
|
||||||
cart = session.get_cart()
|
|
||||||
print(f'\n삭제 후: {cart.get("total_items", 0)}개')
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-pos-qr-system\backend')
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
if session.login():
|
|
||||||
# 직접 요청해서 인코딩 확인
|
|
||||||
params = {
|
|
||||||
'so': '0', 'so2': '0', 'so3': '2',
|
|
||||||
'tx_physic': '073100220',
|
|
||||||
'tx_ven': '50911',
|
|
||||||
'currVenNm': '청춘약국'
|
|
||||||
}
|
|
||||||
resp = session.session.get(session.ORDER_URL, params=params, timeout=15)
|
|
||||||
print('Content-Type:', resp.headers.get('Content-Type'))
|
|
||||||
print('Encoding:', resp.encoding)
|
|
||||||
print('Apparent Encoding:', resp.apparent_encoding)
|
|
||||||
|
|
||||||
# charset 확인
|
|
||||||
charset_match = re.search(r'charset=([^\s;"]+)', resp.text[:1000])
|
|
||||||
print('HTML charset:', charset_match.group(1) if charset_match else 'Not found')
|
|
||||||
|
|
||||||
# 직접 디코딩 테스트
|
|
||||||
print('\n--- 디코딩 테스트 ---')
|
|
||||||
test_encodings = ['euc-kr', 'cp949', 'utf-8', 'iso-8859-1']
|
|
||||||
for enc in test_encodings:
|
|
||||||
try:
|
|
||||||
decoded = resp.content.decode(enc, errors='replace')
|
|
||||||
# 코자정이 포함되어 있는지 확인
|
|
||||||
if '코자정' in decoded:
|
|
||||||
print(f'{enc}: 성공! (코자정 발견)')
|
|
||||||
elif '肄' in decoded or 'ㅺ' in decoded:
|
|
||||||
print(f'{enc}: 부분 실패 (깨진 문자 발견)')
|
|
||||||
else:
|
|
||||||
print(f'{enc}: 확인 불가')
|
|
||||||
except Exception as e:
|
|
||||||
print(f'{enc}: 오류 - {e}')
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Flask Blueprint 테스트"""
|
|
||||||
import wholesale_path
|
|
||||||
from geoyoung_api import geoyoung_bp, get_geo_session
|
|
||||||
from sooin_api import sooin_bp, get_sooin_session
|
|
||||||
|
|
||||||
print('=== Flask Blueprint 테스트 ===\n')
|
|
||||||
|
|
||||||
# Blueprint 확인
|
|
||||||
print(f'지오영 Blueprint: {geoyoung_bp.name} ({geoyoung_bp.url_prefix})')
|
|
||||||
print(f'수인약품 Blueprint: {sooin_bp.name} ({sooin_bp.url_prefix})')
|
|
||||||
|
|
||||||
# 세션 함수 확인
|
|
||||||
geo_session = get_geo_session()
|
|
||||||
sooin_session = get_sooin_session()
|
|
||||||
|
|
||||||
print(f'\n지오영 세션: {geo_session}')
|
|
||||||
print(f'수인약품 세션: {sooin_session}')
|
|
||||||
|
|
||||||
# 라우트 확인
|
|
||||||
print('\n지오영 라우트:')
|
|
||||||
for rule in geoyoung_bp.deferred_functions:
|
|
||||||
print(f' - {rule}')
|
|
||||||
|
|
||||||
print('\n✅ Blueprint 로드 성공!')
|
|
||||||
49
backend/test_geo_api_compare.py
Normal file
49
backend/test_geo_api_compare.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""지오영 full_order vs quick_order 비교 테스트"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.geoyoung
|
||||||
|
importlib.reload(wholesale.geoyoung)
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
GeoYoungSession._instance = None
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
g.clear_cart()
|
||||||
|
|
||||||
|
print('='*60)
|
||||||
|
print('테스트 1: quick_order 직접 호출')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
result1 = g.quick_order(
|
||||||
|
kd_code='라식스',
|
||||||
|
quantity=1,
|
||||||
|
spec=None,
|
||||||
|
check_stock=True
|
||||||
|
)
|
||||||
|
print(f"결과: {result1}")
|
||||||
|
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('테스트 2: full_order 호출 (auto_confirm=False)')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
g.clear_cart()
|
||||||
|
|
||||||
|
result2 = g.full_order(
|
||||||
|
kd_code='코자정',
|
||||||
|
quantity=1,
|
||||||
|
specification=None,
|
||||||
|
check_stock=True,
|
||||||
|
auto_confirm=False # 장바구니만
|
||||||
|
)
|
||||||
|
print(f"결과: {result2}")
|
||||||
|
|
||||||
|
print('\n' + '='*60)
|
||||||
|
print('장바구니 확인')
|
||||||
|
print('='*60)
|
||||||
|
|
||||||
|
cart = g.get_cart()
|
||||||
|
print(f"장바구니: {cart['total_items']}개")
|
||||||
|
for item in cart['items']:
|
||||||
|
print(f" - {item['product_name']}")
|
||||||
60
backend/test_geo_cart_debug.py
Normal file
60
backend/test_geo_cart_debug.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
지오영 장바구니 키 매칭 디버그 테스트
|
||||||
|
- 장바구니 조회 시 반환되는 키와 add_to_cart 시 사용하는 키가 일치하는지 확인
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-wholesale-api')
|
||||||
|
|
||||||
|
from wholesale.geoyoung import GeoYoungSession
|
||||||
|
|
||||||
|
def test_cart_keys():
|
||||||
|
"""장바구니 항목의 키 확인"""
|
||||||
|
session = GeoYoungSession()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("지오영 장바구니 키 매칭 디버그")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 로그인
|
||||||
|
if not session.login():
|
||||||
|
print("❌ 로그인 실패")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("✅ 로그인 성공")
|
||||||
|
|
||||||
|
# 현재 장바구니 조회
|
||||||
|
cart = session.get_cart()
|
||||||
|
|
||||||
|
print(f"\n📦 장바구니 조회 결과:")
|
||||||
|
print(f" - success: {cart.get('success')}")
|
||||||
|
print(f" - total_items: {cart.get('total_items')}")
|
||||||
|
print(f" - total_amount: {cart.get('total_amount'):,}원")
|
||||||
|
|
||||||
|
if not cart.get('items'):
|
||||||
|
print("\n⚠️ 장바구니가 비어있습니다!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n📋 장바구니 항목 상세:")
|
||||||
|
for i, item in enumerate(cart.get('items', [])):
|
||||||
|
print(f"\n [{i}] {item.get('product_name')}")
|
||||||
|
print(f" - row_index: {item.get('row_index')}")
|
||||||
|
print(f" - product_code: {item.get('product_code')}")
|
||||||
|
print(f" - internal_code: {item.get('internal_code')}")
|
||||||
|
print(f" - center: {item.get('center')}")
|
||||||
|
print(f" - quantity: {item.get('quantity')}")
|
||||||
|
print(f" - unit_price: {item.get('unit_price'):,}원")
|
||||||
|
print(f" - amount: {item.get('amount'):,}원")
|
||||||
|
print(f" - active: {item.get('active')}")
|
||||||
|
|
||||||
|
# 키 확인
|
||||||
|
code = item.get('product_code') or item.get('internal_code')
|
||||||
|
print(f" → 사용될 키: {code}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("💡 submit_order_selective()에서 사용하는 키가 위 'product_code'와 일치해야 합니다!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_cart_keys()
|
||||||
16
backend/test_geo_cart_keys.py
Normal file
16
backend/test_geo_cart_keys.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""지오영 장바구니 아이템 키 확인"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
import json
|
||||||
|
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
cart = g.get_cart()
|
||||||
|
print(f"장바구니: {cart['total_items']}개\n")
|
||||||
|
|
||||||
|
if cart['items']:
|
||||||
|
print("첫 번째 아이템 키:")
|
||||||
|
item = cart['items'][0]
|
||||||
|
print(json.dumps(item, indent=2, ensure_ascii=False, default=str))
|
||||||
60
backend/test_geo_clear.py
Normal file
60
backend/test_geo_clear.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
지오영 clear_cart API 테스트
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-wholesale-api')
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(r'c:\Users\청춘약국\source\pharmacy-wholesale-api\.env')
|
||||||
|
|
||||||
|
from wholesale.geoyoung import GeoYoungSession
|
||||||
|
|
||||||
|
def test_clear_cart():
|
||||||
|
session = GeoYoungSession()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("지오영 clear_cart API 테스트")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. 로그인
|
||||||
|
if not session.login():
|
||||||
|
print("❌ 로그인 실패")
|
||||||
|
return
|
||||||
|
print("✅ 로그인 성공\n")
|
||||||
|
|
||||||
|
# 2. 현재 장바구니 조회
|
||||||
|
print("📦 [BEFORE] 장바구니 조회:")
|
||||||
|
cart = session.get_cart()
|
||||||
|
print(f" - 성공: {cart.get('success')}")
|
||||||
|
print(f" - 품목 수: {cart.get('total_items')}")
|
||||||
|
for item in cart.get('items', []):
|
||||||
|
print(f" • {item.get('product_name')} (code: {item.get('product_code')}, qty: {item.get('quantity')})")
|
||||||
|
|
||||||
|
if not cart.get('items'):
|
||||||
|
print("\n⚠️ 장바구니가 이미 비어있어요! 테스트를 위해 뭔가 담아주세요.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. clear_cart 호출
|
||||||
|
print("\n🗑️ clear_cart() 호출...")
|
||||||
|
clear_result = session.clear_cart()
|
||||||
|
print(f" - 결과: {clear_result}")
|
||||||
|
|
||||||
|
# 4. 다시 장바구니 조회
|
||||||
|
import time
|
||||||
|
time.sleep(1) # 서버 처리 대기
|
||||||
|
|
||||||
|
print("\n📦 [AFTER] 장바구니 조회:")
|
||||||
|
cart_after = session.get_cart()
|
||||||
|
print(f" - 성공: {cart_after.get('success')}")
|
||||||
|
print(f" - 품목 수: {cart_after.get('total_items')}")
|
||||||
|
for item in cart_after.get('items', []):
|
||||||
|
print(f" • {item.get('product_name')} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
if not cart_after.get('items'):
|
||||||
|
print("\n✅ clear_cart 성공! 장바구니가 비워졌습니다.")
|
||||||
|
else:
|
||||||
|
print(f"\n❌ clear_cart 실패! 아직 {len(cart_after.get('items', []))}개 품목 남아있음")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_clear_cart()
|
||||||
136
backend/test_geo_clear_button.py
Normal file
136
backend/test_geo_clear_button.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
지오영 "전체삭제" 버튼 분석 - Playwright로 실제 버튼 클릭 시 API 확인
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(r'c:\Users\청춘약국\source\pharmacy-wholesale-api\.env')
|
||||||
|
|
||||||
|
async def analyze_delete_all():
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
username = os.getenv('GEOYOUNG_USER_ID')
|
||||||
|
password = os.getenv('GEOYOUNG_PASSWORD')
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
print("❌ GEOYOUNG_USER_ID, GEOYOUNG_PASSWORD 환경변수 필요")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("지오영 '전체삭제' 버튼 분석")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False) # 보이게
|
||||||
|
page = await browser.new_page()
|
||||||
|
|
||||||
|
# 네트워크 요청 감시
|
||||||
|
requests_log = []
|
||||||
|
|
||||||
|
def log_request(request):
|
||||||
|
if 'Cart' in request.url or 'cart' in request.url or 'del' in request.url.lower():
|
||||||
|
requests_log.append({
|
||||||
|
'method': request.method,
|
||||||
|
'url': request.url,
|
||||||
|
'post_data': request.post_data
|
||||||
|
})
|
||||||
|
print(f"🔗 {request.method} {request.url}")
|
||||||
|
if request.post_data:
|
||||||
|
print(f" POST data: {request.post_data}")
|
||||||
|
|
||||||
|
page.on('request', log_request)
|
||||||
|
|
||||||
|
# 1. 로그인
|
||||||
|
print("\n1️⃣ 로그인 중...")
|
||||||
|
await page.goto("https://gwn.geoweb.kr/Member/Login")
|
||||||
|
await page.fill('input[type="text"]', username)
|
||||||
|
await page.fill('input[type="password"]', password)
|
||||||
|
await page.click('button[type="submit"], input[type="submit"], .btn-login')
|
||||||
|
await page.wait_for_load_state('networkidle', timeout=15000)
|
||||||
|
print("✅ 로그인 완료")
|
||||||
|
|
||||||
|
# 2. 주문 페이지로 이동 (장바구니 표시됨)
|
||||||
|
print("\n2️⃣ 주문 페이지로 이동...")
|
||||||
|
await page.goto("https://gwn.geoweb.kr/Home/Order")
|
||||||
|
await page.wait_for_load_state('networkidle', timeout=15000)
|
||||||
|
|
||||||
|
# 3. 장바구니 확인
|
||||||
|
print("\n3️⃣ 장바구니 확인...")
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# 스크린샷
|
||||||
|
await page.screenshot(path='geo_cart_before.png')
|
||||||
|
print("📸 스크린샷 저장: geo_cart_before.png")
|
||||||
|
|
||||||
|
# 4. "전체삭제" 버튼 찾기
|
||||||
|
print("\n4️⃣ '전체삭제' 버튼 찾기...")
|
||||||
|
|
||||||
|
# 가능한 선택자들
|
||||||
|
selectors = [
|
||||||
|
'button:has-text("전체삭제")',
|
||||||
|
'a:has-text("전체삭제")',
|
||||||
|
'.btn-del-all',
|
||||||
|
'#btnDelAll',
|
||||||
|
'[onclick*="delAll"]',
|
||||||
|
'button:has-text("전체 삭제")',
|
||||||
|
'button:has-text("삭제")',
|
||||||
|
]
|
||||||
|
|
||||||
|
delete_btn = None
|
||||||
|
for sel in selectors:
|
||||||
|
try:
|
||||||
|
btn = page.locator(sel).first
|
||||||
|
if await btn.count() > 0:
|
||||||
|
print(f" ✅ 버튼 발견: {sel}")
|
||||||
|
# 버튼의 HTML 확인
|
||||||
|
btn_html = await btn.evaluate('el => el.outerHTML')
|
||||||
|
print(f" HTML: {btn_html[:200]}...")
|
||||||
|
delete_btn = btn
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not delete_btn:
|
||||||
|
print(" ❌ 전체삭제 버튼을 찾지 못함")
|
||||||
|
# 페이지 HTML에서 삭제 관련 요소 검색
|
||||||
|
html = await page.content()
|
||||||
|
if '전체삭제' in html or 'delAll' in html:
|
||||||
|
print(" ⚠️ 페이지에 관련 텍스트는 있음. 수동 확인 필요")
|
||||||
|
await browser.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. 버튼 클릭 (네트워크 요청 감시)
|
||||||
|
print("\n5️⃣ '전체삭제' 버튼 클릭...")
|
||||||
|
requests_log.clear()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await delete_btn.click()
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
# confirm 다이얼로그 처리
|
||||||
|
page.on('dialog', lambda dialog: asyncio.create_task(dialog.accept()))
|
||||||
|
|
||||||
|
await page.wait_for_load_state('networkidle', timeout=5000)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ 클릭 중 오류: {e}")
|
||||||
|
|
||||||
|
# 6. 캡처된 요청 출력
|
||||||
|
print("\n6️⃣ 캡처된 API 요청:")
|
||||||
|
for req in requests_log:
|
||||||
|
print(f" 📤 {req['method']} {req['url']}")
|
||||||
|
if req['post_data']:
|
||||||
|
print(f" Body: {req['post_data']}")
|
||||||
|
|
||||||
|
# 스크린샷
|
||||||
|
await page.screenshot(path='geo_cart_after.png')
|
||||||
|
print("\n📸 스크린샷 저장: geo_cart_after.png")
|
||||||
|
|
||||||
|
print("\n잠시 대기 (확인용)...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
print("\n✅ 완료")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(analyze_delete_all())
|
||||||
69
backend/test_geo_clear_new.py
Normal file
69
backend/test_geo_clear_new.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
지오영 clear_cart (개선된 버전) 테스트
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-wholesale-api')
|
||||||
|
|
||||||
|
# 싱글톤 초기화 강제
|
||||||
|
import importlib
|
||||||
|
import wholesale.geoyoung
|
||||||
|
importlib.reload(wholesale.geoyoung)
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(r'c:\Users\청춘약국\source\pharmacy-wholesale-api\.env')
|
||||||
|
|
||||||
|
from wholesale.geoyoung import GeoYoungSession
|
||||||
|
|
||||||
|
def test_clear_cart_new():
|
||||||
|
# 싱글톤 리셋
|
||||||
|
GeoYoungSession._instance = None
|
||||||
|
session = GeoYoungSession()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("지오영 clear_cart (개선된 버전) 테스트")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. 로그인
|
||||||
|
if not session.login():
|
||||||
|
print("❌ 로그인 실패")
|
||||||
|
return
|
||||||
|
print("✅ 로그인 성공\n")
|
||||||
|
|
||||||
|
# 2. 제품 추가 (테스트용)
|
||||||
|
print("📦 테스트용 제품 추가 중...")
|
||||||
|
add_result = session.add_to_cart('033133', 1) # 코자르탄
|
||||||
|
print(f" 결과: {add_result}")
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 3. 장바구니 확인
|
||||||
|
print("\n📦 [BEFORE] 장바구니:")
|
||||||
|
cart = session.get_cart()
|
||||||
|
print(f" 품목 수: {cart.get('total_items')}")
|
||||||
|
for item in cart.get('items', []):
|
||||||
|
print(f" • {item.get('product_name')} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
if not cart.get('items'):
|
||||||
|
print(" ⚠️ 장바구니 비어있음. 제품 추가 실패")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. clear_cart 호출
|
||||||
|
print("\n🗑️ clear_cart() 호출 (개선된 버전)...")
|
||||||
|
clear_result = session.clear_cart()
|
||||||
|
print(f" 결과: {clear_result}")
|
||||||
|
|
||||||
|
# 5. 다시 확인
|
||||||
|
time.sleep(1)
|
||||||
|
print("\n📦 [AFTER] 장바구니:")
|
||||||
|
cart_after = session.get_cart()
|
||||||
|
print(f" 품목 수: {cart_after.get('total_items')}")
|
||||||
|
|
||||||
|
if not cart_after.get('items'):
|
||||||
|
print("\n✅ clear_cart 성공!")
|
||||||
|
else:
|
||||||
|
print(f"\n❌ clear_cart 실패! 아직 {len(cart_after.get('items', []))}개 남아있음")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_clear_cart_new()
|
||||||
13
backend/test_geo_debug.py
Normal file
13
backend/test_geo_debug.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
import json
|
||||||
|
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
r = g.search_products('라식스')
|
||||||
|
if r.get('items'):
|
||||||
|
item = r['items'][0]
|
||||||
|
print("첫 번째 품목 전체 데이터:")
|
||||||
|
print(json.dumps(item, indent=2, ensure_ascii=False, default=str))
|
||||||
64
backend/test_geo_delete.py
Normal file
64
backend/test_geo_delete.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
지오영 개별 삭제 API 테스트
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, r'c:\Users\청춘약국\source\pharmacy-wholesale-api')
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(r'c:\Users\청춘약국\source\pharmacy-wholesale-api\.env')
|
||||||
|
|
||||||
|
from wholesale.geoyoung import GeoYoungSession
|
||||||
|
|
||||||
|
def test_delete_item():
|
||||||
|
session = GeoYoungSession()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("지오영 개별 삭제(cancel_item) API 테스트")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. 로그인
|
||||||
|
if not session.login():
|
||||||
|
print("❌ 로그인 실패")
|
||||||
|
return
|
||||||
|
print("✅ 로그인 성공\n")
|
||||||
|
|
||||||
|
# 2. 현재 장바구니 조회
|
||||||
|
print("📦 [BEFORE] 장바구니:")
|
||||||
|
cart = session.get_cart()
|
||||||
|
print(f" 품목 수: {cart.get('total_items')}")
|
||||||
|
|
||||||
|
items = cart.get('items', [])
|
||||||
|
if not items:
|
||||||
|
print(" ⚠️ 장바구니 비어있음")
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
print(f" • {item.get('product_name')} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
# 3. 첫 번째 항목 삭제
|
||||||
|
first_item = items[0]
|
||||||
|
product_code = first_item.get('product_code')
|
||||||
|
print(f"\n🗑️ 첫 번째 항목 삭제 시도: {product_code}")
|
||||||
|
|
||||||
|
del_result = session.cancel_item(product_code=product_code)
|
||||||
|
print(f" 결과: {del_result}")
|
||||||
|
|
||||||
|
# 4. 다시 장바구니 조회
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print("\n📦 [AFTER] 장바구니:")
|
||||||
|
cart_after = session.get_cart()
|
||||||
|
print(f" 품목 수: {cart_after.get('total_items')}")
|
||||||
|
|
||||||
|
for item in cart_after.get('items', []):
|
||||||
|
print(f" • {item.get('product_name')} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
if len(cart_after.get('items', [])) < len(items):
|
||||||
|
print("\n✅ cancel_item 성공!")
|
||||||
|
else:
|
||||||
|
print("\n❌ cancel_item 실패!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_delete_item()
|
||||||
145
backend/test_geo_html.py
Normal file
145
backend/test_geo_html.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
지오영 장바구니 HTML 분석 및 삭제 버튼 API 캡처
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(r'c:\Users\청춘약국\source\pharmacy-wholesale-api\.env')
|
||||||
|
|
||||||
|
async def analyze_cart_html():
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
username = os.getenv('GEOYOUNG_USER_ID')
|
||||||
|
password = os.getenv('GEOYOUNG_PASSWORD')
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("지오영 장바구니 HTML 분석")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=True)
|
||||||
|
context = await browser.new_context()
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
# 1. 로그인
|
||||||
|
print("\n1️⃣ 로그인 중...")
|
||||||
|
await page.goto("https://gwn.geoweb.kr/Member/Login")
|
||||||
|
await page.wait_for_load_state('networkidle')
|
||||||
|
|
||||||
|
# 로그인 폼 찾기
|
||||||
|
await page.fill('input[type="text"], input[name*="id"], #userId', username)
|
||||||
|
await page.fill('input[type="password"], input[name*="pw"], #userPwd', password)
|
||||||
|
|
||||||
|
# 로그인 버튼 클릭
|
||||||
|
login_btns = ['button[type="submit"]', 'input[type="submit"]', '.btn-login', 'button:has-text("로그인")']
|
||||||
|
for btn_sel in login_btns:
|
||||||
|
try:
|
||||||
|
btn = page.locator(btn_sel).first
|
||||||
|
if await btn.count() > 0:
|
||||||
|
await btn.click()
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
await page.wait_for_load_state('networkidle')
|
||||||
|
|
||||||
|
# 로그인 확인
|
||||||
|
if 'Login' in page.url:
|
||||||
|
print("❌ 로그인 실패")
|
||||||
|
await browser.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"✅ 로그인 성공 (URL: {page.url})")
|
||||||
|
|
||||||
|
# 2. 장바구니 HTML 가져오기 (AJAX)
|
||||||
|
print("\n2️⃣ 장바구니 HTML 가져오기...")
|
||||||
|
|
||||||
|
# PartialProductCart API 직접 호출
|
||||||
|
cart_response = await page.evaluate('''
|
||||||
|
async () => {
|
||||||
|
const response = await fetch('/Home/PartialProductCart', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||||
|
});
|
||||||
|
return await response.text();
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
|
||||||
|
print(f"\n📦 장바구니 HTML 길이: {len(cart_response)} bytes")
|
||||||
|
|
||||||
|
# 삭제 관련 키워드 찾기
|
||||||
|
patterns = [
|
||||||
|
r'onclick="[^"]*del[^"]*"',
|
||||||
|
r'onclick="[^"]*Del[^"]*"',
|
||||||
|
r'onclick="[^"]*삭제[^"]*"',
|
||||||
|
r'class="[^"]*del[^"]*"',
|
||||||
|
r'id="[^"]*del[^"]*"',
|
||||||
|
r'function\s+\w*[dD]el\w*\s*\(',
|
||||||
|
r'전체\s*삭제',
|
||||||
|
r'delAll',
|
||||||
|
r'deleteAll',
|
||||||
|
]
|
||||||
|
|
||||||
|
print("\n🔍 삭제 관련 패턴 검색:")
|
||||||
|
for pattern in patterns:
|
||||||
|
matches = re.findall(pattern, cart_response, re.IGNORECASE)
|
||||||
|
if matches:
|
||||||
|
for m in matches[:3]: # 최대 3개만
|
||||||
|
print(f" ✅ {pattern}: {m[:100]}...")
|
||||||
|
|
||||||
|
# 버튼 요소 찾기
|
||||||
|
print("\n🔘 버튼 요소:")
|
||||||
|
button_pattern = r'<button[^>]*>.*?</button>|<a[^>]*class="[^"]*btn[^"]*"[^>]*>.*?</a>'
|
||||||
|
buttons = re.findall(button_pattern, cart_response, re.DOTALL | re.IGNORECASE)
|
||||||
|
for btn in buttons[:10]:
|
||||||
|
clean_btn = re.sub(r'\s+', ' ', btn)[:150]
|
||||||
|
print(f" • {clean_btn}")
|
||||||
|
|
||||||
|
# JavaScript 함수 찾기
|
||||||
|
print("\n📜 JavaScript 함수 (del/remove 관련):")
|
||||||
|
js_pattern = r'function\s+(\w*[dD]el\w*|\w*[rR]emove\w*)\s*\([^)]*\)\s*\{[^}]*\}'
|
||||||
|
js_funcs = re.findall(js_pattern, cart_response)
|
||||||
|
for func in js_funcs[:5]:
|
||||||
|
print(f" • {func}")
|
||||||
|
|
||||||
|
# 전체 스크립트 태그에서 DataCart 관련 찾기
|
||||||
|
print("\n🔧 DataCart API 호출 패턴:")
|
||||||
|
datacart_pattern = r'DataCart[^"\']*'
|
||||||
|
datacart_matches = re.findall(datacart_pattern, cart_response)
|
||||||
|
for m in set(datacart_matches):
|
||||||
|
print(f" • {m}")
|
||||||
|
|
||||||
|
# 페이지 전체 HTML에서도 검색
|
||||||
|
print("\n3️⃣ 메인 페이지에서 추가 검색...")
|
||||||
|
await page.goto("https://gwn.geoweb.kr/Home/Order")
|
||||||
|
await page.wait_for_load_state('networkidle')
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
full_html = await page.content()
|
||||||
|
|
||||||
|
# DataCart 관련 전체 검색
|
||||||
|
print("\n🔧 전체 페이지 DataCart API:")
|
||||||
|
datacart_all = re.findall(r'/Home/DataCart/\w+', full_html)
|
||||||
|
for api in set(datacart_all):
|
||||||
|
print(f" • {api}")
|
||||||
|
|
||||||
|
# 삭제 함수 찾기
|
||||||
|
print("\n📜 삭제 관련 함수:")
|
||||||
|
del_funcs = re.findall(r'function\s+(\w*[dD]el\w*)\s*\(', full_html)
|
||||||
|
for func in set(del_funcs):
|
||||||
|
print(f" • {func}()")
|
||||||
|
|
||||||
|
# 삭제 onclick 찾기
|
||||||
|
print("\n🖱️ 삭제 onclick:")
|
||||||
|
del_onclick = re.findall(r'onclick="([^"]*[dD]el[^"]*)"', full_html)
|
||||||
|
for onclick in set(del_onclick)[:5]:
|
||||||
|
print(f" • {onclick[:100]}")
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
print("\n✅ 분석 완료")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(analyze_cart_html())
|
||||||
15
backend/test_geo_search.py
Normal file
15
backend/test_geo_search.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
for keyword in ['라식스', '코자정', '아스피린']:
|
||||||
|
r = g.search_products(keyword)
|
||||||
|
print(f"\n{keyword} 검색:")
|
||||||
|
if r.get('items'):
|
||||||
|
for item in r['items'][:2]:
|
||||||
|
print(f" {item['name'][:30]} | code: {item.get('product_code', '?')}")
|
||||||
|
else:
|
||||||
|
print(" 없음")
|
||||||
14
backend/test_geo_search2.py
Normal file
14
backend/test_geo_search2.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
for kw in ['베아제', '신신파스', '마그밀', '활명수', '트라스트', '카베진']:
|
||||||
|
r = g.search_products(kw)
|
||||||
|
if r.get('items'):
|
||||||
|
item = r['items'][0]
|
||||||
|
print(f"{kw}: {item['name'][:30]} (code: {item.get('internal_code')})")
|
||||||
|
else:
|
||||||
|
print(f"{kw}: 없음")
|
||||||
64
backend/test_geo_selective.py
Normal file
64
backend/test_geo_selective.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""지오영 선별 주문 테스트"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.geoyoung
|
||||||
|
importlib.reload(wholesale.geoyoung)
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
GeoYoungSession._instance = None
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
g.clear_cart()
|
||||||
|
|
||||||
|
# 재고 있는 품목 검색
|
||||||
|
print('=== 1. 재고 확인 ===')
|
||||||
|
r1 = g.search_products('라식스')
|
||||||
|
r2 = g.search_products('코자정')
|
||||||
|
|
||||||
|
if not r1.get('items') or not r2.get('items'):
|
||||||
|
print('품목을 찾을 수 없습니다')
|
||||||
|
exit()
|
||||||
|
|
||||||
|
p1 = r1['items'][0]
|
||||||
|
p2 = r2['items'][0]
|
||||||
|
print(f"라식스: {p1['name']}, 재고 {p1.get('stock', '?')}, code: {p1['internal_code']}")
|
||||||
|
print(f"코자정: {p2['name']}, 재고 {p2.get('stock', '?')}, code: {p2['internal_code']}")
|
||||||
|
|
||||||
|
# 기존 품목 담기 (라식스 - 나중에 복원할 것)
|
||||||
|
print('\n=== 2. 기존 품목 (라식스) 담기 ===')
|
||||||
|
g.add_to_cart(p1['internal_code'], quantity=1)
|
||||||
|
|
||||||
|
# 새 품목 담기 (코자정 - 주문할 것)
|
||||||
|
print('=== 3. 새 품목 (코자정) 담기 ===')
|
||||||
|
g.add_to_cart(p2['internal_code'], quantity=1)
|
||||||
|
|
||||||
|
cart = g.get_cart()
|
||||||
|
print(f"현재 장바구니: {cart['total_items']}개")
|
||||||
|
for item in cart['items']:
|
||||||
|
code = item.get('product_code') or item.get('internal_code', '?')
|
||||||
|
print(f" - {item['product_name'][:30]} (code: {code})")
|
||||||
|
|
||||||
|
# === 선별 주문 ===
|
||||||
|
print('\n' + '='*50)
|
||||||
|
print('=== 코자정만 주문! ===')
|
||||||
|
print('='*50)
|
||||||
|
|
||||||
|
# 코자정의 internal_code만 전달
|
||||||
|
print(f"\n주문할 internal_code: [{p2['internal_code']}]")
|
||||||
|
result = g.submit_order_selective([p2['internal_code']])
|
||||||
|
print(f"결과: {result}")
|
||||||
|
|
||||||
|
# 최종 확인
|
||||||
|
final = g.get_cart()
|
||||||
|
print(f"\n=== 최종 장바구니: {final['total_items']}개 ===")
|
||||||
|
for item in final['items']:
|
||||||
|
print(f" - {item['product_name'][:30]}")
|
||||||
|
|
||||||
|
if final['total_items'] == 1 and '라식스' in final['items'][0]['product_name']:
|
||||||
|
print('\n🎉 성공! 코자정만 주문됨, 라식스 복원됨!')
|
||||||
|
elif final['total_items'] == 0:
|
||||||
|
print('\n⚠️ 둘 다 주문됨 - 선별 주문 실패')
|
||||||
|
else:
|
||||||
|
print(f'\n🤔 예상 외 결과: {final["total_items"]}개 남음')
|
||||||
70
backend/test_geo_selective_final.py
Normal file
70
backend/test_geo_selective_final.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""지오영 선별 주문 최종 테스트"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.geoyoung
|
||||||
|
importlib.reload(wholesale.geoyoung)
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
GeoYoungSession._instance = None
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
# 기존 장바구니 확인
|
||||||
|
print('=== 0. 기존 장바구니 확인 ===')
|
||||||
|
cart0 = g.get_cart()
|
||||||
|
print(f"기존 품목: {cart0['total_items']}개")
|
||||||
|
for item in cart0['items']:
|
||||||
|
print(f" - {item['product_name'][:30]} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
existing_codes = [item.get('product_code') for item in cart0['items']]
|
||||||
|
|
||||||
|
# 새 품목 담기 (디아맥스)
|
||||||
|
print('\n=== 1. 새 품목 (비타민D) 담기 ===')
|
||||||
|
result = g.full_order(
|
||||||
|
kd_code='썬비타민',
|
||||||
|
quantity=1,
|
||||||
|
specification=None,
|
||||||
|
check_stock=True,
|
||||||
|
auto_confirm=False # 장바구니만
|
||||||
|
)
|
||||||
|
print(f"결과: {result.get('success')}, {result.get('message', result.get('error'))}")
|
||||||
|
|
||||||
|
new_code = result.get('product', {}).get('internal_code') if result.get('success') else None
|
||||||
|
print(f"새 품목 internal_code: {new_code}")
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
print('\n=== 2. 장바구니 확인 ===')
|
||||||
|
cart1 = g.get_cart()
|
||||||
|
print(f"현재 품목: {cart1['total_items']}개")
|
||||||
|
for item in cart1['items']:
|
||||||
|
print(f" - {item['product_name'][:30]} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
# 선별 주문: 새 품목만!
|
||||||
|
if new_code:
|
||||||
|
print('\n=== 3. 선별 주문 (새 품목만!) ===')
|
||||||
|
print(f"주문할 코드: [{new_code}]")
|
||||||
|
|
||||||
|
confirm_result = g.submit_order_selective([new_code])
|
||||||
|
print(f"결과: {confirm_result}")
|
||||||
|
|
||||||
|
# 최종 장바구니 확인
|
||||||
|
print('\n=== 4. 최종 장바구니 ===')
|
||||||
|
cart2 = g.get_cart()
|
||||||
|
print(f"남은 품목: {cart2['total_items']}개")
|
||||||
|
for item in cart2['items']:
|
||||||
|
print(f" - {item['product_name'][:30]}")
|
||||||
|
|
||||||
|
# 기존 품목이 모두 남아있는지 확인
|
||||||
|
remaining_codes = [item.get('product_code') for item in cart2['items']]
|
||||||
|
preserved = all(code in remaining_codes for code in existing_codes if code)
|
||||||
|
|
||||||
|
if preserved and cart2['total_items'] == len(existing_codes):
|
||||||
|
print('\n🎉 성공! 새 품목만 주문됨, 기존 품목 모두 복원!')
|
||||||
|
elif cart2['total_items'] == 0:
|
||||||
|
print('\n⚠️ 모든 품목 주문됨 - 선별 주문 실패')
|
||||||
|
else:
|
||||||
|
print(f'\n🤔 예상 외 결과')
|
||||||
|
else:
|
||||||
|
print('\n❌ 새 품목 담기 실패')
|
||||||
83
backend/test_geo_selective_final2.py
Normal file
83
backend/test_geo_selective_final2.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""지오영 선별 주문 최종 테스트 2"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.geoyoung
|
||||||
|
importlib.reload(wholesale.geoyoung)
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
GeoYoungSession._instance = None
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
# 기존 장바구니 확인
|
||||||
|
print('=== 0. 기존 장바구니 확인 ===')
|
||||||
|
cart0 = g.get_cart()
|
||||||
|
print(f"기존 품목: {cart0['total_items']}개")
|
||||||
|
for item in cart0['items']:
|
||||||
|
print(f" - {item['product_name'][:30]} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
existing_codes = [item.get('product_code') for item in cart0['items']]
|
||||||
|
|
||||||
|
# 새 품목 담기 (타이레놀)
|
||||||
|
print('\n=== 1. 새 품목 (게보린) 담기 ===')
|
||||||
|
result = g.full_order(
|
||||||
|
kd_code='게보린',
|
||||||
|
quantity=1,
|
||||||
|
specification=None,
|
||||||
|
check_stock=True,
|
||||||
|
auto_confirm=False # 장바구니만
|
||||||
|
)
|
||||||
|
print(f"결과: {result.get('success')}, {result.get('message', result.get('error'))}")
|
||||||
|
|
||||||
|
new_code = result.get('product', {}).get('internal_code') if result.get('success') else None
|
||||||
|
print(f"새 품목 internal_code: {new_code}")
|
||||||
|
|
||||||
|
if not new_code:
|
||||||
|
# 다른 품목 시도
|
||||||
|
print('\n=== 1-2. 다른 품목 (판피린) 시도 ===')
|
||||||
|
result = g.full_order(
|
||||||
|
kd_code='판피린',
|
||||||
|
quantity=1,
|
||||||
|
specification=None,
|
||||||
|
check_stock=True,
|
||||||
|
auto_confirm=False
|
||||||
|
)
|
||||||
|
print(f"결과: {result.get('success')}, {result.get('message', result.get('error'))}")
|
||||||
|
new_code = result.get('product', {}).get('internal_code') if result.get('success') else None
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
print('\n=== 2. 장바구니 확인 ===')
|
||||||
|
cart1 = g.get_cart()
|
||||||
|
print(f"현재 품목: {cart1['total_items']}개")
|
||||||
|
for item in cart1['items']:
|
||||||
|
print(f" - {item['product_name'][:30]} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
# 선별 주문: 새 품목만!
|
||||||
|
if new_code:
|
||||||
|
print('\n=== 3. 선별 주문 (새 품목만!) ===')
|
||||||
|
print(f"주문할 코드: [{new_code}]")
|
||||||
|
|
||||||
|
confirm_result = g.submit_order_selective([new_code])
|
||||||
|
print(f"결과: {confirm_result}")
|
||||||
|
|
||||||
|
# 최종 장바구니 확인
|
||||||
|
print('\n=== 4. 최종 장바구니 ===')
|
||||||
|
cart2 = g.get_cart()
|
||||||
|
print(f"남은 품목: {cart2['total_items']}개")
|
||||||
|
for item in cart2['items']:
|
||||||
|
print(f" - {item['product_name'][:30]}")
|
||||||
|
|
||||||
|
# 기존 품목이 모두 남아있는지 확인
|
||||||
|
remaining_codes = [item.get('product_code') for item in cart2['items']]
|
||||||
|
preserved = all(code in remaining_codes for code in existing_codes if code)
|
||||||
|
|
||||||
|
if preserved and cart2['total_items'] == len(existing_codes):
|
||||||
|
print('\n🎉 성공! 새 품목만 주문됨, 기존 품목 모두 복원!')
|
||||||
|
elif cart2['total_items'] == 0:
|
||||||
|
print('\n⚠️ 모든 품목 주문됨 - 선별 주문 실패')
|
||||||
|
else:
|
||||||
|
print(f'\n🤔 결과: 기존 {len(existing_codes)}개 중 {len([c for c in existing_codes if c in remaining_codes])}개 복원')
|
||||||
|
else:
|
||||||
|
print('\n❌ 새 품목 담기 실패')
|
||||||
71
backend/test_geo_selective_final3.py
Normal file
71
backend/test_geo_selective_final3.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""지오영 선별 주문 최종 테스트 - 마그밀"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.geoyoung
|
||||||
|
importlib.reload(wholesale.geoyoung)
|
||||||
|
from wholesale import GeoYoungSession
|
||||||
|
|
||||||
|
GeoYoungSession._instance = None
|
||||||
|
g = GeoYoungSession()
|
||||||
|
g.login()
|
||||||
|
|
||||||
|
# 기존 장바구니 확인
|
||||||
|
print('=== 0. 기존 장바구니 확인 ===')
|
||||||
|
cart0 = g.get_cart()
|
||||||
|
print(f"기존 품목: {cart0['total_items']}개")
|
||||||
|
for item in cart0['items']:
|
||||||
|
print(f" - {item['product_name'][:30]} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
existing_count = cart0['total_items']
|
||||||
|
existing_codes = [item.get('product_code') for item in cart0['items']]
|
||||||
|
|
||||||
|
# 새 품목 담기 (마그밀)
|
||||||
|
print('\n=== 1. 새 품목 (마그밀) 담기 ===')
|
||||||
|
result = g.full_order(
|
||||||
|
kd_code='마그밀',
|
||||||
|
quantity=1,
|
||||||
|
specification=None,
|
||||||
|
check_stock=True,
|
||||||
|
auto_confirm=False # 장바구니만
|
||||||
|
)
|
||||||
|
print(f"결과: {result.get('success')}, {result.get('message', result.get('error'))}")
|
||||||
|
|
||||||
|
new_code = result.get('product', {}).get('internal_code') if result.get('success') else None
|
||||||
|
print(f"새 품목 internal_code: {new_code}")
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
print('\n=== 2. 장바구니 확인 ===')
|
||||||
|
cart1 = g.get_cart()
|
||||||
|
print(f"현재 품목: {cart1['total_items']}개")
|
||||||
|
for item in cart1['items']:
|
||||||
|
print(f" - {item['product_name'][:30]} (code: {item.get('product_code')})")
|
||||||
|
|
||||||
|
# 선별 주문: 새 품목만!
|
||||||
|
if new_code:
|
||||||
|
print('\n=== 3. 선별 주문 (마그밀만!) ===')
|
||||||
|
print(f"주문할 코드: [{new_code}]")
|
||||||
|
|
||||||
|
confirm_result = g.submit_order_selective([new_code])
|
||||||
|
print(f"결과: {confirm_result}")
|
||||||
|
|
||||||
|
# 최종 장바구니 확인
|
||||||
|
print('\n=== 4. 최종 장바구니 ===')
|
||||||
|
cart2 = g.get_cart()
|
||||||
|
print(f"남은 품목: {cart2['total_items']}개")
|
||||||
|
for item in cart2['items']:
|
||||||
|
print(f" - {item['product_name'][:30]}")
|
||||||
|
|
||||||
|
# 기존 품목이 모두 남아있는지 확인
|
||||||
|
remaining_codes = [item.get('product_code') for item in cart2['items']]
|
||||||
|
preserved_count = len([c for c in existing_codes if c in remaining_codes])
|
||||||
|
|
||||||
|
if cart2['total_items'] == existing_count and preserved_count == existing_count:
|
||||||
|
print(f'\n🎉 성공! 마그밀만 주문됨, 기존 {existing_count}개 품목 모두 복원!')
|
||||||
|
elif cart2['total_items'] == 0:
|
||||||
|
print('\n⚠️ 모든 품목 주문됨 - 선별 주문 실패')
|
||||||
|
else:
|
||||||
|
print(f'\n🤔 결과: 기존 {existing_count}개 중 {preserved_count}개 복원, 현재 {cart2["total_items"]}개')
|
||||||
|
else:
|
||||||
|
print('\n❌ 새 품목 담기 실패')
|
||||||
@ -1,112 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""지오영 API 직접 테스트"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
import json
|
|
||||||
|
|
||||||
async def capture_cart_api():
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch(headless=True)
|
|
||||||
page = await browser.new_page()
|
|
||||||
|
|
||||||
# 요청/응답 캡처
|
|
||||||
cart_requests = []
|
|
||||||
|
|
||||||
async def handle_request(request):
|
|
||||||
if 'Cart' in request.url or 'Order' in request.url or 'Add' in request.url:
|
|
||||||
cart_requests.append({
|
|
||||||
'url': request.url,
|
|
||||||
'method': request.method,
|
|
||||||
'headers': dict(request.headers),
|
|
||||||
'data': request.post_data
|
|
||||||
})
|
|
||||||
|
|
||||||
page.on('request', handle_request)
|
|
||||||
|
|
||||||
# 로그인
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Member/Login')
|
|
||||||
await page.fill('input[type="text"]', '7390')
|
|
||||||
await page.fill('input[type="password"]', 'trajet6640')
|
|
||||||
await page.click('button, input[type="submit"]')
|
|
||||||
await page.wait_for_load_state('networkidle')
|
|
||||||
print("로그인 완료")
|
|
||||||
|
|
||||||
# 쿠키 저장
|
|
||||||
cookies = await page.context.cookies()
|
|
||||||
print(f"쿠키: {[c['name'] for c in cookies]}")
|
|
||||||
|
|
||||||
# 검색 페이지
|
|
||||||
await page.goto('https://gwn.geoweb.kr/Home/Index')
|
|
||||||
await page.wait_for_timeout(2000)
|
|
||||||
|
|
||||||
# 검색 (AJAX)
|
|
||||||
await page.evaluate('''
|
|
||||||
$.ajax({
|
|
||||||
url: "/Home/PartialSearchProduct",
|
|
||||||
type: "POST",
|
|
||||||
data: {srchText: "643104281"},
|
|
||||||
success: function(data) {
|
|
||||||
console.log("검색 결과:", data.substring(0, 500));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
''')
|
|
||||||
await page.wait_for_timeout(2000)
|
|
||||||
|
|
||||||
# 장바구니 추가 시도 (JavaScript로)
|
|
||||||
result = await page.evaluate('''
|
|
||||||
async function testCart() {
|
|
||||||
// 장바구니 추가 함수 찾기
|
|
||||||
if (typeof AddCart !== 'undefined') {
|
|
||||||
return "AddCart 함수 존재";
|
|
||||||
}
|
|
||||||
if (typeof fnAddCart !== 'undefined') {
|
|
||||||
return "fnAddCart 함수 존재";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전역 함수 목록
|
|
||||||
var funcs = [];
|
|
||||||
for (var key in window) {
|
|
||||||
if (typeof window[key] === 'function' &&
|
|
||||||
(key.toLowerCase().includes('cart') ||
|
|
||||||
key.toLowerCase().includes('order') ||
|
|
||||||
key.toLowerCase().includes('add'))) {
|
|
||||||
funcs.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "발견된 함수: " + funcs.join(", ");
|
|
||||||
}
|
|
||||||
return testCart();
|
|
||||||
''')
|
|
||||||
print(f"JavaScript 분석: {result}")
|
|
||||||
|
|
||||||
# 페이지 소스에서 장바구니 관련 스크립트 찾기
|
|
||||||
scripts = await page.evaluate('''
|
|
||||||
var scripts = document.querySelectorAll('script');
|
|
||||||
var result = [];
|
|
||||||
scripts.forEach(function(s) {
|
|
||||||
var text = s.textContent || s.innerText || '';
|
|
||||||
if (text.includes('Cart') || text.includes('AddProduct')) {
|
|
||||||
result.push(text.substring(0, 1000));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
''')
|
|
||||||
|
|
||||||
await browser.close()
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("캡처된 Cart/Order 요청:")
|
|
||||||
print("="*60)
|
|
||||||
for r in cart_requests:
|
|
||||||
print(json.dumps(r, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("장바구니 관련 스크립트:")
|
|
||||||
print("="*60)
|
|
||||||
for i, s in enumerate(scripts[:3]):
|
|
||||||
print(f"\n--- Script {i+1} ---")
|
|
||||||
print(s[:800])
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(capture_cart_api())
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
"""
|
|
||||||
통합 테스트: QR 라벨 전체 흐름
|
|
||||||
토큰 생성 → DB 저장 → QR 라벨 이미지 생성
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Path setup
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
|
||||||
|
|
||||||
from utils.qr_token_generator import generate_claim_token, save_token_to_db
|
|
||||||
from utils.qr_label_printer import print_qr_label
|
|
||||||
|
|
||||||
def test_full_flow():
|
|
||||||
"""전체 흐름 테스트"""
|
|
||||||
|
|
||||||
# 1. 테스트 데이터 (새로운 거래 ID)
|
|
||||||
test_tx_id = datetime.now().strftime("TEST%Y%m%d%H%M%S")
|
|
||||||
test_amount = 75000.0
|
|
||||||
test_time = datetime.now()
|
|
||||||
|
|
||||||
print("=" * 80)
|
|
||||||
print("QR 라벨 통합 테스트")
|
|
||||||
print("=" * 80)
|
|
||||||
print(f"거래 ID: {test_tx_id}")
|
|
||||||
print(f"판매 금액: {test_amount:,}원")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 2. 토큰 생성
|
|
||||||
print("[1/3] Claim Token 생성...")
|
|
||||||
token_info = generate_claim_token(test_tx_id, test_amount)
|
|
||||||
|
|
||||||
print(f" [OK] 토큰 원문: {token_info['token_raw'][:50]}...")
|
|
||||||
print(f" [OK] 토큰 해시: {token_info['token_hash'][:32]}...")
|
|
||||||
print(f" [OK] QR URL: {token_info['qr_url']}")
|
|
||||||
print(f" [OK] URL 길이: {len(token_info['qr_url'])} 문자")
|
|
||||||
print(f" [OK] 적립 포인트: {token_info['claimable_points']}P")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 3. DB 저장
|
|
||||||
print("[2/3] SQLite DB 저장...")
|
|
||||||
success, error = save_token_to_db(
|
|
||||||
test_tx_id,
|
|
||||||
token_info['token_hash'],
|
|
||||||
test_amount,
|
|
||||||
token_info['claimable_points'],
|
|
||||||
token_info['expires_at'],
|
|
||||||
token_info['pharmacy_id']
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
print(f" [ERROR] DB 저장 실패: {error}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f" [OK] DB 저장 성공")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 4. QR 라벨 생성 (미리보기 모드)
|
|
||||||
print("[3/3] QR 라벨 이미지 생성...")
|
|
||||||
success, image_path = print_qr_label(
|
|
||||||
token_info['qr_url'],
|
|
||||||
test_tx_id,
|
|
||||||
test_amount,
|
|
||||||
token_info['claimable_points'],
|
|
||||||
test_time,
|
|
||||||
preview_mode=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
print(f" [ERROR] 이미지 생성 실패")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f" [OK] 이미지 저장: {image_path}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 5. 결과 요약
|
|
||||||
print("=" * 80)
|
|
||||||
print("[SUCCESS] 통합 테스트 성공!")
|
|
||||||
print("=" * 80)
|
|
||||||
print(f"QR URL: {token_info['qr_url']}")
|
|
||||||
print(f"이미지 파일: {image_path}")
|
|
||||||
print(f"\n다음 명령으로 확인:")
|
|
||||||
print(f" start {image_path}")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
success = test_full_flow()
|
|
||||||
sys.exit(0 if success else 1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n[ERROR] 테스트 실패: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
sys.exit(1)
|
|
||||||
76
backend/test_order_end.py
Normal file
76
backend/test_order_end.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0]
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1, price=product['price'], stock=product['stock'])
|
||||||
|
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
form = soup.find('form', {'id': 'frmBag'})
|
||||||
|
|
||||||
|
# form action 확인
|
||||||
|
form_action = form.get('action', '')
|
||||||
|
print(f'form action: {form_action}')
|
||||||
|
|
||||||
|
# 올바른 URL 구성
|
||||||
|
ORDER_END_URL = 'http://sooinpharm.co.kr/Service/Order/OrderEnd.asp'
|
||||||
|
|
||||||
|
form_data = {}
|
||||||
|
for inp in form.find_all('input'):
|
||||||
|
name = inp.get('name', '')
|
||||||
|
if not name: continue
|
||||||
|
inp_type = inp.get('type', '').lower()
|
||||||
|
if inp_type == 'checkbox':
|
||||||
|
form_data[name] = 'on' # 체크박스 선택
|
||||||
|
else:
|
||||||
|
form_data[name] = inp.get('value', '')
|
||||||
|
|
||||||
|
# x, y 좌표 (image input 클릭)
|
||||||
|
form_data['x'] = '10'
|
||||||
|
form_data['y'] = '10'
|
||||||
|
|
||||||
|
print(f"chk_0: {form_data.get('chk_0')}")
|
||||||
|
print(f"kind: {form_data.get('kind')}")
|
||||||
|
|
||||||
|
print(f'\nPOST to: {ORDER_END_URL}')
|
||||||
|
resp = s.session.post(
|
||||||
|
ORDER_END_URL,
|
||||||
|
data=form_data,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Referer': f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}'
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f'응답 상태: {resp.status_code}')
|
||||||
|
print(f'응답 길이: {len(resp.text)}')
|
||||||
|
|
||||||
|
# alert 확인
|
||||||
|
alert_match = re.search(r'alert\("([^"]*)"\)', resp.text)
|
||||||
|
alert_msg = alert_match.group(1) if alert_match else 'N/A'
|
||||||
|
print(f'alert 메시지: "{alert_msg}"')
|
||||||
|
|
||||||
|
# 응답 일부 출력
|
||||||
|
print('\n응답 앞부분:')
|
||||||
|
print(resp.text[:1000])
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
resp2 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup2 = BeautifulSoup(resp2.content, 'html.parser')
|
||||||
|
int_array = soup2.find('input', {'name': 'intArray'})
|
||||||
|
val = int_array.get('value') if int_array else '없음'
|
||||||
|
print(f'\n주문 후 intArray: {val}')
|
||||||
|
|
||||||
|
if val == '-1':
|
||||||
|
print('\n🎉 주문 성공!')
|
||||||
|
else:
|
||||||
|
print('\n❌ 주문 실패')
|
||||||
@ -1,8 +0,0 @@
|
|||||||
from sqlalchemy import create_engine, text
|
|
||||||
|
|
||||||
pg_engine = create_engine('postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master')
|
|
||||||
with pg_engine.connect() as conn:
|
|
||||||
result = conn.execute(text("SELECT apc, product_name, company_name, main_ingredient FROM apc WHERE product_name LIKE '%아시엔로%' LIMIT 20"))
|
|
||||||
print('아시엔로 검색 결과:')
|
|
||||||
for row in result:
|
|
||||||
print(f' APC: {row[0]} | {row[1]} | {row[2]} | {row[3]}')
|
|
||||||
69
backend/test_post_data.py
Normal file
69
backend/test_post_data.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""실제 POST 데이터 확인"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.sooin
|
||||||
|
importlib.reload(wholesale.sooin)
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
SooinSession._instance = None
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 2개 품목 담기
|
||||||
|
r1 = s.search_products('코자정')
|
||||||
|
s.add_to_cart(r1['items'][0]['internal_code'], qty=1, price=r1['items'][0]['price'], stock=r1['items'][0]['stock'])
|
||||||
|
r2 = s.search_products('디카맥스')
|
||||||
|
s.add_to_cart(r2['items'][0]['internal_code'], qty=1, price=r2['items'][0]['price'], stock=r2['items'][0]['stock'])
|
||||||
|
|
||||||
|
# row 0 취소 (디카맥스)
|
||||||
|
s.cancel_item(row_index=0)
|
||||||
|
|
||||||
|
# Bag.asp GET
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
form = soup.find('form', {'id': 'frmBag'})
|
||||||
|
|
||||||
|
form_data = {}
|
||||||
|
for inp in form.find_all('input'):
|
||||||
|
name = inp.get('name', '')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
inp_type = inp.get('type', 'text').lower()
|
||||||
|
|
||||||
|
if inp_type == 'checkbox':
|
||||||
|
if inp.get('checked') is not None:
|
||||||
|
form_data[name] = 'on'
|
||||||
|
continue
|
||||||
|
|
||||||
|
form_data[name] = inp.get('value', '')
|
||||||
|
|
||||||
|
form_data['kind'] = 'order'
|
||||||
|
form_data['tx_memo'] = '선별 테스트'
|
||||||
|
|
||||||
|
print('=== POST할 데이터 (체크박스 관련) ===')
|
||||||
|
for k, v in form_data.items():
|
||||||
|
if 'chk' in k.lower():
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
|
||||||
|
print(f"\n=== 실제 POST ===")
|
||||||
|
resp = s.session.post(
|
||||||
|
s.ORDER_END_URL,
|
||||||
|
data=form_data,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
alert_match = re.search(r'alert\("([^"]*)"\)', resp.text)
|
||||||
|
alert_msg = alert_match.group(1) if alert_match else 'N/A'
|
||||||
|
print(f"응답: {alert_msg}")
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f"\n남은 품목: {cart['total_items']}개")
|
||||||
|
for item in cart['items']:
|
||||||
|
print(f" - {item['product_name']}")
|
||||||
@ -1,298 +0,0 @@
|
|||||||
"""
|
|
||||||
ESC/POS QR 코드 인쇄 방식 테스트
|
|
||||||
여러 가지 방법을 한 번에 시도하여 어떤 방식이 작동하는지 확인
|
|
||||||
"""
|
|
||||||
|
|
||||||
import socket
|
|
||||||
import qrcode
|
|
||||||
import time
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
# 프린터 설정 (고정)
|
|
||||||
PRINTER_IP = "192.168.0.174"
|
|
||||||
PRINTER_PORT = 9100
|
|
||||||
|
|
||||||
# 테스트 URL (짧은 버전)
|
|
||||||
TEST_URL = "https://mile.0bin.in/test"
|
|
||||||
|
|
||||||
|
|
||||||
def send_to_printer(data, method_name):
|
|
||||||
"""프린터로 데이터 전송"""
|
|
||||||
try:
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"[{method_name}] 전송 시작...")
|
|
||||||
print(f"데이터 크기: {len(data)} bytes")
|
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.settimeout(10)
|
|
||||||
sock.connect((PRINTER_IP, PRINTER_PORT))
|
|
||||||
sock.sendall(data)
|
|
||||||
sock.close()
|
|
||||||
|
|
||||||
print(f"[{method_name}] ✅ 전송 완료!")
|
|
||||||
time.sleep(2) # 프린터 처리 대기
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[{method_name}] ❌ 실패: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def method_1_native_qr_model2():
|
|
||||||
"""
|
|
||||||
방법 1: 프린터 내장 QR 생성 (GS ( k) - Model 2
|
|
||||||
가장 안정적이지만 프린터 지원 필요
|
|
||||||
"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
|
|
||||||
commands = []
|
|
||||||
|
|
||||||
# 초기화
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
|
|
||||||
# 헤더
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 1 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" 내장 QR (GS ( k)\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# QR 설정
|
|
||||||
# GS ( k pL pH cn fn n (QR Code)
|
|
||||||
# cn = 49 (Model 1/2 선택)
|
|
||||||
# fn = 65 (모델 선택)
|
|
||||||
# n = 50 (Model 2)
|
|
||||||
|
|
||||||
# 모델 설정
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 65, 50])) # Model 2
|
|
||||||
|
|
||||||
# 에러 정정 레벨 설정 (fn=69, n=48=L)
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 69, 48]))
|
|
||||||
|
|
||||||
# 모듈 크기 설정 (fn=67, n=8)
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 67, 8]))
|
|
||||||
|
|
||||||
# QR 데이터 저장 (fn=80)
|
|
||||||
qr_data = TEST_URL.encode('utf-8')
|
|
||||||
data_len = len(qr_data) + 3
|
|
||||||
pL = data_len & 0xFF
|
|
||||||
pH = (data_len >> 8) & 0xFF
|
|
||||||
commands.append(GS + b'(k' + bytes([pL, pH, 49, 80, 48]) + qr_data)
|
|
||||||
|
|
||||||
# QR 인쇄 (fn=81)
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 81, 48]))
|
|
||||||
|
|
||||||
# 푸터
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append(f"URL: {TEST_URL}\n".encode('euc-kr'))
|
|
||||||
commands.append("\n\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 용지 커트
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_2_raster_bitmap_gs_v():
|
|
||||||
"""
|
|
||||||
방법 2: Raster Bit Image (GS v 0)
|
|
||||||
"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
|
|
||||||
commands = []
|
|
||||||
|
|
||||||
# 초기화
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
|
|
||||||
# 헤더
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 2 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" Raster (GS v 0)\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# QR 이미지 생성 (작게: 80x80)
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=2, border=2)
|
|
||||||
qr.add_data(TEST_URL)
|
|
||||||
qr.make(fit=True)
|
|
||||||
qr_image = qr.make_image(fill_color="black", back_color="white")
|
|
||||||
qr_image = qr_image.resize((80, 80))
|
|
||||||
|
|
||||||
# 1비트 흑백으로 변환
|
|
||||||
qr_image = qr_image.convert('1')
|
|
||||||
width, height = qr_image.size
|
|
||||||
pixels = qr_image.load()
|
|
||||||
|
|
||||||
# GS v 0 명령어
|
|
||||||
width_bytes = (width + 7) // 8
|
|
||||||
commands.append(GS + b'v0' + bytes([0])) # 보통 모드
|
|
||||||
commands.append(bytes([width_bytes & 0xFF, (width_bytes >> 8) & 0xFF])) # xL, xH
|
|
||||||
commands.append(bytes([height & 0xFF, (height >> 8) & 0xFF])) # yL, yH
|
|
||||||
|
|
||||||
# 이미지 데이터
|
|
||||||
for y in range(height):
|
|
||||||
for x in range(0, width, 8):
|
|
||||||
byte = 0
|
|
||||||
for bit in range(8):
|
|
||||||
if x + bit < width:
|
|
||||||
if pixels[x + bit, y] == 0: # 검은색
|
|
||||||
byte |= (1 << (7 - bit))
|
|
||||||
commands.append(bytes([byte]))
|
|
||||||
|
|
||||||
# 푸터
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append(f"URL: {TEST_URL}\n".encode('euc-kr'))
|
|
||||||
commands.append("\n\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 용지 커트
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_3_bit_image_esc_star():
|
|
||||||
"""
|
|
||||||
방법 3: Bit Image (ESC *) - 24-dot double-density
|
|
||||||
현재 사용 중인 방식
|
|
||||||
"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
|
|
||||||
commands = []
|
|
||||||
|
|
||||||
# 초기화
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
|
|
||||||
# 헤더
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 3 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" Bit Image (ESC *)\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# QR 이미지 생성 (작게: 80x80)
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=2, border=2)
|
|
||||||
qr.add_data(TEST_URL)
|
|
||||||
qr.make(fit=True)
|
|
||||||
qr_image = qr.make_image(fill_color="black", back_color="white")
|
|
||||||
qr_image = qr_image.resize((80, 80))
|
|
||||||
|
|
||||||
# 1비트 흑백으로 변환
|
|
||||||
qr_image = qr_image.convert('1')
|
|
||||||
width, height = qr_image.size
|
|
||||||
pixels = qr_image.load()
|
|
||||||
|
|
||||||
# ESC * 명령어로 라인별 인쇄
|
|
||||||
for y in range(0, height, 24):
|
|
||||||
line_height = min(24, height - y)
|
|
||||||
|
|
||||||
# ESC * m nL nH
|
|
||||||
nL = width & 0xFF
|
|
||||||
nH = (width >> 8) & 0xFF
|
|
||||||
commands.append(ESC + b'*' + bytes([33, nL, nH])) # m=33 (24-dot double-density)
|
|
||||||
|
|
||||||
# 라인 데이터
|
|
||||||
for x in range(width):
|
|
||||||
byte1, byte2, byte3 = 0, 0, 0
|
|
||||||
|
|
||||||
for bit in range(line_height):
|
|
||||||
pixel_y = y + bit
|
|
||||||
if pixel_y < height:
|
|
||||||
if pixels[x, pixel_y] == 0: # 검은색
|
|
||||||
if bit < 8:
|
|
||||||
byte1 |= (1 << (7 - bit))
|
|
||||||
elif bit < 16:
|
|
||||||
byte2 |= (1 << (15 - bit))
|
|
||||||
else:
|
|
||||||
byte3 |= (1 << (23 - bit))
|
|
||||||
|
|
||||||
commands.append(bytes([byte1, byte2, byte3]))
|
|
||||||
|
|
||||||
commands.append(b'\n')
|
|
||||||
|
|
||||||
# 푸터
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append(f"URL: {TEST_URL}\n".encode('euc-kr'))
|
|
||||||
commands.append("\n\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 용지 커트
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_4_simple_text_only():
|
|
||||||
"""
|
|
||||||
방법 4: 텍스트만 (비교용)
|
|
||||||
"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
|
|
||||||
commands = []
|
|
||||||
|
|
||||||
# 초기화
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
|
|
||||||
# 헤더
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 4 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" 텍스트만 (비교용)\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n".encode('euc-kr'))
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append("QR 이미지 대신 URL만 출력\n".encode('euc-kr'))
|
|
||||||
commands.append("\n".encode('euc-kr'))
|
|
||||||
commands.append(f"URL: {TEST_URL}\n".encode('euc-kr'))
|
|
||||||
commands.append("\n\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 용지 커트
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""메인 실행"""
|
|
||||||
print("="*60)
|
|
||||||
print("ESC/POS QR 코드 인쇄 방식 테스트")
|
|
||||||
print("="*60)
|
|
||||||
print(f"프린터: {PRINTER_IP}:{PRINTER_PORT}")
|
|
||||||
print(f"테스트 URL: {TEST_URL}")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
methods = [
|
|
||||||
("방법 1: 프린터 내장 QR (GS ( k)", method_1_native_qr_model2),
|
|
||||||
("방법 2: Raster Bitmap (GS v 0)", method_2_raster_bitmap_gs_v),
|
|
||||||
("방법 3: Bit Image (ESC *)", method_3_bit_image_esc_star),
|
|
||||||
("방법 4: 텍스트만", method_4_simple_text_only),
|
|
||||||
]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for name, method_func in methods:
|
|
||||||
try:
|
|
||||||
data = method_func()
|
|
||||||
success = send_to_printer(data, name)
|
|
||||||
results.append((name, success))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[{name}] ❌ 함수 실행 오류: {e}")
|
|
||||||
results.append((name, False))
|
|
||||||
|
|
||||||
# 결과 요약
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("테스트 결과 요약")
|
|
||||||
print("="*60)
|
|
||||||
for name, success in results:
|
|
||||||
status = "✅ 성공" if success else "❌ 실패"
|
|
||||||
print(f"{name}: {status}")
|
|
||||||
|
|
||||||
print("\n인쇄된 영수증을 확인하여 어떤 방법이 QR을 제대로 출력했는지 확인하세요!")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,281 +0,0 @@
|
|||||||
"""
|
|
||||||
ESC/POS QR 코드 인쇄 방식 테스트 v2
|
|
||||||
더 많은 변형 시도 (크기, 밀도, 파라미터)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import socket
|
|
||||||
import qrcode
|
|
||||||
import time
|
|
||||||
|
|
||||||
# 프린터 설정
|
|
||||||
PRINTER_IP = "192.168.0.174"
|
|
||||||
PRINTER_PORT = 9100
|
|
||||||
|
|
||||||
# 테스트 URL (더 짧게)
|
|
||||||
TEST_URL = "https://bit.ly/test"
|
|
||||||
|
|
||||||
|
|
||||||
def send_to_printer(data, method_name):
|
|
||||||
"""프린터로 데이터 전송"""
|
|
||||||
try:
|
|
||||||
print(f"\n[{method_name}] 전송 중... ({len(data)} bytes)")
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.settimeout(10)
|
|
||||||
sock.connect((PRINTER_IP, PRINTER_PORT))
|
|
||||||
sock.sendall(data)
|
|
||||||
sock.close()
|
|
||||||
print(f"[{method_name}] ✅ 완료")
|
|
||||||
time.sleep(1.5)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[{method_name}] ❌ 실패: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def method_1_tiny_qr_escstar():
|
|
||||||
"""방법 1: 아주 작은 QR (30x30) + ESC *"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
commands = []
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
commands.append("\n================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 1 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" 작은 QR 30x30 (ESC *)\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 30x30 QR
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=1, border=1)
|
|
||||||
qr.add_data(TEST_URL)
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white").resize((30, 30)).convert('1')
|
|
||||||
width, height = img.size
|
|
||||||
pixels = img.load()
|
|
||||||
|
|
||||||
# ESC * m=0 (8-dot single-density)
|
|
||||||
for y in range(0, height, 8):
|
|
||||||
commands.append(ESC + b'*' + bytes([0, width & 0xFF, (width >> 8) & 0xFF]))
|
|
||||||
for x in range(width):
|
|
||||||
byte = 0
|
|
||||||
for bit in range(min(8, height - y)):
|
|
||||||
if pixels[x, y + bit] == 0:
|
|
||||||
byte |= (1 << (7 - bit))
|
|
||||||
commands.append(bytes([byte]))
|
|
||||||
commands.append(b'\n')
|
|
||||||
|
|
||||||
commands.append(f"\nURL: {TEST_URL}\n\n\n".encode('euc-kr'))
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_2_medium_qr_escstar_mode32():
|
|
||||||
"""방법 2: 중간 QR (50x50) + ESC * mode 32"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
commands = []
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
commands.append("\n================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 2 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" 중간 QR 50x50 (ESC * m=32)\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 50x50 QR
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=2, border=1)
|
|
||||||
qr.add_data(TEST_URL)
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white").resize((50, 50)).convert('1')
|
|
||||||
width, height = img.size
|
|
||||||
pixels = img.load()
|
|
||||||
|
|
||||||
# ESC * m=32 (24-dot single-density)
|
|
||||||
for y in range(0, height, 24):
|
|
||||||
commands.append(ESC + b'*' + bytes([32, width & 0xFF, (width >> 8) & 0xFF]))
|
|
||||||
for x in range(width):
|
|
||||||
byte1 = byte2 = byte3 = 0
|
|
||||||
for bit in range(min(24, height - y)):
|
|
||||||
if pixels[x, y + bit] == 0:
|
|
||||||
if bit < 8:
|
|
||||||
byte1 |= (1 << (7 - bit))
|
|
||||||
elif bit < 16:
|
|
||||||
byte2 |= (1 << (15 - bit))
|
|
||||||
else:
|
|
||||||
byte3 |= (1 << (23 - bit))
|
|
||||||
commands.append(bytes([byte1, byte2, byte3]))
|
|
||||||
commands.append(b'\n')
|
|
||||||
|
|
||||||
commands.append(f"\nURL: {TEST_URL}\n\n\n".encode('euc-kr'))
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_3_native_qr_simple():
|
|
||||||
"""방법 3: 내장 QR (더 간단한 설정)"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
commands = []
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
commands.append("\n================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 3 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" 내장 QR 간단 설정\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
qr_data = TEST_URL.encode('utf-8')
|
|
||||||
data_len = len(qr_data) + 3
|
|
||||||
|
|
||||||
# Model 2
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 65, 50]))
|
|
||||||
# Error correction L
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 69, 48]))
|
|
||||||
# Size 4
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 67, 4]))
|
|
||||||
# Store data
|
|
||||||
commands.append(GS + b'(k' + bytes([data_len & 0xFF, (data_len >> 8) & 0xFF, 49, 80, 48]) + qr_data)
|
|
||||||
# Print
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 81, 48]))
|
|
||||||
|
|
||||||
commands.append(f"\n\nURL: {TEST_URL}\n\n\n".encode('euc-kr'))
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_4_native_qr_model1():
|
|
||||||
"""방법 4: 내장 QR Model 1 (구형)"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
commands = []
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
commands.append("\n================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 4 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" 내장 QR Model 1\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
qr_data = TEST_URL.encode('utf-8')
|
|
||||||
data_len = len(qr_data) + 3
|
|
||||||
|
|
||||||
# Model 1 (n=49)
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 65, 49]))
|
|
||||||
# Error correction L
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 69, 48]))
|
|
||||||
# Size 4
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 67, 4]))
|
|
||||||
# Store data
|
|
||||||
commands.append(GS + b'(k' + bytes([data_len & 0xFF, (data_len >> 8) & 0xFF, 49, 80, 48]) + qr_data)
|
|
||||||
# Print
|
|
||||||
commands.append(GS + b'(k' + bytes([3, 0, 49, 81, 48]))
|
|
||||||
|
|
||||||
commands.append(f"\n\nURL: {TEST_URL}\n\n\n".encode('euc-kr'))
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_5_raster_tiny():
|
|
||||||
"""방법 5: Raster 초소형 (40x40)"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
commands = []
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
commands.append("\n================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 5 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" Raster 40x40 (GS v 0)\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 40x40 QR
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=1, border=1)
|
|
||||||
qr.add_data(TEST_URL)
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white").resize((40, 40)).convert('1')
|
|
||||||
width, height = img.size
|
|
||||||
pixels = img.load()
|
|
||||||
|
|
||||||
width_bytes = (width + 7) // 8
|
|
||||||
commands.append(GS + b'v0' + bytes([0]))
|
|
||||||
commands.append(bytes([width_bytes & 0xFF, (width_bytes >> 8) & 0xFF]))
|
|
||||||
commands.append(bytes([height & 0xFF, (height >> 8) & 0xFF]))
|
|
||||||
|
|
||||||
for y in range(height):
|
|
||||||
for x in range(0, width, 8):
|
|
||||||
byte = 0
|
|
||||||
for bit in range(8):
|
|
||||||
if x + bit < width and pixels[x + bit, y] == 0:
|
|
||||||
byte |= (1 << (7 - bit))
|
|
||||||
commands.append(bytes([byte]))
|
|
||||||
|
|
||||||
commands.append(f"\n\nURL: {TEST_URL}\n\n\n".encode('euc-kr'))
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def method_6_no_align():
|
|
||||||
"""방법 6: 정렬 없이 + 작은 QR"""
|
|
||||||
ESC = b'\x1b'
|
|
||||||
GS = b'\x1d'
|
|
||||||
commands = []
|
|
||||||
commands.append(ESC + b'@')
|
|
||||||
# 정렬 명령 없음!
|
|
||||||
commands.append("\n================================\n".encode('euc-kr'))
|
|
||||||
commands.append(" *** 방법 6 ***\n".encode('euc-kr'))
|
|
||||||
commands.append(" 정렬 없음 + QR 35x35\n".encode('euc-kr'))
|
|
||||||
commands.append("================================\n\n".encode('euc-kr'))
|
|
||||||
|
|
||||||
# 35x35 QR
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=1, border=1)
|
|
||||||
qr.add_data(TEST_URL)
|
|
||||||
qr.make(fit=True)
|
|
||||||
img = qr.make_image(fill_color="black", back_color="white").resize((35, 35)).convert('1')
|
|
||||||
width, height = img.size
|
|
||||||
pixels = img.load()
|
|
||||||
|
|
||||||
# ESC * m=1 (8-dot double-density)
|
|
||||||
for y in range(0, height, 8):
|
|
||||||
commands.append(ESC + b'*' + bytes([1, width & 0xFF, (width >> 8) & 0xFF]))
|
|
||||||
for x in range(width):
|
|
||||||
byte = 0
|
|
||||||
for bit in range(min(8, height - y)):
|
|
||||||
if pixels[x, y + bit] == 0:
|
|
||||||
byte |= (1 << (7 - bit))
|
|
||||||
commands.append(bytes([byte]))
|
|
||||||
commands.append(b'\n')
|
|
||||||
|
|
||||||
commands.append(f"\nURL: {TEST_URL}\n\n\n".encode('euc-kr'))
|
|
||||||
commands.append(GS + b'V' + bytes([1]))
|
|
||||||
return b''.join(commands)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("="*60)
|
|
||||||
print("ESC/POS QR 테스트 v2 - 더 많은 변형")
|
|
||||||
print("="*60)
|
|
||||||
print(f"프린터: {PRINTER_IP}:{PRINTER_PORT}")
|
|
||||||
print(f"테스트 URL: {TEST_URL}")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
methods = [
|
|
||||||
("방법 1: 30x30 ESC * m=0", method_1_tiny_qr_escstar),
|
|
||||||
("방법 2: 50x50 ESC * m=32", method_2_medium_qr_escstar_mode32),
|
|
||||||
("방법 3: 내장 QR 간단", method_3_native_qr_simple),
|
|
||||||
("방법 4: 내장 QR Model 1", method_4_native_qr_model1),
|
|
||||||
("방법 5: 40x40 Raster", method_5_raster_tiny),
|
|
||||||
("방법 6: 정렬 없음 35x35", method_6_no_align),
|
|
||||||
]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for name, method_func in methods:
|
|
||||||
try:
|
|
||||||
data = method_func()
|
|
||||||
success = send_to_printer(data, name)
|
|
||||||
results.append((name, success))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[{name}] ❌ 오류: {e}")
|
|
||||||
results.append((name, False))
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("결과 요약")
|
|
||||||
print("="*60)
|
|
||||||
for name, success in results:
|
|
||||||
print(f"{name}: {'✅' if success else '❌'}")
|
|
||||||
|
|
||||||
print("\n6장의 영수증이 나옵니다. QR이 보이는 번호를 알려주세요!")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,263 +0,0 @@
|
|||||||
"""
|
|
||||||
python-escpos 라이브러리를 사용한 QR 코드 인쇄 테스트
|
|
||||||
훨씬 더 간단하고 안정적!
|
|
||||||
|
|
||||||
설치: pip install python-escpos
|
|
||||||
"""
|
|
||||||
|
|
||||||
from escpos.printer import Network
|
|
||||||
from escpos import escpos
|
|
||||||
import time
|
|
||||||
|
|
||||||
# 프린터 설정
|
|
||||||
PRINTER_IP = "192.168.0.174"
|
|
||||||
PRINTER_PORT = 9100
|
|
||||||
|
|
||||||
# 테스트 URL
|
|
||||||
TEST_URL = "https://mile.0bin.in/test"
|
|
||||||
|
|
||||||
|
|
||||||
def test_method_1_native_qr():
|
|
||||||
"""방법 1: escpos 라이브러리 내장 QR 함수"""
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("방법 1: escpos.qr() - 프린터 내장 QR")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
p = Network(PRINTER_IP, port=PRINTER_PORT)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text(" *** 방법 1 ***\n")
|
|
||||||
p.text(" escpos.qr() 내장\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text("\n")
|
|
||||||
|
|
||||||
# QR 코드 인쇄 (프린터 내장)
|
|
||||||
p.qr(TEST_URL, size=4, center=True)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text(f"URL: {TEST_URL}\n")
|
|
||||||
p.text("\n\n\n")
|
|
||||||
p.cut()
|
|
||||||
|
|
||||||
print("✅ 방법 1 성공!")
|
|
||||||
time.sleep(2)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 방법 1 실패: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def test_method_2_image():
|
|
||||||
"""방법 2: escpos 라이브러리 이미지 함수"""
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("방법 2: escpos.image() - QR을 이미지로 변환하여 인쇄")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import qrcode
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
p = Network(PRINTER_IP, port=PRINTER_PORT)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text(" *** 방법 2 ***\n")
|
|
||||||
p.text(" escpos.image()\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text("\n")
|
|
||||||
|
|
||||||
# QR 이미지 생성
|
|
||||||
qr = qrcode.QRCode(version=1, box_size=3, border=2)
|
|
||||||
qr.add_data(TEST_URL)
|
|
||||||
qr.make(fit=True)
|
|
||||||
qr_img = qr.make_image(fill_color="black", back_color="white")
|
|
||||||
|
|
||||||
# escpos.image()로 인쇄
|
|
||||||
p.image(qr_img, center=True)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text(f"URL: {TEST_URL}\n")
|
|
||||||
p.text("\n\n\n")
|
|
||||||
p.cut()
|
|
||||||
|
|
||||||
print("✅ 방법 2 성공!")
|
|
||||||
time.sleep(2)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 방법 2 실패: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def test_method_3_qr_small():
|
|
||||||
"""방법 3: 작은 QR (size=3)"""
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("방법 3: 작은 QR (size=3)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
p = Network(PRINTER_IP, port=PRINTER_PORT)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text(" *** 방법 3 ***\n")
|
|
||||||
p.text(" 작은 QR (size=3)\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text("\n")
|
|
||||||
|
|
||||||
p.qr(TEST_URL, size=3, center=True)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text(f"URL: {TEST_URL}\n")
|
|
||||||
p.text("\n\n\n")
|
|
||||||
p.cut()
|
|
||||||
|
|
||||||
print("✅ 방법 3 성공!")
|
|
||||||
time.sleep(2)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 방법 3 실패: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def test_method_4_qr_large():
|
|
||||||
"""방법 4: 큰 QR (size=8)"""
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("방법 4: 큰 QR (size=8)")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
p = Network(PRINTER_IP, port=PRINTER_PORT)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text(" *** 방법 4 ***\n")
|
|
||||||
p.text(" 큰 QR (size=8)\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text("\n")
|
|
||||||
|
|
||||||
p.qr(TEST_URL, size=8, center=True)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text(f"URL: {TEST_URL}\n")
|
|
||||||
p.text("\n\n\n")
|
|
||||||
p.cut()
|
|
||||||
|
|
||||||
print("✅ 방법 4 성공!")
|
|
||||||
time.sleep(2)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 방법 4 실패: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def test_method_5_full_receipt():
|
|
||||||
"""방법 5: 완전한 영수증 (청춘약국)"""
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("방법 5: 완전한 영수증")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
p = Network(PRINTER_IP, port=PRINTER_PORT)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text(" *** 방법 5 ***\n")
|
|
||||||
p.text(" 완전한 영수증\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text("\n")
|
|
||||||
|
|
||||||
# 헤더
|
|
||||||
p.set(align='center')
|
|
||||||
p.text("청춘약국\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
|
|
||||||
# 거래 정보
|
|
||||||
p.set(align='left')
|
|
||||||
p.text("거래일시: 2026-01-29 14:30\n")
|
|
||||||
p.text("거래번호: 20260129000042\n")
|
|
||||||
p.text("\n")
|
|
||||||
p.text("결제금액: 50,000원\n")
|
|
||||||
p.text("적립예정: 1,500P\n")
|
|
||||||
p.text("\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
p.text("\n")
|
|
||||||
|
|
||||||
# QR 코드
|
|
||||||
p.qr(TEST_URL, size=6, center=True)
|
|
||||||
|
|
||||||
p.text("\n")
|
|
||||||
p.set(align='center')
|
|
||||||
p.text("QR 촬영하고 포인트 받으세요!\n")
|
|
||||||
p.text("\n")
|
|
||||||
p.text("================================\n")
|
|
||||||
|
|
||||||
p.text("\n\n\n")
|
|
||||||
p.cut()
|
|
||||||
|
|
||||||
print("✅ 방법 5 성공!")
|
|
||||||
time.sleep(2)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 방법 5 실패: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("="*60)
|
|
||||||
print("python-escpos 라이브러리 QR 테스트")
|
|
||||||
print("="*60)
|
|
||||||
print(f"프린터: {PRINTER_IP}:{PRINTER_PORT}")
|
|
||||||
print(f"테스트 URL: {TEST_URL}")
|
|
||||||
print("\n먼저 라이브러리 설치 확인:")
|
|
||||||
print(" pip install python-escpos")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import escpos
|
|
||||||
print("✅ python-escpos 설치됨")
|
|
||||||
except ImportError:
|
|
||||||
print("❌ python-escpos가 설치되지 않았습니다!")
|
|
||||||
print(" 실행: pip install python-escpos")
|
|
||||||
return
|
|
||||||
|
|
||||||
methods = [
|
|
||||||
("방법 1: 내장 QR (size=4)", test_method_1_native_qr),
|
|
||||||
("방법 2: 이미지로 변환", test_method_2_image),
|
|
||||||
("방법 3: 작은 QR (size=3)", test_method_3_qr_small),
|
|
||||||
("방법 4: 큰 QR (size=8)", test_method_4_qr_large),
|
|
||||||
("방법 5: 완전한 영수증", test_method_5_full_receipt),
|
|
||||||
]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for name, method_func in methods:
|
|
||||||
try:
|
|
||||||
success = method_func()
|
|
||||||
results.append((name, success))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[{name}] ❌ 예외 발생: {e}")
|
|
||||||
results.append((name, False))
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("결과 요약")
|
|
||||||
print("="*60)
|
|
||||||
for name, success in results:
|
|
||||||
print(f"{name}: {'✅ 성공' if success else '❌ 실패'}")
|
|
||||||
|
|
||||||
print("\n5장의 영수증을 확인하여 QR이 보이는 번호를 알려주세요!")
|
|
||||||
print("="*60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
122
backend/test_rxusage_playwright.py
Normal file
122
backend/test_rxusage_playwright.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""rx-usage 페이지 Playwright 테스트"""
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_rx_usage_quick_order():
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=False) # 화면 보이게
|
||||||
|
page = browser.new_page()
|
||||||
|
|
||||||
|
# 콘솔 로그 캡처
|
||||||
|
page.on("console", lambda msg: print(f"[CONSOLE] {msg.type}: {msg.text}"))
|
||||||
|
|
||||||
|
# 네트워크 요청/응답 캡처
|
||||||
|
def log_response(response):
|
||||||
|
if 'api/order' in response.url or 'quick-submit' in response.url:
|
||||||
|
print(f"\n[RESPONSE] {response.url}")
|
||||||
|
print(f" Status: {response.status}")
|
||||||
|
try:
|
||||||
|
body = response.json()
|
||||||
|
print(f" Body: {json.dumps(body, ensure_ascii=False, indent=2)}")
|
||||||
|
except:
|
||||||
|
print(f" Body: {response.text()[:500]}")
|
||||||
|
|
||||||
|
page.on("response", log_response)
|
||||||
|
|
||||||
|
print("="*60)
|
||||||
|
print("1. rx-usage 페이지 접속")
|
||||||
|
print("="*60)
|
||||||
|
page.goto("http://localhost:7001/admin/rx-usage")
|
||||||
|
page.wait_for_load_state("networkidle")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("2. 데이터 로드 (조회 버튼 클릭)")
|
||||||
|
print("="*60)
|
||||||
|
# 조회 버튼 클릭
|
||||||
|
search_btn = page.locator("button:has-text('조회')")
|
||||||
|
if search_btn.count() > 0:
|
||||||
|
search_btn.first.click()
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("3. 첫 번째 품목 행 더블클릭 (도매상 모달 열기)")
|
||||||
|
print("="*60)
|
||||||
|
# 테이블 행 찾기
|
||||||
|
rows = page.locator("tr[data-idx]")
|
||||||
|
row_count = rows.count()
|
||||||
|
print(f" 품목 수: {row_count}")
|
||||||
|
|
||||||
|
if row_count > 0:
|
||||||
|
# 첫 번째 품목 더블클릭
|
||||||
|
rows.first.dblclick()
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("4. 도매상 모달에서 지오영 품목 확인")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# 지오영 테이블에서 재고 있는 품목 찾기
|
||||||
|
geo_rows = page.locator(".geo-table tbody tr:not(.no-stock)")
|
||||||
|
geo_count = geo_rows.count()
|
||||||
|
print(f" 지오영 재고 있는 품목: {geo_count}개")
|
||||||
|
|
||||||
|
if geo_count > 0:
|
||||||
|
# 첫 번째 품목 정보 출력
|
||||||
|
first_row = geo_rows.first
|
||||||
|
product_name = first_row.locator(".geo-name").text_content()
|
||||||
|
stock = first_row.locator(".geo-stock").text_content()
|
||||||
|
print(f" 선택할 품목: {product_name}, 재고: {stock}")
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("5. '담기' 버튼 클릭")
|
||||||
|
print("="*60)
|
||||||
|
add_btn = first_row.locator("button.geo-add-btn")
|
||||||
|
if add_btn.count() > 0:
|
||||||
|
add_btn.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# prompt 창에 수량 입력 (기본값 사용)
|
||||||
|
page.on("dialog", lambda dialog: dialog.accept())
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("6. 장바구니 확인")
|
||||||
|
print("="*60)
|
||||||
|
cart_items = page.locator(".cart-item")
|
||||||
|
cart_count = cart_items.count()
|
||||||
|
print(f" 장바구니 품목: {cart_count}개")
|
||||||
|
|
||||||
|
if cart_count > 0:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("7. 퀵주문 버튼 클릭!")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# 퀵주문 버튼 찾기
|
||||||
|
quick_order_btn = page.locator("button.cart-item-order").first
|
||||||
|
if quick_order_btn.count() > 0:
|
||||||
|
quick_order_btn.click()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# confirm 대화상자 수락
|
||||||
|
page.on("dialog", lambda dialog: dialog.accept())
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("8. 결과 확인")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# 토스트 메시지 확인
|
||||||
|
toast = page.locator(".toast")
|
||||||
|
if toast.count() > 0:
|
||||||
|
toast_text = toast.text_content()
|
||||||
|
print(f" 토스트 메시지: {toast_text}")
|
||||||
|
|
||||||
|
print("\n테스트 완료. 10초 후 브라우저 닫힘...")
|
||||||
|
time.sleep(10)
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_rx_usage_quick_order()
|
||||||
59
backend/test_selective_order.py
Normal file
59
backend/test_selective_order.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""선별 주문 테스트 - 체크박스로 특정 품목만 주문"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 1. 품목 2개 담기
|
||||||
|
print('=== 1. 품목 2개 담기 ===')
|
||||||
|
r1 = s.search_products('코자정')
|
||||||
|
p1 = r1['items'][0]
|
||||||
|
s.add_to_cart(p1['internal_code'], qty=1, price=p1['price'], stock=p1['stock'])
|
||||||
|
print(f"담음: {p1['name']}")
|
||||||
|
|
||||||
|
r2 = s.search_products('디카맥스')
|
||||||
|
p2 = r2['items'][0]
|
||||||
|
s.add_to_cart(p2['internal_code'], qty=1, price=p2['price'], stock=p2['stock'])
|
||||||
|
print(f"담음: {p2['name']}")
|
||||||
|
|
||||||
|
# 2. 장바구니 확인
|
||||||
|
print('\n=== 2. 장바구니 확인 ===')
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f"품목 수: {cart['total_items']}")
|
||||||
|
for item in cart['items']:
|
||||||
|
status = '활성' if item.get('active') else '취소'
|
||||||
|
print(f" [{status}] {item['product_name'][:25]} (row:{item['row_index']})")
|
||||||
|
|
||||||
|
# 3. 코자정(row 0)만 취소 → 디카맥스만 주문되어야 함
|
||||||
|
print('\n=== 3. 코자정 취소 (row 0) ===')
|
||||||
|
cancel_result = s.cancel_item(row_index=0)
|
||||||
|
print(f"취소 결과: {cancel_result}")
|
||||||
|
|
||||||
|
# 4. 장바구니 다시 확인
|
||||||
|
print('\n=== 4. 장바구니 재확인 ===')
|
||||||
|
cart2 = s.get_cart()
|
||||||
|
for item in cart2['items']:
|
||||||
|
status = '✅활성' if item.get('active') else '❌취소'
|
||||||
|
print(f" {status} {item['product_name'][:25]}")
|
||||||
|
|
||||||
|
# 5. 주문 (취소 안 된 것만 나감)
|
||||||
|
print('\n=== 5. 주문 전송 ===')
|
||||||
|
order_result = s.submit_order()
|
||||||
|
print(f"주문 결과: {order_result}")
|
||||||
|
|
||||||
|
# 6. 장바구니 확인 - 디카맥스만 주문됐으면, 코자정은 남아있어야 함
|
||||||
|
print('\n=== 6. 주문 후 장바구니 ===')
|
||||||
|
cart3 = s.get_cart()
|
||||||
|
print(f"품목 수: {cart3['total_items']}")
|
||||||
|
for item in cart3['items']:
|
||||||
|
print(f" - {item['product_name'][:25]}")
|
||||||
|
|
||||||
|
if cart3['total_items'] == 1:
|
||||||
|
print('\n🎉 성공! 취소된 품목(코자정)은 남고, 디카맥스만 주문됨!')
|
||||||
|
elif cart3['total_items'] == 0:
|
||||||
|
print('\n⚠️ 둘 다 주문됨 - 체크박스 로직 안 먹힘')
|
||||||
|
else:
|
||||||
|
print('\n🤔 예상 외 결과')
|
||||||
68
backend/test_selective_order2.py
Normal file
68
backend/test_selective_order2.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""선별 주문 테스트 - 모듈 리로드 포함"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
# 모듈 리로드!
|
||||||
|
import importlib
|
||||||
|
import wholesale.sooin
|
||||||
|
importlib.reload(wholesale.sooin)
|
||||||
|
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
# 싱글톤 리셋
|
||||||
|
SooinSession._instance = None
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 1. 품목 2개 담기
|
||||||
|
print('=== 1. 품목 2개 담기 ===')
|
||||||
|
r1 = s.search_products('코자정')
|
||||||
|
p1 = r1['items'][0]
|
||||||
|
s.add_to_cart(p1['internal_code'], qty=1, price=p1['price'], stock=p1['stock'])
|
||||||
|
print(f"담음: {p1['name']}")
|
||||||
|
|
||||||
|
r2 = s.search_products('디카맥스')
|
||||||
|
p2 = r2['items'][0]
|
||||||
|
s.add_to_cart(p2['internal_code'], qty=1, price=p2['price'], stock=p2['stock'])
|
||||||
|
print(f"담음: {p2['name']}")
|
||||||
|
|
||||||
|
# 2. 장바구니 확인
|
||||||
|
print('\n=== 2. 장바구니 확인 ===')
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f"품목 수: {cart['total_items']}")
|
||||||
|
for item in cart['items']:
|
||||||
|
status = '활성' if item.get('active') else '취소'
|
||||||
|
print(f" [{status}] {item['product_name'][:25]} (row:{item['row_index']})")
|
||||||
|
|
||||||
|
# 3. 첫 번째 품목(row 0) 취소 → 두 번째만 주문되어야 함
|
||||||
|
print('\n=== 3. 첫 번째 품목 취소 (row 0) ===')
|
||||||
|
cancel_result = s.cancel_item(row_index=0)
|
||||||
|
print(f"취소 결과: {cancel_result.get('message')}")
|
||||||
|
|
||||||
|
# 4. 장바구니 다시 확인
|
||||||
|
print('\n=== 4. 장바구니 재확인 ===')
|
||||||
|
cart2 = s.get_cart()
|
||||||
|
for item in cart2['items']:
|
||||||
|
status = '✅활성' if item.get('active') else '❌취소'
|
||||||
|
print(f" {status} {item['product_name'][:25]}")
|
||||||
|
|
||||||
|
# 5. 주문 (취소 안 된 것만 나감)
|
||||||
|
print('\n=== 5. 주문 전송 ===')
|
||||||
|
order_result = s.submit_order()
|
||||||
|
print(f"주문 결과: {order_result}")
|
||||||
|
|
||||||
|
# 6. 장바구니 확인
|
||||||
|
print('\n=== 6. 주문 후 장바구니 ===')
|
||||||
|
cart3 = s.get_cart()
|
||||||
|
print(f"품목 수: {cart3['total_items']}")
|
||||||
|
for item in cart3['items']:
|
||||||
|
print(f" - {item['product_name'][:25]}")
|
||||||
|
|
||||||
|
if cart3['total_items'] == 1:
|
||||||
|
print('\n🎉 성공! 취소된 품목은 남고, 나머지만 주문됨!')
|
||||||
|
elif cart3['total_items'] == 0:
|
||||||
|
print('\n⚠️ 둘 다 주문됨 - 체크박스 로직 안 먹힘')
|
||||||
|
else:
|
||||||
|
print(f'\n🤔 예상 외 결과: {cart3["total_items"]}개 남음')
|
||||||
38
backend/test_session.py
Normal file
38
backend/test_session.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
print('1. 로그인...')
|
||||||
|
s.login()
|
||||||
|
|
||||||
|
print('\n2. 장바구니 비우기...')
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
print('\n3. Bag.asp 확인 (비우기 후)...')
|
||||||
|
resp1 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup1 = BeautifulSoup(resp1.content, 'html.parser')
|
||||||
|
int_array1 = soup1.find('input', {'name': 'intArray'})
|
||||||
|
print(f" intArray: {int_array1.get('value') if int_array1 else 'N/A'}")
|
||||||
|
|
||||||
|
print('\n4. 코자정 검색...')
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0] if result.get('items') else None
|
||||||
|
print(f" 제품: {product['name']}, 코드: {product['internal_code']}")
|
||||||
|
|
||||||
|
print('\n5. add_to_cart 호출...')
|
||||||
|
cart_result = s.add_to_cart(product['internal_code'], qty=1,
|
||||||
|
price=product['price'], stock=product['stock'])
|
||||||
|
print(f" 결과: {cart_result}")
|
||||||
|
|
||||||
|
print('\n6. Bag.asp 확인 (담기 후)...')
|
||||||
|
resp2 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup2 = BeautifulSoup(resp2.content, 'html.parser')
|
||||||
|
int_array2 = soup2.find('input', {'name': 'intArray'})
|
||||||
|
print(f" intArray: {int_array2.get('value') if int_array2 else 'N/A'}")
|
||||||
|
|
||||||
|
# 품목 확인
|
||||||
|
import re
|
||||||
|
rows = soup2.find_all('tr', id=re.compile(r'^bagLine'))
|
||||||
|
print(f" 품목 수: {len(rows)}")
|
||||||
@ -1,49 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""수인약품 API 테스트"""
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 현재 디렉토리 추가
|
|
||||||
sys.path.insert(0, '.')
|
|
||||||
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
print('수인약품 API 테스트')
|
|
||||||
print('='*50)
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
|
|
||||||
# 1. 로그인 테스트
|
|
||||||
start = time.time()
|
|
||||||
print('1. 로그인 중...')
|
|
||||||
if session.login():
|
|
||||||
print(f' ✅ 로그인 성공! ({time.time()-start:.1f}초)')
|
|
||||||
else:
|
|
||||||
print(' ❌ 로그인 실패')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# 2. 검색 테스트 (KD코드: 코자정)
|
|
||||||
start = time.time()
|
|
||||||
print('\n2. 검색 테스트 (KD코드: 073100220 - 코자정)...')
|
|
||||||
products = session.search_products('073100220', 'kd_code')
|
|
||||||
elapsed = time.time() - start
|
|
||||||
print(f' 검색 완료: {len(products)}개 ({elapsed:.2f}초)')
|
|
||||||
|
|
||||||
for p in products[:3]:
|
|
||||||
name = p.get('product_name', '')
|
|
||||||
spec = p.get('specification', '')
|
|
||||||
stock = p.get('stock', 0)
|
|
||||||
price = p.get('unit_price', 0)
|
|
||||||
code = p.get('internal_code', '')
|
|
||||||
print(f' - {name} ({spec})')
|
|
||||||
print(f' 재고: {stock}, 단가: {price:,}원, 내부코드: {code}')
|
|
||||||
|
|
||||||
# 3. 장바구니 조회
|
|
||||||
start = time.time()
|
|
||||||
print('\n3. 장바구니 조회...')
|
|
||||||
cart = session.get_cart()
|
|
||||||
elapsed = time.time() - start
|
|
||||||
print(f' 장바구니: {cart.get("total_items", 0)}개 품목 ({elapsed:.2f}초)')
|
|
||||||
|
|
||||||
print('\n' + '='*50)
|
|
||||||
print('✅ 테스트 완료!')
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""수인약품 API 전체 플로우 테스트"""
|
|
||||||
import time
|
|
||||||
from sooin_api import SooinSession
|
|
||||||
|
|
||||||
session = SooinSession()
|
|
||||||
|
|
||||||
print('=== 수인약품 API 전체 테스트 ===')
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 로그인
|
|
||||||
start = time.time()
|
|
||||||
session.login()
|
|
||||||
print(f'1. 로그인: {time.time()-start:.1f}초')
|
|
||||||
|
|
||||||
# 장바구니 비우기
|
|
||||||
start = time.time()
|
|
||||||
session.clear_cart()
|
|
||||||
print(f'2. 장바구니 비우기: {time.time()-start:.2f}초')
|
|
||||||
|
|
||||||
# 검색 + 장바구니 추가
|
|
||||||
start = time.time()
|
|
||||||
result = session.order_product('073100220', 2, '30T')
|
|
||||||
elapsed = time.time() - start
|
|
||||||
success = result.get('success', False)
|
|
||||||
msg = result.get('message', '')
|
|
||||||
print(f'3. 검색+장바구니: {elapsed:.2f}초')
|
|
||||||
print(f' 결과: {success} - {msg}')
|
|
||||||
|
|
||||||
# 장바구니 조회
|
|
||||||
start = time.time()
|
|
||||||
cart = session.get_cart()
|
|
||||||
elapsed = time.time() - start
|
|
||||||
items = cart.get('total_items', 0)
|
|
||||||
amount = cart.get('total_amount', 0)
|
|
||||||
print(f'4. 장바구니 조회: {elapsed:.2f}초')
|
|
||||||
print(f' 품목: {items}개, 금액: {amount:,}원')
|
|
||||||
|
|
||||||
print()
|
|
||||||
print('=== 완료! ===')
|
|
||||||
64
backend/test_submit_detail.py
Normal file
64
backend/test_submit_detail.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
print('1. 로그인 & 장바구니 담기...')
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0]
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1,
|
||||||
|
price=product['price'], stock=product['stock'])
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
int_array = soup.find('input', {'name': 'intArray'})
|
||||||
|
print(f" intArray: {int_array.get('value')}")
|
||||||
|
|
||||||
|
print('\n2. Form 데이터 수집...')
|
||||||
|
form = soup.find('form', {'id': 'frmBag'})
|
||||||
|
form_data = {}
|
||||||
|
for inp in form.find_all('input'):
|
||||||
|
name = inp.get('name', '')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
inp_type = inp.get('type', 'text').lower()
|
||||||
|
if inp_type == 'checkbox':
|
||||||
|
continue
|
||||||
|
form_data[name] = inp.get('value', '')
|
||||||
|
|
||||||
|
# 주요 필드 출력
|
||||||
|
print(f" kind: {form_data.get('kind')}")
|
||||||
|
print(f" intArray: {form_data.get('intArray')}")
|
||||||
|
print(f" currVenCd: {form_data.get('currVenCd')}")
|
||||||
|
|
||||||
|
print('\n3. kind=order로 변경 후 POST...')
|
||||||
|
form_data['kind'] = 'order'
|
||||||
|
form_data['tx_memo'] = '디버그 테스트'
|
||||||
|
|
||||||
|
print(f" 전송할 필드 수: {len(form_data)}")
|
||||||
|
|
||||||
|
resp = s.session.post(
|
||||||
|
s.BAG_URL, # BagOrder.asp
|
||||||
|
data=form_data,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Referer': f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}'
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f'\n4. 응답 분석...')
|
||||||
|
print(f" 상태코드: {resp.status_code}")
|
||||||
|
print(f" 응답 길이: {len(resp.text)}")
|
||||||
|
print(f"\n 응답 내용:\n{resp.text[:1000]}")
|
||||||
|
|
||||||
|
print('\n5. 주문 후 장바구니 확인...')
|
||||||
|
resp2 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup2 = BeautifulSoup(resp2.content, 'html.parser')
|
||||||
|
int_array2 = soup2.find('input', {'name': 'intArray'})
|
||||||
|
print(f" intArray: {int_array2.get('value')}")
|
||||||
43
backend/test_submit_order.py
Normal file
43
backend/test_submit_order.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""submit_order 메서드 테스트"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
# 모듈 리로드
|
||||||
|
import importlib
|
||||||
|
import wholesale.sooin
|
||||||
|
importlib.reload(wholesale.sooin)
|
||||||
|
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
print('1. 로그인...')
|
||||||
|
s.login()
|
||||||
|
|
||||||
|
print('2. 장바구니 비우기...')
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
print('3. 제품 검색 및 추가...')
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0]
|
||||||
|
print(f" 제품: {product['name']} / {product['price']:,}원")
|
||||||
|
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1,
|
||||||
|
price=product['price'], stock=product['stock'])
|
||||||
|
|
||||||
|
print('4. 장바구니 확인...')
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f" 품목 수: {cart['total_items']}")
|
||||||
|
print(f" 총액: {cart['total_amount']:,}원")
|
||||||
|
|
||||||
|
print('\n5. 주문 전송...')
|
||||||
|
order_result = s.submit_order(memo="API 테스트")
|
||||||
|
print(f" 결과: {order_result}")
|
||||||
|
|
||||||
|
if order_result.get('success'):
|
||||||
|
print('\n🎉 주문 성공!')
|
||||||
|
else:
|
||||||
|
print(f"\n❌ 주문 실패: {order_result.get('error')}")
|
||||||
|
|
||||||
|
print('\n6. 주문 후 장바구니...')
|
||||||
|
cart2 = s.get_cart()
|
||||||
|
print(f" 품목 수: {cart2['total_items']}")
|
||||||
66
backend/test_submit_xy.py
Normal file
66
backend/test_submit_xy.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
from wholesale import SooinSession
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
s = SooinSession()
|
||||||
|
print('1. 준비...')
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
result = s.search_products('코자정')
|
||||||
|
product = result['items'][0]
|
||||||
|
s.add_to_cart(product['internal_code'], qty=1,
|
||||||
|
price=product['price'], stock=product['stock'])
|
||||||
|
|
||||||
|
resp = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
|
form = soup.find('form', {'id': 'frmBag'})
|
||||||
|
|
||||||
|
form_data = {}
|
||||||
|
for inp in form.find_all('input'):
|
||||||
|
name = inp.get('name', '')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
inp_type = inp.get('type', 'text').lower()
|
||||||
|
if inp_type == 'checkbox':
|
||||||
|
continue
|
||||||
|
form_data[name] = inp.get('value', '')
|
||||||
|
|
||||||
|
form_data['kind'] = 'order'
|
||||||
|
form_data['tx_memo'] = '좌표 테스트'
|
||||||
|
|
||||||
|
# x, y 좌표 추가!
|
||||||
|
form_data['x'] = '10'
|
||||||
|
form_data['y'] = '10'
|
||||||
|
|
||||||
|
print(f" intArray: {form_data.get('intArray')}")
|
||||||
|
print(f" x, y 추가: {form_data.get('x')}, {form_data.get('y')}")
|
||||||
|
|
||||||
|
print('\n2. POST...')
|
||||||
|
resp = s.session.post(
|
||||||
|
s.BAG_URL,
|
||||||
|
data=form_data,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Referer': f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}'
|
||||||
|
},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" 응답 길이: {len(resp.text)}")
|
||||||
|
|
||||||
|
# alert 내용 확인
|
||||||
|
import re
|
||||||
|
alert_match = re.search(r'alert\("([^"]*)"\)', resp.text)
|
||||||
|
alert_msg = alert_match.group(1) if alert_match else 'N/A'
|
||||||
|
print(f" alert 메시지: '{alert_msg}'")
|
||||||
|
|
||||||
|
print('\n3. 주문 후 장바구니...')
|
||||||
|
resp2 = s.session.get(f'{s.BAG_VIEW_URL}?currVenCd={s.vendor_code}', timeout=15)
|
||||||
|
soup2 = BeautifulSoup(resp2.content, 'html.parser')
|
||||||
|
int_array2 = soup2.find('input', {'name': 'intArray'})
|
||||||
|
print(f" intArray: {int_array2.get('value')}")
|
||||||
|
|
||||||
|
if int_array2.get('value') == '-1':
|
||||||
|
print('\n🎉 주문 성공!')
|
||||||
81
backend/test_temp_save.py
Normal file
81
backend/test_temp_save.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""방안 1: 임시 보관 방식 테스트"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.sooin
|
||||||
|
importlib.reload(wholesale.sooin)
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
SooinSession._instance = None
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 시나리오: 기존 코자정이 담겨있고, 디카맥스만 주문하고 싶음
|
||||||
|
|
||||||
|
print('=== 1. 기존 품목 (코자정) 담기 ===')
|
||||||
|
r1 = s.search_products('코자정')
|
||||||
|
p1 = r1['items'][0]
|
||||||
|
s.add_to_cart(p1['internal_code'], qty=1, price=p1['price'], stock=p1['stock'])
|
||||||
|
print(f"기존 품목: {p1['name']}")
|
||||||
|
|
||||||
|
print('\n=== 2. 새 품목 (디카맥스) 담기 ===')
|
||||||
|
r2 = s.search_products('디카맥스')
|
||||||
|
p2 = r2['items'][0]
|
||||||
|
s.add_to_cart(p2['internal_code'], qty=1, price=p2['price'], stock=p2['stock'])
|
||||||
|
print(f"새 품목: {p2['name']}")
|
||||||
|
|
||||||
|
# 장바구니 확인
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f"\n현재 장바구니: {cart['total_items']}개")
|
||||||
|
|
||||||
|
# === 선별 주문 시작 ===
|
||||||
|
print('\n' + '='*50)
|
||||||
|
print('=== 선별 주문: 디카맥스만 주문 ===')
|
||||||
|
print('='*50)
|
||||||
|
|
||||||
|
# 3. 기존 품목 정보 저장
|
||||||
|
print('\n3. 기존 품목 정보 저장')
|
||||||
|
existing_items = []
|
||||||
|
for item in cart['items']:
|
||||||
|
# 디카맥스는 제외 (이번에 주문할 품목)
|
||||||
|
if '디카맥스' not in item['product_name']:
|
||||||
|
existing_items.append({
|
||||||
|
'internal_code': item['internal_code'],
|
||||||
|
'quantity': item['quantity'],
|
||||||
|
'price': item['unit_price'],
|
||||||
|
'name': item['product_name']
|
||||||
|
})
|
||||||
|
print(f" 저장: {item['product_name']}")
|
||||||
|
|
||||||
|
# 4. 장바구니 비우기
|
||||||
|
print('\n4. 장바구니 비우기')
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 5. 주문할 품목만 다시 담기
|
||||||
|
print('\n5. 디카맥스만 다시 담기')
|
||||||
|
s.add_to_cart(p2['internal_code'], qty=1, price=p2['price'], stock=p2['stock'])
|
||||||
|
|
||||||
|
# 6. 주문
|
||||||
|
print('\n6. 주문!')
|
||||||
|
result = s.submit_order()
|
||||||
|
print(f"결과: {result}")
|
||||||
|
|
||||||
|
# 7. 기존 품목 복원
|
||||||
|
print('\n7. 기존 품목 복원')
|
||||||
|
for item in existing_items:
|
||||||
|
s.add_to_cart(item['internal_code'], qty=item['quantity'], price=item['price'], stock=999)
|
||||||
|
print(f" 복원: {item['name']}")
|
||||||
|
|
||||||
|
# 8. 최종 확인
|
||||||
|
print('\n=== 8. 최종 장바구니 ===')
|
||||||
|
final_cart = s.get_cart()
|
||||||
|
print(f"품목 수: {final_cart['total_items']}")
|
||||||
|
for item in final_cart['items']:
|
||||||
|
print(f" - {item['product_name']}")
|
||||||
|
|
||||||
|
if final_cart['total_items'] == 1 and '코자정' in final_cart['items'][0]['product_name']:
|
||||||
|
print('\n🎉 성공! 디카맥스만 주문되고 코자정은 복원됨!')
|
||||||
|
else:
|
||||||
|
print('\n❌ 실패')
|
||||||
72
backend/test_temp_save2.py
Normal file
72
backend/test_temp_save2.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""방안 1: 재고 있는 품목으로 테스트"""
|
||||||
|
import sys; sys.path.insert(0, '.'); import wholesale_path
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import wholesale.sooin
|
||||||
|
importlib.reload(wholesale.sooin)
|
||||||
|
from wholesale import SooinSession
|
||||||
|
|
||||||
|
SooinSession._instance = None
|
||||||
|
s = SooinSession()
|
||||||
|
s.login()
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 재고 있는 품목 검색
|
||||||
|
print('=== 1. 재고 확인 ===')
|
||||||
|
r1 = s.search_products('코자정')
|
||||||
|
r2 = s.search_products('라식스')
|
||||||
|
|
||||||
|
p1 = r1['items'][0]
|
||||||
|
p2 = r2['items'][0]
|
||||||
|
print(f"코자정: 재고 {p1['stock']}")
|
||||||
|
print(f"라식스: 재고 {p2['stock']}")
|
||||||
|
|
||||||
|
# 기존 품목 담기 (코자정 - 나중에 복원할 것)
|
||||||
|
print('\n=== 2. 기존 품목 (코자정) 담기 ===')
|
||||||
|
s.add_to_cart(p1['internal_code'], qty=1, price=p1['price'], stock=p1['stock'])
|
||||||
|
|
||||||
|
# 새 품목 담기 (라식스 - 주문할 것)
|
||||||
|
print('=== 3. 새 품목 (라식스) 담기 ===')
|
||||||
|
s.add_to_cart(p2['internal_code'], qty=1, price=p2['price'], stock=p2['stock'])
|
||||||
|
|
||||||
|
cart = s.get_cart()
|
||||||
|
print(f"현재 장바구니: {cart['total_items']}개")
|
||||||
|
for item in cart['items']:
|
||||||
|
print(f" - {item['product_name'][:30]}")
|
||||||
|
|
||||||
|
# === 선별 주문 ===
|
||||||
|
print('\n' + '='*50)
|
||||||
|
print('=== 라식스만 주문! ===')
|
||||||
|
print('='*50)
|
||||||
|
|
||||||
|
# 기존 품목 저장
|
||||||
|
existing = [{'ic': p1['internal_code'], 'qty': 1, 'price': p1['price'], 'stock': p1['stock'], 'name': p1['name']}]
|
||||||
|
print(f"\n저장: {p1['name']}")
|
||||||
|
|
||||||
|
# 장바구니 비우기
|
||||||
|
print('장바구니 비우기...')
|
||||||
|
s.clear_cart()
|
||||||
|
|
||||||
|
# 라식스만 담기
|
||||||
|
print('라식스만 담기...')
|
||||||
|
s.add_to_cart(p2['internal_code'], qty=1, price=p2['price'], stock=p2['stock'])
|
||||||
|
|
||||||
|
# 주문
|
||||||
|
print('주문 전송...')
|
||||||
|
result = s.submit_order()
|
||||||
|
print(f"결과: {result}")
|
||||||
|
|
||||||
|
# 복원
|
||||||
|
print('\n코자정 복원...')
|
||||||
|
for e in existing:
|
||||||
|
s.add_to_cart(e['ic'], qty=e['qty'], price=e['price'], stock=e['stock'])
|
||||||
|
|
||||||
|
# 최종 확인
|
||||||
|
final = s.get_cart()
|
||||||
|
print(f"\n=== 최종 장바구니: {final['total_items']}개 ===")
|
||||||
|
for item in final['items']:
|
||||||
|
print(f" - {item['product_name'][:30]}")
|
||||||
|
|
||||||
|
if final['total_items'] == 1:
|
||||||
|
print('\n🎉 성공! 라식스만 주문됨, 코자정 복원됨!')
|
||||||
@ -1,32 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""wholesale 통합 테스트"""
|
|
||||||
import wholesale_path
|
|
||||||
from wholesale import SooinSession, GeoYoungSession
|
|
||||||
|
|
||||||
print('=== 도매상 API 통합 테스트 ===\n')
|
|
||||||
|
|
||||||
# 수인약품 테스트
|
|
||||||
print('1. 수인약품 테스트')
|
|
||||||
sooin = SooinSession()
|
|
||||||
if sooin.login():
|
|
||||||
print(' ✅ 로그인 성공')
|
|
||||||
result = sooin.search_products('073100220')
|
|
||||||
print(f' ✅ 검색: {result["total"]}개 결과')
|
|
||||||
cart = sooin.get_cart()
|
|
||||||
print(f' ✅ 장바구니: {cart["total_items"]}개')
|
|
||||||
else:
|
|
||||||
print(' ❌ 로그인 실패')
|
|
||||||
|
|
||||||
# 지오영 테스트
|
|
||||||
print('\n2. 지오영 테스트')
|
|
||||||
geo = GeoYoungSession()
|
|
||||||
if geo.login():
|
|
||||||
print(' ✅ 로그인 성공')
|
|
||||||
result = geo.search_products('레바미피드')
|
|
||||||
print(f' ✅ 검색: {result["total"]}개 결과')
|
|
||||||
cart = geo.get_cart()
|
|
||||||
print(f' ✅ 장바구니: {cart["total_items"]}개')
|
|
||||||
else:
|
|
||||||
print(' ❌ 로그인 실패')
|
|
||||||
|
|
||||||
print('\n=== 테스트 완료 ===')
|
|
||||||
1148
docs/AI_자동발주시스템_통합기획서_v1.html
Normal file
1148
docs/AI_자동발주시스템_통합기획서_v1.html
Normal file
File diff suppressed because it is too large
Load Diff
692
docs/AI_자동발주시스템_통합기획서_v1.md
Normal file
692
docs/AI_자동발주시스템_통합기획서_v1.md
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
# 🤖 AI 자동발주시스템 통합 기획서
|
||||||
|
|
||||||
|
> **버전**: 1.0
|
||||||
|
> **작성일**: 2026-03-06
|
||||||
|
> **작성자**: 용림 (with 약사님)
|
||||||
|
> **상태**: 기획 완료, 개발 대기
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 목차
|
||||||
|
|
||||||
|
1. [비전 및 목표](#1-비전-및-목표)
|
||||||
|
2. [현재 구현 현황](#2-현재-구현-현황)
|
||||||
|
3. [시스템 아키텍처](#3-시스템-아키텍처)
|
||||||
|
4. [AI 학습 요소](#4-ai-학습-요소)
|
||||||
|
5. [핵심 기능 설계](#5-핵심-기능-설계)
|
||||||
|
6. [데이터 모델](#6-데이터-모델)
|
||||||
|
7. [API 설계](#7-api-설계)
|
||||||
|
8. [자동화 레벨](#8-자동화-레벨)
|
||||||
|
9. [알림 시스템](#9-알림-시스템)
|
||||||
|
10. [개발 로드맵](#10-개발-로드맵)
|
||||||
|
11. [성공 지표](#11-성공-지표)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 비전 및 목표
|
||||||
|
|
||||||
|
### 🎯 비전
|
||||||
|
> **"약사님이 주문에 신경 쓰지 않아도 되는 약국"**
|
||||||
|
|
||||||
|
AI가 사용량, 재고, 도매상 상황, 과거 주문 패턴을 학습하여:
|
||||||
|
- **언제** 주문할지
|
||||||
|
- **어느 도매상**에 주문할지
|
||||||
|
- **어떤 규격**으로 주문할지
|
||||||
|
- **얼마나** 주문할지
|
||||||
|
|
||||||
|
모든 것을 자동으로 결정하고 실행합니다.
|
||||||
|
|
||||||
|
### 핵심 가치
|
||||||
|
|
||||||
|
| AS-IS | TO-BE |
|
||||||
|
|-------|-------|
|
||||||
|
| 매일 재고 확인 | AI가 자동 모니터링 |
|
||||||
|
| 수동으로 도매상 선택 | AI가 최적 도매상 선택 |
|
||||||
|
| 경험에 의존한 주문량 | 데이터 기반 최적 주문량 |
|
||||||
|
| 주문 누락/지연 발생 | 선제적 자동 주문 |
|
||||||
|
| 배송 마감 놓침 | 마감시간 자동 알림 |
|
||||||
|
|
||||||
|
### 핵심 원칙
|
||||||
|
|
||||||
|
> **"AI는 대체하는 것이 아니라, 약사님의 방식을 자동화합니다."**
|
||||||
|
|
||||||
|
- 약사님이 항상 지오영에 먼저 주문하면 → AI도 지오영 우선
|
||||||
|
- 약사님이 300T보다 30T를 선호하면 → AI도 소량 주문
|
||||||
|
- 약사님이 여유 있게 주문하면 → AI도 안전 재고 확보
|
||||||
|
- 약사님이 가격에 민감하면 → AI도 최저가 추적 (OTC/비급여)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 현재 구현 현황
|
||||||
|
|
||||||
|
### 2.1 도매상 API (✅ 완료)
|
||||||
|
|
||||||
|
| 도매상 | 재고조회 | 장바구니 | 주문 | 취소/복원 | 잔고 | 월매출 |
|
||||||
|
|--------|:--------:|:--------:|:----:|:---------:|:----:|:------:|
|
||||||
|
| **지오영** | ✅ | ✅ | ✅ 확정포함 | ✅ | ✅ | ✅ |
|
||||||
|
| **수인약품** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **백제약품** | ✅ | ✅ | ✅ | ⏳ | ✅ | ✅ |
|
||||||
|
|
||||||
|
### 2.2 주문 DB (✅ 완료)
|
||||||
|
|
||||||
|
```
|
||||||
|
orders.db
|
||||||
|
├── wholesalers # 도매상 마스터
|
||||||
|
├── orders # 주문 헤더
|
||||||
|
├── order_items # 주문 품목
|
||||||
|
├── order_logs # 주문 이력
|
||||||
|
├── order_context # AI 학습용 컨텍스트 ⭐
|
||||||
|
├── daily_usage # 일별 사용량 시계열
|
||||||
|
└── order_patterns # AI 분석 결과
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 배송 스케줄 (✅ 확인 완료)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┬──────────┬──────────────┬──────────────┬──────────┐
|
||||||
|
│ 도매상 │ 배송 │ 주문 마감 │ 도착 예정 │ 비고 │
|
||||||
|
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
|
||||||
|
│ 지오영 │ 오전 │ 10:00 │ 11:30 │ 당일 │
|
||||||
|
│ │ 오후 │ 13:00 │ 15:00 │ 당일 │
|
||||||
|
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
|
||||||
|
│ 수인 │ 오후 │ 13:00 │ 14:30 │ 당일 │
|
||||||
|
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
|
||||||
|
│ 백제 │ 익일 │ 16:00 │ 다음날 15:00 │ ⚠️ 익일 │
|
||||||
|
└──────────┴──────────┴──────────────┴──────────────┴──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 UI (✅ 완료)
|
||||||
|
|
||||||
|
- Rx 사용량 페이지 (처방 기반)
|
||||||
|
- 장바구니 모달
|
||||||
|
- 도매상 잔고/월매출 모달
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 시스템 아키텍처
|
||||||
|
|
||||||
|
### 전체 흐름
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ AI 자동발주시스템 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────────┼───────────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||||
|
│ 데이터 수집 │ │ AI 분석 │ │ 자동 실행 │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ • POS 판매 │─────▶│ • 사용량 예측 │─────▶│ • 도매상 API │
|
||||||
|
│ • 처방전 조제 │ │ • 재고 분석 │ │ • 주문 실행 │
|
||||||
|
│ • 현재 재고 │ │ • 주문 추천 │ │ • 결과 피드백 │
|
||||||
|
│ • 도매상 재고 │ │ • 패턴 학습 │ │ │
|
||||||
|
└───────────────┘ └───────────────┘ └───────────────┘
|
||||||
|
│ │ │
|
||||||
|
└───────────────────────┼───────────────────────┘
|
||||||
|
▼
|
||||||
|
┌───────────────────┐
|
||||||
|
│ 학습 루프 │
|
||||||
|
│ │
|
||||||
|
│ 주문 결과 평가 │
|
||||||
|
│ → 모델 업데이트 │
|
||||||
|
│ → 전략 조정 │
|
||||||
|
└───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 컴포넌트 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 데이터 레이어 │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||||
|
│ │ PIT3000 │ │ SQLite │ │ 지오영 │ │ 수인 │ │
|
||||||
|
│ │ (MSSQL) │ │ Orders DB │ │ API │ │ API │ │
|
||||||
|
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
|
||||||
|
│ └───────────────┴───────────────┴───────────────┘ │
|
||||||
|
└────────────────────────────────┬─────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 서비스 레이어 │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ InventorySync │ │ UsageAnalyzer │ │ OrderExecutor │ │
|
||||||
|
│ │ 재고 동기화 │ │ 사용량 분석 │ │ 주문 실행 │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ AIPredictor │ │ AIOptimizer │ │ AILearner │ │
|
||||||
|
│ │ 수요 예측 │ │ 규격/도매상 │ │ 패턴 학습 │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 인터페이스 레이어 │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ 웹 대시보드 │ │ 알림 시스템 │ │ 관리자 앱 │ │
|
||||||
|
│ │ 재고/주문/AI │ │ 카톡/텔레그램 │ │ 수동 개입 │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. AI 학습 요소
|
||||||
|
|
||||||
|
### 4.1 규격 선택 학습 (Spec Selection)
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 중요: 전문의약품(ETC)은 보험약가 고정!
|
||||||
|
- 30T든 300T든 1T당 가격 동일
|
||||||
|
- 단가 효율은 OTC/비급여에서만 의미 있음
|
||||||
|
|
||||||
|
학습 데이터:
|
||||||
|
- 약품별 과거 주문 규격 (30T, 100T, 300T, 500T)
|
||||||
|
- 각 규격 선택 시점의 재고/사용량
|
||||||
|
- 선택 결과 (남은 재고, 다음 주문까지 기간)
|
||||||
|
- 도매상별 규격 재고 현황
|
||||||
|
|
||||||
|
학습 목표:
|
||||||
|
- 사용량 대비 최적 규격 예측
|
||||||
|
- 재고 있는 규격 우선 선택
|
||||||
|
- 낭비 최소화 (유통기한 고려)
|
||||||
|
- 소분 vs 대용량 선호도 파악
|
||||||
|
```
|
||||||
|
|
||||||
|
**예시 시나리오:**
|
||||||
|
| 필요량 | 가능 규격 | AI 선택 | 이유 |
|
||||||
|
|--------|-----------|---------|------|
|
||||||
|
| 280T | 30T(재고50), 100T(품절), 300T(재고10) | 30T x 10 | 100T 품절, 소분 선호 |
|
||||||
|
| 800T | 30T(재고100), 300T(재고5) | 300T x 3 | 대량, 재고 충분 |
|
||||||
|
| 50T | 30T(재고20), 100T(재고10) | 30T x 2 | 소량, 빠른 회전 |
|
||||||
|
|
||||||
|
### 4.2 재고 전략 학습 (Inventory Strategy)
|
||||||
|
|
||||||
|
```
|
||||||
|
학습 데이터:
|
||||||
|
- 주문 시점의 재고 수준
|
||||||
|
- 재고 소진까지 남은 일수
|
||||||
|
- 주문 후 입고까지 리드타임
|
||||||
|
- 품절 발생 이력
|
||||||
|
|
||||||
|
학습 목표:
|
||||||
|
- 약사님의 재고 선호도 파악
|
||||||
|
- 타이트형: 최소 재고 유지 (현금 흐름 중시)
|
||||||
|
- 여유형: 안전 재고 확보 (품절 방지 중시)
|
||||||
|
```
|
||||||
|
|
||||||
|
**재고 전략 프로파일:**
|
||||||
|
```python
|
||||||
|
class InventoryStrategy:
|
||||||
|
TIGHT = {
|
||||||
|
'safety_days': 2, # 안전 재고 2일치
|
||||||
|
'reorder_point': 0.8, # 80% 소진 시 주문
|
||||||
|
'order_coverage': 7 # 7일치 주문
|
||||||
|
}
|
||||||
|
|
||||||
|
MODERATE = {
|
||||||
|
'safety_days': 5,
|
||||||
|
'reorder_point': 0.6,
|
||||||
|
'order_coverage': 14
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSERVATIVE = {
|
||||||
|
'safety_days': 10,
|
||||||
|
'reorder_point': 0.5,
|
||||||
|
'order_coverage': 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 도매상 선택 학습 (Wholesaler Selection)
|
||||||
|
|
||||||
|
```
|
||||||
|
학습 데이터:
|
||||||
|
- 도매상별 주문 빈도
|
||||||
|
- 도매상별 재고 상황
|
||||||
|
- 도매상별 배송 스케줄
|
||||||
|
- 월별 한도 사용량
|
||||||
|
- 분할 주문 패턴
|
||||||
|
|
||||||
|
학습 목표:
|
||||||
|
- 기본 도매상 선호도
|
||||||
|
- 상황별 대체 도매상
|
||||||
|
- 한도 고려한 분배
|
||||||
|
- 배송 시간 고려 (긴급 시)
|
||||||
|
```
|
||||||
|
|
||||||
|
**도매상 선택 로직:**
|
||||||
|
```python
|
||||||
|
def select_wholesaler(drug_code, quantity, need_by_time=None):
|
||||||
|
"""
|
||||||
|
AI가 학습한 도매상 선택 로직
|
||||||
|
|
||||||
|
우선순위:
|
||||||
|
1. 재고 (있는 곳 우선)
|
||||||
|
2. 배송 (need_by_time 충족 가능한 곳)
|
||||||
|
3. 한도 (여유 있는 곳)
|
||||||
|
4. 선호도 (과거 패턴)
|
||||||
|
"""
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for ws in ['geoyoung', 'sooin', 'baekje']:
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
# 1. 재고 체크
|
||||||
|
if has_stock(ws, drug_code, quantity):
|
||||||
|
score += 100
|
||||||
|
else:
|
||||||
|
continue # 재고 없으면 제외
|
||||||
|
|
||||||
|
# 2. 배송 시간 체크
|
||||||
|
if need_by_time:
|
||||||
|
delivery = get_next_delivery(ws, need_by_time)
|
||||||
|
if delivery['can_deliver']:
|
||||||
|
score += 50
|
||||||
|
else:
|
||||||
|
score -= 30 # 감점
|
||||||
|
|
||||||
|
# 3. 한도 체크
|
||||||
|
limit_usage = get_limit_usage(ws)
|
||||||
|
if limit_usage < 0.9:
|
||||||
|
score += 30
|
||||||
|
elif limit_usage >= 1.0:
|
||||||
|
score -= 50 # 한도 초과
|
||||||
|
|
||||||
|
# 4. 학습된 선호도
|
||||||
|
score += ai_model.preference_score(ws, drug_code) * 20
|
||||||
|
|
||||||
|
candidates.append((ws, score))
|
||||||
|
|
||||||
|
return max(candidates, key=lambda x: x[1])[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 주문 타이밍 학습
|
||||||
|
|
||||||
|
```
|
||||||
|
학습 데이터:
|
||||||
|
- 하루 중 주문 시점 (오전/오후)
|
||||||
|
- 요일별 주문 패턴
|
||||||
|
- 배송 마감 시간 전 주문 여부
|
||||||
|
|
||||||
|
학습 목표:
|
||||||
|
- 최적 주문 시점 파악
|
||||||
|
- 배송 마감 놓치지 않기
|
||||||
|
- 분할 주문 (오전/오후) 패턴
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 핵심 기능 설계
|
||||||
|
|
||||||
|
### 5.1 선주문 반영 시스템
|
||||||
|
|
||||||
|
**목적**: 같은 날 이미 주문한 품목 자동 차감
|
||||||
|
|
||||||
|
```python
|
||||||
|
def calculate_order_qty(drug_code, usage_qty, current_stock):
|
||||||
|
# 오늘 "실제로" 주문 완료된 수량 조회
|
||||||
|
today_ordered = get_today_orders(drug_code)
|
||||||
|
|
||||||
|
# 필요량 = 사용량 - 현재고 - 선주문량
|
||||||
|
needed = usage_qty - current_stock - today_ordered
|
||||||
|
|
||||||
|
if needed > 0:
|
||||||
|
return calculate_spec_qty(needed)
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 핵심: 실제 주문만 카운트**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT SUM(oi.total_dose) as today_ordered
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN orders o ON oi.order_id = o.id
|
||||||
|
WHERE oi.drug_code = ?
|
||||||
|
AND o.order_date = DATE('now')
|
||||||
|
AND o.is_dry_run = 0 -- dry_run 제외!
|
||||||
|
AND oi.status IN ('success', 'submitted')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 도매상 한도 관리
|
||||||
|
|
||||||
|
**목적**: 월별 거래 한도 설정 및 자동 분배
|
||||||
|
|
||||||
|
```
|
||||||
|
[한도 도달 시 동작]
|
||||||
|
1. 90% 도달 → 경고 알림
|
||||||
|
2. 100% 도달 → 다른 도매상으로 자동 전환
|
||||||
|
3. 장바구니 단계에서 미리 분류
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 배송 스케줄 기반 주문
|
||||||
|
|
||||||
|
**목적**: 주문 마감시간 + 배송 도착시간 분리 관리
|
||||||
|
|
||||||
|
```
|
||||||
|
AI 판단 예시:
|
||||||
|
|
||||||
|
현재 오전 11시, "오후 3시에 필요"
|
||||||
|
|
||||||
|
→ 지오영 오전: 10시 마감 지남 ❌
|
||||||
|
→ 지오영 오후: 13시 마감 → 15:00 도착 (⚠️ 딱 맞음)
|
||||||
|
→ 수인: 13시 마감 → 14:30 도착 (✅ 여유)
|
||||||
|
→ 백제: 내일 도착 ❌
|
||||||
|
|
||||||
|
결론: 수인 추천 (14:30 도착, 30분 여유)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 주문 실패 시 재시도
|
||||||
|
|
||||||
|
```
|
||||||
|
시나리오 1: 재고 없음
|
||||||
|
- A도매상 재고 0 → B도매상 검색 → 재고 있으면 B로 주문
|
||||||
|
|
||||||
|
시나리오 2: 주문 오류
|
||||||
|
- A도매상 API 오류 → 3회 재시도 → 실패 시 B도매상
|
||||||
|
|
||||||
|
시나리오 3: 부분 성공
|
||||||
|
- 10개 품목 중 7개 성공, 3개 실패
|
||||||
|
- 실패한 3개 → B도매상으로 자동 재시도
|
||||||
|
|
||||||
|
[리포트]
|
||||||
|
- 최종 주문 결과 리포트
|
||||||
|
- 알림: "A도매상 품절로 B도매상으로 변경됨"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 데이터 모델
|
||||||
|
|
||||||
|
### 6.1 핵심 테이블 (기존)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 주문 컨텍스트 (AI 학습용)
|
||||||
|
CREATE TABLE order_context (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
order_item_id INTEGER,
|
||||||
|
|
||||||
|
-- 약품 정보
|
||||||
|
drug_code TEXT,
|
||||||
|
product_name TEXT,
|
||||||
|
|
||||||
|
-- 주문 시점 상황
|
||||||
|
stock_at_order INTEGER,
|
||||||
|
usage_1d INTEGER,
|
||||||
|
usage_7d INTEGER,
|
||||||
|
usage_30d INTEGER,
|
||||||
|
avg_daily_usage REAL,
|
||||||
|
|
||||||
|
-- 주문 결정
|
||||||
|
ordered_spec TEXT,
|
||||||
|
ordered_qty INTEGER,
|
||||||
|
wholesaler_id TEXT,
|
||||||
|
|
||||||
|
-- 선택지 정보 (AI 학습용)
|
||||||
|
available_specs JSON,
|
||||||
|
spec_stocks JSON,
|
||||||
|
selection_reason TEXT,
|
||||||
|
|
||||||
|
-- 예측 vs 실제
|
||||||
|
predicted_days_coverage REAL,
|
||||||
|
actual_days_to_reorder INTEGER,
|
||||||
|
|
||||||
|
-- 결과 평가
|
||||||
|
was_optimal BOOLEAN,
|
||||||
|
stockout_occurred BOOLEAN,
|
||||||
|
|
||||||
|
created_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 신규 테이블
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 도매상 한도 관리
|
||||||
|
CREATE TABLE wholesaler_limits (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
wholesaler_id TEXT NOT NULL,
|
||||||
|
monthly_limit INTEGER DEFAULT 0,
|
||||||
|
warning_threshold REAL DEFAULT 0.9,
|
||||||
|
priority INTEGER DEFAULT 1,
|
||||||
|
is_active INTEGER DEFAULT 1,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (wholesaler_id) REFERENCES wholesalers(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 배송 스케줄
|
||||||
|
CREATE TABLE delivery_schedules (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
wholesaler_id TEXT NOT NULL,
|
||||||
|
delivery_seq INTEGER NOT NULL,
|
||||||
|
delivery_name TEXT,
|
||||||
|
order_cutoff_time TEXT NOT NULL, -- 주문 마감 (HH:MM)
|
||||||
|
delivery_days_offset INTEGER DEFAULT 0, -- 0=당일, 1=익일
|
||||||
|
delivery_arrival_time TEXT NOT NULL, -- 도착 예정 (HH:MM)
|
||||||
|
weekdays TEXT, -- JSON [1,2,3,4,5]
|
||||||
|
is_active INTEGER DEFAULT 1,
|
||||||
|
UNIQUE(wholesaler_id, delivery_seq)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 실제 배송 스케줄 데이터
|
||||||
|
INSERT INTO delivery_schedules VALUES
|
||||||
|
('geoyoung', 1, '오전배송', '10:00', 0, '11:30'),
|
||||||
|
('geoyoung', 2, '오후배송', '13:00', 0, '15:00'),
|
||||||
|
('sooin', 1, '오후배송', '13:00', 0, '14:30'),
|
||||||
|
('baekje', 1, '익일배송', '16:00', 1, '15:00');
|
||||||
|
|
||||||
|
-- 월별 사용량 추적
|
||||||
|
CREATE TABLE wholesaler_monthly_usage (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
wholesaler_id TEXT NOT NULL,
|
||||||
|
year_month TEXT NOT NULL,
|
||||||
|
total_orders INTEGER DEFAULT 0,
|
||||||
|
total_amount INTEGER DEFAULT 0,
|
||||||
|
UNIQUE(wholesaler_id, year_month)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 주문 재시도 로그
|
||||||
|
CREATE TABLE order_fallback_log (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
order_item_id INTEGER NOT NULL,
|
||||||
|
original_wholesaler TEXT NOT NULL,
|
||||||
|
original_error TEXT,
|
||||||
|
fallback_wholesaler TEXT NOT NULL,
|
||||||
|
fallback_result TEXT,
|
||||||
|
created_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 기존 테이블 확장
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- orders 테이블 확장
|
||||||
|
ALTER TABLE orders ADD COLUMN is_dry_run INTEGER DEFAULT 0;
|
||||||
|
|
||||||
|
-- order_items 테이블 확장
|
||||||
|
ALTER TABLE order_items ADD COLUMN fallback_from_wholesaler TEXT;
|
||||||
|
ALTER TABLE order_items ADD COLUMN prior_order_qty INTEGER DEFAULT 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. API 설계
|
||||||
|
|
||||||
|
### 7.1 도매상 관리 API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/wholesaler/limits` | GET | 한도 조회 |
|
||||||
|
| `/api/wholesaler/limits/{id}` | PUT | 한도 설정 |
|
||||||
|
| `/api/wholesaler/schedules` | GET | 배송 스케줄 |
|
||||||
|
| `/api/wholesaler/can-deliver-by` | POST | 배송 가능 여부 |
|
||||||
|
|
||||||
|
### 7.2 주문 API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/order/today/{drug_code}` | GET | 오늘 주문량 |
|
||||||
|
| `/api/order/recommend-spec` | POST | 규격 추천 |
|
||||||
|
| `/api/order/create` | POST | 주문 생성 |
|
||||||
|
| `/api/order/submit` | POST | 주문 제출 (dry_run 지원) |
|
||||||
|
| `/api/order/retry` | POST | 실패 재시도 |
|
||||||
|
|
||||||
|
### 7.3 AI API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/ai/daily-analysis` | GET | 일일 분석 |
|
||||||
|
| `/api/ai/recommendations` | GET | 주문 추천 |
|
||||||
|
| `/api/ai/training-data` | GET | 학습 데이터 |
|
||||||
|
| `/api/ai/patterns/{drug_code}` | GET | 패턴 분석 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 자동화 레벨
|
||||||
|
|
||||||
|
### Level 0: 수동
|
||||||
|
- AI 추천만 제공
|
||||||
|
- 모든 주문은 수동 실행
|
||||||
|
|
||||||
|
### Level 1: 반자동
|
||||||
|
- AI가 주문 계획 생성
|
||||||
|
- 약사님 승인 후 자동 실행
|
||||||
|
- 알림: 승인 요청
|
||||||
|
|
||||||
|
### Level 2: 조건부 자동
|
||||||
|
- 신뢰도 높은 주문은 자동 실행
|
||||||
|
- 신뢰도 낮은 주문만 승인 요청
|
||||||
|
- 조건:
|
||||||
|
- 자주 주문하는 품목
|
||||||
|
- 금액 임계값 이하
|
||||||
|
- 긴급하지 않은 주문
|
||||||
|
|
||||||
|
### Level 3: 완전 자동
|
||||||
|
- 모든 주문 자동 실행
|
||||||
|
- 이상 상황만 알림
|
||||||
|
- 약사님은 대시보드로 모니터링
|
||||||
|
|
||||||
|
```python
|
||||||
|
def should_auto_execute(order_plan):
|
||||||
|
level = settings.automation_level
|
||||||
|
|
||||||
|
if level == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if level == 1:
|
||||||
|
return False # 항상 승인 필요
|
||||||
|
|
||||||
|
if level == 2:
|
||||||
|
conditions = [
|
||||||
|
order_plan['confidence'] > 0.9,
|
||||||
|
order_plan['estimated_cost'] < 100000,
|
||||||
|
order_plan['drug_code'] in trusted_drugs,
|
||||||
|
order_plan['urgency'] != 'critical'
|
||||||
|
]
|
||||||
|
return all(conditions)
|
||||||
|
|
||||||
|
if level == 3:
|
||||||
|
return not is_anomaly(order_plan)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 알림 시스템
|
||||||
|
|
||||||
|
### 알림 유형
|
||||||
|
|
||||||
|
| 유형 | 조건 | 우선순위 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 승인 요청 | 자동 실행 안 되는 주문 | 높음 |
|
||||||
|
| 주문 완료 | 자동 주문 실행됨 | 보통 |
|
||||||
|
| 한도 경고 | 90% 도달 | 높음 |
|
||||||
|
| 품절 긴급 | 재고 0, 당일 필요 | 긴급 |
|
||||||
|
| 배송 마감 | 마감 30분 전 | 높음 |
|
||||||
|
| 도매상 변경 | 품절로 다른 도매상 | 보통 |
|
||||||
|
|
||||||
|
### 알림 예시
|
||||||
|
|
||||||
|
```
|
||||||
|
📦 주문 승인 요청
|
||||||
|
|
||||||
|
약품: 콩코르정 2.5mg
|
||||||
|
현재고: 45개 (3일치)
|
||||||
|
추천 주문: 300T x 2박스
|
||||||
|
도매상: 지오영 (점심배송 11:00 마감)
|
||||||
|
예상 금액: 72,000원
|
||||||
|
|
||||||
|
[승인] [수정] [거절]
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 배송 마감 알림
|
||||||
|
|
||||||
|
지오영 오후배송 마감 30분 전!
|
||||||
|
현재 장바구니: 5품목
|
||||||
|
|
||||||
|
13:00까지 주문하지 않으면 다음 배송은 내일입니다.
|
||||||
|
|
||||||
|
[지금 주문] [나중에]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 개발 로드맵
|
||||||
|
|
||||||
|
### Phase 1: 핵심 기반 (1주차)
|
||||||
|
- [x] 도매상 API 연동 (3개)
|
||||||
|
- [x] 주문 DB 스키마
|
||||||
|
- [x] dry_run 테스트 모드
|
||||||
|
- [ ] 선주문 조회 API
|
||||||
|
- [ ] 도매상 한도 테이블
|
||||||
|
- [ ] 배송 스케줄 테이블
|
||||||
|
|
||||||
|
### Phase 2: 주문 자동화 (2주차)
|
||||||
|
- [ ] 규격 추천 API
|
||||||
|
- [ ] 한도 체크 로직
|
||||||
|
- [ ] 주문 재시도 로직
|
||||||
|
- [ ] 장바구니 동기화
|
||||||
|
|
||||||
|
### Phase 3: UI 개선 (2주차)
|
||||||
|
- [ ] 한도 대시보드
|
||||||
|
- [ ] 주문 화면 (선주문 반영)
|
||||||
|
- [ ] 배송 스케줄 표시
|
||||||
|
|
||||||
|
### Phase 4: AI 학습 (3주차)
|
||||||
|
- [ ] 피드백 루프 구현
|
||||||
|
- [ ] 주문 평가 시스템
|
||||||
|
- [ ] 패턴 학습 (규격, 도매상)
|
||||||
|
- [ ] 수요 예측 (단순 이동평균)
|
||||||
|
|
||||||
|
### Phase 5: 완전 자동화 (4주차~)
|
||||||
|
- [ ] Level 1 자동화
|
||||||
|
- [ ] 알림 시스템 연동
|
||||||
|
- [ ] Level 2 조건부 자동화
|
||||||
|
- [ ] 모니터링 대시보드
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 성공 지표 (KPI)
|
||||||
|
|
||||||
|
| 지표 | 현재 | 목표 |
|
||||||
|
|------|------|------|
|
||||||
|
| 주문 소요 시간 | 30분/일 | 0분 (자동) |
|
||||||
|
| 품절 발생률 | 5% | <1% |
|
||||||
|
| 재고 회전율 | - | +20% |
|
||||||
|
| 배송 마감 놓침 | 가끔 | 0회 |
|
||||||
|
| 주문 비용 절감 | - | 5-10% (OTC) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 참고 문서
|
||||||
|
|
||||||
|
- 어제 작성 (AI 비전/모델): `docs/AI_ERP_AUTO_ORDER_SYSTEM.md`
|
||||||
|
- 오늘 작성 (API/DB 상세): `docs/자동발주시스템_고도화_기획서_v2.md`
|
||||||
|
- 도매상 API 분석: `docs/GEOYOUNG_API_REVERSE_ENGINEERING.md`
|
||||||
|
- Rx 사용량 가이드: `docs/RX_USAGE_GEOYOUNG_GUIDE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 🐉 **용림**: 이 문서는 AI_ERP_AUTO_ORDER_SYSTEM.md(비전/AI모델)와
|
||||||
|
> 자동발주시스템_고도화_기획서_v2.md(API/DB상세)를 통합한 마스터 기획서입니다.
|
||||||
823
docs/자동발주시스템_고도화_기획서_v2.md
Normal file
823
docs/자동발주시스템_고도화_기획서_v2.md
Normal file
@ -0,0 +1,823 @@
|
|||||||
|
# 🏥 자동발주시스템 고도화 기획서 v2
|
||||||
|
|
||||||
|
> **작성일**: 2026-03-06
|
||||||
|
> **작성자**: 용림 (with 약사님)
|
||||||
|
> **상태**: 기획 검토 중
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 목차
|
||||||
|
|
||||||
|
1. [현재 구현 현황](#1-현재-구현-현황)
|
||||||
|
2. [핵심 목표](#2-핵심-목표)
|
||||||
|
3. [신규 기능 기획](#3-신규-기능-기획)
|
||||||
|
4. [API 개발 계획](#4-api-개발-계획)
|
||||||
|
5. [DB 스키마 확장](#5-db-스키마-확장)
|
||||||
|
6. [UI 개선 계획](#6-ui-개선-계획)
|
||||||
|
7. [개발 우선순위](#7-개발-우선순위)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 현재 구현 현황
|
||||||
|
|
||||||
|
### 1.1 도매상 API (✅ 완료)
|
||||||
|
|
||||||
|
| 도매상 | 재고조회 | 장바구니 | 주문 | 취소/복원 | 잔고 | 월매출 |
|
||||||
|
|--------|:--------:|:--------:|:----:|:---------:|:----:|:------:|
|
||||||
|
| **지오영** | ✅ | ✅ | ✅ 확정포함 | ✅ (삭제만) | ✅ | ✅ |
|
||||||
|
| **수인약품** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| **백제약품** | ✅ | ✅ | ✅ | ⏳ | ✅ | ✅ |
|
||||||
|
|
||||||
|
### 1.2 주문 DB 스키마 (✅ 완료)
|
||||||
|
|
||||||
|
```
|
||||||
|
orders.db
|
||||||
|
├── wholesalers # 도매상 정보
|
||||||
|
├── orders # 주문 헤더
|
||||||
|
├── order_items # 주문 품목
|
||||||
|
├── order_logs # 주문 로그
|
||||||
|
├── order_context # AI 학습용 컨텍스트
|
||||||
|
├── daily_usage # 일별 사용량
|
||||||
|
└── order_patterns # 주문 패턴 (AI용)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 통합 주문 API (✅ 완료)
|
||||||
|
|
||||||
|
| 엔드포인트 | 기능 | dry_run |
|
||||||
|
|------------|------|:-------:|
|
||||||
|
| `POST /api/order/create` | 주문 생성 (draft) | - |
|
||||||
|
| `POST /api/order/submit` | 주문 제출 | ✅ |
|
||||||
|
| `POST /api/order/quick-submit` | 빠른 주문 | ✅ |
|
||||||
|
| `GET /api/order/history` | 주문 이력 | - |
|
||||||
|
| `GET /api/order/ai/training-data` | AI 학습 데이터 | - |
|
||||||
|
|
||||||
|
### 1.4 UI 현황 (✅ 완료)
|
||||||
|
|
||||||
|
- **Rx 사용량 페이지**: 처방 기반 사용량 조회 + 주문수량 계산
|
||||||
|
- **장바구니 모달**: 선택 품목 담기 + 도매상 선택
|
||||||
|
- **도매상 잔고 모달**: 잔고 + 월매출 동시 표시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 핵심 목표
|
||||||
|
|
||||||
|
### 🎯 최종 목표
|
||||||
|
> **사용량 기반 AI 분석 통합주문 및 자동화주문 시스템**
|
||||||
|
|
||||||
|
### 2.1 핵심 시나리오
|
||||||
|
|
||||||
|
```
|
||||||
|
📅 하루 주문 흐름
|
||||||
|
|
||||||
|
[오전 10시] ─────────────────────────────────────────────
|
||||||
|
│
|
||||||
|
├─ 사용량 집계: 아세탑 500T 사용
|
||||||
|
├─ 규격 판단: 30T x 10개? 300T x 2개?
|
||||||
|
│ └─ 도매상 재고 확인 → 재고 있는 것 우선
|
||||||
|
├─ 도매상 선택: A도매상 (배송 3회/일)
|
||||||
|
│ └─ 월 한도 확인 (5000만원 중 3000만원 사용)
|
||||||
|
├─ 주문 실행: 300T x 2개 = 600T
|
||||||
|
└─ 로깅: orders.db에 기록
|
||||||
|
|
||||||
|
[오후 4시] ──────────────────────────────────────────────
|
||||||
|
│
|
||||||
|
├─ 사용량 재집계: 아세탑 910T 사용
|
||||||
|
├─ 선주문 반영: 오전 300T 주문 확인
|
||||||
|
│ └─ 남은 필요량: 910 - 300 = 610T
|
||||||
|
├─ 추가 주문: 300T x 2개 + 30T x 1개 = 630T
|
||||||
|
└─ 도매상 재선택 (한도/재고 기반)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 핵심 해결 과제
|
||||||
|
|
||||||
|
| # | 과제 | 현재 상태 | 목표 |
|
||||||
|
|---|------|----------|------|
|
||||||
|
| 1 | 선주문 반영 | ❌ 미구현 | 같은 날 선주문량 자동 차감 |
|
||||||
|
| 2 | 규격 자동 선택 | ⏳ 부분 | 재고+경제성 기반 자동 판단 |
|
||||||
|
| 3 | 도매상 한도 관리 | ❌ 미구현 | 월별 한도 설정/알림 |
|
||||||
|
| 4 | 장바구니 동기화 | ⏳ 조회만 | 양방향 동기화 |
|
||||||
|
| 5 | 실패 시 재시도 | ❌ 미구현 | A실패→B 자동 재시도 |
|
||||||
|
| 6 | 배송 스케줄 | ❌ 미구현 | 배송 횟수 고려 주문 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 신규 기능 기획
|
||||||
|
|
||||||
|
### 3.1 도매상 월별 한도 관리 🆕
|
||||||
|
|
||||||
|
**목적**: 도매상별 월 거래 한도 설정 및 자동 분배
|
||||||
|
|
||||||
|
```
|
||||||
|
예시:
|
||||||
|
- A도매상 (지오영): 월 5000만원 한도
|
||||||
|
- B도매상 (수인): 월 3000만원 한도
|
||||||
|
- C도매상 (백제): 월 2000만원 한도
|
||||||
|
|
||||||
|
[한도 도달 시 동작]
|
||||||
|
1. A도매상 한도 90% → 경고 알림
|
||||||
|
2. A도매상 한도 100% → B도매상으로 자동 전환
|
||||||
|
3. 장바구니 단계에서 미리 분류 (주문 확정 전 조정 가능)
|
||||||
|
```
|
||||||
|
|
||||||
|
**UI 요구사항**:
|
||||||
|
- 도매상별 한도 설정 화면
|
||||||
|
- 현재 사용량/잔여 한도 표시
|
||||||
|
- 한도 초과 시 경고 + 대안 도매상 제안
|
||||||
|
|
||||||
|
### 3.2 선주문 반영 시스템 🆕
|
||||||
|
|
||||||
|
**목적**: 같은 날 이미 주문한 품목 자동 차감
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 로직 예시
|
||||||
|
def calculate_order_qty(drug_code, usage_qty, current_stock):
|
||||||
|
# 오늘 "실제로" 주문 완료된 수량 조회
|
||||||
|
today_ordered = get_today_orders(drug_code) # 300T
|
||||||
|
|
||||||
|
# 필요량 = 사용량 - 현재고 - 선주문량
|
||||||
|
needed = usage_qty - current_stock - today_ordered
|
||||||
|
|
||||||
|
# 필요량이 양수일 때만 주문
|
||||||
|
if needed > 0:
|
||||||
|
return calculate_spec_qty(needed) # 규격별 수량 계산
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 핵심: 실제 주문만 카운트**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 선주문 조회 쿼리
|
||||||
|
SELECT SUM(oi.total_dose) as today_ordered
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN orders o ON oi.order_id = o.id
|
||||||
|
WHERE oi.drug_code = ?
|
||||||
|
AND o.order_date = DATE('now')
|
||||||
|
AND o.is_dry_run = 0 -- ⭐ dry_run 제외!
|
||||||
|
AND oi.status IN ('success', 'submitted') -- 실제 완료된 것만
|
||||||
|
```
|
||||||
|
|
||||||
|
**DB 스키마 수정 필요**:
|
||||||
|
- `orders` 테이블에 `is_dry_run INTEGER DEFAULT 0` 컬럼 추가
|
||||||
|
- 선주문 조회 시 `is_dry_run=0`인 것만 카운트
|
||||||
|
- `status`가 `success` 또는 `submitted`인 것만 (pending/failed 제외)
|
||||||
|
|
||||||
|
### 3.3 규격 자동 선택 로직 🆕
|
||||||
|
|
||||||
|
**목적**: 재고 기반 최적 규격 선택
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 단가 참고사항:
|
||||||
|
- 전문의약품(ETC): 보험약가 고정 → 30T든 300T든 1T당 가격 동일
|
||||||
|
- 일반의약품(OTC): 도매상/규격별 단가 차이 가능
|
||||||
|
- 비급여 약품: 도매상간 가격 비교 의미 있음
|
||||||
|
|
||||||
|
우선순위:
|
||||||
|
1. 재고 있는 규격 (품절 규격 제외)
|
||||||
|
2. 필요량과 가장 근접한 규격 (과주문 최소화)
|
||||||
|
3. 소분 선호 (30T x 10 > 300T x 1) - 유통기한/재고관리 유리
|
||||||
|
4. 사용자 선호 패턴 (AI 학습 데이터 기반)
|
||||||
|
|
||||||
|
예시:
|
||||||
|
- 필요량: 280T
|
||||||
|
- 가능 규격: 30T(재고50), 100T(품절), 300T(재고10)
|
||||||
|
- 선택: 30T x 10개 = 300T (100T 품절, 소분 선호)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 주문 실패 시 재시도 🆕
|
||||||
|
|
||||||
|
**목적**: A도매상 실패 → B도매상 자동 재시도
|
||||||
|
|
||||||
|
```
|
||||||
|
[재시도 시나리오]
|
||||||
|
|
||||||
|
시나리오 1: 재고 없음
|
||||||
|
- A도매상 재고 0 → B도매상 검색 → 재고 있으면 B로 주문
|
||||||
|
|
||||||
|
시나리오 2: 주문 오류
|
||||||
|
- A도매상 API 오류 → 3회 재시도 → 실패 시 B도매상
|
||||||
|
|
||||||
|
시나리오 3: 부분 성공
|
||||||
|
- 10개 품목 중 7개 성공, 3개 실패
|
||||||
|
- 실패한 3개 → B도매상으로 자동 재시도
|
||||||
|
|
||||||
|
[리포트]
|
||||||
|
- 최종 주문 결과 리포트 (어느 도매상에서 성공/실패)
|
||||||
|
- 알림: "A도매상 품절로 B도매상으로 주문 변경됨"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 장바구니 동기화 🆕
|
||||||
|
|
||||||
|
**목적**: 약국 시스템 ↔ 도매상 사이트 장바구니 일치
|
||||||
|
|
||||||
|
```
|
||||||
|
[동기화 흐름]
|
||||||
|
|
||||||
|
1. 약국에서 장바구니 담기
|
||||||
|
└─ 도매상 API로 add_to_cart 호출
|
||||||
|
└─ 성공 시 로컬 DB에도 기록
|
||||||
|
|
||||||
|
2. 주문 확정 전 동기화 체크
|
||||||
|
└─ 도매상 get_cart() 호출
|
||||||
|
└─ 로컬 DB와 비교
|
||||||
|
└─ 불일치 시 알림 (누군가 도매상 사이트에서 직접 수정?)
|
||||||
|
|
||||||
|
3. 주문 확정
|
||||||
|
└─ 도매상 submit_order() 호출
|
||||||
|
└─ 결과 로깅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 배송 스케줄 관리 🆕
|
||||||
|
|
||||||
|
**목적**: 도매상별 **주문 마감시간** + **배송 도착시간** 분리 관리
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 핵심: 두 가지 시간을 구분!
|
||||||
|
|
||||||
|
1. 주문 마감시간 (order_cutoff) - 언제까지 주문해야 하나
|
||||||
|
2. 배송 도착시간 (delivery_arrival) - 실제 약국에 언제 도착하나
|
||||||
|
|
||||||
|
┌──────────┬──────────┬──────────────┬──────────────┬──────────┐
|
||||||
|
│ 도매상 │ 배송 │ 주문 마감 │ 도착 예정 │ 비고 │
|
||||||
|
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
|
||||||
|
│ 지오영 │ 오전 │ 10:00 │ 11:30 │ 당일 │
|
||||||
|
│ │ 오후 │ 13:00 │ 15:00 │ 당일 │
|
||||||
|
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
|
||||||
|
│ 수인 │ 오후 │ 13:00 │ 14:30 │ 당일 │
|
||||||
|
├──────────┼──────────┼──────────────┼──────────────┼──────────┤
|
||||||
|
│ 백제 │ 익일 │ 16:00 │ 다음날 15:00 │ ⚠️ 익일 │
|
||||||
|
└──────────┴──────────┴──────────────┴──────────────┴──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI 판단 시나리오**:
|
||||||
|
```
|
||||||
|
상황: 오전 9시, "오늘 오후 2시에 필요"
|
||||||
|
|
||||||
|
→ 지오영 오전: 10시 마감 전 → 11:30 도착 (✅ 여유)
|
||||||
|
→ 지오영 오후: 13시 마감 전 → 15:00 도착 (❌ 늦음)
|
||||||
|
→ 수인: 13시 마감 전 → 14:30 도착 (✅ 가능)
|
||||||
|
→ 백제: 16시 마감 → 내일 15시 (❌ 늦음)
|
||||||
|
→ 결론: 지오영 오전배송 추천 (가장 빠름)
|
||||||
|
|
||||||
|
상황: 오전 11시, "오늘 오후 3시에 필요"
|
||||||
|
|
||||||
|
→ 지오영 오전: 10시 마감 지남 ❌
|
||||||
|
→ 지오영 오후: 13시 마감 전 → 15:00 도착 (⚠️ 딱 맞음)
|
||||||
|
→ 수인: 13시 마감 전 → 14:30 도착 (✅ 여유)
|
||||||
|
→ 결론: 수인 추천 (14:30 도착)
|
||||||
|
|
||||||
|
상황: 오후 2시, "오늘 필요"
|
||||||
|
|
||||||
|
→ 지오영: 오전/오후 마감 모두 지남 ❌
|
||||||
|
→ 수인: 13시 마감 지남 ❌
|
||||||
|
→ 백제: 16시 마감 전 → 내일 15시 도착
|
||||||
|
→ 결론: 오늘 배송 불가! 백제 익일배송만 가능 → 알림 발송
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API 개발 계획
|
||||||
|
|
||||||
|
### 4.1 신규 API 목록
|
||||||
|
|
||||||
|
#### 🔧 도매상 한도 관리 API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/wholesaler/limits` | GET | 전체 도매상 한도 조회 |
|
||||||
|
| `/api/wholesaler/limits/{id}` | GET | 특정 도매상 한도 상세 |
|
||||||
|
| `/api/wholesaler/limits/{id}` | PUT | 한도 설정/수정 |
|
||||||
|
| `/api/wholesaler/limits/{id}/usage` | GET | 현재 사용량 조회 |
|
||||||
|
| `/api/wholesaler/limits/check` | POST | 주문 전 한도 체크 |
|
||||||
|
|
||||||
|
```json
|
||||||
|
// PUT /api/wholesaler/limits/geoyoung
|
||||||
|
{
|
||||||
|
"monthly_limit": 50000000,
|
||||||
|
"warning_threshold": 0.9,
|
||||||
|
"priority": 1,
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔧 배송 스케줄 API 🆕
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/wholesaler/schedules` | GET | 전체 배송 스케줄 |
|
||||||
|
| `/api/wholesaler/schedules/{id}` | GET | 특정 도매상 스케줄 |
|
||||||
|
| `/api/wholesaler/schedules/{id}` | PUT | 스케줄 수정 |
|
||||||
|
| `/api/wholesaler/next-delivery` | GET | 다음 가능한 배송 조회 |
|
||||||
|
| `/api/wholesaler/can-deliver-by` | POST | 특정 시간까지 배송 가능 여부 |
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/wholesaler/schedules/geoyoung
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"wholesaler": "geoyoung",
|
||||||
|
"schedules": [
|
||||||
|
{
|
||||||
|
"seq": 1,
|
||||||
|
"name": "오전배송",
|
||||||
|
"order_cutoff": "08:30",
|
||||||
|
"arrival": "10:30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seq": 2,
|
||||||
|
"name": "점심배송",
|
||||||
|
"order_cutoff": "11:00",
|
||||||
|
"arrival": "13:30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seq": 3,
|
||||||
|
"name": "오후배송",
|
||||||
|
"order_cutoff": "15:00",
|
||||||
|
"arrival": "17:30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/wholesaler/can-deliver-by
|
||||||
|
// "오후 3시까지 받을 수 있는 도매상은?"
|
||||||
|
{
|
||||||
|
"need_by": "15:00",
|
||||||
|
"drug_codes": ["670400830", "654301800"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"current_time": "10:30",
|
||||||
|
"need_by": "15:00",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"wholesaler": "geoyoung",
|
||||||
|
"delivery": "점심배송",
|
||||||
|
"order_cutoff": "11:00",
|
||||||
|
"arrival": "13:30",
|
||||||
|
"status": "✅ 주문 가능 (30분 남음)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wholesaler": "sooin",
|
||||||
|
"delivery": "오전배송",
|
||||||
|
"order_cutoff": "09:00",
|
||||||
|
"arrival": "11:00",
|
||||||
|
"status": "❌ 마감됨"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wholesaler": "sooin",
|
||||||
|
"delivery": "오후배송",
|
||||||
|
"order_cutoff": "14:00",
|
||||||
|
"arrival": "17:00",
|
||||||
|
"status": "❌ 도착 늦음 (17:00)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recommendation": "geoyoung 점심배송 (11:00 마감, 13:30 도착)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔧 선주문 조회 API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/order/today` | GET | 오늘 주문 조회 |
|
||||||
|
| `/api/order/today/{drug_code}` | GET | 특정 약품 오늘 주문량 |
|
||||||
|
| `/api/order/pending` | GET | 아직 확정 안된 주문 |
|
||||||
|
|
||||||
|
```json
|
||||||
|
// GET /api/order/today/670400830
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"drug_code": "670400830",
|
||||||
|
"today_ordered_qty": 300,
|
||||||
|
"orders": [
|
||||||
|
{
|
||||||
|
"order_no": "ORD-20260306-001",
|
||||||
|
"wholesaler": "geoyoung",
|
||||||
|
"specification": "300T",
|
||||||
|
"qty": 1,
|
||||||
|
"status": "submitted",
|
||||||
|
"ordered_at": "2026-03-06T10:30:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔧 규격 추천 API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/order/recommend-spec` | POST | 규격 추천 (단건) |
|
||||||
|
| `/api/order/recommend-specs` | POST | 규격 추천 (복수) |
|
||||||
|
| `/api/order/optimize` | POST | 전체 주문 최적화 |
|
||||||
|
|
||||||
|
```json
|
||||||
|
// POST /api/order/recommend-spec
|
||||||
|
{
|
||||||
|
"drug_code": "670400830",
|
||||||
|
"needed_qty": 280,
|
||||||
|
"prefer_wholesaler": "geoyoung"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"drug_type": "ETC", // ETC: 보험약가 고정, OTC: 단가 비교 가능
|
||||||
|
"recommendations": [
|
||||||
|
{
|
||||||
|
"wholesaler": "geoyoung",
|
||||||
|
"spec": "30T",
|
||||||
|
"qty": 10,
|
||||||
|
"total_dose": 300,
|
||||||
|
"stock": 50,
|
||||||
|
"unit_price": 1200,
|
||||||
|
"total_price": 12000,
|
||||||
|
"reason": "재고 충분, 소분 선호"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wholesaler": "sooin",
|
||||||
|
"spec": "300T",
|
||||||
|
"qty": 1,
|
||||||
|
"total_dose": 300,
|
||||||
|
"stock": 5,
|
||||||
|
"unit_price": 12000,
|
||||||
|
"total_price": 12000,
|
||||||
|
"reason": "재고 있음 (ETC 단가 동일)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔧 주문 재시도 API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/order/retry` | POST | 실패 품목 재시도 |
|
||||||
|
| `/api/order/fallback` | POST | 다른 도매상으로 재주문 |
|
||||||
|
| `/api/order/redistribute` | POST | 한도 기반 재분배 |
|
||||||
|
|
||||||
|
```json
|
||||||
|
// POST /api/order/retry
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"item_ids": [456, 457], // 실패한 품목
|
||||||
|
"fallback_wholesaler": "sooin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔧 장바구니 동기화 API
|
||||||
|
|
||||||
|
| 엔드포인트 | 메서드 | 기능 |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `/api/cart/sync` | POST | 전체 동기화 |
|
||||||
|
| `/api/cart/compare` | GET | 로컬 vs 도매상 비교 |
|
||||||
|
| `/api/cart/resolve` | POST | 불일치 해결 |
|
||||||
|
|
||||||
|
### 4.2 기존 API 확장
|
||||||
|
|
||||||
|
#### 📝 `/api/order/create` 확장
|
||||||
|
|
||||||
|
```json
|
||||||
|
// 기존
|
||||||
|
{
|
||||||
|
"wholesaler_id": "geoyoung",
|
||||||
|
"items": [...]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 확장
|
||||||
|
{
|
||||||
|
"wholesaler_id": "geoyoung",
|
||||||
|
"items": [...],
|
||||||
|
"options": {
|
||||||
|
"check_prior_orders": true, // 선주문 반영
|
||||||
|
"auto_spec_select": true, // 규격 자동 선택
|
||||||
|
"respect_limits": true, // 한도 준수
|
||||||
|
"allow_fallback": true, // 실패 시 다른 도매상
|
||||||
|
"fallback_order": ["sooin", "baekje"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📝 `/api/order/submit` 확장
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"dry_run": false,
|
||||||
|
"options": {
|
||||||
|
"retry_on_fail": 3, // 실패 시 재시도 횟수
|
||||||
|
"fallback_enabled": true,
|
||||||
|
"notify_on_fallback": true // 도매상 변경 시 알림
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. DB 스키마 확장
|
||||||
|
|
||||||
|
### 5.1 신규 테이블
|
||||||
|
|
||||||
|
#### `wholesaler_limits` - 도매상 한도 관리
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE wholesaler_limits (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
wholesaler_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- 한도 설정
|
||||||
|
monthly_limit INTEGER DEFAULT 0, -- 월 한도 (원)
|
||||||
|
warning_threshold REAL DEFAULT 0.9, -- 경고 임계값 (90%)
|
||||||
|
|
||||||
|
-- 우선순위
|
||||||
|
priority INTEGER DEFAULT 1, -- 1이 최우선
|
||||||
|
|
||||||
|
-- 상태
|
||||||
|
is_active INTEGER DEFAULT 1,
|
||||||
|
|
||||||
|
-- 메타
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (wholesaler_id) REFERENCES wholesalers(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `delivery_schedules` - 배송 스케줄 🆕
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE delivery_schedules (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
wholesaler_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- 배송 회차
|
||||||
|
delivery_seq INTEGER NOT NULL, -- 1, 2, 3...
|
||||||
|
delivery_name TEXT, -- '오전배송', '오후배송', '익일배송'
|
||||||
|
|
||||||
|
-- ⭐ 주문 마감시간
|
||||||
|
order_cutoff_time TEXT NOT NULL, -- 'HH:MM' (예: '10:00')
|
||||||
|
|
||||||
|
-- ⭐ 배송 도착
|
||||||
|
delivery_days_offset INTEGER DEFAULT 0, -- 0=당일, 1=익일, 2=2일후...
|
||||||
|
delivery_arrival_time TEXT NOT NULL, -- 'HH:MM' (예: '11:30')
|
||||||
|
|
||||||
|
-- 요일별 운영 (NULL=매일)
|
||||||
|
weekdays TEXT, -- JSON [1,2,3,4,5] (평일만)
|
||||||
|
|
||||||
|
-- 상태
|
||||||
|
is_active INTEGER DEFAULT 1,
|
||||||
|
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (wholesaler_id) REFERENCES wholesalers(id),
|
||||||
|
UNIQUE(wholesaler_id, delivery_seq)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 실제 배송 스케줄 (2026-03-06 확인)
|
||||||
|
INSERT INTO delivery_schedules
|
||||||
|
(wholesaler_id, delivery_seq, delivery_name, order_cutoff_time, delivery_days_offset, delivery_arrival_time)
|
||||||
|
VALUES
|
||||||
|
-- 지오영 (2배송, 당일)
|
||||||
|
('geoyoung', 1, '오전배송', '10:00', 0, '11:30'),
|
||||||
|
('geoyoung', 2, '오후배송', '13:00', 0, '15:00'),
|
||||||
|
-- 수인 (1배송, 당일)
|
||||||
|
('sooin', 1, '오후배송', '13:00', 0, '14:30'),
|
||||||
|
-- 백제 (1배송, 익일!) ⚠️
|
||||||
|
('baekje', 1, '익일배송', '16:00', 1, '15:00'); -- days_offset=1 → 다음날
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `wholesaler_monthly_usage` - 월별 사용량 추적
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE wholesaler_monthly_usage (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
wholesaler_id TEXT NOT NULL,
|
||||||
|
year_month TEXT NOT NULL, -- 'YYYY-MM'
|
||||||
|
|
||||||
|
-- 집계
|
||||||
|
total_orders INTEGER DEFAULT 0, -- 주문 건수
|
||||||
|
total_items INTEGER DEFAULT 0, -- 주문 품목 수
|
||||||
|
total_amount INTEGER DEFAULT 0, -- 총 주문 금액
|
||||||
|
|
||||||
|
-- 상태별 집계
|
||||||
|
success_amount INTEGER DEFAULT 0,
|
||||||
|
failed_amount INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
-- 메타
|
||||||
|
last_updated TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE(wholesaler_id, year_month)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `order_fallback_log` - 재시도 로그
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE order_fallback_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
order_item_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- 원래 도매상
|
||||||
|
original_wholesaler TEXT NOT NULL,
|
||||||
|
original_error TEXT, -- 실패 사유
|
||||||
|
|
||||||
|
-- 재시도 도매상
|
||||||
|
fallback_wholesaler TEXT NOT NULL,
|
||||||
|
fallback_result TEXT, -- 'success', 'failed'
|
||||||
|
fallback_message TEXT,
|
||||||
|
|
||||||
|
-- 메타
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (order_item_id) REFERENCES order_items(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `cart_sync_log` - 장바구니 동기화 로그
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE cart_sync_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
wholesaler_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- 동기화 정보
|
||||||
|
sync_type TEXT, -- 'full', 'partial', 'compare'
|
||||||
|
local_items INTEGER,
|
||||||
|
remote_items INTEGER,
|
||||||
|
matched INTEGER,
|
||||||
|
mismatched INTEGER,
|
||||||
|
|
||||||
|
-- 상세
|
||||||
|
detail_json TEXT,
|
||||||
|
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 기존 테이블 확장
|
||||||
|
|
||||||
|
#### `orders` 확장 ⭐ 중요
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- dry_run 구분 (선주문 조회 시 제외용)
|
||||||
|
ALTER TABLE orders ADD COLUMN is_dry_run INTEGER DEFAULT 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `order_items` 확장
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE order_items ADD COLUMN fallback_from_wholesaler TEXT;
|
||||||
|
ALTER TABLE order_items ADD COLUMN fallback_reason TEXT;
|
||||||
|
ALTER TABLE order_items ADD COLUMN prior_order_qty INTEGER DEFAULT 0; -- 선주문량
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `order_context` 확장
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE order_context ADD COLUMN limit_check_result TEXT;
|
||||||
|
ALTER TABLE order_context ADD COLUMN recommended_by TEXT; -- 'user', 'ai', 'system'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. UI 개선 계획
|
||||||
|
|
||||||
|
### 6.1 도매상 한도 대시보드 🆕
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 💰 도매상 한도 현황 (2026년 3월) │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 🏢 지오영 │
|
||||||
|
│ ████████████████████░░░░░ 35,124,164 / 50,000,000 │
|
||||||
|
│ 70.2% 사용 | 남은 한도: 14,875,836원 │
|
||||||
|
│ [배송 3회] 09:00, 13:00, 17:00 │
|
||||||
|
│ │
|
||||||
|
│ 🏢 수인약품 │
|
||||||
|
│ ██████████████░░░░░░░░░░ 14,293,001 / 30,000,000 │
|
||||||
|
│ 47.6% 사용 | 남은 한도: 15,706,999원 │
|
||||||
|
│ [배송 2회] 09:00, 17:00 │
|
||||||
|
│ │
|
||||||
|
│ 🏢 백제약품 │
|
||||||
|
│ ███████████████████░░░░░ 14,563,978 / 20,000,000 │
|
||||||
|
│ 72.8% 사용 | 남은 한도: 5,436,022원 ⚠️ 주의 │
|
||||||
|
│ [배송 2회] 09:00, 17:00 │
|
||||||
|
│ │
|
||||||
|
│ [⚙️ 한도 설정] [📊 상세 리포트] │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 주문 화면 개선
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 📦 주문 생성 - 2026-03-06 오후 배치 │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ [선주문 반영 ✓] 오늘 오전 주문: 15품목, 2,340,000원 │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 품목 │ 필요량 │ 선주문 │ 추가주문 │ │
|
||||||
|
│ ├─────────────────────────────────────────────────┤ │
|
||||||
|
│ │ 아세탑정 │ 910T │ 300T │ 610T │ │
|
||||||
|
│ │ └ 추천: 300T x 2 (지오영) │ │
|
||||||
|
│ │ └ 대안: 30T x 21 (수인, 재고 충분) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 레바미피드정 │ 500T │ 0T │ 500T │ │
|
||||||
|
│ │ └ 추천: 30T x 17 (지오영) ⚠️ 품절위험 │ │
|
||||||
|
│ │ └ 대안: 100T x 5 (백제) │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 📊 도매상 분배 미리보기 │
|
||||||
|
│ - 지오영: 8품목 (1,200,000원) [한도 여유 ✓] │
|
||||||
|
│ - 수인: 3품목 (450,000원) │
|
||||||
|
│ - 백제: 2품목 (350,000원) [한도 주의 ⚠️] │
|
||||||
|
│ │
|
||||||
|
│ [🔄 재분배] [✅ 주문 확정] [💾 장바구니 저장] │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 알림/노티피케이션
|
||||||
|
|
||||||
|
```
|
||||||
|
[알림 유형]
|
||||||
|
|
||||||
|
📢 한도 알림
|
||||||
|
- "지오영 한도 90% 도달 (4,500만원/5,000만원)"
|
||||||
|
- "백제약품 한도 초과! 신규 주문 불가"
|
||||||
|
|
||||||
|
📢 도매상 변경 알림
|
||||||
|
- "아세탑정: 지오영 품절 → 수인약품으로 변경됨"
|
||||||
|
|
||||||
|
📢 주문 결과 알림
|
||||||
|
- "오후 주문 완료: 15품목 중 14개 성공, 1개 재시도 중"
|
||||||
|
|
||||||
|
📢 배송 알림
|
||||||
|
- "지오영 점심 배송 마감 30분 전 (12:30까지)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 개발 우선순위
|
||||||
|
|
||||||
|
### Phase 1: 핵심 기능 (1주차)
|
||||||
|
|
||||||
|
| 순위 | 기능 | 예상 공수 | 의존성 |
|
||||||
|
|:----:|------|:--------:|--------|
|
||||||
|
| 1 | 선주문 조회 API | 0.5일 | - |
|
||||||
|
| 2 | 도매상 한도 테이블 + API | 1일 | - |
|
||||||
|
| 3 | 규격 추천 API | 1일 | 선주문 API |
|
||||||
|
| 4 | 한도 체크 로직 | 0.5일 | 한도 테이블 |
|
||||||
|
|
||||||
|
### Phase 2: 자동화 (2주차)
|
||||||
|
|
||||||
|
| 순위 | 기능 | 예상 공수 | 의존성 |
|
||||||
|
|:----:|------|:--------:|--------|
|
||||||
|
| 5 | 주문 재시도 로직 | 1일 | Phase 1 |
|
||||||
|
| 6 | 장바구니 동기화 | 1일 | - |
|
||||||
|
| 7 | UI: 한도 대시보드 | 1일 | 한도 API |
|
||||||
|
| 8 | UI: 주문 화면 개선 | 1일 | 규격 추천 API |
|
||||||
|
|
||||||
|
### Phase 3: 고도화 (3주차)
|
||||||
|
|
||||||
|
| 순위 | 기능 | 예상 공수 | 의존성 |
|
||||||
|
|:----:|------|:--------:|--------|
|
||||||
|
| 9 | 배송 스케줄 관리 | 1일 | - |
|
||||||
|
| 10 | 알림 시스템 | 1일 | - |
|
||||||
|
| 11 | AI 학습 파이프라인 | 2일 | Phase 1-2 데이터 |
|
||||||
|
| 12 | 자동 스케줄링 | 1일 | 배송 스케줄 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 검토 요청 사항
|
||||||
|
|
||||||
|
### 1. 한도 기본값
|
||||||
|
도매상별 초기 한도 얼마로 설정?
|
||||||
|
- 지오영: ____만원
|
||||||
|
- 수인: ____만원
|
||||||
|
- 백제: ____만원
|
||||||
|
|
||||||
|
### 2. 배송 스케줄 ✅ 확인 완료
|
||||||
|
|
||||||
|
| 도매상 | 배송 | 주문 마감 | 도착 예정 | 비고 |
|
||||||
|
|--------|------|----------|----------|------|
|
||||||
|
| **지오영** | 오전 | 10:00 | 11:30 | 당일 |
|
||||||
|
| | 오후 | 13:00 | 15:00 | 당일 |
|
||||||
|
| **수인** | 오후 | 13:00 | 14:30 | 당일 |
|
||||||
|
| **백제** | 익일 | 16:00 | 다음날 15:00 | ⚠️ 익일배송 |
|
||||||
|
|
||||||
|
### 3. 알림 채널
|
||||||
|
어디로 받으실 건가요?
|
||||||
|
- [ ] 텔레그램
|
||||||
|
- [ ] 카카오톡
|
||||||
|
- [ ] 웹 알림
|
||||||
|
- [ ] 기타: ____
|
||||||
|
|
||||||
|
### 4. 재시도 정책
|
||||||
|
- A도매상 실패 시 바로 B로?
|
||||||
|
- 몇 번까지 재시도?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 🐉 **용림 메모**: 기획서 검토 후 Phase 1부터 순차 개발 예정.
|
||||||
|
> 약사님 확인 후 수정사항 반영하겠습니다!
|
||||||
Loading…
Reference in New Issue
Block a user