feat(order): 수인약품 선별 주문 및 rx-usage 주문 전송 개선

- order_api.py: 수인 주문 시 submit_order_selective() 사용 (기존 품목 보존)
- admin_rx_usage.html: cart_only=false로 변경 (장바구니+주문확정)
- 버튼 텍스트 변경: 장바구니 담기 → 주문 전송
This commit is contained in:
thug0bin 2026-03-06 21:52:40 +09:00
parent 5519f5ae62
commit be95f8b3d1
2 changed files with 595 additions and 68 deletions

View File

@ -152,14 +152,21 @@ def api_submit_order():
return jsonify(result)
def submit_geoyoung_order(order: dict, dry_run: bool) -> dict:
"""지오영 주문 제출"""
def submit_geoyoung_order(order: dict, dry_run: bool, cart_only: bool = True) -> dict:
"""
지오영 주문 제출
Args:
order: 주문 정보
dry_run: True=시뮬레이션만, False=실제 주문
cart_only: True=장바구니만, False=주문 확정까지
"""
order_id = order['id']
items = order['items']
# 상태 업데이트
update_order_status(order_id, 'pending',
f'주문 제출 시작 (dry_run={dry_run})')
f'주문 제출 시작 (dry_run={dry_run}, cart_only={cart_only})')
results = []
success_count = 0
@ -286,20 +293,22 @@ def submit_geoyoung_order(order: dict, dry_run: bool) -> dict:
spec = item.get('specification', '')
try:
# 지오영 주문 실행 (빠른 API - 장바구니+확정)
# 지오영 주문 실행
# cart_only=True: 장바구니만 (auto_confirm=False)
# cart_only=False: 주문 확정까지 (auto_confirm=True)
result = geo_session.full_order(
kd_code=kd_code,
quantity=order_qty,
specification=spec if spec else None,
check_stock=True,
auto_confirm=True,
auto_confirm=not cart_only, # cart_only면 확정 안함
memo=f"자동주문 - {item.get('product_name', '')}"
)
if result.get('success'):
status = 'success'
result_code = 'OK'
result_message = result.get('message', '주문 완료')
result_code = 'CART_ADDED' if cart_only else 'OK'
result_message = '장바구니 추가 완료' if cart_only else result.get('message', '주문 완료')
success_count += 1
else:
status = 'failed'
@ -338,6 +347,19 @@ def submit_geoyoung_order(order: dict, dry_run: bool) -> dict:
})
# 주문 상태 업데이트
if cart_only:
# 장바구니만 담은 경우
if failed_count == 0:
update_order_status(order_id, 'pending',
f'지오영 장바구니 추가 완료: {success_count}개 (사이트에서 확정 필요)')
elif success_count == 0:
update_order_status(order_id, 'failed',
f'장바구니 추가 실패: {failed_count}개 품목')
else:
update_order_status(order_id, 'partial',
f'부분 성공: {success_count}개 장바구니, {failed_count}개 실패')
else:
# 실제 주문까지 한 경우
if failed_count == 0:
update_order_status(order_id, 'submitted',
f'주문 제출 완료: {success_count}개 품목')
@ -351,12 +373,15 @@ def submit_geoyoung_order(order: dict, dry_run: bool) -> dict:
return {
'success': True,
'dry_run': dry_run,
'cart_only': cart_only,
'order_id': order_id,
'order_no': order['order_no'],
'wholesaler': 'geoyoung',
'total_items': len(items),
'success_count': success_count,
'failed_count': failed_count,
'results': results
'results': results,
'note': '지오영 장바구니에 담김. 지오영 사이트에서 최종 확정 필요.' if cart_only else None
}
except Exception as e:
@ -409,9 +434,10 @@ def api_quick_submit():
POST /api/order/quick-submit
{
"wholesaler_id": "geoyoung" | "sooin",
"wholesaler_id": "geoyoung" | "sooin" | "baekje",
"items": [...],
"dry_run": true
"dry_run": true,
"cart_only": true // true=장바구니만, false=실제 주문까지
}
"""
data = request.get_json()
@ -438,13 +464,14 @@ def api_quick_submit():
# 3. 주문 제출
dry_run = data.get('dry_run', True)
cart_only = data.get('cart_only', True) # 기본값: 장바구니만
if order['wholesaler_id'] == 'geoyoung':
submit_result = submit_geoyoung_order(order, dry_run)
submit_result = submit_geoyoung_order(order, dry_run, cart_only=cart_only)
elif order['wholesaler_id'] == 'sooin':
submit_result = submit_sooin_order(order, dry_run)
submit_result = submit_sooin_order(order, dry_run, cart_only=cart_only)
elif order['wholesaler_id'] == 'baekje':
submit_result = submit_baekje_order(order, dry_run)
submit_result = submit_baekje_order(order, dry_run, cart_only=cart_only)
else:
submit_result = {'success': False, 'error': f"Wholesaler {order['wholesaler_id']} not supported"}
@ -453,14 +480,21 @@ def api_quick_submit():
return jsonify(submit_result)
def submit_sooin_order(order: dict, dry_run: bool) -> dict:
"""수인약품 주문 제출"""
def submit_sooin_order(order: dict, dry_run: bool, cart_only: bool = True) -> dict:
"""
수인약품 주문 제출
Args:
order: 주문 정보
dry_run: True=시뮬레이션만, False=실제 주문
cart_only: True=장바구니만, False=주문 확정까지
"""
order_id = order['id']
items = order['items']
# 상태 업데이트
update_order_status(order_id, 'pending',
f'수인 주문 시작 (dry_run={dry_run})')
f'수인 주문 시작 (dry_run={dry_run}, cart_only={cart_only})')
results = []
success_count = 0
@ -572,23 +606,15 @@ def submit_sooin_order(order: dict, dry_run: bool) -> dict:
kd_code = item.get('kd_code') or item.get('drug_code')
order_qty = item['order_qty']
spec = item.get('specification', '')
internal_code = item.get('internal_code')
try:
# internal_code가 없으면 검색해서 찾기
if not internal_code:
search_result = sooin_session.search_products(kd_code)
if search_result.get('success'):
for sooin_item in search_result.get('items', []):
if spec in sooin_item.get('spec', '') or sooin_item.get('spec', '') in spec:
internal_code = sooin_item.get('internal_code')
break
if not internal_code:
raise ValueError(f"내부 코드를 찾을 수 없음: {kd_code} {spec}")
# 장바구니 추가
cart_result = sooin_session.add_to_cart(internal_code, order_qty)
# quick_order 사용 (검색 → 가격/재고 정보 포함하여 장바구니 추가)
cart_result = sooin_session.quick_order(
kd_code=kd_code,
quantity=order_qty,
spec=spec if spec else None,
check_stock=False # 재고 체크는 이미 테스트에서 했으므로 스킵
)
if cart_result.get('success'):
status = 'success'
@ -609,6 +635,9 @@ def submit_sooin_order(order: dict, dry_run: bool) -> dict:
update_item_result(item['id'], status, result_code, result_message)
# quick_order 결과에서 internal_code 가져오기
internal_code = cart_result.get('product', {}).get('internal_code') if cart_result.get('success') else None
save_order_context(item['id'], {
'drug_code': item['drug_code'],
'product_name': item['product_name'],
@ -629,20 +658,57 @@ def submit_sooin_order(order: dict, dry_run: bool) -> dict:
'order_qty': order_qty,
'status': status,
'result_code': result_code,
'result_message': result_message
'result_message': result_message,
'internal_code': internal_code # 선별 주문용
})
# 주문 확정은 별도로 (장바구니에 담기만 한 상태)
if success_count > 0:
# cart_only=False면 주문 확정까지 진행 (선별 주문!)
if not cart_only and success_count > 0:
try:
# 이번에 담은 품목의 internal_code만 수집
ordered_codes = [r['internal_code'] for r in results
if r['status'] == 'success' and r.get('internal_code')]
if ordered_codes:
# 선별 주문: 기존 품목은 건드리지 않고, 이번에 담은 것만 주문
confirm_result = sooin_session.submit_order_selective(ordered_codes)
if confirm_result.get('success'):
restored_info = f", 기존 {confirm_result.get('restored_count', 0)}개 복원" if confirm_result.get('restored_count', 0) > 0 else ""
update_order_status(order_id, 'submitted',
f'수인 주문 확정 완료: {success_count}{restored_info}')
# 결과 메시지 업데이트
for r in results:
if r['status'] == 'success':
r['result_code'] = 'OK'
r['result_message'] = '주문 확정 완료'
else:
update_order_status(order_id, 'partial',
f'수인 장바구니 담김, 확정 실패: {confirm_result.get("error", "알 수 없는 오류")}')
else:
update_order_status(order_id, 'partial',
f'수인 장바구니 담김, internal_code 없음')
except Exception as e:
logger.error(f"수인 주문 확정 오류: {e}")
update_order_status(order_id, 'partial',
f'수인 장바구니 담김, 확정 중 오류: {str(e)}')
elif success_count > 0:
update_order_status(order_id, 'pending',
f'수인 장바구니 추가 완료: {success_count}개 (확정 필요)')
else:
update_order_status(order_id, 'failed',
f'수인 주문 실패: {failed_count}')
# 응답 생성
if cart_only:
note = '수인약품 장바구니에 담김. 사이트에서 최종 확정 필요.'
else:
note = None
return {
'success': True,
'dry_run': dry_run,
'cart_only': cart_only,
'order_id': order_id,
'order_no': order['order_no'],
'wholesaler': 'sooin',
@ -650,7 +716,7 @@ def submit_sooin_order(order: dict, dry_run: bool) -> dict:
'success_count': success_count,
'failed_count': failed_count,
'results': results,
'note': '실제 주문 시 장바구니에 담김. 수인약품 사이트에서 최종 확정 필요.' if not dry_run else None
'note': note if not dry_run else None
}
except Exception as e:
@ -663,14 +729,21 @@ def submit_sooin_order(order: dict, dry_run: bool) -> dict:
}
def submit_baekje_order(order: dict, dry_run: bool) -> dict:
"""백제약품 주문 제출"""
def submit_baekje_order(order: dict, dry_run: bool, cart_only: bool = True) -> dict:
"""
백제약품 주문 제출
Args:
order: 주문 정보
dry_run: True=시뮬레이션만, False=실제 주문
cart_only: True=장바구니만, False=주문 확정까지
"""
order_id = order['id']
items = order['items']
# 상태 업데이트
update_order_status(order_id, 'pending',
f'백제약품 주문 시작 (dry_run={dry_run})')
f'백제약품 주문 시작 (dry_run={dry_run}, cart_only={cart_only})')
results = []
success_count = 0
@ -828,17 +901,42 @@ def submit_baekje_order(order: dict, dry_run: bool) -> dict:
'result_message': result_message
})
# 상태 업데이트
if success_count > 0:
# cart_only=False면 주문 확정까지 진행
if not cart_only and success_count > 0:
try:
confirm_result = baekje_session.submit_order()
if confirm_result.get('success'):
update_order_status(order_id, 'submitted',
f'백제 주문 확정 완료: {success_count}')
# 결과 메시지 업데이트
for r in results:
if r['status'] == 'success':
r['result_code'] = 'OK'
r['result_message'] = '주문 확정 완료'
else:
update_order_status(order_id, 'partial',
f'백제 장바구니 담김, 확정 실패: {confirm_result.get("error", "알 수 없는 오류")}')
except Exception as e:
logger.error(f"백제 주문 확정 오류: {e}")
update_order_status(order_id, 'partial',
f'백제 장바구니 담김, 확정 중 오류: {str(e)}')
elif success_count > 0:
update_order_status(order_id, 'pending',
f'백제 장바구니 추가 완료: {success_count}개 (확정 필요)')
else:
update_order_status(order_id, 'failed',
f'백제 주문 실패: {failed_count}')
# 응답 생성
if cart_only:
note = '백제약품 장바구니에 담김. 백제몰(ibjp.co.kr)에서 최종 확정 필요.'
else:
note = None
return {
'success': True,
'dry_run': dry_run,
'cart_only': cart_only,
'order_id': order_id,
'order_no': order['order_no'],
'wholesaler': 'baekje',
@ -846,7 +944,7 @@ def submit_baekje_order(order: dict, dry_run: bool) -> dict:
'success_count': success_count,
'failed_count': failed_count,
'results': results,
'note': '실제 주문 시 장바구니에 담김. 백제몰(ibjp.co.kr)에서 최종 확정 필요.' if not dry_run else None
'note': note if not dry_run else None
}
except Exception as e:

View File

@ -528,6 +528,25 @@
color: var(--text-muted);
margin-top: 4px;
}
.cart-item-actions {
display: flex;
gap: 6px;
align-items: center;
}
.cart-item-order {
background: rgba(16, 185, 129, 0.2);
border: none;
color: var(--accent-emerald);
padding: 4px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.cart-item-order:hover {
background: rgba(16, 185, 129, 0.4);
}
.cart-item-remove {
background: rgba(244, 63, 94, 0.2);
border: none;
@ -1049,8 +1068,11 @@
<div class="cart-item-name">${escapeHtml(item.product_name)}</div>
<div class="cart-item-qty">${item.supplier || '-'} · ${item.qty}개</div>
</div>
<div class="cart-item-actions">
<button class="cart-item-order" onclick="orderSingleItem('${item.drug_code}')">📤주문</button>
<button class="cart-item-remove" onclick="removeFromCart('${item.drug_code}')"></button>
</div>
</div>
`).join('');
}
}
@ -1131,28 +1153,256 @@
}
}
// 다중 도매상 선택을 위한 전역 변수
let pendingWholesalerItems = {};
let pendingOtherItems = [];
function openWholesalerSelectModal(itemsByWholesaler, otherItems) {
let msg = `장바구니에 여러 도매상 품목이 있습니다.\n\n`;
pendingWholesalerItems = itemsByWholesaler;
pendingOtherItems = otherItems;
const modal = document.getElementById('multiWholesalerModal');
const body = document.getElementById('multiWholesalerBody');
const wsIds = Object.keys(itemsByWholesaler);
let html = `
<div class="multi-ws-summary">
<p style="margin-bottom:16px;color:var(--text-secondary);">
장바구니에 <b>${wsIds.length}개 도매상</b>의 품목이 있습니다.
</p>
`;
// 각 도매상별 품목 표시
wsIds.forEach(wsId => {
const ws = WHOLESALERS[wsId];
msg += `${ws.icon} ${ws.name}: ${itemsByWholesaler[wsId].length}개\n`;
const items = itemsByWholesaler[wsId];
html += `
<div class="multi-ws-card ${wsId}">
<div class="multi-ws-header">
<span class="multi-ws-icon">${ws.icon}</span>
<span class="multi-ws-name">${ws.name}</span>
<span class="multi-ws-count">${items.length}개 품목</span>
<label class="multi-ws-checkbox">
<input type="checkbox" id="ws_check_${wsId}" checked>
<span>포함</span>
</label>
</div>
<div class="multi-ws-items">
${items.slice(0, 3).map(item => `
<div class="multi-ws-item">· ${item.product_name} (${item.qty}개)</div>
`).join('')}
${items.length > 3 ? `<div class="multi-ws-item more">... 외 ${items.length - 3}개</div>` : ''}
</div>
</div>
`;
});
if (otherItems.length > 0) {
msg += `📋 기타: ${otherItems.length}개\n`;
html += `
<div class="multi-ws-card other">
<div class="multi-ws-header">
<span class="multi-ws-icon">📋</span>
<span class="multi-ws-name">기타 (API 미지원)</span>
<span class="multi-ws-count">${otherItems.length}개 품목</span>
</div>
<div class="multi-ws-items">
<div class="multi-ws-item" style="color:var(--text-muted);">클립보드 복사로 처리됩니다</div>
</div>
</div>
`;
}
msg += `\n어느 도매상부터 주문하시겠습니까?`;
msg += `\n\n[확인] = ${WHOLESALERS[wsIds[0]].name} 먼저`;
msg += `\n[취소] = ${WHOLESALERS[wsIds[1]].name} 먼저`;
html += `</div>`;
if (confirm(msg)) {
openOrderConfirmModal(wsIds[0], itemsByWholesaler[wsIds[0]]);
body.innerHTML = html;
modal.classList.add('show');
}
function closeMultiWholesalerModal() {
document.getElementById('multiWholesalerModal').classList.remove('show');
}
// 선택된 도매상 전체 일괄 처리
async function executeAllWholesalers(dryRun = false) {
const wsIds = Object.keys(pendingWholesalerItems);
// 체크된 도매상만 필터
const selectedWsIds = wsIds.filter(wsId => {
const checkbox = document.getElementById(`ws_check_${wsId}`);
return checkbox && checkbox.checked;
});
if (selectedWsIds.length === 0) {
showToast('선택된 도매상이 없습니다', 'error');
return;
}
// 버튼 비활성화
const btnTest = document.getElementById('btnMultiTest');
const btnReal = document.getElementById('btnMultiReal');
btnTest.disabled = true;
btnReal.disabled = true;
btnReal.textContent = '처리 중...';
const allResults = [];
let totalSuccess = 0;
let totalFailed = 0;
// 각 도매상 순차 처리
for (const wsId of selectedWsIds) {
const ws = WHOLESALERS[wsId];
const items = pendingWholesalerItems[wsId];
showToast(`${ws.icon} ${ws.name} 처리 중... (${selectedWsIds.indexOf(wsId) + 1}/${selectedWsIds.length})`, 'info');
try {
const payload = {
wholesaler_id: wsId,
items: items.map(item => ({
drug_code: item.drug_code,
kd_code: item.geoyoung_code || item.sooin_code || item.drug_code,
internal_code: item.internal_code,
product_name: item.product_name,
manufacturer: item.supplier,
specification: item.specification || '',
order_qty: item.qty,
usage_qty: item.usage_qty || 0,
current_stock: item.current_stock || 0
})),
reference_period: `${document.getElementById('startDate').value}~${document.getElementById('endDate').value}`,
dry_run: dryRun,
cart_only: false // 장바구니 + 주문 확정까지
};
const response = await fetch('/api/order/quick-submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(180000) // 주문 확정까지 3분
});
const result = await response.json();
result.wholesaler_id = wsId;
allResults.push(result);
if (result.success) {
totalSuccess += result.success_count || 0;
totalFailed += result.failed_count || 0;
} else {
openOrderConfirmModal(wsIds[1], itemsByWholesaler[wsIds[1]]);
totalFailed += items.length;
}
} catch (err) {
allResults.push({
wholesaler_id: wsId,
success: false,
error: err.message,
success_count: 0,
failed_count: items.length
});
totalFailed += items.length;
}
}
// 기타 품목 클립보드 처리
if (pendingOtherItems.length > 0) {
submitOrderClipboardItems(pendingOtherItems);
}
closeMultiWholesalerModal();
showMultiOrderResultModal(allResults, totalSuccess, totalFailed, dryRun);
// 버튼 복원
btnTest.disabled = false;
btnReal.disabled = false;
btnReal.textContent = '📤 전체 주문 전송';
}
// 특정 품목만 클립보드 복사
function submitOrderClipboardItems(items) {
let orderText = `📋 기타 품목 (API 미지원)\n━━━━━━━━━━━━━━━━━━\n`;
items.forEach((item, i) => {
orderText += `${i+1}. ${item.product_name} - ${item.qty}개\n`;
});
navigator.clipboard.writeText(orderText);
}
// 다중 도매상 결과 모달
function showMultiOrderResultModal(results, totalSuccess, totalFailed, isDryRun) {
const modal = document.getElementById('orderResultModal');
const content = document.getElementById('orderResultContent');
const header = modal.querySelector('.order-modal-header h3');
const headerDiv = modal.querySelector('.order-modal-header');
headerDiv.style.background = 'linear-gradient(135deg, #059669, #10b981)';
header.innerHTML = '📋 전체 주문 결과';
const statusEmoji = totalFailed === 0 ? '✅' : totalSuccess === 0 ? '❌' : '⚠️';
const modeText = isDryRun ? '[테스트]' : '';
let html = `
<div class="result-header ${totalFailed === 0 ? 'success' : 'partial'}">
<span class="result-emoji">${statusEmoji}</span>
<span class="result-title">${modeText} ${results.length}개 도매상 처리 완료</span>
</div>
<div class="result-summary">
<div class="result-stat">
<span class="stat-label">도매상</span>
<span class="stat-value">${results.length}개</span>
</div>
<div class="result-stat">
<span class="stat-label">총 성공</span>
<span class="stat-value success">${totalSuccess}개</span>
</div>
<div class="result-stat">
<span class="stat-label">총 실패</span>
<span class="stat-value ${totalFailed > 0 ? 'failed' : ''}">${totalFailed}개</span>
</div>
</div>
`;
// 각 도매상별 결과
results.forEach(result => {
const wsId = result.wholesaler_id || result.wholesaler;
const ws = WHOLESALERS[wsId] || {icon: '📦', name: wsId, gradient: 'var(--bg-card)'};
const isSuccess = result.success && result.failed_count === 0;
html += `
<div class="multi-result-card" style="margin-bottom:12px;">
<div class="ws-header ${wsId}" style="margin-bottom:8px;">
<span>${ws.icon}</span>
<span class="ws-name">${ws.name}</span>
<span style="margin-left:auto;font-size:12px;color:${isSuccess ? 'var(--accent-emerald)' : 'var(--accent-rose)'};">
${isSuccess ? '✓ 완료' : '⚠ 일부실패'}
</span>
</div>
<div style="padding:0 12px 12px;">
${result.success ? `
<div style="font-size:12px;color:var(--text-secondary);">
성공: ${result.success_count}개 / 실패: ${result.failed_count}개
${result.order_no ? ` · 주문번호: ${result.order_no}` : ''}
</div>
` : `
<div style="font-size:12px;color:var(--accent-rose);">
오류: ${result.error || '처리 실패'}
</div>
`}
</div>
</div>
`;
});
if (isDryRun) {
html += `<div class="result-note">💡 테스트 모드입니다. "전체 장바구니 담기" 버튼으로 실제 진행하세요.</div>`;
} else if (totalSuccess > 0) {
html += `<div class="result-note" style="background:rgba(16,185,129,0.1);color:#10b981;">
🛒 각 도매상 장바구니에 담겼습니다. <b>각 사이트에서 최종 확정</b>이 필요합니다.
</div>`;
}
content.innerHTML = html;
modal.classList.add('show');
}
function openOrderConfirmModal(wholesalerId, items) {
@ -1188,6 +1438,72 @@
currentOrderWholesaler = null;
}
// ──────────────── 개별 품목 실제 주문 ────────────────
async function orderSingleItem(drugCode) {
const item = cart.find(i => i.drug_code === drugCode);
if (!item) {
showToast('품목을 찾을 수 없습니다', 'error');
return;
}
// 도매상 결정
let wholesaler = 'geoyoung';
for (const [wsId, ws] of Object.entries(WHOLESALERS)) {
if (ws.filterFn(item)) {
wholesaler = wsId;
break;
}
}
const ws = WHOLESALERS[wholesaler];
// 확인 다이얼로그
if (!confirm(`${ws.icon} ${ws.name}에 실제 주문하시겠습니까?\n\n품목: ${item.product_name}\n수량: ${item.qty}개\n\n⚠ 이 작업은 실제 주문을 진행합니다!`)) {
return;
}
showToast(`📤 ${item.product_name} 주문 중...`, 'info');
try {
const payload = {
wholesaler_id: wholesaler,
items: [{
drug_code: item.drug_code,
kd_code: item.geoyoung_code || item.sooin_code || item.drug_code,
internal_code: item.internal_code,
product_name: item.product_name,
manufacturer: item.supplier,
specification: item.specification || '',
order_qty: item.qty,
usage_qty: item.usage_qty || 0,
current_stock: item.current_stock || 0
}],
reference_period: `${document.getElementById('startDate').value}~${document.getElementById('endDate').value}`,
dry_run: false,
cart_only: false // 실제 주문까지!
};
const response = await fetch('/api/order/quick-submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success && result.success_count > 0) {
showToast(`✅ ${item.product_name} 주문 완료!`, 'success');
// 장바구니에서 제거
removeFromCart(drugCode);
} else {
const errorMsg = result.results?.[0]?.result_message || result.error || '주문 실패';
showToast(`❌ ${errorMsg}`, 'error');
}
} catch (err) {
showToast(`❌ 오류: ${err.message}`, 'error');
}
}
async function executeOrder(dryRun = true) {
const wholesaler = currentOrderWholesaler || 'geoyoung';
@ -1206,7 +1522,7 @@
btnTest.disabled = true;
btnReal.disabled = true;
btnTest.textContent = dryRun ? '처리 중...' : '🧪 테스트';
btnReal.textContent = !dryRun ? '처리 중...' : '🚀 실제 주문';
btnReal.textContent = !dryRun ? '처리 중...' : '🛒 장바구니 담기';
try {
const payload = {
@ -1223,7 +1539,8 @@
current_stock: item.current_stock || 0
})),
reference_period: `${document.getElementById('startDate').value}~${document.getElementById('endDate').value}`,
dry_run: dryRun
dry_run: dryRun,
cart_only: false // 장바구니 + 주문 확정까지
};
// 타임아웃 설정
@ -1257,7 +1574,7 @@
btnTest.disabled = false;
btnReal.disabled = false;
btnTest.textContent = '🧪 테스트';
btnReal.textContent = '🚀 실제 주문';
btnReal.textContent = '📤 주문 전송';
}
}
@ -1276,12 +1593,23 @@
headerDiv.style.background = ws.gradient;
const isDryRun = result.dry_run;
const isCartOnly = result.cart_only;
const statusEmoji = result.failed_count === 0 ? '✅' : result.success_count === 0 ? '❌' : '⚠️';
// 결과 타이틀
let resultTitle = '';
if (isDryRun) {
resultTitle = `[테스트] ${ws.name} 시뮬레이션 완료`;
} else if (isCartOnly) {
resultTitle = `${ws.name} 장바구니 담기 ${result.failed_count === 0 ? '완료' : '처리됨'}`;
} else {
resultTitle = `${ws.name} 주문 ${result.failed_count === 0 ? '완료' : '처리됨'}`;
}
let html = `
<div class="result-header ${result.failed_count === 0 ? 'success' : 'partial'}">
<span class="result-emoji">${statusEmoji}</span>
<span class="result-title">${isDryRun ? '[테스트]' : ''} ${ws.name} 주문 ${result.failed_count === 0 ? '완료' : '처리됨'}</span>
<span class="result-title">${resultTitle}</span>
</div>
<div class="result-summary">
<div class="result-stat">
@ -1322,12 +1650,21 @@
html += '</tbody></table>';
if (isDryRun && result.success_count > 0) {
html += `<div class="result-note">💡 테스트 모드입니다. 실제 주문은 "실제 주문" 버튼을 누르세요.</div>`;
html += `<div class="result-note">💡 테스트 모드입니다. "장바구니 담기" 버튼으로 실제 진행하세요.</div>`;
}
// 수인/백제 실제 주문 시 안내
// 장바구니만 담은 경우 안내
if (!isDryRun && isCartOnly && result.success_count > 0) {
html += `<div class="result-note" style="background:rgba(6,182,212,0.1);color:#06b6d4;">
🛒 ${ws.name} 장바구니에 담겼습니다. <b>사이트에서 최종 확정</b>이 필요합니다.
${wholesalerId === 'geoyoung' ? '<br>개별 품목은 📤주문 버튼으로 바로 주문할 수 있습니다.' : ''}
</div>`;
}
// 도매상별 안내
if (!isDryRun && result.note) {
const noteColors = {
geoyoung: 'rgba(6,182,212,0.1);color:#06b6d4',
sooin: 'rgba(168,85,247,0.1);color:#a855f7',
baekje: 'rgba(245,158,11,0.1);color:#f59e0b'
};
@ -2113,6 +2450,79 @@
font-size: 12px;
color: var(--accent-cyan);
}
/* 다중 도매상 선택 모달 스타일 */
.multi-ws-summary {
padding: 0 4px;
}
.multi-ws-card {
background: var(--bg-card);
border-radius: 12px;
margin-bottom: 12px;
overflow: hidden;
border: 1px solid var(--border);
}
.multi-ws-card.geoyoung {
border-left: 3px solid var(--accent-cyan);
}
.multi-ws-card.sooin {
border-left: 3px solid var(--accent-purple);
}
.multi-ws-card.baekje {
border-left: 3px solid var(--accent-amber);
}
.multi-ws-card.other {
border-left: 3px solid var(--text-muted);
opacity: 0.7;
}
.multi-ws-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
background: rgba(255,255,255,0.02);
}
.multi-ws-icon {
font-size: 20px;
}
.multi-ws-name {
font-weight: 600;
font-size: 14px;
flex: 1;
}
.multi-ws-count {
font-size: 12px;
color: var(--text-muted);
background: rgba(255,255,255,0.1);
padding: 3px 10px;
border-radius: 12px;
}
.multi-ws-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--accent-emerald);
cursor: pointer;
}
.multi-ws-checkbox input {
width: 18px;
height: 18px;
accent-color: var(--accent-emerald);
}
.multi-ws-items {
padding: 10px 16px 14px;
border-top: 1px solid rgba(255,255,255,0.05);
}
.multi-ws-item {
font-size: 12px;
color: var(--text-secondary);
padding: 3px 0;
}
.multi-ws-item.more {
color: var(--text-muted);
font-style: italic;
}
</style>
<!-- 주문 확인 모달 -->
@ -2124,7 +2534,10 @@
</div>
<div class="order-modal-body">
<p style="margin-bottom:12px;color:var(--text-secondary);">
<span id="orderConfirmCount">0</span>개 품목을 <span id="orderConfirmWholesaler">지오영</span>에 주문합니다.
<span id="orderConfirmCount">0</span>개 품목을 <span id="orderConfirmWholesaler">지오영</span> 장바구니에 담습니다.
</p>
<p style="margin-bottom:12px;font-size:12px;color:var(--accent-amber);">
⚠️ 장바구니 담기만 진행됩니다. 도매상 사이트에서 최종 확정이 필요합니다.
</p>
<table class="order-confirm-table">
<thead><tr><th>품목명</th><th>규격</th><th>수량</th></tr></thead>
@ -2133,7 +2546,7 @@
</div>
<div class="order-modal-footer">
<button class="btn-order-test" id="btnOrderTest" onclick="executeOrder(true)">🧪 테스트</button>
<button class="btn-order-real" id="btnOrderReal" onclick="executeOrder(false)">🚀 실제 주문</button>
<button class="btn-order-real" id="btnOrderReal" onclick="executeOrder(false)">📤 주문 전송</button>
</div>
</div>
</div>
@ -2153,6 +2566,22 @@
</div>
</div>
<!-- 다중 도매상 선택 모달 -->
<div class="order-modal" id="multiWholesalerModal">
<div class="order-modal-content" style="max-width:550px;">
<div class="order-modal-header" style="background:linear-gradient(135deg, #059669, #10b981);">
<h3>🛒 전체 도매상 주문</h3>
<button class="order-close" onclick="closeMultiWholesalerModal()"></button>
</div>
<div class="order-modal-body" id="multiWholesalerBody">
</div>
<div class="order-modal-footer">
<button class="btn-order-test" id="btnMultiTest" onclick="executeAllWholesalers(true)">🧪 테스트</button>
<button class="btn-order-real" id="btnMultiReal" onclick="executeAllWholesalers(false)">📤 전체 주문 전송</button>
</div>
</div>
</div>
<!-- 잔고 조회 모달 -->
<div class="order-modal" id="balanceModal">
<div class="order-modal-content" style="max-width:500px;">