Files
AriWeb-Suite/portal/lifecycle/public/lifecycle.js
T

29 lines
11 KiB
JavaScript

/* portal/lifecycle/public/lifecycle.js — 생명주기 화면(§23.3)·사이트 수 반환 체크리스트(§14)·메일 생명주기 */
(() => {
const { api, esc, state, toast, confirmDanger, poll, fmt } = window.aw;
const LB = { draft: ['neutral', '초안'], building: ['neutral', '제작 중'], deploying: ['running', '배포 중'], live: ['ok', '운영 중'], needs_domain: ['warning', '도메인 설정 필요'], paused: ['warning', '일시 중지'], cancel_scheduled: ['warning', '해지 예약'], readonly_retention: ['warning', '읽기 전용 보존'], pending_delete: ['failed', '삭제 대기'], deleted: ['neutral', '삭제 완료'], delete_hold: ['failed', '삭제 보류'], active: ['ok', '운영 중'] };
const badge = (k) => { const [c, l] = LB[k] || ['neutral', k]; return `<span class="badge ${c}">${esc(l)}</span>`; };
const d8 = (x) => (x ? esc(fmt(x)) : '—');
const el = document.getElementById('lc-site');
if (el) { const id = el.dataset.site; let choice = null;
const render = async () => { let d; try { d = await api('GET', `/api/lifecycle/sites/${id}`); } catch (e) { state(el, e.status === 403 ? 'forbidden' : 'failed', e.message, { retry: render }); return false; } const v = d.view; const sc = d.siteCount; const ci = d.cancelImpact; const pi = d.pauseImpact; choice = choice || v.mailChoice;
el.innerHTML = `${v.frozen ? `<div class="alert critical">${esc(v.frozenNote)}</div>` : ''}<div class="card"><h2>${badge(v.site.lifecycle)} ${esc(v.site.name)}</h2><dl class="kv"><dt>공개 서비스</dt><dd>${esc(v.publicAccess)}</dd><dt>관리자 접근</dt><dd>${esc(v.adminAccess)}</dd><dt>운영 사이트 수</dt><dd>${v.siteCountConsumed ? '소비 중(일시 중지도 포함)' : '소비하지 않음'}</dd><dt>용량 권리</dt><dd>${v.capacityConsumed.length ? v.capacityConsumed.map((a) => `${esc(a.resource)} ${a.amount}`).join(', ') : '없음'}</dd><dt>청구</dt><dd>${esc(v.billing)}</dd><dt>도메인</dt><dd>${v.domains.length ? v.domains.map((x) => `${esc(x.name)}${x.primary ? '(대표)' : ''}: ${x.routed ? '연결' : '해제'}`).join(', ') : '없음'}</dd><dt>메일</dt><dd>${v.mail.length ? v.mail.map((m) => `${esc(m.domain)}: ${esc((LB[m.lifecycle] || [0, m.lifecycle])[1])}`).join(', ') : '없음'}</dd><dt>백업</dt><dd>${v.backup ? `옵션 ${esc(v.backup.status)}${v.backup.deleteAfter ? ` · 백업 삭제 예정 ${d8(v.backup.deleteAfter)}` : ''}` : '없음'}</dd><dt>내보내기</dt><dd>${v.exportAvailable ? '가능' : '불가(삭제됨)'}</dd><dt>재개</dt><dd>${v.resumeAvailable ? '가능' : '불가'}</dd><dt>되돌릴 수 없는 시점</dt><dd>${esc(v.pointOfNoReturn)}</dd><dt>보존 종료</dt><dd>${d8(v.retentionUntil)}</dd><dt>삭제 예정</dt><dd>${d8(v.deleteAfter)}${v.deleteHold ? ` <span class="err">삭제 보류 — ${esc(v.deleteHold.reason)} · 문의 ${esc(v.deleteHold.contact)}</span>` : ''}</dd><dt>다음 알림</dt><dd>${d8(v.nextNoticeAt)}</dd><dt>보존 정책</dt><dd>사이트 ${v.retentionPolicy.site_days}일 · 메일 ${v.retentionPolicy.mail_days}일 · 소스 ${v.retentionPolicy.source_days}일 · 백업 ${v.retentionPolicy.backup_days}일 (${esc(v.retentionPolicy.source)})</dd></dl><p class="sub">${esc(v.mailBackupNote)}</p>
<div class="actions">${d.canManage && !v.frozen ? `${['live', 'needs_domain'].includes(v.site.lifecycle) ? '<button class="btn" id="lc-pause">일시 중지</button>' : ''}${v.site.lifecycle === 'paused' ? '<button class="btn primary" id="lc-resume">재개</button>' : ''}${['live', 'needs_domain', 'paused', 'draft', 'building'].includes(v.site.lifecycle) ? '<button class="btn danger" id="lc-cancel">해지</button>' : ''}${v.site.lifecycle === 'cancel_scheduled' && v.resumeAvailable ? '<button class="btn" id="lc-undo">해지 예약 취소</button>' : ''}` : ''}${v.exportAvailable ? '<button class="btn" id="lc-export">내보내기 요청</button>' : ''}</div></div>
<div class="card"><h2>운영 사이트 수 반환 (8단계)</h2><ol class="steps">${sc.steps.map((s) => `<li class="${s.ok ? 'done' : 'pending'}"><span class="s" aria-hidden="true">${s.ok ? '✔' : '○'}</span><span><b>${esc(s.title)}</b> — ${esc(s.note || '')}${s.link ? ` <a href="${esc(s.link)}">이동</a>` : ''}</span></li>`).join('')}</ol>${v.mail.length ? `<div class="f"><label>대표 메일 처리</label><select id="lc-mc"><option value="">선택</option><option value="keep" ${choice === 'keep' ? 'selected' : ''}>독립 메일 서비스로 전환해 유지</option><option value="end" ${choice === 'end' ? 'selected' : ''}>종료(30일 읽기·내보내기 후 삭제)</option></select></div>` : ''}<label class="f"><span><input type="checkbox" id="lc-ack"> 데이터와 보존 기한(보존 종료 ${d8(v.retentionUntil)}, 삭제 ${d8(v.deleteAfter)})을 확인했습니다</span></label><div class="actions"><button class="btn danger" id="lc-return" ${sc.consumes && d.canManage && !v.frozen ? '' : 'disabled'}>사이트 수 반환</button></div></div>
${v.events.length ? `<div class="card"><h2>상태 변경 기록</h2><table class="r"><thead><tr><th>시각</th><th>변경</th><th>주체</th><th>사유</th></tr></thead><tbody>${v.events.map((e) => `<tr><td data-l="시각">${d8(e.at)}</td><td data-l="변경">${esc((LB[e.from_state] || [0, e.from_state || '—'])[1])} → ${esc((LB[e.to_state] || [0, e.to_state])[1])}</td><td data-l="주체">${e.by === 'system' ? '자동' : '사용자'}</td><td data-l="사유">${esc(e.reason || '')}</td></tr>`).join('')}</tbody></table></div>` : ''}`;
const q = (i) => document.getElementById(i); const go = async (p, body, msg) => { try { await api('POST', `/api/lifecycle/sites/${id}/${p}`, body); toast(msg || '처리했습니다'); render(); } catch (e) { toast(e.message + (e.detail && e.detail.link ? ' — 상품 구매로 이동' : '')); if (e.detail && e.detail.link) setTimeout(() => (location.href = e.detail.link), 1500); } };
if (q('lc-pause')) q('lc-pause').onclick = async () => { if (await confirmDanger({ title: '일시 중지', target: v.site.name, impacts: [`공개 접속: ${pi.publicAccess}`, `관리자 접근: ${pi.adminAccess}`, `메일: ${pi.mail}`, `백업: ${pi.backups}`, `사용량·비용: ${pi.usageAndCost}`, `재개: ${pi.resume}`], confirmLabel: '중지' })) go('pause', { confirm: true }); };
if (q('lc-resume')) q('lc-resume').onclick = () => go('resume', {}, '재개했습니다');
if (q('lc-undo')) q('lc-undo').onclick = () => go('undo-cancel', {}, '해지 예약을 취소했습니다');
if (q('lc-export')) q('lc-export').onclick = () => go('export', {}, '내보내기를 요청했습니다');
if (q('lc-cancel')) q('lc-cancel').onclick = async () => { const mc = q('lc-mc') ? q('lc-mc').value : null; if (ci.mailOptions && !mc) return toast('대표 메일 처리를 먼저 선택하세요'); if (!q('lc-ack').checked) return toast('보존 기한 확인에 표시하세요'); if (await confirmDanger({ title: '해지', target: `${ci.target.name} (연결: 도메인 ${ci.linked.domains.join(', ') || '없음'} · 메일 ${ci.linked.siteMail.join(', ') || '없음'} · 백업 ${ci.linked.backupOption || '없음'})`, impacts: [...ci.immediateStops.map((s) => '즉시 중지: ' + s), `남는 데이터: ${ci.remainingData}`, `보존 종료 ${fmt(ci.retentionUntil)} · 삭제 ${fmt(ci.deleteAfter)}`, ci.export, ci.unpaid.length ? `미결제 ${ci.unpaid.map((o) => o.order_no).join(', ')}` : '미결제 없음', ci.refund, `되돌릴 수 없는 시점: ${ci.pointOfNoReturn}`, ci.siteCount], typeToConfirm: '해지', confirmLabel: '해지' })) go('cancel', { confirm: true, acknowledge: true, mailChoice: mc }, '해지를 예약했습니다'); };
if (q('lc-mc')) q('lc-mc').onchange = async () => { choice = q('lc-mc').value || null; const r = await api('GET', `/api/lifecycle/sites/${id}/site-count-return?mailChoice=${encodeURIComponent(choice || '')}&acknowledge=${q('lc-ack').checked ? 1 : 0}`); d.siteCount = r.checklist; render(); };
if (q('lc-return')) q('lc-return').onclick = async () => { const mc = q('lc-mc') ? q('lc-mc').value : null; if (!q('lc-ack').checked) return toast('데이터와 보존 기한 확인에 표시하세요'); if (await confirmDanger({ title: '운영 사이트 수 반환', target: v.site.name, impacts: ['공개 라우팅이 해제되고 대표 메일은 선택대로 처리됩니다', '반환 뒤 이 사이트는 공개 운영에 사이트 수를 다시 확보해야 합니다', mc === 'keep' ? '대표 메일은 독립 메일로 전환됩니다(권리 필요)' : mc === 'end' ? '대표 메일은 종료됩니다' : '대표 메일 없음'], typeToConfirm: '반환', confirmLabel: '반환' })) go('site-count-return', { mailChoice: mc, acknowledge: true, confirm: true }, '사이트 수를 반환했습니다'); };
return true; };
poll(render, 10000); }
const ml = document.getElementById('lc-mail');
if (ml) { const id = ml.dataset.id; const render = async () => { let d; try { d = await api('GET', `/api/lifecycle/mail/${id}`); } catch (e) { state(ml, e.status === 403 ? 'forbidden' : 'failed', e.message); return; } const v = d.view;
ml.innerHTML = `${v.frozen ? '<div class="alert critical">소유권 분쟁으로 설정 변경·삭제가 동결되었습니다</div>' : ''}<div class="card"><h2>${badge(v.mailService.lifecycle)} ${esc(v.mailService.name)} <span class="sub">${esc(v.mailService.domain || '')}</span></h2><dl class="kv"><dt>송수신</dt><dd>${esc(v.sendReceive)}</dd><dt>관리자 접근</dt><dd>${esc(v.adminAccess)}</dd><dt>권리 소비</dt><dd>${esc(v.consumes)}</dd><dt>청구</dt><dd>${esc(v.billing)}</dd><dt>내보내기</dt><dd>${v.exportAvailable ? '가능' : '불가'}</dd><dt>재개</dt><dd>${v.resumeAvailable ? '가능' : '불가'}</dd><dt>되돌릴 수 없는 시점</dt><dd>${esc(v.pointOfNoReturn)}</dd><dt>보존 종료</dt><dd>${d8(v.retentionUntil)}</dd><dt>삭제 예정</dt><dd>${d8(v.deleteAfter)}${v.deleteHold ? ` <span class="err">삭제 보류 — 문의 ${esc(v.deleteHold.contact)}</span>` : ''}</dd><dt>다음 알림</dt><dd>${d8(v.nextNoticeAt)}</dd></dl><p class="sub">${esc(v.backupNote)}</p><div class="actions">${v.mailService.lifecycle === 'active' && !v.frozen ? '<button class="btn danger" id="lcm-cancel">메일 서비스 해지</button>' : ''}</div></div>`;
const b = document.getElementById('lcm-cancel'); if (b) b.onclick = async () => { if (await confirmDanger({ title: '메일 서비스 해지', target: v.mailService.domain || v.mailService.name, impacts: ['송수신이 즉시 중지됩니다', `기존 메일 읽기·내보내기는 ${v.retentionPolicy.mail_days}일 동안 가능합니다`, '보존 종료 뒤 삭제되며 복구할 수 없습니다', '메일 원문은 고객용 백업 대상이 아닙니다'], typeToConfirm: '해지', confirmLabel: '해지' })) { try { await api('POST', `/api/lifecycle/mail/${id}/cancel`, { confirm: true, acknowledge: true }); toast('해지했습니다'); render(); } catch (e) { toast(e.message); } } }; }; render(); }
})();