/* portal/groups/public/groups.js — 사용자 그룹 설정 화면: 그룹 정보·담당자·구성원·초대·정지(영향 확인)·소유권 이전·탈퇴. */ (() => { const root = document.getElementById('group-root'); document.addEventListener('click', async (e) => { const b = e.target.closest('[data-action=accept-invite]'); if (!b) return; try { const r = await aw.api('POST', '/api/invites/accept', { token: b.dataset.token }); aw.toast(r.message); location.href = '/group'; } catch (err) { document.getElementById('inv-err').textContent = err.message; } }); if (!root) return; const canAdmin = ['owner', 'admin'].includes(root.dataset.role); const me = root.dataset.me; async function load() { try { const d = await aw.api('GET', '/api/group'); const g = d.group; const R = (r) => ({ owner: '소유자', admin: '관리자', billing: '결제 담당', member: '구성원', viewer: '조회 전용' })[r] || r; const memberOpts = (sel) => d.members.filter((m) => m.status === 'active').map((m) => ``).join(''); root.innerHTML = `

그룹 정보

${canAdmin ? '' : '

그룹 정보는 소유자·관리자만 바꿀 수 있습니다

'}

구성원 (${d.members.length})

${canAdmin ? '' : ''}
${d.members.map((m) => ``).join('')}
이름이메일별칭역할상태
${aw.esc(m.displayName)}${m.userId === me ? ' ● 나' : ''}${aw.esc(m.email)}${aw.esc(m.alias || '')} ✎${canAdmin && m.role !== 'owner' && m.userId !== me ? `` : R(m.role)}${m.status === 'active' ? '● 활성' : '⏸ 정지'}${canAdmin && m.role !== 'owner' && m.userId !== me ? (m.status === 'active' ? ` ` : ` `) + `` : ''}${root.dataset.role === 'owner' && m.userId !== me && m.status === 'active' ? ` ` : ''}

별칭(user-01 …)은 화면 표시용이며 로그인과 권한의 기준이 아닙니다.

${canAdmin ? `

초대 대기 (${d.invites.length})

${d.invites.length ? `${d.invites.map((i) => ``).join('')}
이메일역할만료
${aw.esc(i.email)}${R(i.role)}${i.expired ? '✘ 만료됨' : aw.esc(aw.fmt(i.expires_at))}${i.resend_count ? ` 재발송 ${i.resend_count}회` : ''}
` : '
비어 있음
'}
` : ''} ${g.kind !== 'personal' && root.dataset.role !== 'owner' ? '

그룹 탈퇴

' : ''}`; const gf = document.getElementById('gform'); gf.onsubmit = async (ev) => { ev.preventDefault(); const f = gf.elements; try { const r = await aw.api('PATCH', '/api/group', { name: f.name.value, contactEmail: f.contactEmail.value, contactPhone: f.contactPhone.value, billingUserId: f.billingUserId.value, techUserId: f.techUserId.value }); aw.markClean(); aw.toast(r.message || '저장했습니다'); } catch (err) { aw.toast(err.message); } }; root.querySelectorAll('select[data-act=role]').forEach((s) => { s.onchange = async () => { try { await aw.api('PATCH', '/api/group/members/' + s.dataset.uid, { role: s.value }); aw.toast('역할을 바꿨습니다'); } catch (err) { aw.toast(err.message); load(); } }; }); } catch (err) { aw.state(root, err.status === 403 ? 'forbidden' : 'failed', err.message, { retry: load }); } } root.addEventListener('click', async (e) => { const b = e.target.closest('[data-act]'); if (!b || b.tagName === 'SELECT') return; const act = b.dataset.act; const uid = b.dataset.uid; try { if (act === 'invite') { const m = aw.modal('

이메일로 초대

'); m.el.querySelector('#inv').onsubmit = async (ev) => { ev.preventDefault(); try { await aw.api('POST', '/api/group/invites', { email: m.el.querySelector('#iemail').value, role: m.el.querySelector('#irole').value }); m.close(); aw.toast('초대 메일을 보냈습니다'); load(); } catch (err) { m.el.querySelector('#inv-err').textContent = err.message; } }; } else if (act === 'alias') { const v = prompt('새 별칭(2~24자)'); if (v) { await aw.api('PATCH', '/api/group/members/' + uid, { alias: v }); load(); } } else if (act === 'suspend') { const r = await aw.api('GET', `/api/group/members/${uid}/impact`); const i = r.impact; if (await aw.confirmDanger({ title: '구성원 정지', target: b.closest('tr').children[1].textContent, impacts: [`이 구성원의 로그인 세션 ${i.sessions}개가 종료됩니다`, `아직 실행되지 않은 작업 ${i.pendingJobs.length}개가 취소됩니다${i.pendingJobs.length ? ': ' + i.pendingJobs.map((j) => j.label).join(', ') : ''}`, ...i.untouched.map((u) => `유지됨: ${u}`)], confirmLabel: '정지' })) { await aw.api('POST', `/api/group/members/${uid}/suspend`); load(); } } else if (act === 'unsuspend') { await aw.api('POST', `/api/group/members/${uid}/unsuspend`); load(); } else if (act === 'remove') { if (await aw.confirmDanger({ title: '구성원 제거', target: b.closest('tr').children[1].textContent, impacts: ['이 그룹의 사이트 권한이 모두 제거됩니다', '이 그룹을 보던 로그인 세션이 종료됩니다', '그룹의 상품·사이트·데이터는 유지됩니다'], confirmLabel: '제거' })) { await aw.api('DELETE', '/api/group/members/' + uid); load(); } } else if (act === 'transfer') { if (await aw.confirmDanger({ title: '그룹 소유권 이전', target: b.closest('tr').children[1].textContent, impacts: ['새 소유자가 결제·해지·구성원 관리의 최종 권한을 갖습니다', '현재 소유자는 관리자 역할이 됩니다', '되돌리려면 새 소유자가 다시 이전해야 합니다'], typeToConfirm: '이전', confirmLabel: '소유권 이전' })) { await aw.api('POST', '/api/group/transfer-ownership', { userId: uid }); location.reload(); } } else if (act === 'leave') { if (await aw.confirmDanger({ title: '그룹 탈퇴', target: '현재 그룹', impacts: ['이 그룹의 사이트·메일·결제 화면에 더 이상 접근할 수 없습니다'], confirmLabel: '탈퇴' })) { await aw.api('POST', '/api/group/leave'); location.href = '/'; } } else if (act === 'resend') { await aw.api('POST', `/api/group/invites/${b.dataset.id}/resend`); aw.toast('다시 보냈습니다'); load(); } else if (act === 'cancel-invite') { await aw.api('DELETE', `/api/group/invites/${b.dataset.id}`); load(); } } catch (err) { aw.toast(err.message); } }); load(); })();