/* 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 `${esc(l)}`; };
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 ? `
${esc(v.frozenNote)}
` : ''}${badge(v.site.lifecycle)} ${esc(v.site.name)}
- 공개 서비스
- ${esc(v.publicAccess)}
- 관리자 접근
- ${esc(v.adminAccess)}
- 운영 사이트 수
- ${v.siteCountConsumed ? '소비 중(일시 중지도 포함)' : '소비하지 않음'}
- 용량 권리
- ${v.capacityConsumed.length ? v.capacityConsumed.map((a) => `${esc(a.resource)} ${a.amount}`).join(', ') : '없음'}
- 청구
- ${esc(v.billing)}
- 도메인
- ${v.domains.length ? v.domains.map((x) => `${esc(x.name)}${x.primary ? '(대표)' : ''}: ${x.routed ? '연결' : '해제'}`).join(', ') : '없음'}
- 메일
- ${v.mail.length ? v.mail.map((m) => `${esc(m.domain)}: ${esc((LB[m.lifecycle] || [0, m.lifecycle])[1])}`).join(', ') : '없음'}
- 백업
- ${v.backup ? `옵션 ${esc(v.backup.status)}${v.backup.deleteAfter ? ` · 백업 삭제 예정 ${d8(v.backup.deleteAfter)}` : ''}` : '없음'}
- 내보내기
- ${v.exportAvailable ? '가능' : '불가(삭제됨)'}
- 재개
- ${v.resumeAvailable ? '가능' : '불가'}
- 되돌릴 수 없는 시점
- ${esc(v.pointOfNoReturn)}
- 보존 종료
- ${d8(v.retentionUntil)}
- 삭제 예정
- ${d8(v.deleteAfter)}${v.deleteHold ? ` 삭제 보류 — ${esc(v.deleteHold.reason)} · 문의 ${esc(v.deleteHold.contact)}` : ''}
- 다음 알림
- ${d8(v.nextNoticeAt)}
- 보존 정책
- 사이트 ${v.retentionPolicy.site_days}일 · 메일 ${v.retentionPolicy.mail_days}일 · 소스 ${v.retentionPolicy.source_days}일 · 백업 ${v.retentionPolicy.backup_days}일 (${esc(v.retentionPolicy.source)})
${esc(v.mailBackupNote)}
${d.canManage && !v.frozen ? `${['live', 'needs_domain'].includes(v.site.lifecycle) ? '' : ''}${v.site.lifecycle === 'paused' ? '' : ''}${['live', 'needs_domain', 'paused', 'draft', 'building'].includes(v.site.lifecycle) ? '' : ''}${v.site.lifecycle === 'cancel_scheduled' && v.resumeAvailable ? '' : ''}` : ''}${v.exportAvailable ? '' : ''}
운영 사이트 수 반환 (8단계)
${sc.steps.map((s) => `- ${s.ok ? '✔' : '○'}${esc(s.title)} — ${esc(s.note || '')}${s.link ? ` 이동` : ''}
`).join('')}
${v.mail.length ? `
` : ''}
${v.events.length ? `상태 변경 기록
| 시각 | 변경 | 주체 | 사유 |
${v.events.map((e) => `| ${d8(e.at)} | ${esc((LB[e.from_state] || [0, e.from_state || '—'])[1])} → ${esc((LB[e.to_state] || [0, e.to_state])[1])} | ${e.by === 'system' ? '자동' : '사용자'} | ${esc(e.reason || '')} |
`).join('')}
` : ''}`;
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 ? '소유권 분쟁으로 설정 변경·삭제가 동결되었습니다
' : ''}${badge(v.mailService.lifecycle)} ${esc(v.mailService.name)} ${esc(v.mailService.domain || '')}
- 송수신
- ${esc(v.sendReceive)}
- 관리자 접근
- ${esc(v.adminAccess)}
- 권리 소비
- ${esc(v.consumes)}
- 청구
- ${esc(v.billing)}
- 내보내기
- ${v.exportAvailable ? '가능' : '불가'}
- 재개
- ${v.resumeAvailable ? '가능' : '불가'}
- 되돌릴 수 없는 시점
- ${esc(v.pointOfNoReturn)}
- 보존 종료
- ${d8(v.retentionUntil)}
- 삭제 예정
- ${d8(v.deleteAfter)}${v.deleteHold ? ` 삭제 보류 — 문의 ${esc(v.deleteHold.contact)}` : ''}
- 다음 알림
- ${d8(v.nextNoticeAt)}
${esc(v.backupNote)}
${v.mailService.lifecycle === 'active' && !v.frozen ? '' : ''}
`;
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(); }
})();