/** * 대시보드(열람페이지) — 최근 완료 회차의 총평 + 성향 코드 + 결과요약 (PRD §3.9.2) * 13개 지표 그래프는 상세열람페이지(HistoryDetail)에만 표시 * Phase D-2: MD3 디자인 토큰 + 공용 컴포넌트(PillButton, StatusBadge, InfoBanner, MatIcon) 적용 */ import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import styled from 'styled-components'; import { http } from '../lib/api'; import { decodeProfileCode } from '../data/profiles'; import PillButton from '../components/PillButton'; import StatusBadge from '../components/StatusBadge'; interface IndicatorBiotype { quadrant: string; // Ⅰ ~ Ⅳ label: string; // 정상 / 마스크형 / 복합형 / 신경증적 message: string; S: number; C: number; } interface Indicator { code: string; name: string; t: number; // T-score 0~100 (높을수록 나쁨) band: string; // 정상 / 경계 / 유의 score: number; // = t z: number; // (t−50)/10 (후방 호환) ztograph: number; // = t (후방 호환) S: number | null; C: number | null; wAdj: number | null; cT?: number; biotype: IndicatorBiotype | null; active: boolean; gate: string | null; } interface DashboardResp { history: { round: number; testDate: string; status: string; paid: boolean; }; analysis: { headState: { state: string; label: string; avgcnt: number; abovecnt: number; significant: number; biotype: string }; nxIq: { nxPercentile: number; band: string; nxIq: number; cT: number }; overallBiotype: IndicatorBiotype; indicators: Indicator[]; profile: { code: string; type: string; summary: string }; profileFull?: any; }; } const Container = styled.div` padding-top: 16px; `; const Card = styled.div` background: var(--md3-surface); border-radius: 16px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06); border: 1px solid var(--md3-outline-variant); `; const SectionTitle = styled.h3` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 20px; font-weight: 700; color: var(--md3-on-surface); margin: 0 0 12px; `; const ProfileCode = styled.div` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 48px; font-weight: 800; color: var(--md3-primary); letter-spacing: 4px; margin-bottom: 12px; `; const ProfileTypes = styled.ul` list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; li { display: flex; align-items: baseline; gap: 8px; font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 14px; color: var(--md3-on-surface-variant); .letter { flex-shrink: 0; min-width: 28px; text-align: center; font-weight: 800; font-size: 15px; color: var(--md3-primary); background: var(--md3-primary-fixed-dim); border-radius: 8px; padding: 4px 8px; } .en { font-weight: 600; color: var(--md3-on-surface); } } `; const ProfileSummary = styled.div` margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--md3-outline-variant); `; const ProfileTypeName = styled.div` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 15px; font-weight: 700; color: var(--md3-on-surface); margin-bottom: 4px; `; const ProfileDesc = styled.div` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 13px; color: var(--md3-on-surface-variant); line-height: 1.6; `; const IQCard = styled(Card)` text-align: left; `; const IQValue = styled.div` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 42px; font-weight: 800; color: var(--md3-primary); line-height: 1; margin-bottom: 4px; `; const IQLabel = styled.div` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 13px; color: var(--md3-on-surface-variant); `; const SummaryCard = styled(Card)` padding: 20px; `; const SummaryHeader = styled.div` display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; `; const SummaryTitle = styled.div` display: flex; flex-direction: column; gap: 4px; `; const RoundInfo = styled.div` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 13px; color: var(--md3-on-surface-variant); `; const StatusBadgeCustom = styled(StatusBadge)` && { min-width: 72px; } `; const Counts = styled.div` display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; `; const CountBadge = styled(StatusBadge)` && { font-size: 12px; padding: 4px 10px; min-width: auto; } `; const BiotypeRow = styled.div` display: flex; align-items: center; gap: 8px; padding-top: 12px; border-top: 1px solid var(--md3-outline-variant); `; const BiotypeLabel = styled.span` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 12px; color: var(--md3-on-surface-variant); `; const BiotypeBadge = styled.div<{ $color?: string; $size?: 'sm' | 'md' | 'lg' }>` cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: ${(p) => (p.$size === 'sm' ? '4px' : p.$size === 'lg' ? '6px' : '5px')}; padding: ${(p) => (p.$size === 'sm' ? '2px 8px' : p.$size === 'lg' ? '6px 12px' : '4px 10px')}; border-radius: 9999px; border: none; background: ${(p) => p.$color ?? 'var(--md3-primary)'}; color: #fff; font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: ${(p) => (p.$size === 'sm' ? '10px' : p.$size === 'lg' ? '12px' : '11px')}; font-weight: 600; letter-spacing: 0.02em; line-height: 1; white-space: nowrap; box-sizing: border-box; &:hover { filter: brightness(0.9); } `; const EmptyState = styled.div` text-align: center; padding: 60px 16px; color: var(--md3-on-surface-variant); `; const ActionButton = styled(PillButton)` && { margin-top: 16px; width: 100%; } `; const PopupOverlay = styled.div` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 16px; `; const PopupBox = styled.div` background: var(--md3-surface); border-radius: 16px; padding: 24px; max-width: 360px; width: 100%; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); position: relative; `; const PopupClose = styled.button` position: absolute; top: 12px; right: 12px; width: 32px; height: 32px; border-radius: 50%; border: none; background: var(--md3-surface-variant); color: var(--md3-on-surface-variant); font-size: 18px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1; &:hover { background: var(--md3-outline-variant); } `; const PopupTitle = styled.h3` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 18px; font-weight: 700; color: var(--md3-on-surface); margin: 0 0 12px; `; const PopupContent = styled.p` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 14px; color: var(--md3-on-surface-variant); margin: 0; line-height: 1.6; `; const BIO_COLOR: Record = { 'Ⅰ': '#2ecc71', 'Ⅱ': '#f1a33c', 'Ⅲ': '#e74c3c', 'Ⅳ': '#f1a33c' }; const hsLabel: Record = { Good: '양호', Warning: '주의', Dangers: '상담필요' }; export default function Dashboard() { const nav = useNavigate(); const [searchParams] = useSearchParams(); const roundParam = searchParams.get('round'); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [showBioPopup, setShowBioPopup] = useState(false); useEffect(() => { const qs = roundParam ? `?round=${roundParam}` : ''; http .get<{ dashboard: DashboardResp | null }>(`/api/dashboard${qs}`) .then((r) => { setData(r.dashboard); setLoading(false); }) .catch(() => setLoading(false)); }, [roundParam]); if (loading) { return ( 불러오는 중… ); } if (!data) { return (

아직 완료된 검사가 없습니다.

nav('/test/start')}> 검사 시작하기
); } const { analysis } = data; const hs = analysis.headState; const ob = analysis.overallBiotype; const bioColor = BIO_COLOR[ob?.quadrant ?? ''] ?? '#ccc'; return ( {/* 1. 성향 코드 — 코드 + 각 자리 유형 + 유형명/총평 */} 성향 코드 {analysis.profile.code} {decodeProfileCode(analysis.profile.code).map((t, i) => (
  • {analysis.profile.code[i]} {t.en} ({t.ko})
  • ))}
    {analysis.profile.type} {analysis.profile.summary}
    {/* 2. CNT 지능지수 */} CNT 지능지수 {analysis.nxIq.nxIq.toFixed(1)} 백분위 {analysis.nxIq.nxPercentile.toFixed(1)}% · {analysis.nxIq.band} {/* 3. 결과 요약 — 상태 배지를 우측에 배치 */}

    결과 요약

    {data.history.round}회차 · {data.history.testDate}
    {hsLabel[hs.state] ?? hs.state}
    정상 {hs.avgcnt} 경계 {hs.abovecnt} 유의 {hs.significant} {ob && ( 당신의 전체 바이오타입: setShowBioPopup(true)}> {ob.label}형 )}
    {/* 상세 리포트 보기 버튼 */} nav(`/history/${data.history.round}`)}> 상세 리포트 보기 {/* 바이오타입 팝업 */} {showBioPopup && ob && ( setShowBioPopup(false)}> e.stopPropagation()}> setShowBioPopup(false)}>× {ob.quadrant}. {ob.label} {ob.message} )}
    ); }