/* portal/relocation/public/relocation.js — 서버 이전 화면(§21). 경계값·조건·진행·취소·되돌리기 */
(() => {
const { api, esc, state, toast, confirmDanger, poll, job, fmt } = window.aw;
const el = document.getElementById('rl-site'); if (!el) return; const siteId = el.dataset.site;
const ST = { awaiting_approval: ['waiting', '승인 대기'], queued: ['waiting', '처리 대기'], running: ['running', '진행 중'], paused: ['warning', '일시 중지'], cancelling: ['warning', '취소 중'], cancelled: ['neutral', '취소됨'], done: ['done', '완료'], failed: ['failed', '실패'], reverting: ['running', '되돌리는 중'], reverted: ['ok', '되돌림 완료'], rejected: ['failed', '거절됨'], needs_user: ['warning', '사용자 확인 필요'] };
const badge = (k) => { const [c, l] = ST[k] || ['neutral', k]; return `${esc(l)}`; };
const v = (x, u = '') => (x == null ? '미확정(후속 상품정책)' : esc(x) + u);
const bounds = (b) => `
- 취소 가능 마지막 단계
- ${esc(b.lastCancelLabel)}
- 쓰기 제한
- ${b.writeFreeze ? `${esc(b.writeFreeze.startsAt)} ~ ${esc(b.writeFreeze.endsAt)} — ${esc(b.writeFreeze.note)}` : '없음(정적 사이트)'}
- 목표 중단 시간
- ${v(b.targetDowntimeMin, '분')}
- 새 위치가 유일한 운영 위치가 되는 시점
- ${esc(b.soleLocationAt)}
- 전환 후 되돌리기 기간
- ${v(b.revertHours, '시간')}
- 이전 위치 보존·삭제
- ${b.oldRetentionDays != null ? `전환 후 ${b.oldRetentionDays}일 보존 뒤 삭제` : v(null)}
- DNS·인증서 반영 대기 중
- ${esc(b.dnsWaitHandling)}
- 비용
- ${b.chargeable ? v(b.cost) + ' ' + esc(b.currency) : '없음(플랫폼 사유)'}
`;
const render = async () => {
let d; try { d = await api('GET', `/api/relocation/sites/${siteId}`); } catch (e) { state(el, e.status === 403 ? 'forbidden' : e.code === 'hal_unavailable' ? 'stale' : 'failed', e.message, { retry: render }); return false; }
if (!d.managed) { state(el, 'empty', '외부 배포 사이트는 Ariweb 가 서버를 관리하지 않아 서버 이전 대상이 아닙니다'); return false; }
const open = d.relocations.find((r) => !['done', 'failed', 'cancelled', 'reverted', 'rejected'].includes(r.status));
el.innerHTML = `이전 안내
- 현재 위치
- ${esc(d.currentLocation)}
- 실행 형태
- ${d.runtime === 'static' ? '정적 사이트(조건이 맞으면 플랫폼이 자동 이전할 수 있습니다)' : '동적 사이트(운영자 승인과 작업 시간 확인 뒤 이전)'}
- 자동 이전 조건
- ${d.auto.ok ? '충족' : d.auto.reasons.map(esc).join('
')}
고객 요청 이전(유료)
${bounds(d.boundaries.customer)}
플랫폼 사유 이전(장애 회피·용량 조정, 무료)
${bounds(d.boundaries.platform)}
${open ? '' : ''}
${d.relocations.length ? d.relocations.map((r) => `${badge(r.status)} ${r.kind === 'customer_paid' ? '고객 요청' : r.kind === 'auto_static' ? '자동(정적)' : '승인형'} · 요청 ${esc(fmt(r.created_at))}
이유: ${esc(r.reason || '')}${r.scheduled_at ? ` · 예정 ${esc(fmt(r.scheduled_at))}` : ''}${r.old_retained_until ? ` · 이전 위치 보존 종료 ${esc(fmt(r.old_retained_until))}` : ''}${r.charged ? ` · 비용 ${r.cost}` : ' · 과금 없음'}
${['awaiting_approval', 'queued', 'running'].includes(r.status) ? `` : ''}${r.status === 'done' ? `` : ''}
`).join('') : ''}`;
for (const r of d.relocations) if (r.job) job(`#rl-job-${r.id}`, r.job, { onAction: async (a) => { try { if (a === 'cancel') await api('POST', `/api/relocation/${r.id}/cancel`); toast('처리했습니다'); render(); } catch (e) { toast(e.message); } } });
el.querySelectorAll('[data-cancel]').forEach((b) => (b.onclick = async () => { try { await api('POST', `/api/relocation/${b.dataset.cancel}/cancel`); toast('취소했습니다'); render(); } catch (e) { toast(e.message); } }));
el.querySelectorAll('[data-revert]').forEach((b) => (b.onclick = async () => { if (!(await confirmDanger({ title: '이전 위치로 되돌리기', target: '이 사이트', impacts: ['전환 뒤 새 위치에서 생긴 변경은 사라질 수 있습니다', '되돌리기 기간 안에서만 가능합니다'], confirmLabel: '되돌리기' }))) return; try { await api('POST', `/api/relocation/${b.dataset.revert}/revert`, { confirm: true }); toast('되돌리기를 시작했습니다'); render(); } catch (e) { toast(e.message); } }));
const req = document.getElementById('rl-req'); if (req) req.onclick = async () => { if (!(await confirmDanger({ title: '서버 이전 요청(유료)', target: '이 사이트', impacts: ['운영자 승인 뒤 안내된 시간에 진행됩니다', d.boundaries.customer.writeFreeze ? '최종 반영~전환 사이 쓰기가 제한됩니다' : '정적 사이트라 쓰기 제한은 없습니다', `비용: ${d.boundaries.customer.cost ?? '미확정'} ${d.boundaries.customer.currency}`], confirmLabel: '요청' }))) return; try { await api('POST', `/api/relocation/sites/${siteId}`, { reason: '고객 요청', confirm: true }); toast('이전을 요청했습니다'); render(); } catch (e) { toast(e.message); } };
return true;
};
poll(render, 6000);
})();