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으로 마이그레이션
131 lines
4.5 KiB
TypeScript
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>
|
|
))}
|
|
</>
|
|
);
|
|
}
|