/* portal/transfer/public/transfer.js — 소유권 이전 화면(§22): 목록·새 요청(범위 미리보기)·상세(수락·거절·취소·분쟁) */
(() => {
const { api, esc, state, toast, confirmDanger, fmt } = window.aw;
const ST = { requested: ['waiting', '수락 대기'], accepted: ['waiting', '권리 확인'], on_hold: ['warning', '보류(권리 부족)'], reviewing: ['waiting', '운영자 심사'], approved: ['running', '승인됨'], completed: ['done', '완료'], rejected: ['failed', '거절(운영자)'], declined: ['failed', '거절(받는 쪽)'], cancelled: ['neutral', '취소'], expired: ['neutral', '만료'], disputed: ['failed', '분쟁 보류'] };
const badge = (k) => { const [c, l] = ST[k] || ['neutral', k]; return `${esc(l)}`; };
const scopeHtml = (sc, ex) => !sc ? '' : `
이전 대상
${sc.sites.map((s) => `${esc(s.name)} ${esc(s.lifecycle)}${s.siteCount === 'in_use' ? ' · 운영 사이트 수 사용 중' : ''}- 도메인
- ${s.domains.map((d) => esc(d.name) + (d.is_primary ? '(대표)' : '')).join(', ') || '없음'}
- 사이트 대표 메일
- ${s.siteMail.map((m) => esc(m.domain)).join(', ') || '없음'}
- 백업
- ${s.backup ? `옵션 ${esc(s.backup.status)} · 백업 ${s.backup.backups}개` : '없음'}
- 배정 용량
- ${s.allocations.map((a) => `${esc(a.resource)} ${a.amount}`).join(', ') || '없음'}
- 진행 작업
- ${s.inProgress.length ? s.inProgress.map((c) => esc(c.type + ' ' + c.status)).join(', ') : '없음'}
- 미납
- ${s.unpaid.length ? s.unpaid.map((o) => `${esc(o.order_no)} ${o.total}`).join(', ') : '없음'}
`).join('')}${sc.mailServices.map((m) => `${esc(m.name)} (독립 메일 ${esc(m.domain)}) 메일함 ${m.mailboxes}개 · 배정 ${m.allocations.map((a) => `${esc(a.resource)} ${a.amount}`).join(', ') || '없음'}
`).join('')}이전하지 않는 항목: ${ex.map(esc).join(', ')}
`;
const list = document.getElementById('tr-list');
if (list) (async () => { try { const d = await api('GET', '/api/transfer'); if (!d.transfers.length) return state(list, 'empty', '소유권 이전 요청이 없습니다'); list.innerHTML = ``; } catch (e) { state(list, e.status === 403 ? 'forbidden' : 'failed', e.message); } })();
const nw = document.getElementById('tr-new');
if (nw) (async () => { try { const d = await api('GET', '/api/transfer/candidates'); nw.innerHTML = ``;
const sel = () => ({ siteIds: [...nw.querySelectorAll('input[name=site]:checked')].map((i) => i.value), mailServiceIds: [...nw.querySelectorAll('input[name=mail]:checked')].map((i) => i.value) });
document.getElementById('tr-prev').onclick = async () => { try { const r = await api('POST', '/api/transfer/preview', sel()); document.getElementById('tr-preview').innerHTML = scopeHtml(r.scope, d.excluded) + (r.scope.blocking.length ? `진행 중 작업이 끝나야 요청할 수 있습니다: ${r.scope.blocking.map((b) => esc(b.subject + ' ' + b.type)).join(', ')}
` : ''); } catch (e) { toast(e.message); } };
document.getElementById('tr-form').onsubmit = async (ev) => { ev.preventDefault(); const s = sel(); const to = document.getElementById('to').value.trim(); if (!(await confirmDanger({ title: '소유권 이전 요청', target: `${to} 에게 사이트 ${s.siteIds.length}개 · 메일 ${s.mailServiceIds.length}개`, impacts: ['받는 쪽 수락과 운영자 심사 뒤 한 번에 이전됩니다', '완료 시각부터 청구 책임이 넘어갑니다', '이전 뒤 우리 그룹 구성원은 접근할 수 없습니다'], typeToConfirm: '이전', confirmLabel: '요청' }))) return; try { const r = await api('POST', '/api/transfer', { ...s, toEmail: to, confirm: true }); window.aw.markClean(); location.href = `/transfers/${r.transfer.id}`; } catch (e) { toast(e.message); } };
} catch (e) { state(nw, e.status === 403 ? 'forbidden' : 'failed', e.message); } })();
const det = document.getElementById('tr-detail');
if (det) { const id = det.dataset.id; const token = new URLSearchParams(location.search).get('token'); const render = async () => { try { const d = await api('GET', `/api/transfer/${id}${token ? '?token=' + encodeURIComponent(token) : ''}`); const x = d.transfer;
det.innerHTML = `${badge(x.status)} ${esc(x.fromGroupName)} → ${esc(x.toGroupName)}
- 요청
- ${esc(fmt(x.created_at))}
- 만료
- ${esc(fmt(x.expires_at))}
- 취소 가능
- ${esc(x.cancelDeadline)} (보내는 쪽) · ${esc(x.declineDeadline)} (받는 쪽)
- 청구
- ${x.billing.senderEndsAt ? `종료/시작 ${esc(fmt(x.billing.senderEndsAt))} · ` : ''}${esc(x.billing.note)}
${x.hold ? `- 보류 사유
- ${x.hold.map((h) => `${esc(h.label)} ${h.shortage} 부족 — ${esc(h.product)}`).join('
')} ` : ''}${x.status === 'disputed' ? `- 분쟁
- 기존 서비스 유지 · 설정 변경·삭제·추가 이전 동결 · 문의: 운영자 지원 창구 ${esc(x.dispute_note || '')}
` : ''}${x.completion ? `- 완료
- 사이트 ${x.completion.moved.sites} · 도메인 ${x.completion.moved.domains} · 메일 ${x.completion.moved.mailServices} · 배정 ${x.completion.moved.allocations}
` : ''}
${scopeHtml(x.scopeDetail, x.excluded)}
${x.canAccept ? '' : ''}${x.canRecheck ? '' : ''}${x.canDecline ? '' : ''}${x.canCancel ? '' : ''}${x.canDispute ? '' : ''}
`;
const q = (i) => document.getElementById(i); const act = async (p, body) => { try { await api('POST', `/api/transfer/${id}/${p}`, body); toast('처리했습니다'); render(); } catch (e) { toast(e.message); } };
if (q('t-acc')) q('t-acc').onclick = async () => { if (await confirmDanger({ title: '소유권 이전 수락', target: '현재 사용자 그룹으로 받습니다', impacts: ['우리 그룹의 상품 권리로 대상을 받습니다(부족하면 보류)', '운영자 심사 뒤 완료 시각부터 청구 책임이 시작됩니다'], confirmLabel: '수락' })) act('accept', { confirm: true, token }); };
if (q('t-re')) q('t-re').onclick = () => act('recheck'); if (q('t-dec')) q('t-dec').onclick = () => act('decline', { reason: '' }); if (q('t-can')) q('t-can').onclick = async () => { if (await confirmDanger({ title: '요청 취소', target: '이 이전 요청', impacts: ['받는 쪽에 취소가 알려집니다'], confirmLabel: '취소' })) act('cancel'); };
if (q('t-dis')) q('t-dis').onclick = async () => { if (await confirmDanger({ title: '분쟁 신고', target: '이 이전 요청', impacts: ['기존 서비스는 유지됩니다', '설정 변경·삭제·추가 이전이 동결됩니다', '운영자가 확인할 때까지 보류됩니다'], typeToConfirm: '분쟁', confirmLabel: '신고' })) act('dispute', { confirm: true, note: '' }); };
} catch (e) { state(det, e.status === 403 ? 'forbidden' : e.status === 404 ? 'empty' : 'failed', e.message); } }; render(); }
})();