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

29 lines
5.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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();
})();