/* 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 = `${d.transfers.map((x) => ``).join('')}
상태방향상대대상요청만료
${badge(x.status)}${x.role === 'sender' ? '보냄' : '받음'}${esc(x.role === 'sender' ? x.toGroupName : x.fromGroupName)}사이트 ${(x.scope.siteIds || []).length} · 메일 ${(x.scope.mailServiceIds || []).length}${esc(fmt(x.created_at))}${esc(fmt(x.expires_at))}
`; } 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 = `

대상 선택

${d.sites.length ? d.sites.map((s) => ``).join('') : '

사이트가 없습니다

'}

독립 메일 서비스(명시적으로 고른 것만 포함)

${d.mailServices.length ? d.mailServices.map((m) => ``).join('') : '

없음

'}
받는 사람이 가입 사용자이면 알림으로, 아니면 이메일로 수락 링크가 전달됩니다. 요청은 7일 뒤 만료되며 운영자 승인 전까지 취소할 수 있습니다

이전하지 않는 항목: ${d.excluded.map(esc).join(', ')}

`; 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(); } })();