kcg-ai-monitoring/frontend/src/design-system/sections/CatalogSection.tsx
htlee 52749638ef refactor(frontend): 쇼케이스 SSOT 구조 — 카탈로그 레지스트리 + variant 메타
Phase A: 쇼케이스의 카탈로그/variant 정보를 중앙 상수로 끌어올림

- shared/constants/catalogRegistry.ts 신규
  - 19+ 카탈로그의 id/showcaseId/titleKo/titleEn/description/source/items를
    단일 레지스트리로 통합 관리
  - 새 카탈로그 추가 = 레지스트리에 1줄 추가로 쇼케이스 자동 노출
  - CATALOG_REGISTRY + getCatalogById()
- lib/theme/variantMeta.ts 신규
  - BADGE_INTENT_META: 8 intent의 titleKo/titleEn/description
  - BUTTON_VARIANT_META: 5 variant의 titleKo/titleEn/description
  - BADGE_INTENT_ORDER/SIZE_ORDER, BUTTON_VARIANT_ORDER/SIZE_ORDER
- 쇼케이스 섹션 리팩토링 — 하드코딩 제거
  - CatalogSection: CATALOG_REGISTRY 자동 열거 (CATALOGS 배열 삭제)
  - BadgeSection: BADGE_INTENT_META에서 의미 가이드 + titleKo 참조
  - ButtonSection: BUTTON_VARIANT_META에서 의미 가이드 + titleKo 참조

효과:
- 카탈로그의 라벨/색상/intent 변경 시 쇼케이스와 실 페이지 동시 반영
- Badge/Button의 variant 의미가 variantMeta 한 곳에서 관리됨
- 쇼케이스 섹션에 분산돼 있던 하드코딩 제거 (INTENT_USAGE, VARIANT_USAGE 등)

다음 단계: 실 페이지를 PageContainer/PageHeader/Button/Input으로 마이그레이션
2026-04-08 11:42:43 +09:00

131 lines
4.5 KiB
TypeScript

import { type ReactNode } from 'react';
import { TrkSectionHeader, Trk } from '../lib/Trk';
import { Badge } from '@shared/components/ui/badge';
import type { BadgeIntent } from '@lib/theme/variants';
import { CATALOG_REGISTRY, type CatalogEntry } from '@shared/constants/catalogRegistry';
/**
* 카탈로그 섹션 — `CATALOG_REGISTRY`를 자동 열거.
* 새 카탈로그 추가는 `catalogRegistry.ts`에 한 줄 추가하면 끝.
* 여기는 렌더링 로직만 담당.
*/
interface AnyMeta {
code: string;
intent?: BadgeIntent;
fallback?: { ko: string; en: string };
classes?: string | { bg?: string; text?: string; border?: string };
label?: string;
}
function getKoLabel(meta: AnyMeta): string {
return meta.fallback?.ko ?? meta.label ?? meta.code;
}
function getEnLabel(meta: AnyMeta): string | undefined {
return meta.fallback?.en;
}
function getFallbackClasses(meta: AnyMeta): string | undefined {
if (typeof meta.classes === 'string') return meta.classes;
if (typeof meta.classes === 'object' && meta.classes) {
return [meta.classes.bg, meta.classes.text, meta.classes.border].filter(Boolean).join(' ');
}
return undefined;
}
function renderBadge(meta: AnyMeta, label: string): ReactNode {
if (meta.intent) {
return (
<Badge intent={meta.intent} size="sm">
{label}
</Badge>
);
}
const classes =
getFallbackClasses(meta) ??
'bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-500/20 dark:text-slate-300 dark:border-slate-500/30';
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded-md border text-[12px] font-semibold ${classes}`}
>
{label}
</span>
);
}
function CatalogBadges({ entry }: { entry: CatalogEntry }) {
const items = Object.values(entry.items) as AnyMeta[];
return (
<div className="space-y-1.5">
{items.map((meta) => {
const koLabel = getKoLabel(meta);
const enLabel = getEnLabel(meta);
const trkId = `${entry.showcaseId}-${meta.code}`;
return (
<Trk key={meta.code} id={trkId} className="flex items-center gap-3 rounded-sm">
<code className="text-[10px] text-hint font-mono whitespace-nowrap w-32 shrink-0 truncate">
{meta.code}
</code>
<div className="flex-1">{renderBadge(meta, koLabel)}</div>
<div className="flex-1">
{enLabel ? (
renderBadge(meta, enLabel)
) : (
<span className="text-[10px] text-hint italic">(no en)</span>
)}
</div>
</Trk>
);
})}
</div>
);
}
export function CatalogSection() {
return (
<>
<TrkSectionHeader
id="TRK-SEC-catalog"
title={`분류 카탈로그 (${CATALOG_REGISTRY.length}+)`}
description="백엔드 enum/code_master 기반 SSOT. 쇼케이스와 실 페이지가 동일 레지스트리 참조."
/>
<Trk id="TRK-CAT-intro" className="ds-sample mb-4">
<p className="text-xs text-label leading-relaxed">
API를 (/intent/ ):
</p>
<code className="ds-code mt-2">
{`import { getAlertLevelIntent, getAlertLevelLabel } from '@shared/constants/alertLevels';
<Badge intent={getAlertLevelIntent(event.level)} size="sm">
{getAlertLevelLabel(event.level, t, lang)}
</Badge>`}
</code>
<p className="text-[11px] text-hint mt-2">
<strong> </strong>: <code className="font-mono">shared/constants/catalogRegistry.ts</code>
.
</p>
</Trk>
{CATALOG_REGISTRY.map((entry) => (
<Trk key={entry.id} id={entry.showcaseId} className="ds-sample mb-3">
<div className="mb-2">
<div className="flex items-baseline gap-2 flex-wrap">
<h3 className="text-sm font-semibold text-heading">
{entry.titleKo} · {entry.titleEn}
</h3>
<code className="text-[10px] text-hint font-mono">{entry.showcaseId}</code>
</div>
<p className="text-[11px] text-hint">{entry.description}</p>
{entry.source && (
<p className="text-[10px] text-hint italic mt-0.5">: {entry.source}</p>
)}
</div>
<CatalogBadges entry={entry} />
</Trk>
))}
</>
);
}