feat: 도매상 API 통합 및 스키마 업데이트
- wholesale 패키지 연동 (SooinSession, GeoYoungSession) - Flask Blueprint 분리 (sooin_api.py, geoyoung_api.py) - order_context 스키마 확장 (wholesaler_id, internal_code 등) - 수인약품 개별 취소 기능 (cancel_item, restore_item) - 문서 추가: WHOLESALE_API_INTEGRATION.md - 테스트 스크립트들
This commit is contained in:
@@ -1054,8 +1054,172 @@
|
||||
// ──────────────── 주문 제출 ────────────────
|
||||
function submitOrder() {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
// 지오영 품목만 필터
|
||||
const geoItems = cart.filter(c => c.supplier === '지오영' || c.geoyoung_code);
|
||||
|
||||
if (geoItems.length === 0) {
|
||||
// 지오영 품목 없으면 기존 방식 (클립보드)
|
||||
submitOrderClipboard();
|
||||
return;
|
||||
}
|
||||
|
||||
// 지오영 주문 모달 열기
|
||||
openOrderConfirmModal(geoItems);
|
||||
}
|
||||
|
||||
function openOrderConfirmModal(items) {
|
||||
const modal = document.getElementById('orderConfirmModal');
|
||||
const tbody = document.getElementById('orderConfirmBody');
|
||||
|
||||
let html = '';
|
||||
items.forEach((item, idx) => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${escapeHtml(item.product_name)}</td>
|
||||
<td class="mono">${item.specification || '-'}</td>
|
||||
<td class="mono">${item.qty}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
tbody.innerHTML = html;
|
||||
document.getElementById('orderConfirmCount').textContent = items.length;
|
||||
modal.classList.add('show');
|
||||
}
|
||||
|
||||
function closeOrderConfirmModal() {
|
||||
document.getElementById('orderConfirmModal').classList.remove('show');
|
||||
}
|
||||
|
||||
async function executeOrder(dryRun = true) {
|
||||
const geoItems = cart.filter(c => c.supplier === '지오영' || c.geoyoung_code);
|
||||
|
||||
if (geoItems.length === 0) {
|
||||
showToast('지오영 품목이 없습니다', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 버튼 비활성화
|
||||
const btnTest = document.getElementById('btnOrderTest');
|
||||
const btnReal = document.getElementById('btnOrderReal');
|
||||
btnTest.disabled = true;
|
||||
btnReal.disabled = true;
|
||||
btnTest.textContent = dryRun ? '처리 중...' : '🧪 테스트';
|
||||
btnReal.textContent = !dryRun ? '처리 중...' : '🚀 실제 주문';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
wholesaler_id: 'geoyoung',
|
||||
items: geoItems.map(item => ({
|
||||
drug_code: item.drug_code,
|
||||
kd_code: item.geoyoung_code || item.drug_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
|
||||
};
|
||||
|
||||
// 실제 주문은 시간이 오래 걸림 (Playwright 사용)
|
||||
const timeoutMs = dryRun ? 60000 : 180000; // 테스트 1분, 실제 3분
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
const response = await fetch('/api/order/quick-submit', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
closeOrderConfirmModal();
|
||||
|
||||
if (result.success) {
|
||||
showOrderResultModal(result);
|
||||
} else {
|
||||
showToast(`❌ 주문 실패: ${result.error}`, 'error');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
showToast(`❌ 오류: ${err.message}`, 'error');
|
||||
} finally {
|
||||
btnTest.disabled = false;
|
||||
btnReal.disabled = false;
|
||||
btnTest.textContent = '🧪 테스트';
|
||||
btnReal.textContent = '🚀 실제 주문';
|
||||
}
|
||||
}
|
||||
|
||||
function showOrderResultModal(result) {
|
||||
const modal = document.getElementById('orderResultModal');
|
||||
const content = document.getElementById('orderResultContent');
|
||||
|
||||
const isDryRun = result.dry_run;
|
||||
const statusEmoji = result.failed_count === 0 ? '✅' : result.success_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 ? '[테스트]' : ''} 주문 ${result.failed_count === 0 ? '완료' : '처리됨'}</span>
|
||||
</div>
|
||||
<div class="result-summary">
|
||||
<div class="result-stat">
|
||||
<span class="stat-label">주문번호</span>
|
||||
<span class="stat-value mono">${result.order_no}</span>
|
||||
</div>
|
||||
<div class="result-stat">
|
||||
<span class="stat-label">성공</span>
|
||||
<span class="stat-value success">${result.success_count}개</span>
|
||||
</div>
|
||||
<div class="result-stat">
|
||||
<span class="stat-label">실패</span>
|
||||
<span class="stat-value ${result.failed_count > 0 ? 'failed' : ''}">${result.failed_count}개</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="result-table">
|
||||
<thead><tr><th>품목</th><th>수량</th><th>결과</th></tr></thead>
|
||||
<tbody>`;
|
||||
|
||||
(result.results || []).forEach(item => {
|
||||
const isSuccess = item.status === 'success';
|
||||
html += `
|
||||
<tr class="${isSuccess ? '' : 'failed-row'}">
|
||||
<td>${escapeHtml(item.product_name)}</td>
|
||||
<td class="mono">${item.order_qty}</td>
|
||||
<td class="${isSuccess ? 'result-ok' : 'result-fail'}">
|
||||
${isSuccess ? '✓' : '✗'} ${item.result_code}
|
||||
${item.result_message ? `<br><small>${escapeHtml(item.result_message)}</small>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
|
||||
if (isDryRun && result.success_count > 0) {
|
||||
html += `<div class="result-note">💡 테스트 모드입니다. 실제 주문은 "실제 주문" 버튼을 누르세요.</div>`;
|
||||
}
|
||||
|
||||
content.innerHTML = html;
|
||||
modal.classList.add('show');
|
||||
}
|
||||
|
||||
function closeOrderResultModal() {
|
||||
document.getElementById('orderResultModal').classList.remove('show');
|
||||
}
|
||||
|
||||
// 기존 클립보드 방식 (지오영 아닌 품목용)
|
||||
function submitOrderClipboard() {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
// 제조사별 그룹화
|
||||
const bySupplier = {};
|
||||
cart.forEach(item => {
|
||||
const sup = item.supplier || '미지정';
|
||||
@@ -1063,7 +1227,6 @@
|
||||
bySupplier[sup].push(item);
|
||||
});
|
||||
|
||||
// 주문서 텍스트 생성
|
||||
let orderText = `💊 청춘약국 전문의약품 발주서\n`;
|
||||
orderText += `━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||||
orderText += `📅 작성일: ${new Date().toLocaleDateString('ko-KR')}\n`;
|
||||
@@ -1081,7 +1244,6 @@
|
||||
orderText += `\n━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||||
orderText += `총 ${cart.length}개 품목\n`;
|
||||
|
||||
// 클립보드 복사
|
||||
navigator.clipboard.writeText(orderText).then(() => {
|
||||
showToast('📋 주문서가 클립보드에 복사되었습니다!', 'success');
|
||||
}).catch(() => {
|
||||
@@ -1116,6 +1278,573 @@
|
||||
document.getElementById('searchInput').addEventListener('keypress', e => {
|
||||
if (e.key === 'Enter') loadUsageData();
|
||||
});
|
||||
|
||||
// ──────────────── 지오영 재고 조회 ────────────────
|
||||
let currentGeoyoungItem = null;
|
||||
|
||||
function openGeoyoungModal(idx) {
|
||||
const item = usageData[idx];
|
||||
if (!item) return;
|
||||
|
||||
currentGeoyoungItem = item;
|
||||
|
||||
// 모달 열기
|
||||
document.getElementById('geoModalProductName').textContent = item.product_name;
|
||||
document.getElementById('geoModalDrugCode').textContent = item.drug_code;
|
||||
document.getElementById('geoModalUsage').textContent = item.total_dose.toLocaleString() + '개';
|
||||
document.getElementById('geoModalStock').textContent = item.current_stock.toLocaleString() + '개';
|
||||
|
||||
document.getElementById('geoyoungModal').classList.add('show');
|
||||
|
||||
// 로딩 표시
|
||||
document.getElementById('geoResultBody').innerHTML = `
|
||||
<div class="geo-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>지오영 재고 조회 중...</div>
|
||||
</div>`;
|
||||
|
||||
// API 호출 (보험코드로 먼저 시도)
|
||||
searchGeoyoung(item.drug_code, item.product_name);
|
||||
}
|
||||
|
||||
function closeGeoyoungModal() {
|
||||
document.getElementById('geoyoungModal').classList.remove('show');
|
||||
currentGeoyoungItem = null;
|
||||
}
|
||||
|
||||
async function searchGeoyoung(kdCode, productName) {
|
||||
const resultBody = document.getElementById('geoResultBody');
|
||||
|
||||
try {
|
||||
// 1차: 보험코드(KD코드)로 검색
|
||||
let response = await fetch(`/api/geoyoung/stock?kd_code=${encodeURIComponent(kdCode)}`);
|
||||
let data = await response.json();
|
||||
|
||||
// 결과 없으면 성분명으로 재검색
|
||||
if (data.success && data.count === 0) {
|
||||
document.getElementById('geoResultBody').innerHTML = `
|
||||
<div class="geo-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>성분명으로 재검색 중...</div>
|
||||
</div>`;
|
||||
|
||||
response = await fetch(`/api/geoyoung/stock-by-name?product_name=${encodeURIComponent(productName)}`);
|
||||
data = await response.json();
|
||||
}
|
||||
|
||||
if (!data.success) {
|
||||
resultBody.innerHTML = `
|
||||
<div class="geo-error">
|
||||
<div>❌ ${data.message || '조회 실패'}</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.count === 0) {
|
||||
resultBody.innerHTML = `
|
||||
<div class="geo-empty">
|
||||
<div>📭 지오영에 해당 제품이 없습니다</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 검색어 표시
|
||||
if (data.extracted_ingredient) {
|
||||
document.getElementById('geoSearchKeyword').textContent = `검색: "${data.extracted_ingredient}"`;
|
||||
document.getElementById('geoSearchKeyword').style.display = 'block';
|
||||
}
|
||||
|
||||
// 결과 렌더링
|
||||
renderGeoyoungResults(data.items);
|
||||
|
||||
} catch (err) {
|
||||
resultBody.innerHTML = `
|
||||
<div class="geo-error">
|
||||
<div>❌ 네트워크 오류: ${err.message}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderGeoyoungResults(items) {
|
||||
const resultBody = document.getElementById('geoResultBody');
|
||||
|
||||
// 재고 있는 것 먼저 정렬
|
||||
items.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
|
||||
|
||||
let html = `<table class="geo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>제품명</th>
|
||||
<th>규격</th>
|
||||
<th>재고</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
items.forEach((item, idx) => {
|
||||
const hasStock = item.stock > 0;
|
||||
html += `
|
||||
<tr class="${hasStock ? '' : 'no-stock'}">
|
||||
<td>
|
||||
<div class="geo-product">
|
||||
<span class="geo-name">${escapeHtml(item.product_name)}</span>
|
||||
<span class="geo-code">${item.insurance_code}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="geo-spec">${item.specification}</td>
|
||||
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
|
||||
<td>
|
||||
${hasStock ? `<button class="geo-add-btn" onclick="addGeoyoungToCart(${idx})">담기</button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
|
||||
// 전역에 저장 (담기용)
|
||||
window.geoyoungItems = items;
|
||||
|
||||
resultBody.innerHTML = html;
|
||||
}
|
||||
|
||||
function addGeoyoungToCart(idx) {
|
||||
const item = window.geoyoungItems[idx];
|
||||
if (!item || !currentGeoyoungItem) return;
|
||||
|
||||
// 수량 계산 (규격에서 숫자 추출)
|
||||
const specMatch = item.specification.match(/(\d+)/);
|
||||
const specQty = specMatch ? parseInt(specMatch[1]) : 1;
|
||||
|
||||
// 필요 수량 계산
|
||||
const needed = currentGeoyoungItem.total_dose;
|
||||
const suggestedQty = Math.ceil(needed / specQty);
|
||||
|
||||
const qty = prompt(`주문 수량 (${item.specification} 기준)\n\n필요량: ${needed}개\n규격: ${specQty}개/단위\n추천: ${suggestedQty}단위 (${suggestedQty * specQty}개)`, suggestedQty);
|
||||
|
||||
if (!qty || isNaN(qty)) return;
|
||||
|
||||
// 장바구니에 추가 (지오영 정보 포함)
|
||||
const cartItem = {
|
||||
drug_code: currentGeoyoungItem.drug_code,
|
||||
product_name: item.product_name,
|
||||
supplier: '지오영',
|
||||
qty: parseInt(qty),
|
||||
specification: item.specification,
|
||||
geoyoung_code: item.insurance_code
|
||||
};
|
||||
|
||||
// 기존 항목 체크
|
||||
const existing = cart.find(c => c.drug_code === currentGeoyoungItem.drug_code && c.specification === item.specification);
|
||||
if (existing) {
|
||||
existing.qty = parseInt(qty);
|
||||
} else {
|
||||
cart.push(cartItem);
|
||||
}
|
||||
|
||||
updateCartUI();
|
||||
closeGeoyoungModal();
|
||||
showToast(`✅ ${item.product_name} (${item.specification}) ${qty}개 추가`, 'success');
|
||||
}
|
||||
|
||||
// 테이블 행 더블클릭으로 지오영 모달 열기
|
||||
document.addEventListener('dblclick', function(e) {
|
||||
const row = e.target.closest('tr[data-idx]');
|
||||
if (row) {
|
||||
const idx = parseInt(row.dataset.idx);
|
||||
openGeoyoungModal(idx);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 지오영 재고 조회 모달 -->
|
||||
<div class="geo-modal" id="geoyoungModal">
|
||||
<div class="geo-modal-content">
|
||||
<div class="geo-modal-header">
|
||||
<h3>🏭 지오영 재고 조회</h3>
|
||||
<button class="geo-close" onclick="closeGeoyoungModal()">✕</button>
|
||||
</div>
|
||||
<div class="geo-modal-info">
|
||||
<div class="geo-info-row">
|
||||
<span class="geo-label">약품명</span>
|
||||
<span class="geo-value" id="geoModalProductName">-</span>
|
||||
</div>
|
||||
<div class="geo-info-row">
|
||||
<span class="geo-label">보험코드</span>
|
||||
<span class="geo-value mono" id="geoModalDrugCode">-</span>
|
||||
</div>
|
||||
<div class="geo-info-row">
|
||||
<span class="geo-label">사용량</span>
|
||||
<span class="geo-value highlight" id="geoModalUsage">-</span>
|
||||
</div>
|
||||
<div class="geo-info-row">
|
||||
<span class="geo-label">현재고</span>
|
||||
<span class="geo-value" id="geoModalStock">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="geo-search-info" id="geoSearchKeyword" style="display:none;"></div>
|
||||
<div class="geo-result" id="geoResultBody">
|
||||
<div class="geo-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div>지오영 재고 조회 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 지오영 모달 스타일 */
|
||||
.geo-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 500;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.geo-modal.show { display: flex; }
|
||||
|
||||
.geo-modal-content {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--accent-cyan);
|
||||
box-shadow: 0 8px 32px rgba(6, 182, 212, 0.3);
|
||||
}
|
||||
|
||||
.geo-modal-header {
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #0891b2, var(--accent-cyan));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.geo-modal-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.geo-close {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.geo-modal-info {
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.geo-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.geo-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.geo-value {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.geo-value.mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.geo-value.highlight {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.geo-search-info {
|
||||
padding: 8px 20px;
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.geo-result {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.geo-loading, .geo-error, .geo-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.geo-error { color: var(--accent-rose); }
|
||||
|
||||
.geo-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.geo-table th {
|
||||
padding: 10px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.geo-table td {
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.geo-table tr:hover {
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
.geo-table tr.no-stock {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.geo-product {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.geo-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
.geo-code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.geo-spec {
|
||||
font-weight: 600;
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
.geo-stock {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.geo-stock.in-stock { color: var(--accent-emerald); }
|
||||
.geo-stock.out-stock { color: var(--text-muted); }
|
||||
|
||||
.geo-add-btn {
|
||||
padding: 6px 12px;
|
||||
background: var(--accent-cyan);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.geo-add-btn:hover {
|
||||
background: #0891b2;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 주문 확인 모달 */
|
||||
.order-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
z-index: 600;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.order-modal.show { display: flex; }
|
||||
|
||||
.order-modal-content {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--accent-violet);
|
||||
box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.order-modal-header {
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(135deg, #7c3aed, var(--accent-violet));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.order-modal-header h3 { font-size: 16px; font-weight: 700; }
|
||||
.order-close {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.order-modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.order-confirm-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.order-confirm-table th {
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.order-confirm-table td {
|
||||
padding: 10px 8px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.order-confirm-table .mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.order-modal-footer {
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-card);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-order-test {
|
||||
padding: 10px 20px;
|
||||
background: var(--accent-amber);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-order-real {
|
||||
padding: 10px 20px;
|
||||
background: var(--accent-emerald);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-order-test:disabled, .btn-order-real:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 주문 결과 모달 */
|
||||
.result-header {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.result-header.success { background: rgba(16, 185, 129, 0.1); }
|
||||
.result-header.partial { background: rgba(245, 158, 11, 0.1); }
|
||||
.result-emoji { font-size: 32px; display: block; margin-bottom: 8px; }
|
||||
.result-title { font-size: 16px; font-weight: 600; }
|
||||
|
||||
.result-summary {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.result-stat {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.stat-label { display: block; font-size: 11px; color: var(--text-muted); }
|
||||
.stat-value { display: block; font-size: 16px; font-weight: 700; margin-top: 4px; }
|
||||
.stat-value.success { color: var(--accent-emerald); }
|
||||
.stat-value.failed { color: var(--accent-rose); }
|
||||
|
||||
.result-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.result-table th {
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.result-table td {
|
||||
padding: 10px 8px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.result-table .failed-row { background: rgba(244, 63, 94, 0.1); }
|
||||
.result-ok { color: var(--accent-emerald); }
|
||||
.result-fail { color: var(--accent-rose); }
|
||||
|
||||
.result-note {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(6, 182, 212, 0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 주문 확인 모달 -->
|
||||
<div class="order-modal" id="orderConfirmModal">
|
||||
<div class="order-modal-content">
|
||||
<div class="order-modal-header">
|
||||
<h3>🏭 지오영 주문 확인</h3>
|
||||
<button class="order-close" onclick="closeOrderConfirmModal()">✕</button>
|
||||
</div>
|
||||
<div class="order-modal-body">
|
||||
<p style="margin-bottom:12px;color:var(--text-secondary);">
|
||||
<span id="orderConfirmCount">0</span>개 품목을 지오영에 주문합니다.
|
||||
</p>
|
||||
<table class="order-confirm-table">
|
||||
<thead><tr><th>품목명</th><th>규격</th><th>수량</th></tr></thead>
|
||||
<tbody id="orderConfirmBody"></tbody>
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 주문 결과 모달 -->
|
||||
<div class="order-modal" id="orderResultModal">
|
||||
<div class="order-modal-content" style="max-width:600px;">
|
||||
<div class="order-modal-header" style="background:linear-gradient(135deg, #059669, var(--accent-emerald));">
|
||||
<h3>📋 주문 결과</h3>
|
||||
<button class="order-close" onclick="closeOrderResultModal()">✕</button>
|
||||
</div>
|
||||
<div class="order-modal-body" id="orderResultContent">
|
||||
</div>
|
||||
<div class="order-modal-footer">
|
||||
<button class="btn-order-test" onclick="closeOrderResultModal()">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user