- react-router-dom 도입, /design 경로에 디자인 토큰/컴포넌트 카탈로그 페이지 추가 - SCAT 지도에서 하드코딩된 제주 해안선 좌표 제거, 인접 구간 기반 동적 방향 계산으로 전환 - @/ path alias 추가, SVG 아이콘 에셋 추가
463 lines
16 KiB
TypeScript
463 lines
16 KiB
TypeScript
// TypographyContent.tsx — WING-OPS Typography 콘텐츠 (다크/라이트 테마 지원)
|
|
|
|
import type { DesignTheme } from './designTheme';
|
|
|
|
// ---------- 데이터 타입 ----------
|
|
|
|
interface FontFamily {
|
|
name: string;
|
|
className: string;
|
|
stack: string;
|
|
usage: string;
|
|
sampleText: string;
|
|
}
|
|
|
|
interface TypographyToken {
|
|
className: string;
|
|
size: string;
|
|
font: string;
|
|
weight: string;
|
|
usage: string;
|
|
sampleText: string;
|
|
sampleStyle: React.CSSProperties;
|
|
}
|
|
|
|
// ---------- Font Family 데이터 ----------
|
|
|
|
const FONT_FAMILIES: FontFamily[] = [
|
|
{
|
|
name: 'Noto Sans KR',
|
|
className: 'font-korean',
|
|
stack: "'Noto Sans KR', sans-serif",
|
|
usage: '기본 UI 텍스트, 레이블, 설명 등 한국어 콘텐츠 전반에 사용됩니다. 프로젝트에서 가장 많이 사용되는 폰트입니다.',
|
|
sampleText: '해양 방제 운영 지원 시스템 WING-OPS',
|
|
},
|
|
{
|
|
name: 'JetBrains Mono',
|
|
className: 'font-mono',
|
|
stack: "'JetBrains Mono', monospace",
|
|
usage: '좌표, 수치 데이터, 코드, 토큰 이름 등 고정폭이 필요한 콘텐츠에 사용됩니다.',
|
|
sampleText: '126.978° E, 37.566° N — #0a0e1a',
|
|
},
|
|
{
|
|
name: 'Outfit',
|
|
className: 'font-sans',
|
|
stack: "'Outfit', 'Noto Sans KR', sans-serif",
|
|
usage: '영문 헤딩과 브랜드 타이틀에 사용됩니다. body 기본 폰트 스택에 포함되어 있습니다.',
|
|
sampleText: 'WING-OPS Design System v1.0',
|
|
},
|
|
];
|
|
|
|
// ---------- Typography Token 데이터 ----------
|
|
|
|
const TYPOGRAPHY_TOKENS: TypographyToken[] = [
|
|
{
|
|
className: '.wing-title',
|
|
size: '15px',
|
|
font: 'font-korean',
|
|
weight: 'Bold (700)',
|
|
usage: '패널 제목',
|
|
sampleText: '확산 예측 시뮬레이션',
|
|
sampleStyle: { fontSize: '15px', fontWeight: 700, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-section-header',
|
|
size: '13px',
|
|
font: 'font-korean',
|
|
weight: 'Bold (700)',
|
|
usage: '섹션 헤더',
|
|
sampleText: '기본 정보 입력',
|
|
sampleStyle: { fontSize: '13px', fontWeight: 700, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-label',
|
|
size: '11px',
|
|
font: 'font-korean',
|
|
weight: 'Semibold (600)',
|
|
usage: '필드 레이블',
|
|
sampleText: '유출량 (kL)',
|
|
sampleStyle: { fontSize: '11px', fontWeight: 600, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-btn',
|
|
size: '11px',
|
|
font: 'font-korean',
|
|
weight: 'Semibold (600)',
|
|
usage: '버튼 텍스트',
|
|
sampleText: '시뮬레이션 실행',
|
|
sampleStyle: { fontSize: '11px', fontWeight: 600, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-value',
|
|
size: '11px',
|
|
font: 'font-mono',
|
|
weight: 'Semibold (600)',
|
|
usage: '수치 / 데이터 값',
|
|
sampleText: '35.1284° N, 129.0598° E',
|
|
sampleStyle: { fontSize: '11px', fontWeight: 600, fontFamily: "'JetBrains Mono', monospace" },
|
|
},
|
|
{
|
|
className: '.wing-input',
|
|
size: '11px',
|
|
font: 'font-korean',
|
|
weight: 'Normal (400)',
|
|
usage: '입력 필드',
|
|
sampleText: '서해 대산항 인근 해역',
|
|
sampleStyle: { fontSize: '11px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-section-desc',
|
|
size: '10px',
|
|
font: 'font-korean',
|
|
weight: 'Normal (400)',
|
|
usage: '섹션 설명',
|
|
sampleText: '예측 결과는 기상 조건에 따라 달라질 수 있습니다.',
|
|
sampleStyle: { fontSize: '10px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-subtitle',
|
|
size: '10px',
|
|
font: 'font-korean',
|
|
weight: 'Normal (400)',
|
|
usage: '보조 설명',
|
|
sampleText: '최근 업데이트: 2026-03-24 09:00 KST',
|
|
sampleStyle: { fontSize: '10px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-meta',
|
|
size: '9px',
|
|
font: 'font-korean',
|
|
weight: 'Normal (400)',
|
|
usage: '메타 정보',
|
|
sampleText: 'v2.1 | 해양환경공단',
|
|
sampleStyle: { fontSize: '9px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
{
|
|
className: '.wing-badge',
|
|
size: '9px',
|
|
font: 'font-korean',
|
|
weight: 'Bold (700)',
|
|
usage: '뱃지 / 태그',
|
|
sampleText: '진행중',
|
|
sampleStyle: { fontSize: '9px', fontWeight: 700, fontFamily: "'Noto Sans KR', sans-serif" },
|
|
},
|
|
];
|
|
|
|
// ---------- Props ----------
|
|
|
|
interface TypographyContentProps {
|
|
theme: DesignTheme;
|
|
}
|
|
|
|
// ---------- 컴포넌트 ----------
|
|
|
|
export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
|
const t = theme;
|
|
const isDark = t.mode === 'dark';
|
|
|
|
return (
|
|
<div className="pt-24 px-8 pb-16 flex flex-col gap-16 items-start justify-start max-w-[1440px]">
|
|
|
|
{/* ── 섹션 1: 헤더 + 개요 ── */}
|
|
<div
|
|
className="w-full border-b border-solid pb-8 flex flex-col gap-6"
|
|
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<h1
|
|
className="font-sans text-3xl leading-9 font-bold"
|
|
style={{ color: t.textPrimary }}
|
|
>
|
|
Typography
|
|
</h1>
|
|
<p
|
|
className="font-korean text-sm leading-5"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
WING-OPS 인터페이스에서 사용되는 타이포그래피 체계입니다. 폰트 패밀리, 크기, 두께를 토큰과 컴포넌트 클래스로 정의하여 시각적 계층 구조와 일관성을 유지합니다.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<h3
|
|
className="font-korean text-sm font-bold"
|
|
style={{ color: t.textPrimary }}
|
|
>
|
|
개요
|
|
</h3>
|
|
<ul
|
|
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
<li>폰트 크기, 폰트 두께, 폰트 패밀리를 각각 토큰으로 정의합니다.</li>
|
|
<li>컴포넌트 클래스(<code style={{ color: t.textAccent, fontSize: '12px' }}>.wing-*</code>)로 조합하여 일관된 텍스트 스타일을 적용합니다.</li>
|
|
<li>시스템 폰트를 기반으로 다양한 환경에서 일관된 사용자 경험을 보장합니다.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 섹션 2: 글꼴 (Font Family) ── */}
|
|
<div className="w-full flex flex-col gap-8">
|
|
<div className="flex flex-col gap-2">
|
|
<h2
|
|
className="font-sans text-2xl leading-8 font-bold"
|
|
style={{ color: t.textPrimary }}
|
|
>
|
|
글꼴
|
|
</h2>
|
|
<p
|
|
className="font-korean text-sm leading-5"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
사용자의 디바이스 환경을 고려하여, 시스템 폰트와 웹 폰트를 조합하여 사용합니다. 한국어 UI에 최적화된 폰트 스택으로 다양한 기기에서 일관된 가독성을 보장합니다.
|
|
</p>
|
|
</div>
|
|
|
|
{/* body 기본 폰트 스택 코드 블록 */}
|
|
<div
|
|
className="rounded-lg border border-solid px-5 py-4 overflow-x-auto"
|
|
style={{
|
|
backgroundColor: isDark ? '#0f1524' : '#f1f5f9',
|
|
borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0',
|
|
}}
|
|
>
|
|
<pre
|
|
className="font-mono text-sm leading-6"
|
|
style={{ color: isDark ? '#b0b8cc' : '#475569' }}
|
|
>
|
|
<span style={{ color: t.textAccent }}>font-family</span>
|
|
{`: 'Outfit', 'Noto Sans KR', sans-serif;`}
|
|
</pre>
|
|
</div>
|
|
|
|
{/* 폰트 카드 3종 */}
|
|
<div className="flex flex-col gap-6">
|
|
{FONT_FAMILIES.map((font) => (
|
|
<div
|
|
key={font.name}
|
|
className="rounded-lg border border-solid overflow-hidden"
|
|
style={{
|
|
backgroundColor: t.cardBg,
|
|
borderColor: t.cardBorder,
|
|
boxShadow: t.cardShadow,
|
|
}}
|
|
>
|
|
{/* 카드 헤더 */}
|
|
<div
|
|
className="flex flex-row items-center gap-4 px-5 py-4 border-b border-solid"
|
|
style={{ borderColor: isDark ? 'rgba(66,71,84,0.15)' : '#e2e8f0' }}
|
|
>
|
|
<span
|
|
className="font-sans text-lg font-bold"
|
|
style={{ color: t.textPrimary }}
|
|
>
|
|
{font.name}
|
|
</span>
|
|
<span
|
|
className="font-mono text-[11px] rounded border border-solid px-2 py-0.5"
|
|
style={{
|
|
color: t.textAccent,
|
|
backgroundColor: isDark ? 'rgba(6,182,212,0.05)' : 'rgba(6,182,212,0.08)',
|
|
borderColor: isDark ? 'rgba(6,182,212,0.20)' : 'rgba(6,182,212,0.25)',
|
|
}}
|
|
>
|
|
{font.className}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 카드 본문 */}
|
|
<div className="px-5 py-5 flex flex-col gap-4">
|
|
{/* 폰트 스택 */}
|
|
<div
|
|
className="font-mono text-xs leading-5 rounded px-3 py-2"
|
|
style={{
|
|
color: isDark ? '#8690a6' : '#64748b',
|
|
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)',
|
|
}}
|
|
>
|
|
{font.stack}
|
|
</div>
|
|
|
|
{/* 용도 설명 */}
|
|
<p
|
|
className="font-korean text-xs leading-5"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
{font.usage}
|
|
</p>
|
|
|
|
{/* 샘플 렌더 */}
|
|
<div className="flex flex-col gap-3 pt-2">
|
|
{/* Regular */}
|
|
<div className="flex flex-col gap-1">
|
|
<span
|
|
className="font-mono text-[9px] uppercase"
|
|
style={{ letterSpacing: '1px', color: t.textMuted }}
|
|
>
|
|
Regular
|
|
</span>
|
|
<span
|
|
className={`${font.className} text-xl leading-7`}
|
|
style={{ color: t.textPrimary, fontWeight: 400 }}
|
|
>
|
|
{font.sampleText}
|
|
</span>
|
|
</div>
|
|
{/* Bold */}
|
|
<div className="flex flex-col gap-1">
|
|
<span
|
|
className="font-mono text-[9px] uppercase"
|
|
style={{ letterSpacing: '1px', color: t.textMuted }}
|
|
>
|
|
Bold
|
|
</span>
|
|
<span
|
|
className={`${font.className} text-xl leading-7 font-bold`}
|
|
style={{ color: t.textPrimary }}
|
|
>
|
|
{font.sampleText}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 섹션 3: 타이포그래피 토큰 ── */}
|
|
<div className="w-full flex flex-col gap-8">
|
|
<div className="flex flex-col gap-2">
|
|
<h2
|
|
className="font-sans text-2xl leading-8 font-bold"
|
|
style={{ color: t.textPrimary }}
|
|
>
|
|
타이포그래피 토큰
|
|
</h2>
|
|
<ul
|
|
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
<li>Tailwind @apply 기반 컴포넌트 클래스로 정의됩니다 (<code style={{ color: t.textAccent, fontSize: '12px' }}>wing.css</code>).</li>
|
|
<li>크기는 접근성을 위해 px 단위로 명시적으로 지정합니다.</li>
|
|
<li>실제 UI에서는 클래스명을 직접 사용하거나, 동일한 속성 조합으로 적용합니다.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{/* 토큰 테이블 */}
|
|
<div
|
|
className="rounded-lg border border-solid overflow-hidden w-full"
|
|
style={{
|
|
backgroundColor: t.tableContainerBg,
|
|
borderColor: t.cardBorder,
|
|
boxShadow: t.cardShadow,
|
|
}}
|
|
>
|
|
{/* 헤더 */}
|
|
<div
|
|
className="grid"
|
|
style={{
|
|
gridTemplateColumns: '160px 70px 110px 130px 120px 1fr',
|
|
backgroundColor: t.tableHeaderBg,
|
|
borderBottom: `1px solid ${t.tableRowBorder}`,
|
|
}}
|
|
>
|
|
{(['Class', 'Size', 'Font', 'Weight', '용도', 'Sample'] as const).map((col) => (
|
|
<div key={col} className="py-3 px-4">
|
|
<span
|
|
className="font-mono text-[10px] font-medium uppercase"
|
|
style={{ letterSpacing: '1px', color: t.textMuted }}
|
|
>
|
|
{col}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 데이터 행 */}
|
|
{TYPOGRAPHY_TOKENS.map((token, rowIdx) => (
|
|
<div
|
|
key={token.className}
|
|
className="grid items-center"
|
|
style={{
|
|
gridTemplateColumns: '160px 70px 110px 130px 120px 1fr',
|
|
borderTop: rowIdx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
|
|
}}
|
|
>
|
|
{/* Class */}
|
|
<div className="py-3 px-4">
|
|
<span
|
|
className="font-mono rounded border border-solid px-2 py-0.5"
|
|
style={{
|
|
fontSize: '11px',
|
|
lineHeight: '17px',
|
|
color: t.textAccent,
|
|
backgroundColor: t.cardBg,
|
|
borderColor: t.cardBorder,
|
|
}}
|
|
>
|
|
{token.className}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Size */}
|
|
<div className="py-3 px-4">
|
|
<span
|
|
className="font-mono text-[11px]"
|
|
style={{ color: t.textPrimary }}
|
|
>
|
|
{token.size}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Font */}
|
|
<div className="py-3 px-4">
|
|
<span
|
|
className="font-mono text-[11px]"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
{token.font}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Weight */}
|
|
<div className="py-3 px-4">
|
|
<span
|
|
className="font-mono text-[11px]"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
{token.weight}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 용도 */}
|
|
<div className="py-3 px-4">
|
|
<span
|
|
className="font-korean text-xs"
|
|
style={{ color: t.textSecondary }}
|
|
>
|
|
{token.usage}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Sample */}
|
|
<div className="py-3 px-4">
|
|
<span
|
|
style={{
|
|
...token.sampleStyle,
|
|
color: t.textPrimary,
|
|
}}
|
|
>
|
|
{token.sampleText}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TypographyContent;
|