Files
AriWeb-Suite/portal/commerce/public/order.js
T

29 lines
5.3 KiB
JavaScript
Raw Normal View History

/* portal/commerce/public/order.js — 주문 흐름(§6.2): 상품·옵션 → 적용 대상 → 가격·청구 주기 → 약관·제한 → 결제 방식 → 제출. 멱등 키로 연타·새로 고침 중복을 막는다. */
(() => {
const el = document.getElementById('order-new'); const esc = aw.esc; const money = (n) => Number(n).toLocaleString('ko-KR') + '원';
const idem = sessionStorage.getItem('order-idem') || (crypto.randomUUID ? crypto.randomUUID() : String(Date.now())); sessionStorage.setItem('order-idem', idem);
let data; let pv; const sel = { includeBase: true, items: [] };
async function load() {
try { data = await aw.api('GET', '/api/commerce/products'); pv = data.products.find((p) => p.id === el.dataset.product) || data.products[0]; if (!pv) { aw.state(el, 'empty', '판매 중인 상품이 없습니다'); return; }
if (el.dataset.option) { sel.includeBase = false; sel.items = [{ optionId: el.dataset.option, qty: 1, targetId: el.dataset.target || null }]; } render(); }
catch (e) { aw.state(el, 'failed', e.message, { retry: load }); }
}
async function render() {
let quote; try { quote = await aw.api('POST', '/api/commerce/quote', { productVersionId: pv.id, includeBase: sel.includeBase, items: sel.items }); } catch (e) { aw.state(el, 'failed', e.message, { retry: render }); return; }
const q = quote.quote; const pi = quote.paymentInstructions;
el.innerHTML = `<div class="card"><h2>1. 상품과 옵션</h2><p><label><input type="checkbox" id="base" ${sel.includeBase ? 'checked' : ''}> ${esc(pv.name)} 기본 상품 (${money(pv.price)}/${pv.billingCycle === 'monthly' ? '월' : '년'})</label></p>
<table class="r"><thead><tr><th>옵션</th><th>가격</th><th>수량</th><th>적용 대상</th></tr></thead><tbody>${pv.options.map((o) => { const it = sel.items.find((x) => x.optionId === o.id); return `<tr><td data-l="옵션">${esc(o.name)}</td><td data-l="가격">${money(o.price)}${o.per === 'site' ? ' / 사이트' : ''}</td><td data-l="수량"><input type="number" min="0" max="${o.max_qty || 99}" value="${it ? it.qty : 0}" data-opt="${o.id}" style="width:5em"></td><td data-l="대상">${o.per === 'site' ? `<select data-target="${o.id}"><option value="">사이트 선택</option>${data.sites.map((s) => `<option value="${esc(s.id)}" ${it && it.targetId === s.id ? 'selected' : ''}>${esc(s.name)}</option>`).join('')}</select>` : '사용자 그룹'}</td></tr>`; }).join('')}</tbody></table></div>
<div class="card"><h2>2. 가격과 청구 주기</h2><table><tbody>${q.items.map((i) => `<tr><td>${esc(i.name)} × ${i.qty}</td><td class="right">${money(i.amount)} + 세금 ${money(i.tax)}</td></tr>`).join('')}<tr><th>합계(세금 포함)</th><th class="right">${money(q.total)} ${q.currency}</th></tr></tbody></table><p class="sub">청구 주기: ${pv.billingCycle === 'monthly' ? '매월' : '매년'} · 갱신 전 청구 예정일과 금액을 알려 드립니다</p></div>
<div class="card"><h2>3. 약관과 제한</h2><ul>${quote.terms.notes.map((n) => `<li>${esc(n)}</li>`).join('')}</ul>${pv.deferred.length ? `<p><span class="badge warning">! 미확정 정책</span> ${pv.deferred.map((d) => esc(d.key)).join(', ')} — 확정 전에는 약속으로 보지 마세요</p>` : ''}<p><label><input type="checkbox" id="terms"> 약관·개인정보 처리방침·제한 사항에 동의합니다</label></p></div>
<div class="card"><h2>4. 결제 방식</h2><p><label><input type="radio" name="pm" value="manual_transfer" checked> 계좌 이체(수동 결제 확인)</label></p><p class="sub">${esc(pi.bank)} ${esc(pi.account)} (예금주 ${esc(pi.holder)}) · 입금 기한 ${pi.deadline_days}일 · ${esc(pi.note || '')}</p><p class="sub">운영자가 입금을 확인하고 승인한 뒤에만 사이트 수·용량·메일함·백업 권리를 쓸 수 있습니다.</p></div>
<div class="actions"><button class="btn primary" id="submit" ${q.items.length ? '' : 'disabled'}>주문 제출</button><a class="btn" href="/products">취소</a></div><p class="err" id="err"></p>`;
el.querySelector('#base').onchange = (e) => { sel.includeBase = e.target.checked; render(); };
el.querySelectorAll('[data-opt]').forEach((inp) => { inp.onchange = () => { const id = inp.dataset.opt; const qty = Number(inp.value) || 0; const t = el.querySelector(`[data-target="${id}"]`); sel.items = sel.items.filter((x) => x.optionId !== id); if (qty > 0) sel.items.push({ optionId: id, qty, targetId: t ? t.value || null : null }); render(); }; });
el.querySelectorAll('[data-target]').forEach((s) => { s.onchange = () => { const it = sel.items.find((x) => x.optionId === s.dataset.target); if (it) it.targetId = s.value || null; }; });
el.querySelector('#submit').onclick = async () => { const btn = el.querySelector('#submit'); btn.disabled = true; const err = el.querySelector('#err'); err.textContent = '';
try { const r = await aw.api('POST', '/api/commerce/orders', { productVersionId: pv.id, includeBase: sel.includeBase, items: sel.items, paymentMethod: 'manual_transfer', acceptTerms: el.querySelector('#terms').checked, idempotencyKey: idem }); sessionStorage.removeItem('order-idem'); location.href = `/orders/${r.order.id}`; }
catch (e) { err.textContent = e.message + (e.detail?.action === 'verify_email' ? ' — 내 계정에서 이메일 확인을 완료하세요' : ''); btn.disabled = false; } };
}
load();
})();