/** * portal/ui/layout.mjs — 고객 포털 공통 화면 틀(§2.1·§2.3). * * 서버가 HTML 뼈대(상단: 현재 그룹·알림·도움말·사용자 메뉴 / 왼쪽: 메뉴 / 본문: 제목·현재 위치·주요 작업 버튼·주의 항목)를 내고, * 각 화면의 데이터는 같은 origin 의 /api/* 를 fetch 하는 작은 화면 스크립트(/ui/app.js + 영역별 js)가 채운다. * 모든 화면은 '불러오는 중·비어 있음·정보 지연·부분 실패·전체 실패·권한 없음' 상태를 공통 컴포넌트(aw-state)로 보인다. * 색만으로 상태를 구분하지 않는다: 상태 배지는 항상 글자(라벨)와 아이콘 문자를 함께 쓴다. 360px 너비에서 표의 핵심 값과 버튼이 가려지지 않게 카드형으로 접힌다. */ import { esc } from '../../lib/http.mjs'; import { t } from '../../lib/i18n.mjs'; const MENU = [ ['dashboard', '/', '▦'], ['sites', '/sites', '▣'], ['mail', '/mail', '✉'], ['domains', '/domains', '◎'], ['backup', '/backup', '⟲'], ['billing', '/billing', '₩'], ['usage', '/usage', '▥'], ['group', '/group', '⚇'], ]; /** * 전체 화면 HTML. * @param {object} o { title, nav(현재 메뉴 키), crumbs:[{label,href}], actions:[{label,href|onclick,kind}], user, group, groups:[], unread, body(html), scripts:[js 경로], alerts:[{severity,text,href}] } */ export function page(o) { const user = o.user || null; const group = o.group || null; const groups = o.groups || []; const nav = MENU.map(([k, href, ic]) => `${esc(t('nav.' + k))}`).join(''); const crumbs = (o.crumbs || []).map((c, i, a) => (i === a.length - 1 || !c.href) ? `${esc(c.label)}` : `${esc(c.label)}`).join(' '); const actions = (o.actions || []).map((a) => a.href ? `${esc(a.label)}` : ``).join(''); const alerts = (o.alerts || []).map((a) => `${a.severity === 'critical' ? '⚠ ' : a.severity === 'info' ? 'ⓘ ' : '! '}${esc(a.text)}`).join(''); return `${esc(o.title || '')} · ${esc(t('app.portal'))}
${esc(t('app.name'))} ${user ? `
` : ``}
${user ? `` : ''}
${crumbs}

${esc(o.title || '')}

${actions}
${alerts}
${o.body || ''}
${(o.scripts || []).map((s) => ``).join('')} `; } /** 상태 배지(글자+기호, 색은 보조) */ export function badge(kind, code, label) { const sym = { ok: '●', done: '✔', failed: '✘', warning: '!', info: 'ⓘ', running: '◌', waiting: '◔', paused: '⏸', neutral: '○' }[kind] || '○'; return ` ${esc(label)}`; } /** 공통 상태 패널(불러오는 중·비어 있음·지연·부분 실패·전체 실패·권한 없음) */ export function statePanel(state, text, { href, retry } = {}) { const labels = { loading: t('state.loading'), empty: t('state.empty'), stale: t('state.stale'), partial: t('state.partial'), failed: t('state.failed'), forbidden: t('state.forbidden') }; return `
${esc(labels[state] || state)}${text ? `

${esc(text)}

` : ''}${href ? `이동` : ''}${retry ? `` : ''}
`; } /** 파괴적 작업 확인 상자 데이터(화면 스크립트 aw.confirmDanger 가 그린다): 대상·영향·입력 확인 */ export function dangerSpec({ title, target, impacts = [], typeToConfirm = null, confirmLabel = '실행' }) { return JSON.stringify({ title, target, impacts, typeToConfirm, confirmLabel }); }