- design.md v1.3: SCR-13~17 매핑·상세 스펙 순증 (기존 SCR-01~12 무수정) - React 이식 5화면 + Recharts 도입 + chartColors(§1 토큰)·shared.css 공통 프리미티브 - App.tsx 라우트 5종 + AppShell NavLink 배선 (신규 4항목, /_styleguide 네비 비노출) - QA: tsc 위양성 교정(tsconfig.app 기준)·미사용 import 제거 → tsc/vite build EXIT 0 - reviewer: 배포 가능(Critical/Major 0) — Minor 티켓 BACKLOG 등재, R15-01 홀 필터 배선 즉시 수정 - docs: UNDEVELOPED_BACKLOG.md 신설(세션 미개발 통합) + WORK_STATUS 갱신 - stitch: 재취득 4화면 + component_guide 2종 + _icons 7종 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
6.0 KiB
TypeScript
141 lines
6.0 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Bar,
|
|
BarChart,
|
|
CartesianGrid,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts';
|
|
import { CHART } from '../chartColors';
|
|
import {
|
|
ADMIN_KPIS,
|
|
LIVE_EVENTS,
|
|
TENANTS,
|
|
VISITOR_TREND,
|
|
} from './sampleAdmin';
|
|
import './admin.css';
|
|
|
|
const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`;
|
|
const THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`];
|
|
|
|
/*
|
|
* SCR-16 관리자 대시보드 (M18 랜딩). Stitch admin_dashboard 이식.
|
|
* - 시스템 개요: KPI 3 + 방문객 추이 막대(오늘/주간) + 진행중 이벤트 리스트.
|
|
* ★ 경계면(혼합): 지표 대부분 샘플(집계/센서 미구축). 플로팅 AI 봇은 P2 → 미구현(비노출).
|
|
* M18 본체(사용자·역할·감사로그·설정)는 후속 화면 세트(/admin/*).
|
|
*/
|
|
export function AdminDashboardPage() {
|
|
const [range, setRange] = useState<'today' | 'weekly'>('today');
|
|
const [tenant, setTenant] = useState(TENANTS[0]);
|
|
|
|
return (
|
|
<div className="kx-admin">
|
|
<header className="kx-admin__head">
|
|
<div>
|
|
<h1 className="kx-admin__title">시스템 개요</h1>
|
|
<p className="kx-admin__subtitle">실시간 지표 및 운영 상태</p>
|
|
</div>
|
|
<div className="kx-admin__head-actions">
|
|
{/* 플랫폼 슈퍼관리자 전용 테넌트 스위처(샘플) */}
|
|
<select
|
|
className="kx-select"
|
|
aria-label="테넌트 전환"
|
|
value={tenant}
|
|
onChange={(e) => setTenant(e.target.value)}
|
|
>
|
|
{TENANTS.map((t) => (
|
|
<option key={t} value={t}>{t}</option>
|
|
))}
|
|
</select>
|
|
<span className="kx-admin__status" role="status">
|
|
<span className="kx-admin__status-dot" aria-hidden="true" /> 시스템 정상
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="kx-admin__degraded-bar">
|
|
<span className="kx-bi__degraded">집계 API 대기 (샘플)</span>
|
|
실시간 방문객·주차·추이 위젯은 시연용 샘플입니다. 시스템관리(M18) 지표는 후속 연동됩니다.
|
|
</div>
|
|
|
|
<section className="kx-admin__kpis" aria-label="핵심 지표">
|
|
{ADMIN_KPIS.map((k) => (
|
|
<div key={k.key} className={`kx-kpi kx-kpi--${k.tone ?? 'normal'}`}>
|
|
<span className="kx-kpi__label">{k.label}</span>
|
|
<strong className="kx-kpi__value tnum">{k.value}</strong>
|
|
{k.delta && (
|
|
<span className={`kx-delta kx-delta--${k.deltaDir ?? 'up'}`}>
|
|
{k.tone === 'warn' ? '⚠' : k.deltaDir === 'down' ? '▼' : '▲'} {k.delta}
|
|
</span>
|
|
)}
|
|
{k.bar != null && (
|
|
<div className="kx-barlist__track" style={{ marginTop: 8 }}>
|
|
<span className="kx-barlist__fill is-error" style={{ width: `${k.bar}%` }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</section>
|
|
|
|
<div className="kx-admin__split">
|
|
<section className="kx-card kx-admin__chart-card" aria-label="방문객 추이">
|
|
<div className="kx-card__head">
|
|
<h2>방문객 추이 ({range === 'today' ? '오늘' : '주간 평균'})</h2>
|
|
<div className="kx-seg" role="tablist" aria-label="추이 기간">
|
|
<button role="tab" aria-selected={range === 'today'} className={`kx-seg__btn ${range === 'today' ? 'is-active' : ''}`} onClick={() => setRange('today')}>오늘</button>
|
|
<button role="tab" aria-selected={range === 'weekly'} className={`kx-seg__btn ${range === 'weekly' ? 'is-active' : ''}`} onClick={() => setRange('weekly')}>주간</button>
|
|
</div>
|
|
</div>
|
|
<div className="kx-admin__chart">
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={VISITOR_TREND} margin={{ top: 8, right: 8, bottom: 0, left: -8 }}>
|
|
<CartesianGrid stroke={CHART.neutral200} strokeDasharray="3 3" vertical={false} />
|
|
<XAxis dataKey="hour" tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={{ stroke: CHART.neutral200 }} />
|
|
<YAxis tick={{ fontSize: 12, fill: CHART.slate }} tickLine={false} axisLine={false} width={44} />
|
|
<Tooltip cursor={{ fill: CHART.primary050 }} contentStyle={TOOLTIP_STYLE} />
|
|
<Bar dataKey={range} name="방문객" fill={CHART.primary600} radius={[4, 4, 0, 0]} maxBarSize={34} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</section>
|
|
|
|
<aside className="kx-admin__live" aria-label="진행 중 이벤트">
|
|
<div className="kx-card__head">
|
|
<h2>진행 중 이벤트</h2>
|
|
</div>
|
|
<div className="kx-admin__live-list">
|
|
{LIVE_EVENTS.map((e) => (
|
|
<article key={e.id} className="kx-livecard">
|
|
<div className="kx-livecard__thumb" style={{ backgroundImage: `url(${THUMBS[e.thumbIdx]})` }} aria-hidden="true" />
|
|
<div className="kx-livecard__body">
|
|
<div className="kx-livecard__top">
|
|
<span className={`kx-livecard__dot ${e.live ? 'is-live' : 'is-prep'}`} aria-hidden="true" />
|
|
<span className="kx-livecard__state">{e.live ? 'LIVE NOW' : '준비중'}</span>
|
|
</div>
|
|
<h3 className="kx-livecard__title">{e.title}</h3>
|
|
<p className="kx-livecard__meta">{e.hallTime}</p>
|
|
{e.progress != null && (
|
|
<div className="kx-barlist__track">
|
|
<span className="kx-barlist__fill is-warning" style={{ width: `${e.progress}%` }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
{/* 플로팅 AI 컨시어지 버튼: PLANNING M18 미포함 → P2/후속. 현재 비노출. */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const TOOLTIP_STYLE = {
|
|
fontSize: 12,
|
|
borderRadius: 8,
|
|
border: '1px solid #E4E7EC',
|
|
boxShadow: '0 4px 12px rgba(16,24,40,0.1)',
|
|
} as const;
|