kintex/src/frontend/src/screens/admin/AdminDashboardPage.tsx
zio 4cee6c1f66 feat(frontend): 신규 5화면 SCR-13~17 — 경영분석·현장운영·전시일정·관리자·스타일가이드
- 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>
2026-07-11 21:56:28 +09:00

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;