상품·주문·수동 결제 승인·권리·청구·축소·해지·AI Wallet 표시·사용량 배정 화면·경고 엔진 (§6·§13·§27)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c90ac27596
commit
c397452acd
@@ -0,0 +1,28 @@
|
||||
/* portal/commerce/public/billing.js — 결제 화면(§6.3~6.5): 활성 상품·다음 청구일·예상 금액·결제 대기·주문 목록·권리·환불/크레딧·축소 보류·해지·AI Wallet·초과 사용·도메인 등록 안내. */
|
||||
(() => {
|
||||
const el = document.getElementById('billing'); const esc = aw.esc; const money = (n) => Number(n).toLocaleString('ko-KR') + '원'; const GB = 1024 ** 3;
|
||||
const fmt = (n, unit) => (unit.startsWith('bytes') ? (n / GB).toFixed(1) + ' GB' : Number(n).toLocaleString('ko-KR'));
|
||||
async function load() {
|
||||
try { const d = await aw.api('GET', '/api/commerce/billing');
|
||||
let h = `<div class="cards"><div class="card"><h2>이번 달 예상</h2><p class="stat">${money(d.monthEstimate)}</p><p class="sub">다음 청구일 ${d.nextBillingAt ? aw.fmt(d.nextBillingAt) : '없음'}</p></div><div class="card"><h2>결제 대기</h2><p class="stat">${money(d.awaitingTotal)}</p><p class="sub">${d.awaitingOrders.length}건</p></div><div class="card"><h2>크레딧 잔액</h2><p class="stat">${money(d.creditBalance)}</p></div></div>`;
|
||||
h += `<div class="card"><h2>활성 상품과 옵션</h2>${d.subscriptions.length ? d.subscriptions.map((s) => `<dl class="kv"><dt>상품</dt><dd>${esc(s.product.name)} v${s.product.version} <span class="badge ${s.status === 'active' ? 'ok' : 'warning'}">${s.status === 'active' ? '●' : '!'} ${esc(s.label)}</span></dd><dt>항목</dt><dd>${s.items.map((i) => esc(i.name) + '×' + i.qty).join(', ')}</dd><dt>청구</dt><dd>${money(s.total)} / ${s.billing_cycle === 'monthly' ? '월' : '년'} · 다음 ${s.next_billing_at ? aw.fmt(s.next_billing_at) : '-'}</dd>${s.payment_due_at ? `<dt>갱신 입금 기한</dt><dd>${aw.fmt(s.payment_due_at)} · 미납 제한 예정 ${aw.fmt(s.restrict_at)}</dd>` : ''}${s.cancel_at ? `<dt>해지 적용일</dt><dd>${aw.fmt(s.cancel_at)}</dd>` : ''}</dl><div class="actions">${s.status === 'cancel_scheduled' ? '<button class="btn" data-act="undo-cancel">해지 철회</button>' : '<button class="btn danger" data-act="cancel">상품 해지</button>'}<a class="btn" href="/products">상품 변경·축소</a></div>`).join('') : '<p class="muted">활성 상품이 없습니다.</p><a class="btn primary" href="/products">상품 보기</a>'}</div>`;
|
||||
if (d.hold) h += `<div class="card" id="downsize"><h2>상품 축소 보류</h2><p>새 한도 안에 들어오지 않는 항목이 있어 축소가 보류되었습니다. 아래 항목을 정리한 뒤 "다시 검증" 을 누르세요.</p><ul>${d.hold.shortages.map((s) => `<li><b>${esc(s.label)}</b>: ${fmt(s.reduceBy, s.unit)} 줄이기 (새 한도 ${fmt(s.newLimit, s.unit)}, 배정+예약 ${fmt(s.inUse, s.unit)}, 사용 ${fmt(s.used, s.unit)})<ul>${s.items.map((i) => `<li>${esc(i.subjectType)} ${esc(i.subjectId)}: 배정 ${fmt(i.amount, s.unit)}</li>`).join('')}</ul></li>`).join('')}</ul><p class="sub">마지막 검증 ${aw.fmt(d.hold.last_checked_at)}</p><div class="actions"><button class="btn primary" data-act="revalidate">다시 검증</button><button class="btn" data-act="cancel-hold">축소 요청 취소</button></div></div>`;
|
||||
h += `<div class="card"><h2>활성화된 권리</h2><table class="r"><thead><tr><th>자원</th><th>구매</th><th>배정</th><th>사용</th></tr></thead><tbody>${d.entitlements.filter((e) => e.purchased).map((e) => `<tr><td data-l="자원">${esc(e.label)}</td><td data-l="구매">${fmt(e.purchased, e.unit)}</td><td data-l="배정">${fmt(e.allocated, e.unit)}</td><td data-l="사용">${fmt(e.used, e.unit)}</td></tr>`).join('') || '<tr><td colspan="4" class="muted">없음</td></tr>'}</tbody></table></div>`;
|
||||
h += `<div class="card"><h2>주문</h2>${d.orders.length ? `<table class="r"><thead><tr><th>번호</th><th>종류</th><th>금액</th><th>상태</th><th>일시</th></tr></thead><tbody>${d.orders.map((o) => `<tr><td data-l="번호"><a href="/orders/${esc(o.id)}">${esc(o.order_no)}</a></td><td data-l="종류">${esc({ new: '신규', renewal: '갱신', downsize: '축소' }[o.kind] || o.kind)}</td><td data-l="금액">${money(o.total)}</td><td data-l="상태">${esc(o.statusLabel)}</td><td data-l="일시">${aw.fmt(o.created_at)}</td></tr>`).join('')}</tbody></table>` : '<p class="muted">주문이 없습니다.</p>'}</div>`;
|
||||
h += `<div class="card"><h2>환불·크레딧</h2>${d.credits.length ? `<table class="r"><thead><tr><th>종류</th><th>금액</th><th>사유</th><th>적용일</th></tr></thead><tbody>${d.credits.map((c) => `<tr><td data-l="종류">${esc({ refund: '환불', credit: '크레딧', adjust: '조정' }[c.kind])}</td><td data-l="금액">${money(c.amount)}</td><td data-l="사유">${esc(c.reason)}</td><td data-l="적용일">${aw.fmt(c.applies_at)}</td></tr>`).join('')}</tbody></table>` : '<p class="muted">없음</p>'}</div>`;
|
||||
h += `<div class="card"><h2>AI Wallet</h2><p class="sub">제작(AI) 기능 전용이며 호스팅 디스크·트래픽·메일 사용량과 분리되어 있습니다.</p>${d.wallet && d.wallet.balance !== undefined ? `<p class="stat">${esc(d.wallet.unit ? d.wallet.balanceText || d.wallet.balance : money(d.wallet.balance))}</p>${(d.wallet.ledger || []).slice(0, 10).map((l) => `<p class="sub">${aw.fmt(l.at)} ${esc(l.type)} ${esc(String(l.amount))} ${esc(l.memo || '')}${l.refunded ? ' (실패 환급)' : ''}</p>`).join('')}<a class="btn" href="${esc(d.wallet.chargeUrl || '/sites')}">충전·이력</a>` : `<p class="muted">${esc(d.wallet?.message || '')}</p>`}</div>`;
|
||||
h += `<div class="card"><h2>초과 사용 허용</h2><p class="sub">자동 초과 사용과 자동 추가 과금은 기본으로 꺼져 있습니다. 허용하려면 항목별 최대 금액 또는 최대 사용량과 기간을 정해야 하며, 철회하면 즉시 100% 제한이 다시 적용됩니다.</p>${d.overage.filter((o) => !o.revoked_at).map((o) => `<p>${esc(o.resource)}: 상한 ${o.cap_amount ? money(o.cap_amount) : o.cap_units + ' 단위'} · ${o.period_start.slice(0, 10)}~${o.period_end.slice(0, 10)} <button class="btn" data-act="revoke-overage" data-res="${esc(o.resource)}">철회</button></p>`).join('') || '<p class="muted">허용한 항목 없음</p>'}<form id="ov" class="f"><label>자원</label><select name="resource"><option value="web_out">월간 웹 아웃바운드</option><option value="mail_msgs">월간 메일 통수</option></select><label>최대 금액(원)</label><input name="capAmount" type="number" min="0"><label>기간</label><input name="periodStart" type="date" required> ~ <input name="periodEnd" type="date" required><div class="actions"><button class="btn">초과 사용 허용</button></div></form></div>`;
|
||||
h += `<div class="card"><h2>도메인 등록</h2>${d.domainRegistration.enabled ? '<p>등록기관을 통해 도메인을 검색·등록할 수 있습니다.</p>' : `<p class="muted">${esc(d.domainRegistration.note)}</p>`}</div>`;
|
||||
el.innerHTML = h;
|
||||
el.querySelectorAll('[data-act]').forEach((b) => { b.onclick = async () => { const a = b.dataset.act; try {
|
||||
if (a === 'cancel') { const p = await aw.api('GET', '/api/commerce/cancel/preview'); if (!(await aw.confirmDanger({ title: '상품 해지', target: '현재 그룹의 활성 상품', impacts: [`적용일: ${p.effectiveAt ? aw.fmt(p.effectiveAt) : '즉시'}`, ...p.effects, `보존: 사이트 ${p.retention?.site}일 · 메일 ${p.retention?.mail}일 · 소스 ${p.retention?.source}일`], typeToConfirm: '해지', confirmLabel: '해지 예약' }))) return; await aw.api('POST', '/api/commerce/cancel'); }
|
||||
if (a === 'undo-cancel') await aw.api('POST', '/api/commerce/cancel/undo');
|
||||
if (a === 'revalidate') { const r = await aw.api('POST', '/api/commerce/downsize/revalidate'); aw.toast(r.fits ? '정리가 확인되어 축소 주문이 만들어졌습니다' : '아직 정리할 항목이 남아 있습니다'); }
|
||||
if (a === 'cancel-hold') await aw.api('POST', '/api/commerce/downsize/cancel');
|
||||
if (a === 'revoke-overage') { if (!(await aw.confirmDanger({ title: '초과 사용 철회', target: b.dataset.res, impacts: ['철회 즉시 100% 제한이 다시 적용됩니다'] }))) return; await aw.api('POST', '/api/commerce/overage', { revoke: true, resource: b.dataset.res }); }
|
||||
load(); } catch (e) { aw.toast(e.message); } }; });
|
||||
el.querySelector('#ov').onsubmit = async (ev) => { ev.preventDefault(); const f = new FormData(ev.target); try { await aw.api('POST', '/api/commerce/overage', { resource: f.get('resource'), capAmount: Number(f.get('capAmount')) || null, periodStart: new Date(f.get('periodStart')).toISOString(), periodEnd: new Date(f.get('periodEnd') + 'T23:59:59').toISOString() }); load(); } catch (e) { aw.toast(e.message); } };
|
||||
} catch (e) { aw.state(el, e.status === 403 ? 'forbidden' : 'failed', e.message, { retry: load }); }
|
||||
}
|
||||
load();
|
||||
})();
|
||||
@@ -0,0 +1,17 @@
|
||||
/* portal/commerce/public/order-detail.js — 주문 상세: 상태·금액·입금 안내·다음 단계·취소 가능 단계·남는 서비스. 처리 중이면 주기 갱신. */
|
||||
(() => {
|
||||
const el = document.getElementById('order'); const esc = aw.esc; const money = (n, c) => Number(n).toLocaleString('ko-KR') + (c === 'KRW' ? '원' : ' ' + c);
|
||||
async function load() {
|
||||
try { const { order: o } = await aw.api('GET', `/api/commerce/orders/${el.dataset.id}`); const pi = o.payment_instructions || {}; const kind = o.status === 'approved' ? 'done' : ['rejected', 'expired', 'cancelled'].includes(o.status) ? 'failed' : 'waiting';
|
||||
el.innerHTML = `<div class="card"><div class="ph-row"><span class="badge ${kind}">${kind === 'done' ? '✔' : kind === 'failed' ? '✘' : '◔'} ${esc(o.statusLabel)}</span><span class="sub">마지막 갱신 ${aw.fmt(o.updated_at)}</span></div><p>${esc(o.nextStep)}</p>${o.remaining ? `<p class="sub">${esc(o.remaining)}</p>` : ''}
|
||||
<dl class="kv"><dt>주문 번호</dt><dd class="mono">${esc(o.order_no)}</dd><dt>종류</dt><dd>${esc({ new: '신규', renewal: '갱신', downsize: '축소' }[o.kind] || o.kind)}</dd><dt>금액</dt><dd>${money(o.amount, o.currency)} + 세금 ${money(o.tax, o.currency)} = <b>${money(o.total, o.currency)}</b></dd><dt>결제 방식</dt><dd>계좌 이체(수동 결제 확인)</dd>${o.due_at ? `<dt>입금 기한</dt><dd>${aw.fmt(o.due_at)}</dd>` : ''}${o.paid_at ? `<dt>입금 확인</dt><dd>${aw.fmt(o.paid_at)}</dd>` : ''}${o.approved_at ? `<dt>승인</dt><dd>${aw.fmt(o.approved_at)}</dd>` : ''}<dt>취소 가능 단계</dt><dd>${esc(o.lastCancellableStep)}</dd></dl>
|
||||
${o.status === 'awaiting_payment' ? `<h3>입금 안내</h3><p>${esc(pi.bank || '')} <span class="copy" data-action="copy" data-copy="${esc(pi.account || '')}">${esc(pi.account || '')}</span> (예금주 ${esc(pi.holder || '')})</p><p class="sub">${esc(pi.note || '')}</p>` : ''}
|
||||
<table class="r"><thead><tr><th>항목</th><th>수량</th><th>적용 대상</th><th>단가</th><th>세금</th><th>합계</th></tr></thead><tbody>${o.items.map((i) => `<tr><td data-l="항목">${esc(i.name)}</td><td data-l="수량">${i.qty}</td><td data-l="대상">${i.target_type === 'site' ? '사이트 ' + esc(i.target_id || '') : '사용자 그룹'}</td><td data-l="단가">${money(i.unit_price, o.currency)}</td><td data-l="세금">${money(i.tax, o.currency)}</td><td data-l="합계">${money(i.amount + i.tax, o.currency)}</td></tr>`).join('')}</tbody></table>
|
||||
${o.policyNotes ? `<p class="sub">입금 기한 ${o.policyNotes.paymentDeadlineDays}일 · 미납 ${o.policyNotes.restrictionAfterUnpaidDays}일 후 ${esc(o.policyNotes.restrictionEffect || '')}</p>` : ''}
|
||||
<div class="actions"><a class="btn" href="/billing/invoice/${esc(o.id)}" target="_blank">${o.status === 'approved' ? '영수증' : '청구서'} 보기</a>${o.canCancel ? '<button class="btn danger" id="cancel">주문 취소</button>' : ''}${['rejected', 'expired'].includes(o.status) ? '<a class="btn primary" href="/products">다시 주문</a><a class="btn" href="/help">문의</a>' : ''}</div></div>`;
|
||||
const c = el.querySelector('#cancel'); if (c) c.onclick = async () => { if (!(await aw.confirmDanger({ title: '주문 취소', target: o.order_no, impacts: ['권리 변화 없음', '입금한 경우 운영자에게 환불을 문의하세요'], confirmLabel: '취소' }))) return; try { await aw.api('POST', `/api/commerce/orders/${o.id}/cancel`); load(); } catch (e) { aw.toast(e.message); } };
|
||||
return kind === 'waiting';
|
||||
} catch (e) { aw.state(el, e.status === 404 ? 'empty' : 'failed', e.message, { retry: load }); return false; }
|
||||
}
|
||||
aw.poll(load, 10000);
|
||||
})();
|
||||
@@ -0,0 +1,28 @@
|
||||
/* 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();
|
||||
})();
|
||||
@@ -0,0 +1,20 @@
|
||||
/* portal/commerce/public/products.js — 상품 목록(§6.1). 가격·한도는 서버(product_versions)에서 온다. 미확정 정책은 숫자 대신 '미확정 정책' 으로 보인다. */
|
||||
(() => {
|
||||
const el = document.getElementById('products'); const esc = aw.esc; const GB = 1024 ** 3;
|
||||
const money = (n) => Number(n).toLocaleString('ko-KR') + '원'; const gb = (b) => (b / GB).toFixed(0) + ' GB';
|
||||
async function load() {
|
||||
try {
|
||||
const d = await aw.api('GET', '/api/commerce/products');
|
||||
if (!d.products.length) { aw.state(el, 'empty', '판매 중인 상품이 없습니다. 운영자에게 문의하세요'); return; }
|
||||
el.innerHTML = `<p class="alert info">ⓘ 결제 방식: ${esc(d.paymentMethods.map((m) => m.label).join(', '))} — 자동 결제는 지원하지 않습니다.</p>` + `<div class="cards">${d.products.map((p) => `<div class="card"><h2>${esc(p.name)} <span class="sub">v${p.version}</span></h2><p class="sub">${esc(p.audience)}</p><p>${esc(p.description || '')}</p><p class="stat">${money(p.price)}<span class="sub">/${p.billingCycle === 'monthly' ? '월' : '년'} + 세금 ${money(p.tax)} = ${money(p.total)}</span></p>
|
||||
<dl class="kv"><dt>사이트 수</dt><dd>${p.limits.sites}개</dd><dt>웹 디스크</dt><dd>${gb(p.limits.web_disk)}</dd><dt>월간 웹 아웃바운드</dt><dd>${gb(p.limits.web_out)}</dd><dt>메일 저장 공간</dt><dd>${gb(p.limits.mail_storage)}</dd><dt>월간 메일 통수</dt><dd>${p.limits.mail_msgs.toLocaleString('ko-KR')}통(송신+수신)</dd><dt>메일함</dt><dd>${p.limits.mailboxes}개</dd></dl>
|
||||
<h3>추가 옵션</h3><ul>${p.options.map((o) => `<li>${esc(o.name)} — ${money(o.price)}${o.per === 'site' ? ' (사이트당)' : ''}</li>`).join('')}</ul>
|
||||
<h3>제공 범위</h3><ul>${p.includes.map((x) => `<li>${esc(x)}</li>`).join('')}</ul><h3>제외</h3><ul>${p.excludes.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>
|
||||
${p.deferred.length ? `<h3>미확정 정책</h3><ul>${p.deferred.map((x) => `<li><span class="badge warning">! 미확정 정책</span> ${esc(x.key)} — ${esc(x.note)}</li>`).join('')}</ul>` : ''}
|
||||
<p class="sub">사용량 경고 ${p.warnLevels.join('·')}% · 승인 방식: 운영자 수동 승인 · 적용 ${esc(p.effectiveFrom.slice(0, 10))}</p>
|
||||
<div class="actions"><a class="btn primary" href="/order/new?product=${esc(p.id)}">주문</a></div></div>`).join('')}</div>` +
|
||||
`<div class="card"><h2>도메인 등록</h2>${d.domainRegistration.enabled ? `<p>등록기관(${esc(d.domainRegistration.registrar || '')})을 통해 도메인을 검색·등록할 수 있습니다.</p>` : `<p class="muted">${esc(d.domainRegistration.note)}</p>`}</div>`;
|
||||
} catch (e) { aw.state(el, 'failed', e.message, { retry: load }); }
|
||||
}
|
||||
load();
|
||||
})();
|
||||
Reference in New Issue
Block a user