feat(design): Stitch MCP 기반 디자인 시스템 카탈로그 + SCAT 하드코딩 해안선 제거

- react-router-dom 도입, /design 경로에 디자인 토큰/컴포넌트 카탈로그 페이지 추가
- SCAT 지도에서 하드코딩된 제주 해안선 좌표 제거, 인접 구간 기반 동적 방향 계산으로 전환
- @/ path alias 추가, SVG 아이콘 에셋 추가
This commit is contained in:
leedano 2026-03-24 16:36:50 +09:00
부모 d6d476e9bd
커밋 d0491c3f0f
43개의 변경된 파일3850개의 추가작업 그리고 76개의 파일을 삭제

79
docs/DESIGN-SYSTEM.md Normal file
파일 보기

@ -0,0 +1,79 @@
# WING-OPS 디자인 시스템
## 개요
WING-OPS UI 디자인 시스템의 비주얼 레퍼런스 카탈로그.
Google Stitch MCP로 생성된 스크린을 기반으로 일관된 UI 구현을 유도한다.
## Stitch 프로젝트
- **프로젝트명**: WING-OPS Design System v1
- **프로젝트 ID**: `5453076280291618640`
## 스크린 목록
| # | 스크린 | Screen ID | 용도 |
|---|--------|-----------|------|
| 1 | Design Tokens | `ce520225d85c4c38b2024e93ec6a4fb2` | 색상, 타이포그래피, 간격, 라운딩 토큰 |
| 2 | Component Catalog (Buttons/Badges) | `42fa9cf1a3d341a7972a1bc10ba00a8c` | 버튼 variant, 뱃지, 아이콘 버튼 |
| 3 | Form Components | `7331ad8a598f4cc59f62a14226c1d023` | 입력, 선택, 날짜, 토글, 폼 레이아웃 |
| 4 | Table & List Patterns | `5967382c70f9422ba3a0f4da79922ecf` | 데이터 테이블, 사이드바 리스트, 페이지네이션 |
| 5 | Modal Catalog | `440be91f8db7423cbb5cc89e6dd6f9ca` | 모달 3사이즈, 확인 다이얼로그, 폼 모달 |
| 6 | Operational Shell (Layout) | `86fd57c9f3c749d288f6270838a9387d` | TopBar, SubMenu, 3컬럼 레이아웃 |
| 7 | Container & Navigation | `201c2c0c47b74fcfb3427d029319fa9d` | 카드, 섹션, 탭바, KV 행, 헤더바 |
## 디자인 토큰 요약
### 배경색
| 토큰 | Tailwind | 값 | 용도 |
|------|----------|-----|------|
| `--bg0` | `bg-bg-0` | `#0a0e1a` | 페이지 배경, 입력 배경 |
| `--bg1` | `bg-bg-1` | `#0f1524` | 사이드바, 패널 |
| `--bg2` | `bg-bg-2` | `#121929` | 테이블 헤더 |
| `--bg3` | `bg-bg-3` | `#1a2236` | 카드, 버튼(secondary) |
| `--bgH` | `bg-bg-hover` | `#1e2844` | 호버 상태 |
### 텍스트
| 토큰 | Tailwind | 값 | 용도 |
|------|----------|-----|------|
| `--t1` | `text-text-1` | `#edf0f7` | 주요 텍스트 |
| `--t2` | `text-text-2` | `#b0b8cc` | 보조 텍스트 |
| `--t3` | `text-text-3` | `#8690a6` | 비활성, 플레이스홀더 |
### 보더
| 토큰 | Tailwind | 값 |
|------|----------|-----|
| `--bd` | `border-border` | `#1e2a42` |
| `--bdL` | `border-border-light` | `#2a3a5c` |
### 강조색
| 토큰 | Tailwind | 값 | 용도 |
|------|----------|-----|------|
| `--cyan` | `text-primary-cyan` | `#06b6d4` | Primary accent, 활성 상태 |
| `--blue` | `text-primary-blue` | `#3b82f6` | Secondary accent |
| `--purple` | `text-primary-purple` | `#a855f7` | Tertiary accent |
### 상태색
| 토큰 | 값 | 용도 |
|------|-----|------|
| `--red` | `#ef4444` | 위험, 삭제 |
| `--orange` | `#f97316` | 주의 |
| `--yellow` | `#eab308` | 경고 |
| `--green` | `#22c55e` | 정상, 성공 |
### 타이포그래피
| 크기 | 용도 | 폰트 |
|------|------|------|
| 9px | 메타정보 | Noto Sans KR |
| 10px | 테이블/KV 데이터 | Noto Sans KR |
| 11px | 입력, 버튼, 값 표시 | Noto Sans KR |
| 13px | 섹션 헤더 | Noto Sans KR, bold |
| 15px | 패널 타이틀 | Noto Sans KR, bold |
| 수치 데이터 | 모든 숫자 | JetBrains Mono |
### Border Radius
| 크기 | 값 | 용도 |
|------|-----|------|
| sm | 6px | 버튼, 입력, 소형 카드 |
| md | 10px | 카드, 모달, 패널 |

파일 보기

@ -32,6 +32,7 @@
"maplibre-gl": "^5.19.0", "maplibre-gl": "^5.19.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
"react-window": "^2.2.7", "react-window": "^2.2.7",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
@ -3366,6 +3367,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/core-assert": { "node_modules/core-assert": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz",
@ -5451,6 +5465,44 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
"license": "MIT",
"dependencies": {
"react-router": "7.13.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-window": { "node_modules/react-window": {
"version": "2.2.7", "version": "2.2.7",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz", "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz",
@ -5660,6 +5712,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/set-value": { "node_modules/set-value": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",

파일 보기

@ -34,6 +34,7 @@
"maplibre-gl": "^5.19.0", "maplibre-gl": "^5.19.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
"react-window": "^2.2.7", "react-window": "^2.2.7",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",

파일 보기

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Routes, Route } from 'react-router-dom'
import { GoogleOAuthProvider } from '@react-oauth/google' import { GoogleOAuthProvider } from '@react-oauth/google'
import type { MainTab } from '@common/types/navigation' import type { MainTab } from '@common/types/navigation'
import { MainLayout } from '@common/components/layout/MainLayout' import { MainLayout } from '@common/components/layout/MainLayout'
@ -19,6 +20,7 @@ import { IncidentsView } from '@tabs/incidents'
import { AdminView } from '@tabs/admin' import { AdminView } from '@tabs/admin'
import { ScatView } from '@tabs/scat' import { ScatView } from '@tabs/scat'
import { RescueView } from '@tabs/rescue' import { RescueView } from '@tabs/rescue'
import { DesignPage } from '@/pages/design/DesignPage'
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || '' const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || ''
@ -108,9 +110,14 @@ function App() {
} }
return ( return (
<MainLayout activeMainTab={activeMainTab} onMainTabChange={setActiveMainTab}> <Routes>
{renderView()} <Route path="/design" element={<DesignPage />} />
</MainLayout> <Route path="*" element={
<MainLayout activeMainTab={activeMainTab} onMainTabChange={setActiveMainTab}>
{renderView()}
</MainLayout>
} />
</Routes>
) )
} }

파일 보기

@ -0,0 +1,3 @@
<svg width="17" height="15" viewBox="0 0 17 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14.25L8.25 0L16.5 14.25H0V14.25M2.5875 12.75H13.9125L8.25 3L2.5875 12.75V12.75M8.25 12C8.4625 12 8.64063 11.9281 8.78438 11.7844C8.92813 11.6406 9 11.4625 9 11.25C9 11.0375 8.92813 10.8594 8.78438 10.7156C8.64063 10.5719 8.4625 10.5 8.25 10.5C8.0375 10.5 7.85937 10.5719 7.71562 10.7156C7.57187 10.8594 7.5 11.0375 7.5 11.25C7.5 11.4625 7.57187 11.6406 7.71562 11.7844C7.85937 11.9281 8.0375 12 8.25 12V12M7.5 9.75H9V6H7.5V9.75V9.75M8.25 7.875V7.875V7.875V7.875V7.875" fill="#FFB4AB"/>
</svg>

After

Width:  |  Height:  |  크기: 601 B

파일 보기

@ -0,0 +1,3 @@
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 15C5.9875 15 5.20625 14.8625 4.40625 14.5875C3.60625 14.3125 2.88125 13.9375 2.23125 13.4625C1.58125 12.9875 1.04688 12.4312 0.628125 11.7937C0.209375 11.1562 0 10.475 0 9.75V7.5L3 9.75L1.8375 10.9125C2.2 11.55 2.775 12.1 3.5625 12.5625C4.35 13.025 5.1625 13.3188 6 13.4438V6.75H3.75V5.25H6V4.36875C5.5625 4.20625 5.20312 3.93437 4.92188 3.55312C4.64062 3.17187 4.5 2.7375 4.5 2.25C4.5 1.625 4.71875 1.09375 5.15625 0.65625C5.59375 0.21875 6.125 0 6.75 0C7.375 0 7.90625 0.21875 8.34375 0.65625C8.78125 1.09375 9 1.625 9 2.25C9 2.7375 8.85938 3.17187 8.57812 3.55312C8.29688 3.93437 7.9375 4.20625 7.5 4.36875V5.25H9.75V6.75H7.5V13.4438C8.3375 13.3188 9.15 13.025 9.9375 12.5625C10.725 12.1 11.3 11.55 11.6625 10.9125L10.5 9.75L13.5 7.5V9.75C13.5 10.475 13.2906 11.1562 12.8719 11.7937C12.4531 12.4312 11.9188 12.9875 11.2688 13.4625C10.6188 13.9375 9.89375 14.3125 9.09375 14.5875C8.29375 14.8625 7.5125 15 6.75 15V15M6.75 3C6.9625 3 7.14063 2.92812 7.28438 2.78437C7.42813 2.64062 7.5 2.4625 7.5 2.25C7.5 2.0375 7.42813 1.85938 7.28438 1.71563C7.14063 1.57188 6.9625 1.5 6.75 1.5C6.5375 1.5 6.35937 1.57188 6.21562 1.71563C6.07187 1.85938 6 2.0375 6 2.25C6 2.4625 6.07187 2.64062 6.21562 2.78437C6.35937 2.92812 6.5375 3 6.75 3V3" fill="#22D3EE"/>
</svg>

After

Width:  |  Height:  |  크기: 1.3 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.25 15C1.8375 15 1.48437 14.8531 1.19062 14.5594C0.896875 14.2656 0.75 13.9125 0.75 13.5V5.04375C0.525 4.90625 0.34375 4.72813 0.20625 4.50938C0.06875 4.29063 0 4.0375 0 3.75V1.5C0 1.0875 0.146875 0.734375 0.440625 0.440625C0.734375 0.146875 1.0875 0 1.5 0H13.5C13.9125 0 14.2656 0.146875 14.5594 0.440625C14.8531 0.734375 15 1.0875 15 1.5V3.75C15 4.0375 14.9312 4.29063 14.7937 4.50938C14.6562 4.72813 14.475 4.90625 14.25 5.04375V13.5C14.25 13.9125 14.1031 14.2656 13.8094 14.5594C13.5156 14.8531 13.1625 15 12.75 15H2.25V15M2.25 5.25V13.5V13.5V13.5H12.75V13.5V13.5V5.25H2.25V5.25M1.5 3.75H13.5V3.75V3.75V1.5V1.5V1.5H1.5V1.5V1.5V3.75V3.75V3.75V3.75M5.25 9H9.75V7.5H5.25V9V9M7.5 9.375V9.375V9.375V9.375V9.375V9.375V9.375V9.375V9.375V9.375" fill="#FFB873"/>
</svg>

After

Width:  |  Height:  |  크기: 872 B

파일 보기

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 13.5V12L1.5 10.5V13.5H0V13.5M3 13.5V9L4.5 7.5V7.5V13.5H3V13.5M6 13.5V7.5L7.5 9.01875V13.5H6V13.5M9 13.5V9.01875L10.5 7.51875V13.5H9V13.5M12 13.5V6L13.5 4.5V13.5H12V13.5M0 9.61875V7.5L5.25 2.25L8.25 5.25L13.5 0V2.11875L8.25 7.36875L5.25 4.36875L0 9.61875V9.61875" fill="#06B6D4" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  크기: 414 B

파일 보기

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20C8.63333 20 7.34167 19.7375 6.125 19.2125C4.90833 18.6875 3.84583 17.9708 2.9375 17.0625C2.02917 16.1542 1.3125 15.0917 0.7875 13.875C0.2625 12.6583 0 11.3667 0 10C0 8.61667 0.270833 7.31667 0.8125 6.1C1.35417 4.88333 2.0875 3.825 3.0125 2.925C3.9375 2.025 5.01667 1.3125 6.25 0.7875C7.48333 0.2625 8.8 0 10.2 0C11.5333 0 12.7917 0.229167 13.975 0.6875C15.1583 1.14583 16.1958 1.77917 17.0875 2.5875C17.9792 3.39583 18.6875 4.35417 19.2125 5.4625C19.7375 6.57083 20 7.76667 20 9.05C20 10.9667 19.4167 12.4375 18.25 13.4625C17.0833 14.4875 15.6667 15 14 15H12.15C12 15 11.8958 15.0417 11.8375 15.125C11.7792 15.2083 11.75 15.3 11.75 15.4C11.75 15.6 11.875 15.8875 12.125 16.2625C12.375 16.6375 12.5 17.0667 12.5 17.55C12.5 18.3833 12.2708 19 11.8125 19.4C11.3542 19.8 10.75 20 10 20V20M10 10V10V10V10V10V10V10V10V10V10V10V10V10V10V10V10V10M4.5 11C4.93333 11 5.29167 10.8583 5.575 10.575C5.85833 10.2917 6 9.93333 6 9.5C6 9.06667 5.85833 8.70833 5.575 8.425C5.29167 8.14167 4.93333 8 4.5 8C4.06667 8 3.70833 8.14167 3.425 8.425C3.14167 8.70833 3 9.06667 3 9.5C3 9.93333 3.14167 10.2917 3.425 10.575C3.70833 10.8583 4.06667 11 4.5 11V11M7.5 7C7.93333 7 8.29167 6.85833 8.575 6.575C8.85833 6.29167 9 5.93333 9 5.5C9 5.06667 8.85833 4.70833 8.575 4.425C8.29167 4.14167 7.93333 4 7.5 4C7.06667 4 6.70833 4.14167 6.425 4.425C6.14167 4.70833 6 5.06667 6 5.5C6 5.93333 6.14167 6.29167 6.425 6.575C6.70833 6.85833 7.06667 7 7.5 7V7M12.5 7C12.9333 7 13.2917 6.85833 13.575 6.575C13.8583 6.29167 14 5.93333 14 5.5C14 5.06667 13.8583 4.70833 13.575 4.425C13.2917 4.14167 12.9333 4 12.5 4C12.0667 4 11.7083 4.14167 11.425 4.425C11.1417 4.70833 11 5.06667 11 5.5C11 5.93333 11.1417 6.29167 11.425 6.575C11.7083 6.85833 12.0667 7 12.5 7V7M15.5 11C15.9333 11 16.2917 10.8583 16.575 10.575C16.8583 10.2917 17 9.93333 17 9.5C17 9.06667 16.8583 8.70833 16.575 8.425C16.2917 8.14167 15.9333 8 15.5 8C15.0667 8 14.7083 8.14167 14.425 8.425C14.1417 8.70833 14 9.06667 14 9.5C14 9.93333 14.1417 10.2917 14.425 10.575C14.7083 10.8583 15.0667 11 15.5 11V11M10 18C10.15 18 10.2708 17.9583 10.3625 17.875C10.4542 17.7917 10.5 17.6833 10.5 17.55C10.5 17.3167 10.375 17.0417 10.125 16.725C9.875 16.4083 9.75 15.9333 9.75 15.3C9.75 14.6 9.99167 14.0417 10.475 13.625C10.9583 13.2083 11.55 13 12.25 13H14C15.1 13 16.0417 12.6792 16.825 12.0375C17.6083 11.3958 18 10.4 18 9.05C18 7.03333 17.2292 5.35417 15.6875 4.0125C14.1458 2.67083 12.3167 2 10.2 2C7.93333 2 6 2.775 4.4 4.325C2.8 5.875 2 7.76667 2 10C2 12.2167 2.77917 14.1042 4.3375 15.6625C5.89583 17.2208 7.78333 18 10 18V18" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 2.6 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.05 10.5L0 9.45L4.2 5.25L0 1.05L1.05 0L5.25 4.2L9.45 0L10.5 1.05L6.3 5.25L10.5 9.45L9.45 10.5L5.25 6.3L1.05 10.5V10.5" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 250 B

파일 보기

@ -0,0 +1,3 @@
<svg width="16" height="15" viewBox="0 0 16 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.475 15L5.175 12.6C5.0125 12.5375 4.85937 12.4625 4.71562 12.375C4.57187 12.2875 4.43125 12.1938 4.29375 12.0938L2.0625 13.0312L0 9.46875L1.93125 8.00625C1.91875 7.91875 1.9125 7.83438 1.9125 7.75313C1.9125 7.67188 1.9125 7.5875 1.9125 7.5C1.9125 7.4125 1.9125 7.32812 1.9125 7.24687C1.9125 7.16562 1.91875 7.08125 1.93125 6.99375L0 5.53125L2.0625 1.96875L4.29375 2.90625C4.43125 2.80625 4.575 2.7125 4.725 2.625C4.875 2.5375 5.025 2.4625 5.175 2.4L5.475 0H9.6L9.9 2.4C10.0625 2.4625 10.2156 2.5375 10.3594 2.625C10.5031 2.7125 10.6437 2.80625 10.7812 2.90625L13.0125 1.96875L15.075 5.53125L13.1438 6.99375C13.1563 7.08125 13.1625 7.16562 13.1625 7.24687C13.1625 7.32812 13.1625 7.4125 13.1625 7.5C13.1625 7.5875 13.1625 7.67188 13.1625 7.75313C13.1625 7.83438 13.15 7.91875 13.125 8.00625L15.0562 9.46875L12.9937 13.0312L10.7812 12.0938C10.6437 12.1938 10.5 12.2875 10.35 12.375C10.2 12.4625 10.05 12.5375 9.9 12.6L9.6 15H5.475V15M6.7875 13.5H8.26875L8.53125 11.5125C8.91875 11.4125 9.27812 11.2656 9.60938 11.0719C9.94063 10.8781 10.2438 10.6437 10.5188 10.3687L12.375 11.1375L13.1062 9.8625L11.4937 8.64375C11.5562 8.46875 11.6 8.28437 11.625 8.09062C11.65 7.89687 11.6625 7.7 11.6625 7.5C11.6625 7.3 11.65 7.10313 11.625 6.90938C11.6 6.71563 11.5562 6.53125 11.4937 6.35625L13.1062 5.1375L12.375 3.8625L10.5188 4.65C10.2438 4.3625 9.94063 4.12187 9.60938 3.92812C9.27812 3.73437 8.91875 3.5875 8.53125 3.4875L8.2875 1.5H6.80625L6.54375 3.4875C6.15625 3.5875 5.79687 3.73437 5.46562 3.92812C5.13437 4.12187 4.83125 4.35625 4.55625 4.63125L2.7 3.8625L1.96875 5.1375L3.58125 6.3375C3.51875 6.525 3.475 6.7125 3.45 6.9C3.425 7.0875 3.4125 7.2875 3.4125 7.5C3.4125 7.7 3.425 7.89375 3.45 8.08125C3.475 8.26875 3.51875 8.45625 3.58125 8.64375L1.96875 9.8625L2.7 11.1375L4.55625 10.35C4.83125 10.6375 5.13437 10.8781 5.46562 11.0719C5.79687 11.2656 6.15625 11.4125 6.54375 11.5125L6.7875 13.5V13.5M7.575 10.125C8.3 10.125 8.91875 9.86875 9.43125 9.35625C9.94375 8.84375 10.2 8.225 10.2 7.5C10.2 6.775 9.94375 6.15625 9.43125 5.64375C8.91875 5.13125 8.3 4.875 7.575 4.875C6.8375 4.875 6.21562 5.13125 5.70937 5.64375C5.20312 6.15625 4.95 6.775 4.95 7.5C4.95 8.225 5.20312 8.84375 5.70937 9.35625C6.21562 9.86875 6.8375 10.125 7.575 10.125V10.125M7.5375 7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5V7.5" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 2.5 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="14" height="9" viewBox="0 0 14 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 9V7.5H13.5V9H0V9M0 5.25V3.75H13.5V5.25H0V5.25M0 1.5V0H13.5V1.5H0V1.5" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 200 B

파일 보기

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.45 13.5L7.725 8.775C7.35 9.075 6.91875 9.3125 6.43125 9.4875C5.94375 9.6625 5.425 9.75 4.875 9.75C3.5125 9.75 2.35938 9.27813 1.41562 8.33438C0.471875 7.39063 0 6.2375 0 4.875C0 3.5125 0.471875 2.35938 1.41562 1.41562C2.35938 0.471875 3.5125 0 4.875 0C6.2375 0 7.39063 0.471875 8.33438 1.41562C9.27813 2.35938 9.75 3.5125 9.75 4.875C9.75 5.425 9.6625 5.94375 9.4875 6.43125C9.3125 6.91875 9.075 7.35 8.775 7.725L13.5 12.45L12.45 13.5V13.5M4.875 8.25C5.8125 8.25 6.60938 7.92188 7.26562 7.26562C7.92188 6.60938 8.25 5.8125 8.25 4.875C8.25 3.9375 7.92188 3.14062 7.26562 2.48438C6.60938 1.82812 5.8125 1.5 4.875 1.5C3.9375 1.5 3.14062 1.82812 2.48438 2.48438C1.82812 3.14062 1.5 3.9375 1.5 4.875C1.5 5.8125 1.82812 6.60938 2.48438 7.26562C3.14062 7.92188 3.9375 8.25 4.875 8.25V8.25" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 915 B

파일 보기

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.95 16C10.3 16 10.5958 15.8792 10.8375 15.6375C11.0792 15.3958 11.2 15.1 11.2 14.75C11.2 14.4 11.0792 14.1042 10.8375 13.8625C10.5958 13.6208 10.3 13.5 9.95 13.5C9.6 13.5 9.30417 13.6208 9.0625 13.8625C8.82083 14.1042 8.7 14.4 8.7 14.75C8.7 15.1 8.82083 15.3958 9.0625 15.6375C9.30417 15.8792 9.6 16 9.95 16V16M9.05 12.15H10.9C10.9 11.6 10.9625 11.1667 11.0875 10.85C11.2125 10.5333 11.5667 10.1 12.15 9.55C12.5833 9.11667 12.925 8.70417 13.175 8.3125C13.425 7.92083 13.55 7.45 13.55 6.9C13.55 5.96667 13.2083 5.25 12.525 4.75C11.8417 4.25 11.0333 4 10.1 4C9.15 4 8.37917 4.25 7.7875 4.75C7.19583 5.25 6.78333 5.85 6.55 6.55L8.2 7.2C8.28333 6.9 8.47083 6.575 8.7625 6.225C9.05417 5.875 9.5 5.7 10.1 5.7C10.6333 5.7 11.0333 5.84583 11.3 6.1375C11.5667 6.42917 11.7 6.75 11.7 7.1C11.7 7.43333 11.6 7.74583 11.4 8.0375C11.2 8.32917 10.95 8.6 10.65 8.85C9.91667 9.5 9.46667 9.99167 9.3 10.325C9.13333 10.6583 9.05 11.2667 9.05 12.15V12.15M10 20C8.61667 20 7.31667 19.7375 6.1 19.2125C4.88333 18.6875 3.825 17.975 2.925 17.075C2.025 16.175 1.3125 15.1167 0.7875 13.9C0.2625 12.6833 0 11.3833 0 10C0 8.61667 0.2625 7.31667 0.7875 6.1C1.3125 4.88333 2.025 3.825 2.925 2.925C3.825 2.025 4.88333 1.3125 6.1 0.7875C7.31667 0.2625 8.61667 0 10 0C11.3833 0 12.6833 0.2625 13.9 0.7875C15.1167 1.3125 16.175 2.025 17.075 2.925C17.975 3.825 18.6875 4.88333 19.2125 6.1C19.7375 7.31667 20 8.61667 20 10C20 11.3833 19.7375 12.6833 19.2125 13.9C18.6875 15.1167 17.975 16.175 17.075 17.075C16.175 17.975 15.1167 18.6875 13.9 19.2125C12.6833 19.7375 11.3833 20 10 20V20M10 18C12.2333 18 14.125 17.225 15.675 15.675C17.225 14.125 18 12.2333 18 10C18 7.76667 17.225 5.875 15.675 4.325C14.125 2.775 12.2333 2 10 2C7.76667 2 5.875 2.775 4.325 4.325C2.775 5.875 2 7.76667 2 10C2 12.2333 2.775 14.125 4.325 15.675C5.875 17.225 7.76667 18 10 18V18M10 10V10V10V10V10V10V10V10V10V10" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 1.9 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 19.05L0 12.05L1.65 10.8L9 16.5L16.35 10.8L18 12.05L9 19.05V19.05M9 14L0 7L9 0L18 7L9 14V14M9 7V7V7V7V7V7M9 11.45L14.75 7L9 2.55L3.25 7L9 11.45V11.45" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 282 B

파일 보기

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6V0H18V6H10V6M0 10V0H8V10H0V10M10 18V8H18V18H10V18M0 18V12H8V18H0V18M2 8H6V2H2V8V8M12 16H16V10H12V16V16M12 4H16V2H12V4V4M2 16H6V14H2V16V16M6 8V8V8V8V8V8M12 4V4V4V4V4V4M12 10V10V10V10V10V10M6 14V14V14V14V14V14" fill="#4CD7F6"/>
</svg>

After

Width:  |  Height:  |  크기: 343 B

파일 보기

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8V0H8V8H0V8M0 18V10H8V18H0V18M10 8V0H18V8H10V8M10 18V10H18V18H10V18M2 6H6V2H2V6V6M12 6H16V2H12V6V6M12 16H16V12H12V16V16M2 16H6V12H2V16V16M12 6V6V6V6V6V6M12 12V12V12V12V12V12M6 12V12V12V12V12V12M6 6V6V6V6V6V6" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 341 B

파일 보기

@ -0,0 +1,3 @@
<svg width="31" height="35" viewBox="0 0 31 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.59998 23.7501V21.8501H9.49998V15.2001C9.49998 13.8859 9.89581 12.7182 10.6875 11.697C11.4791 10.6757 12.5083 10.0068 13.775 9.6901V9.0251C13.775 8.62926 13.9135 8.29281 14.1906 8.01572C14.4677 7.73864 14.8041 7.6001 15.2 7.6001C15.5958 7.6001 15.9323 7.73864 16.2094 8.01572C16.4864 8.29281 16.625 8.62926 16.625 9.0251V9.6901C17.8916 10.0068 18.9208 10.6757 19.7125 11.697C20.5041 12.7182 20.9 13.8859 20.9 15.2001V21.8501H22.8V23.7501H7.59998V23.7501M15.2 16.6251V16.6251V16.6251V16.6251V16.6251V16.6251V16.6251V16.6251V16.6251M15.2 26.6001C14.6775 26.6001 14.2302 26.4141 13.8581 26.042C13.486 25.6699 13.3 25.2226 13.3 24.7001H17.1C17.1 25.2226 16.9139 25.6699 16.5419 26.042C16.1698 26.4141 15.7225 26.6001 15.2 26.6001V26.6001M11.4 21.8501H19V15.2001C19 14.1551 18.6279 13.2605 17.8837 12.5163C17.1396 11.7722 16.245 11.4001 15.2 11.4001C14.155 11.4001 13.2604 11.7722 12.5162 12.5163C11.7721 13.2605 11.4 14.1551 11.4 15.2001V21.8501V21.8501" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 1.1 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.08333 6.125H4.66667V4.95833H5.25C5.41528 4.95833 5.55382 4.90243 5.66563 4.79063C5.77743 4.67882 5.83333 4.54028 5.83333 4.375V3.79167C5.83333 3.62639 5.77743 3.48785 5.66563 3.37604C5.55382 3.26424 5.41528 3.20833 5.25 3.20833H4.08333V6.125V6.125M4.66667 4.375V3.79167H5.25V4.375H4.66667V4.375M6.41667 6.125H7.58333C7.74861 6.125 7.88715 6.0691 7.99896 5.95729C8.11076 5.84549 8.16667 5.70694 8.16667 5.54167V3.79167C8.16667 3.62639 8.11076 3.48785 7.99896 3.37604C7.88715 3.26424 7.74861 3.20833 7.58333 3.20833H6.41667V6.125V6.125M7 5.54167V3.79167H7.58333V5.54167H7V5.54167M8.75 6.125H9.33333V4.95833H9.91667V4.375H9.33333V3.79167H9.91667V3.20833H8.75V6.125V6.125M3.5 9.33333C3.17917 9.33333 2.90451 9.2191 2.67604 8.99063C2.44757 8.76215 2.33333 8.4875 2.33333 8.16667V1.16667C2.33333 0.845833 2.44757 0.571181 2.67604 0.342708C2.90451 0.114236 3.17917 0 3.5 0H10.5C10.8208 0 11.0955 0.114236 11.324 0.342708C11.5524 0.571181 11.6667 0.845833 11.6667 1.16667V8.16667C11.6667 8.4875 11.5524 8.76215 11.324 8.99063C11.0955 9.2191 10.8208 9.33333 10.5 9.33333H3.5V9.33333M3.5 8.16667H10.5V8.16667V8.16667V1.16667V1.16667V1.16667H3.5V1.16667V1.16667V8.16667V8.16667V8.16667V8.16667M1.16667 11.6667C0.845833 11.6667 0.571181 11.5524 0.342708 11.324C0.114236 11.0955 0 10.8208 0 10.5V2.33333H1.16667V10.5V10.5V10.5H9.33333V11.6667H1.16667V11.6667M3.5 1.16667V1.16667V1.16667V1.16667V8.16667V8.16667V8.16667V8.16667V8.16667V8.16667V1.16667V1.16667V1.16667V1.16667" fill="#3B82F6" fill-opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  크기: 1.6 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.08333 6.125H4.66667V4.95833H5.25C5.41528 4.95833 5.55382 4.90243 5.66563 4.79063C5.77743 4.67882 5.83333 4.54028 5.83333 4.375V3.79167C5.83333 3.62639 5.77743 3.48785 5.66563 3.37604C5.55382 3.26424 5.41528 3.20833 5.25 3.20833H4.08333V6.125V6.125M4.66667 4.375V3.79167H5.25V4.375H4.66667V4.375M6.41667 6.125H7.58333C7.74861 6.125 7.88715 6.0691 7.99896 5.95729C8.11076 5.84549 8.16667 5.70694 8.16667 5.54167V3.79167C8.16667 3.62639 8.11076 3.48785 7.99896 3.37604C7.88715 3.26424 7.74861 3.20833 7.58333 3.20833H6.41667V6.125V6.125M7 5.54167V3.79167H7.58333V5.54167H7V5.54167M8.75 6.125H9.33333V4.95833H9.91667V4.375H9.33333V3.79167H9.91667V3.20833H8.75V6.125V6.125M3.5 9.33333C3.17917 9.33333 2.90451 9.2191 2.67604 8.99063C2.44757 8.76215 2.33333 8.4875 2.33333 8.16667V1.16667C2.33333 0.845833 2.44757 0.571181 2.67604 0.342708C2.90451 0.114236 3.17917 0 3.5 0H10.5C10.8208 0 11.0955 0.114236 11.324 0.342708C11.5524 0.571181 11.6667 0.845833 11.6667 1.16667V8.16667C11.6667 8.4875 11.5524 8.76215 11.324 8.99063C11.0955 9.2191 10.8208 9.33333 10.5 9.33333H3.5V9.33333M3.5 8.16667H10.5V8.16667V8.16667V1.16667V1.16667V1.16667H3.5V1.16667V1.16667V8.16667V8.16667V8.16667V8.16667M1.16667 11.6667C0.845833 11.6667 0.571181 11.5524 0.342708 11.324C0.114236 11.0955 0 10.8208 0 10.5V2.33333H1.16667V10.5V10.5V10.5H9.33333V11.6667H1.16667V11.6667M3.5 1.16667V1.16667V1.16667V1.16667V8.16667V8.16667V8.16667V8.16667V8.16667V8.16667V1.16667V1.16667V1.16667V1.16667" fill="#3B82F6"/>
</svg>

After

Width:  |  Height:  |  크기: 1.6 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.535 26.6001L14.155 23.5601C13.9491 23.4809 13.7552 23.3859 13.5731 23.2751C13.391 23.1643 13.2129 23.0455 13.0387 22.9188L10.2125 24.1063L7.59998 19.5938L10.0462 17.7413C10.0304 17.6305 10.0225 17.5236 10.0225 17.4207C10.0225 17.3178 10.0225 17.2109 10.0225 17.1001C10.0225 16.9893 10.0225 16.8824 10.0225 16.7795C10.0225 16.6766 10.0304 16.5697 10.0462 16.4588L7.59998 14.6063L10.2125 10.0938L13.0387 11.2813C13.2129 11.1547 13.395 11.0359 13.585 10.9251C13.775 10.8143 13.965 10.7193 14.155 10.6401L14.535 7.6001H19.76L20.14 10.6401C20.3458 10.7193 20.5398 10.8143 20.7219 10.9251C20.9039 11.0359 21.0821 11.1547 21.2562 11.2813L24.0825 10.0938L26.695 14.6063L24.2487 16.4588C24.2646 16.5697 24.2725 16.6766 24.2725 16.7795C24.2725 16.8824 24.2725 16.9893 24.2725 17.1001C24.2725 17.2109 24.2725 17.3178 24.2725 17.4207C24.2725 17.5236 24.2566 17.6305 24.225 17.7413L26.6712 19.5938L24.0587 24.1063L21.2562 22.9188C21.0821 23.0455 20.9 23.1643 20.71 23.2751C20.52 23.3859 20.33 23.4809 20.14 23.5601L19.76 26.6001H14.535V26.6001M16.1975 24.7001H18.0737L18.4062 22.1826C18.8971 22.0559 19.3523 21.8699 19.7719 21.6245C20.1914 21.3791 20.5754 21.0822 20.9237 20.7338L23.275 21.7076L24.2012 20.0926L22.1587 18.5488C22.2379 18.3272 22.2933 18.0936 22.325 17.8482C22.3566 17.6028 22.3725 17.3534 22.3725 17.1001C22.3725 16.8468 22.3566 16.5974 22.325 16.352C22.2933 16.1066 22.2379 15.873 22.1587 15.6513L24.2012 14.1076L23.275 12.4926L20.9237 13.4901C20.5754 13.1259 20.1914 12.8211 19.7719 12.5757C19.3523 12.3303 18.8971 12.1443 18.4062 12.0176L18.0975 9.5001H16.2212L15.8887 12.0176C15.3979 12.1443 14.9427 12.3303 14.5231 12.5757C14.1035 12.8211 13.7196 13.118 13.3712 13.4663L11.02 12.4926L10.0937 14.1076L12.1362 15.6276C12.0571 15.8651 12.0016 16.1026 11.97 16.3401C11.9383 16.5776 11.9225 16.8309 11.9225 17.1001C11.9225 17.3534 11.9383 17.5988 11.97 17.8363C12.0016 18.0738 12.0571 18.3113 12.1362 18.5488L10.0937 20.0926L11.02 21.7076L13.3712 20.7101C13.7196 21.0743 14.1035 21.3791 14.5231 21.6245C14.9427 21.8699 15.3979 22.0559 15.8887 22.1826L16.1975 24.7001V24.7001M17.195 20.4251C18.1133 20.4251 18.8971 20.1005 19.5462 19.4513C20.1954 18.8022 20.52 18.0184 20.52 17.1001C20.52 16.1818 20.1954 15.398 19.5462 14.7488C18.8971 14.0997 18.1133 13.7751 17.195 13.7751C16.2608 13.7751 15.4731 14.0997 14.8319 14.7488C14.1906 15.398 13.87 16.1818 13.87 17.1001C13.87 18.0184 14.1906 18.8022 14.8319 19.4513C15.4731 20.1005 16.2608 20.4251 17.195 20.4251V20.4251M17.1475 17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001V17.1001" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 2.8 KiB

파일 보기

@ -0,0 +1,3 @@
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 16V3H0V0H13V3H8V16H5V16M14 16V8H11V5H20V8H17V16H14V16" fill="#94A3B8"/>
</svg>

After

Width:  |  Height:  |  크기: 187 B

파일 보기

@ -0,0 +1,10 @@
<svg width="630" height="96" viewBox="0 0 630 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2_537)">
<path d="M123 76.7998C155 38.3998 187 31.9998 219 57.5998C251 83.1998 283 76.7998 315 38.3998C347 -0.000192642 379 12.7998 411 76.7998C443 140.8 475 121.6 507 19.1998" stroke="#4CD7F6" stroke-width="1.92"/>
</g>
<defs>
<clipPath id="clip0_2_537">
<rect width="630" height="96" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  크기: 451 B

파일 보기

@ -1,5 +1,6 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import './index.css' import './index.css'
import App from './App.tsx' import App from './App.tsx'
@ -17,8 +18,10 @@ const queryClient = new QueryClient({
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <BrowserRouter>
<App /> <QueryClientProvider client={queryClient}>
</QueryClientProvider> <App />
</QueryClientProvider>
</BrowserRouter>
</StrictMode>, </StrictMode>,
) )

파일 보기

@ -0,0 +1,491 @@
// ColorPaletteContent.tsx — WING-OPS Color Palette 콘텐츠 (다크/라이트 테마 지원)
import type { DesignTheme } from './designTheme';
// ---------- 데이터 타입 ----------
interface PrimitiveColorStep {
label: string;
hex: string;
}
interface PrimitiveColorGroup {
name: string;
steps: PrimitiveColorStep[];
}
interface SemanticToken {
token: string;
dark: string;
light: string;
usage: string[];
}
interface SemanticCategory {
name: string;
tokens: SemanticToken[];
}
// ---------- Primitive Colors 데이터 ----------
const PRIMITIVE_COLORS: PrimitiveColorGroup[] = [
{
name: 'Navy',
steps: [
{ label: '0', hex: '#0a0e1a' },
{ label: '1', hex: '#0f1524' },
{ label: '2', hex: '#121929' },
{ label: '3', hex: '#1a2236' },
{ label: 'hover', hex: '#1e2844' },
],
},
{
name: 'Cyan',
steps: [
{ label: '00', hex: '#ecfeff' }, { label: '10', hex: '#cffafe' },
{ label: '20', hex: '#a5f3fc' }, { label: '30', hex: '#67e8f9' },
{ label: '40', hex: '#22d3ee' }, { label: '50', hex: '#06b6d4' },
{ label: '60', hex: '#0891b2' }, { label: '70', hex: '#0e7490' },
{ label: '80', hex: '#155e75' }, { label: '90', hex: '#164e63' },
{ label: '100', hex: '#083344' },
],
},
{
name: 'Blue',
steps: [
{ label: '00', hex: '#eff6ff' }, { label: '10', hex: '#dbeafe' },
{ label: '20', hex: '#bfdbfe' }, { label: '30', hex: '#93c5fd' },
{ label: '40', hex: '#60a5fa' }, { label: '50', hex: '#3b82f6' },
{ label: '60', hex: '#2563eb' }, { label: '70', hex: '#1d4ed8' },
{ label: '80', hex: '#1e40af' }, { label: '90', hex: '#1e3a8a' },
{ label: '100', hex: '#172554' },
],
},
{
name: 'Red',
steps: [
{ label: '00', hex: '#fef2f2' }, { label: '10', hex: '#fee2e2' },
{ label: '20', hex: '#fecaca' }, { label: '30', hex: '#fca5a5' },
{ label: '40', hex: '#f87171' }, { label: '50', hex: '#ef4444' },
{ label: '60', hex: '#dc2626' }, { label: '70', hex: '#b91c1c' },
{ label: '80', hex: '#991b1b' }, { label: '90', hex: '#7f1d1d' },
{ label: '100', hex: '#450a0a' },
],
},
{
name: 'Green',
steps: [
{ label: '00', hex: '#f0fdf4' }, { label: '10', hex: '#dcfce7' },
{ label: '20', hex: '#bbf7d0' }, { label: '30', hex: '#86efac' },
{ label: '40', hex: '#4ade80' }, { label: '50', hex: '#22c55e' },
{ label: '60', hex: '#16a34a' }, { label: '70', hex: '#15803d' },
{ label: '80', hex: '#166534' }, { label: '90', hex: '#14532d' },
{ label: '100', hex: '#052e16' },
],
},
{
name: 'Orange',
steps: [
{ label: '00', hex: '#fff7ed' }, { label: '10', hex: '#ffedd5' },
{ label: '20', hex: '#fed7aa' }, { label: '30', hex: '#fdba74' },
{ label: '40', hex: '#fb923c' }, { label: '50', hex: '#f97316' },
{ label: '60', hex: '#ea580c' }, { label: '70', hex: '#c2410c' },
{ label: '80', hex: '#9a3412' }, { label: '90', hex: '#7c2d12' },
{ label: '100', hex: '#431407' },
],
},
{
name: 'Yellow',
steps: [
{ label: '00', hex: '#fefce8' }, { label: '10', hex: '#fef9c3' },
{ label: '20', hex: '#fef08a' }, { label: '30', hex: '#fde047' },
{ label: '40', hex: '#facc15' }, { label: '50', hex: '#eab308' },
{ label: '60', hex: '#ca8a04' }, { label: '70', hex: '#a16207' },
{ label: '80', hex: '#854d0e' }, { label: '90', hex: '#713f12' },
{ label: '100', hex: '#422006' },
],
},
];
// ---------- Semantic Colors 데이터 ----------
const SEMANTIC_CATEGORIES: SemanticCategory[] = [
{
name: 'Text',
tokens: [
{ token: 'text-1', dark: '#edf0f7', light: '#0f172a', usage: ['기본 텍스트 색상', '아이콘 기본 색상'] },
{ token: 'text-2', dark: '#b0b8cc', light: '#475569', usage: ['보조 텍스트 색상'] },
{ token: 'text-3', dark: '#8690a6', light: '#94a3b8', usage: ['비활성 텍스트', '플레이스홀더'] },
],
},
{
name: 'Background',
tokens: [
{ token: 'bg-0', dark: '#0a0e1a', light: '#f8fafc', usage: ['페이지 배경'] },
{ token: 'bg-1', dark: '#0f1524', light: '#ffffff', usage: ['사이드바', '패널'] },
{ token: 'bg-2', dark: '#121929', light: '#f1f5f9', usage: ['테이블 헤더'] },
{ token: 'bg-3', dark: '#1a2236', light: '#e2e8f0', usage: ['카드 배경'] },
{ token: 'bg-hover', dark: '#1e2844', light: '#cbd5e1', usage: ['호버 상태'] },
],
},
{
name: 'Border',
tokens: [
{ token: 'border', dark: '#1e2a42', light: '#cbd5e1', usage: ['기본 구분선'] },
{ token: 'border-light', dark: '#2a3a5c', light: '#e2e8f0', usage: ['연한 구분선'] },
],
},
{
name: 'Accent',
tokens: [
{ token: 'primary-cyan', dark: '#06b6d4', light: '#06b6d4', usage: ['주요 강조', '활성 상태'] },
{ token: 'primary-blue', dark: '#3b82f6', light: '#0891b2', usage: ['보조 강조'] },
{ token: 'primary-purple', dark: '#a855f7', light: '#6366f1', usage: ['3차 강조'] },
],
},
{
name: 'Status',
tokens: [
{ token: 'status-red', dark: '#ef4444', light: '#dc2626', usage: ['위험', '삭제'] },
{ token: 'status-orange', dark: '#f97316', light: '#c2410c', usage: ['주의'] },
{ token: 'status-yellow', dark: '#eab308', light: '#b45309', usage: ['경고'] },
{ token: 'status-green', dark: '#22c55e', light: '#047857', usage: ['정상', '성공'] },
],
},
];
// ---------- Props ----------
interface ColorPaletteContentProps {
theme: DesignTheme;
}
// ---------- 컴포넌트 ----------
export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
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 }}
>
Color
</h1>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
WING-OPS . Primitive Token( ) Semantic Token( ) .
</p>
</div>
{/* 토큰 네이밍 다이어그램 카드 */}
<div
className="inline-flex flex-row items-center gap-3 rounded-md border border-solid px-5 py-4"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
<span
className="font-mono text-sm font-bold"
style={{ color: t.textPrimary }}
>
bg
</span>
<span
className="font-mono text-sm"
style={{ color: isDark ? 'rgba(66,71,84,0.60)' : '#cbd5e1' }}
>
</span>
<span
className="font-mono text-sm font-bold"
style={{ color: t.textPrimary }}
>
0
</span>
<div className="flex flex-row gap-4 ml-4">
<div className="flex flex-col gap-0.5">
<span
className="font-mono text-[10px] uppercase"
style={{ letterSpacing: '1px', color: t.textAccent }}
>
Category
</span>
<span
className="font-korean text-[11px]"
style={{ color: t.textSecondary }}
>
(bg, text, border, status)
</span>
</div>
<div
className="w-px self-stretch"
style={{ backgroundColor: isDark ? 'rgba(66,71,84,0.30)' : '#e2e8f0' }}
/>
<div className="flex flex-col gap-0.5">
<span
className="font-mono text-[10px] uppercase"
style={{ letterSpacing: '1px', color: t.textAccent }}
>
Value
</span>
<span
className="font-korean text-[11px]"
style={{ color: t.textSecondary }}
>
(03, hover, red, cyan)
</span>
</div>
</div>
</div>
</div>
{/* ── 섹션 2: Primitive Tokens ── */}
<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 }}
>
Primitive Tokens
</h2>
<ul
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
style={{ color: t.textSecondary }}
>
<li>Tailwind CSS .</li>
<li> (Hue) 00( ) 100( ) 11 .</li>
<li>Navy는 UI .</li>
</ul>
</div>
<div className="flex flex-col gap-6">
{PRIMITIVE_COLORS.map((group) => (
<div key={group.name} className="flex flex-col gap-2">
{/* 그룹명 */}
<span
className="font-mono text-xs font-bold uppercase"
style={{ letterSpacing: '1.2px', color: t.textMuted }}
>
{group.name}
</span>
{/* 가로 컬러 바 */}
<div className="flex flex-row w-full">
{group.steps.map((step) => (
<div key={step.label} className="flex flex-col" style={{ flex: 1 }}>
<div
style={{
backgroundColor: step.hex,
height: '48px',
}}
/>
<div
className="flex flex-col gap-0.5 pt-1"
style={{ borderTop: `1px solid ${isDark ? 'rgba(66,71,84,0.15)' : '#e2e8f0'}` }}
>
<span
className="font-mono"
style={{ fontSize: '9px', lineHeight: '13px', color: t.textMuted }}
>
{step.label}
</span>
<span
className="font-mono"
style={{ fontSize: '9px', lineHeight: '13px', color: t.textMuted, opacity: 0.7 }}
>
{step.hex}
</span>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
{/* ── 섹션 3: Semantic Colors ── */}
<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 }}
>
Semantic Colors
</h2>
<ul
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
style={{ color: t.textSecondary }}
>
<li>Primitive Token을 .</li>
<li>/ .</li>
<li> Semantic Token을 , Primitive Token을 .</li>
</ul>
</div>
<div className="flex flex-col gap-8">
{SEMANTIC_CATEGORIES.map((category) => (
<div key={category.name} className="flex flex-col gap-3">
{/* 카테고리 제목 (좌측 2px cyan accent bar) */}
<div className="flex flex-row items-center gap-3">
<div
className="w-0.5 self-stretch rounded-full shrink-0"
style={{ backgroundColor: t.textAccent, minHeight: '20px' }}
/>
<span
className="font-sans text-sm font-bold uppercase"
style={{ letterSpacing: '0.8px', color: t.textPrimary }}
>
{category.name}
</span>
</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: '200px 1fr 1fr 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{(['Token', 'Dark Mode', 'Light Mode', 'Usage'] 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>
{/* 데이터 행 */}
{category.tokens.map((token, rowIdx) => (
<div
key={token.token}
className="grid"
style={{
gridTemplateColumns: '200px 1fr 1fr 1fr',
borderTop: rowIdx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
{/* Token 컬럼 */}
<div className="py-3 px-4 flex items-center">
<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.token}
</span>
</div>
{/* Dark Mode 컬럼 */}
<div className="py-3 px-4 flex items-center">
<div
className="inline-flex flex-row items-center gap-2 rounded-md px-3 py-2"
style={{ backgroundColor: '#171b28' }}
>
<div
className="rounded-sm shrink-0"
style={{
width: '24px',
height: '24px',
backgroundColor: token.dark,
border: '1px solid rgba(255,255,255,0.08)',
}}
/>
<span
className="font-mono text-[11px]"
style={{ color: 'rgba(237,240,247,0.70)' }}
>
{token.dark}
</span>
</div>
</div>
{/* Light Mode 컬럼 */}
<div className="py-3 px-4 flex items-center">
<div
className="inline-flex flex-row items-center gap-2 rounded-md px-3 py-2"
style={{ backgroundColor: '#f8fafc' }}
>
<div
className="rounded-sm shrink-0"
style={{
width: '24px',
height: '24px',
backgroundColor: token.light,
border: '1px solid rgba(0,0,0,0.08)',
}}
/>
<span
className="font-mono text-[11px]"
style={{ color: 'rgba(15,23,42,0.70)' }}
>
{token.light}
</span>
</div>
</div>
{/* Usage 컬럼 */}
<div className="py-3 px-4 flex items-center">
<ul className="flex flex-col gap-0.5 list-none">
{token.usage.map((u) => (
<li key={u} className="flex flex-row items-center gap-1.5">
<span
className="w-1 h-1 rounded-full shrink-0"
style={{ backgroundColor: t.textMuted }}
/>
<span
className="font-korean text-xs"
style={{ color: t.textSecondary }}
>
{u}
</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default ColorPaletteContent;

파일 보기

@ -0,0 +1,48 @@
import { ButtonCatalogSection } from './components/ButtonCatalogSection';
import { IconBadgeSection } from './components/IconBadgeSection';
import { CardSection } from './components/CardSection';
export const ComponentsContent = () => {
return (
<div className="pt-20 px-8 pb-16 flex flex-col gap-[121.5px] items-start justify-start max-w-[1440px]">
{/* 헤더 */}
<div className="flex flex-col gap-2 items-start justify-start self-stretch">
<h1
className="text-[#dfe2f3] font-korean text-3xl leading-9 font-medium self-stretch"
style={{ letterSpacing: '-0.75px' }}
>
</h1>
<p className="text-[#bcc9cd] font-korean text-sm leading-5 font-medium max-w-2xl">
WING-OPS . .
</p>
</div>
{/* 섹션 */}
<ButtonCatalogSection />
<IconBadgeSection />
<CardSection />
{/* 푸터 */}
<div
className="border-t border-solid border-[rgba(22,78,99,0.10)] p-8 flex flex-row items-center justify-between self-stretch"
style={{ opacity: 0.4 }}
>
<span
className="text-[#64748b] font-sans text-[10px] leading-[15px] font-bold uppercase"
style={{ letterSpacing: '1px' }}
>
© 2024 WING-OPS
</span>
<span
className="text-[#22d3ee] font-korean text-[10px] leading-[15px] font-medium uppercase"
style={{ letterSpacing: '1px' }}
>
v2.4
</span>
</div>
</div>
);
};
export default ComponentsContent;

파일 보기

@ -0,0 +1,489 @@
// DesignContent.tsx — 디자인 토큰 탭 콘텐츠 (다크/라이트 테마 지원)
import type { DesignTheme } from './designTheme';
// ---------- 06 타이포그래피 스케일 데이터 ----------
interface TypoRow {
size: string;
sampleNode: (theme: DesignTheme) => React.ReactNode;
properties: string;
isData?: boolean;
}
// ---------- 공통 섹션 타이틀 ----------
interface SectionTitleProps {
num: string;
title: string;
sub?: string;
rightNode?: React.ReactNode;
theme: DesignTheme;
}
const SectionTitle = ({ num, title, sub, rightNode, theme }: SectionTitleProps) => (
<div className="flex flex-col gap-1">
<div className="flex flex-row items-center justify-between">
<p
className="font-sans text-lg leading-7 font-bold"
style={{ letterSpacing: '0.45px', color: theme.sectionTitle }}
>
{num} {title}
</p>
{rightNode}
</div>
{sub && (
<p
className="font-mono text-[10px] leading-[15px] uppercase"
style={{ letterSpacing: theme.sectionSubSpacing, color: theme.sectionSub }}
>
{sub}
</p>
)}
</div>
);
// ---------- 타이포그래피 행 ----------
const TYPO_ROWS: TypoRow[] = [
{
size: '9px / Meta',
sampleNode: (t) => (
<span className="font-korean text-[9px]" style={{ color: t.typoSampleText }}> Meta info</span>
),
properties: 'Regular / 400',
},
{
size: '10px / Table',
sampleNode: (t) => (
<span className="font-korean text-[10px]" style={{ color: t.typoSampleText }}> Table data</span>
),
properties: 'Medium / 500',
},
{
size: '11px / Action',
sampleNode: (t) => (
<span
className="inline-flex items-center rounded-md border border-solid py-2 px-4"
style={{ backgroundColor: t.typoActionBg, borderColor: t.typoActionBorder }}
>
<span className="font-korean text-[11px] font-medium" style={{ color: t.typoActionText }}>
/ Input/Button text
</span>
</span>
),
properties: 'Medium / 500',
},
{
size: '13px / Header',
sampleNode: (t) => (
<span className="font-korean text-[13px] font-bold" style={{ color: t.textPrimary }}>
Section Header
</span>
),
properties: 'Bold / 700',
},
{
size: '15px / Title',
sampleNode: (t) => (
<span className="font-korean text-[15px] font-bold" style={{ color: t.textPrimary }}>
Panel Title
</span>
),
properties: 'ExtraBold / 800',
},
{
size: 'Data / Mono',
sampleNode: (t) => (
<div className="flex flex-col gap-1">
<span className="font-mono text-[11px]" style={{ color: t.typoDataText }}>1,234.56 km²</span>
<span className="font-mono text-[11px]" style={{ color: t.typoCoordText }}>35° 06' 12&quot; N</span>
</div>
),
properties: 'JetBrains Mono / 11px',
isData: true,
},
];
// ---------- 컴포넌트 ----------
export interface DesignContentProps {
theme: DesignTheme;
}
export const DesignContent = ({ theme }: DesignContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
return (
<div className="pt-24 px-8 pb-16 flex flex-col gap-12 items-start justify-start max-w-[1440px]">
{/* ── 헤더 섹션 ── */}
<div
className="w-full border-b border-solid pb-8 flex flex-row items-end justify-between"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
>
<div className="flex flex-col gap-2">
<h1
className="font-sans text-4xl leading-10 font-bold"
style={{ letterSpacing: '-0.9px', color: t.textPrimary }}
>
</h1>
<p className="font-korean text-base leading-6 font-light" style={{ color: t.textSecondary }}>
Comprehensive design token reference for the WING-OPS operational interface.
</p>
</div>
{/* 상태 뱃지 */}
<div
className="rounded border border-solid p-2 flex flex-row gap-3 items-center shrink-0"
style={{
backgroundColor: t.systemActiveBg,
borderColor: t.systemActiveBorder,
boxShadow: isDark ? 'none' : t.systemActiveShadow,
}}
>
<span
className="rounded-xl w-3 h-3 shrink-0"
style={{ backgroundColor: t.textAccent, boxShadow: t.systemActiveShadow }}
/>
<span
className="font-mono text-xs leading-4 uppercase"
style={{ letterSpacing: '1.2px', color: t.textAccent }}
>
System Active
</span>
</div>
</div>
{/* ── 2컬럼 그리드 ── */}
<div
className="w-full grid gap-10"
style={{ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' }}
>
{/* ── 행 1 좌측: 01 배경색 ── */}
<div className="flex flex-col gap-4">
<SectionTitle num="01" title="배경색" sub="Surface Hierarchy" theme={t} />
<div className="flex flex-col gap-4">
{t.bgTokens.map((item) => (
<div
key={item.token}
className="rounded-[10px] border border-solid p-6 flex flex-row gap-4 items-center"
style={{
backgroundColor: isDark ? item.bg : t.cardBg,
borderColor: item.isHover ? t.cardBorderHover : t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{/* 색상 스와치 */}
<div
className="w-24 h-24 rounded-sm border border-solid shrink-0"
style={{
backgroundColor: item.bg,
borderColor: item.isHover ? t.swatchBorderHover : t.swatchBorder,
boxShadow: 'inset 0px 2px 4px 1px rgba(0, 0, 0, 0.05)',
}}
/>
{/* 정보 */}
<div className="flex flex-col gap-1">
<span className="font-mono text-xs leading-4" style={{ color: t.textAccent }}>{item.token}</span>
<span className="font-korean text-sm leading-5 font-bold" style={{ color: t.textPrimary }}>
{item.hex}
</span>
<span className="font-korean text-[11px] leading-[16.5px]" style={{ color: t.textSecondary }}>
{item.desc}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 행 1 우측: 02 테두리 색상 + 03 텍스트 색상 ── */}
<div className="flex flex-col gap-8">
{/* 02 테두리 색상 */}
<div className="flex flex-col gap-4">
<SectionTitle num="02" title="테두리 색상" theme={t} />
<div className="grid grid-cols-2 gap-4">
{t.borderTokens.map((item) => (
<div
key={item.token}
className="rounded-md border border-solid p-4 flex flex-col gap-2"
style={{
backgroundColor: t.borderCardBg,
borderColor: item.border,
boxShadow: t.borderCardShadow,
}}
>
<span className="font-mono text-[10px]" style={{ color: t.textAccent }}>{item.token}</span>
<span className="font-korean text-sm font-bold pb-2" style={{ color: t.textPrimary }}>
{item.hex}
</span>
<div className="h-1 self-stretch rounded-sm" style={{ backgroundColor: item.barBg }} />
</div>
))}
</div>
</div>
{/* 03 텍스트 색상 */}
<div className="flex flex-col gap-4">
<SectionTitle num="03" title="텍스트 색상" theme={t} />
<div
className="rounded-lg border border-solid p-8 flex flex-col gap-6"
style={{
backgroundColor: t.textSectionBg,
borderColor: t.textSectionBorder,
}}
>
{t.textTokens.map((item) => (
<div key={item.token} className="flex flex-col gap-[3px]">
<span className="font-mono text-[10px]" style={{ color: t.textAccent }}>{item.token}</span>
<span className={item.sampleClass}>{item.sampleText}</span>
<span className="font-korean text-xs" style={{ color: item.descColor }}>
{item.desc}
</span>
</div>
))}
</div>
</div>
</div>
{/* ── 행 2 좌측: 04 강조 색상 ── */}
<div className="flex flex-col gap-4">
<SectionTitle num="04" title="강조 색상" theme={t} />
<div className="flex flex-col gap-4">
{t.accentTokens.map((item) => (
<div
key={item.token}
className="rounded-lg border border-solid p-4 flex flex-row items-center justify-between"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{/* 좌측: 색상 원 + 이름/토큰 */}
<div className="flex flex-row items-center gap-4">
<div
className={`w-12 h-12 ${t.badgeRadius} shrink-0`}
style={{
backgroundColor: item.color,
boxShadow: item.glow,
}}
/>
<div className="flex flex-col gap-1">
<span className="font-korean text-sm font-bold" style={{ color: t.textPrimary }}>{item.name}</span>
<span className="font-mono text-[10px]" style={{ color: t.textMuted }}>
{item.token} / {item.color}
</span>
</div>
</div>
{/* 우측: 뱃지 */}
<div
className={`${t.badgeRadius} border border-solid py-1 px-3`}
style={{
backgroundColor: item.badgeBg,
borderColor: item.badgeBorder,
}}
>
<span
className="font-korean text-[11px] font-medium"
style={{ color: item.badgeText }}
>
{item.badge}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 행 2 우측: 05 상태 표시기 ── */}
<div className="flex flex-col gap-4">
<SectionTitle num="05" title="상태 표시기" theme={t} />
<div className="grid grid-cols-2 gap-4">
{t.statusTokens.map((item) => (
<div
key={item.hex}
className={`${t.badgeRadius} border border-solid p-4 flex flex-row gap-3 items-center`}
style={{
backgroundColor: item.bg,
borderColor: item.border,
}}
>
<span
className={`w-2.5 h-2.5 ${t.badgeRadius} shrink-0`}
style={{
backgroundColor: item.color,
...(item.glow ? { boxShadow: item.glow } : {}),
}}
/>
<span
className="font-korean text-[13px] font-bold flex-1"
style={{ color: item.color }}
>
{item.label}
</span>
<span className="font-mono text-[10px] opacity-40" style={{ color: item.color }}>
{item.hex}
</span>
</div>
))}
</div>
</div>
{/* ── 행 3: 06 타이포그래피 스케일 (전체 열 span) ── */}
<div className="col-span-2 flex flex-col gap-4">
<SectionTitle
num="06"
title="타이포그래피 스케일"
theme={t}
rightNode={
<div className="flex flex-row gap-2 items-center">
<span
className="rounded-sm py-0.5 px-2 font-korean text-[10px] font-bold"
style={{ backgroundColor: t.fontBadgePrimaryBg, color: t.fontBadgePrimaryText }}
>
Noto Sans KR
</span>
<span
className="rounded-sm border border-solid py-0.5 px-2 font-korean text-[10px] font-bold"
style={{ borderColor: t.fontBadgeSecondaryBorder, color: t.fontBadgeSecondaryText }}
>
JetBrains Mono
</span>
</div>
}
/>
<div
className="rounded-lg border border-solid overflow-hidden w-full"
style={{ backgroundColor: t.tableContainerBg, borderColor: t.cardBorder }}
>
{/* 헤더 행 */}
<div className="flex flex-row items-start" style={{ backgroundColor: t.tableHeaderBg }}>
{(['Size / Role', 'Sample String', 'Properties'] as const).map((col, i) => (
<div
key={col}
className="flex-1 py-4 px-8 border-b border-solid"
style={{ textAlign: i === 2 ? 'right' : 'left', borderColor: t.tableRowBorder }}
>
<span
className="font-mono text-[10px] font-medium uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
</span>
</div>
))}
</div>
{/* 데이터 행 */}
{TYPO_ROWS.map((row) => (
<div
key={row.size}
className="flex flex-row items-center border-t border-solid"
style={{
borderColor: t.tableRowBorder,
backgroundColor: row.isData ? t.tableDataRowBg : undefined,
}}
>
{/* Size */}
<div className="flex-1 py-4 px-8">
<span className="font-mono text-[10px]" style={{ color: t.typoSizeText }}>{row.size}</span>
</div>
{/* Sample */}
<div className="flex-1 py-4 px-8">{row.sampleNode(t)}</div>
{/* Properties */}
<div className="flex-1 py-4 px-8 text-right" style={{ opacity: 0.5 }}>
<span className="font-mono text-[10px]" style={{ color: t.typoPropertiesText }}>{row.properties}</span>
</div>
</div>
))}
</div>
</div>
{/* ── 행 4: 07 테두리 곡률 (전체 열 span) ── */}
<div className="col-span-2 flex flex-col gap-4">
<SectionTitle num="07" title="테두리 곡률" theme={t} />
<div className="grid grid-cols-2 gap-8">
{/* radius-sm */}
<div className="flex flex-col gap-2">
<span className="font-mono text-xs leading-4" style={{ color: t.textMuted }}>{t.radiusSmLabel}</span>
<div
className="rounded-md border border-solid p-6 h-32 flex flex-col justify-end"
style={{
backgroundColor: t.radiusCardBg,
borderColor: t.radiusCardBorder,
boxShadow: t.radiusCardShadow,
}}
>
<span
className="font-korean text-[10px] font-bold uppercase"
style={{ letterSpacing: '1px', color: t.textAccent }}
>
Small Elements
</span>
<p className="font-korean text-xs leading-[19.5px] mt-1" style={{ color: t.radiusDescText }}>
Applied to tactical buttons, search inputs, and micro-cards for a precise, sharp industrial feel.
</p>
</div>
</div>
{/* radius-md */}
<div className="flex flex-col gap-2">
<span className="font-mono text-xs leading-4" style={{ color: t.textMuted }}>{t.radiusMdLabel}</span>
<div
className="rounded-[10px] border border-solid p-6 h-32 flex flex-col justify-end"
style={{
backgroundColor: t.radiusCardBg,
borderColor: t.radiusCardBorder,
boxShadow: t.radiusCardShadow,
}}
>
<span
className="font-korean text-[10px] font-bold uppercase"
style={{ letterSpacing: '1px', color: t.textAccent }}
>
Structural Panels
</span>
<p className="font-korean text-xs leading-[19.5px] mt-1" style={{ color: t.radiusDescText }}>
Applied to telemetry cards, floating modals, and primary operational panels to soften high-density data.
</p>
</div>
</div>
</div>
</div>
</div>
{/* ── 푸터 ── */}
<div
className="w-full border-t border-solid pt-12 flex flex-row items-center justify-between"
style={{ borderColor: t.footerBorder }}
>
{/* 좌측 */}
<div className="flex flex-row gap-8">
{['Precision Engineering', 'Safety Compliant', 'Optimized v8.42'].map((label) => (
<span
key={label}
className="font-mono text-[10px] uppercase"
style={{ letterSpacing: '1px', color: t.footerText }}
>
{label}
</span>
))}
</div>
{/* 우측 */}
<div className="flex flex-row gap-2 items-center">
<span
className="font-mono text-[10px] uppercase"
style={{ letterSpacing: '1px', color: t.footerText }}
>
Generated for Terminal:
</span>
<span
className="font-mono text-[10px] font-medium uppercase"
style={{ letterSpacing: '1px', color: t.footerAccent }}
>
1440x900_PR_MKT
</span>
</div>
</div>
</div>
);
};
export default DesignContent;

파일 보기

@ -0,0 +1,96 @@
import type { DesignTheme } from './designTheme';
export type DesignTab = 'foundations' | 'components';
interface DesignHeaderProps {
activeTab: DesignTab;
onTabChange: (tab: DesignTab) => void;
theme: DesignTheme;
onThemeToggle: () => void;
}
const TABS: { label: string; id: DesignTab }[] = [
{ label: 'Foundations', id: 'foundations' },
{ label: 'Components', id: 'components' },
];
export const DesignHeader = ({ activeTab, onTabChange, theme, onThemeToggle }: DesignHeaderProps) => {
const isDark = theme.mode === 'dark';
return (
<header
className="h-16 px-8 flex flex-row items-center justify-between shrink-0 border-b border-solid"
style={{
backgroundColor: theme.headerBg,
borderColor: theme.headerBorder,
}}
>
{/* 좌측: 로고 + 버전 뱃지 */}
<div className="flex flex-row items-center gap-3">
<span
className="font-sans text-2xl leading-8 font-bold"
style={{ letterSpacing: '2.4px', color: theme.textAccent }}
>
WING-OPS
</span>
<div
className="rounded-sm border border-solid py-1 px-2"
style={{
backgroundColor: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
borderColor: isDark ? 'rgba(255,255,255,0.10)' : '#e2e8f0',
}}
>
<span
className="font-sans text-[10px] leading-[15px] uppercase"
style={{ letterSpacing: '2px', color: theme.textMuted }}
>
Design System v1.0
</span>
</div>
</div>
{/* 중앙: 탭 네비게이션 */}
<nav className="flex flex-row gap-8">
{TABS.map(({ label, id }) => {
const isActive = activeTab === id;
return (
<button
key={label}
type="button"
onClick={() => onTabChange(id)}
className={`font-sans text-base leading-6 bg-transparent cursor-pointer ${
isActive ? 'font-bold border-b-2 pb-1' : 'font-medium border-none'
}`}
style={{
letterSpacing: '-0.4px',
color: isActive ? theme.textAccent : theme.textMuted,
borderColor: isActive ? theme.textAccent : 'transparent',
}}
>
{label}
</button>
);
})}
</nav>
{/* 우측: 테마 토글 */}
<div className="flex flex-row items-center">
<button
type="button"
onClick={onThemeToggle}
className="w-10 h-10 rounded-md border border-solid flex items-center justify-center cursor-pointer"
style={{
backgroundColor: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
borderColor: isDark ? 'rgba(255,255,255,0.10)' : '#e2e8f0',
}}
title={isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
>
<span className="text-lg">{isDark ? '☀️' : '🌙'}</span>
</button>
</div>
</header>
);
};
export default DesignHeader;

파일 보기

@ -0,0 +1,66 @@
import { useState } from 'react';
import { DesignHeader } from './DesignHeader';
import type { DesignTab } from './DesignHeader';
import { DesignSidebar } from './DesignSidebar';
import type { MenuItemId } from './DesignSidebar';
import { ComponentsContent } from './ComponentsContent';
import { ColorPaletteContent } from './ColorPaletteContent';
import { TypographyContent } from './TypographyContent';
import { RadiusContent } from './RadiusContent';
import { LayoutContent } from './LayoutContent';
import { getTheme } from './designTheme';
import type { ThemeMode } from './designTheme';
const FIRST_ITEM: Record<DesignTab, MenuItemId> = {
foundations: 'color',
components: 'buttons',
};
export const DesignPage = () => {
const [activeTab, setActiveTab] = useState<DesignTab>('foundations');
const [themeMode, setThemeMode] = useState<ThemeMode>('dark');
const [sidebarItem, setSidebarItem] = useState<MenuItemId>('color');
const theme = getTheme(themeMode);
const handleTabChange = (tab: DesignTab) => {
setActiveTab(tab);
setSidebarItem(FIRST_ITEM[tab]);
};
const renderContent = () => {
if (activeTab === 'foundations') {
switch (sidebarItem) {
case 'color':
return <ColorPaletteContent theme={theme} />;
case 'typography':
return <TypographyContent theme={theme} />;
case 'radius':
return <RadiusContent theme={theme} />;
case 'layout':
return <LayoutContent theme={theme} />;
default:
return <ColorPaletteContent theme={theme} />;
}
}
return <ComponentsContent />;
};
return (
<div
className="h-screen w-screen overflow-hidden flex flex-col"
style={{ backgroundColor: theme.pageBg }}
>
<DesignHeader activeTab={activeTab} onTabChange={handleTabChange} theme={theme} onThemeToggle={() => setThemeMode(themeMode === 'dark' ? 'light' : 'dark')} />
<div className="flex flex-1 overflow-hidden">
<DesignSidebar theme={theme} activeTab={activeTab} activeItem={sidebarItem} onItemChange={setSidebarItem} />
<main className="flex-1 overflow-y-auto">
{renderContent()}
</main>
</div>
</div>
);
};
export default DesignPage;

파일 보기

@ -0,0 +1,107 @@
import type { DesignTheme } from './designTheme';
import type { DesignTab } from './DesignHeader';
import wingColorPaletteIcon from '../../assets/icons/wing-color-palette.svg';
import wingElevationIcon from '../../assets/icons/wing-elevation.svg';
import wingFoundationsIcon from '../../assets/icons/wing-foundations.svg';
import wingLayoutGridIcon from '../../assets/icons/wing-layout-grid.svg';
import wingTypographyIcon from '../../assets/icons/wing-typography.svg';
export type FoundationsMenuItemId = 'color' | 'typography' | 'radius' | 'layout';
export type ComponentsMenuItemId = 'buttons' | 'text-inputs' | 'controls' | 'badge' | 'dialog' | 'tabs' | 'popup' | 'navigation';
export type MenuItemId = FoundationsMenuItemId | ComponentsMenuItemId;
interface MenuItem {
id: MenuItemId;
label: string;
icon: string;
}
const FOUNDATIONS_MENU: MenuItem[] = [
{ id: 'color', label: 'Color', icon: wingColorPaletteIcon },
{ id: 'typography', label: 'Typography', icon: wingTypographyIcon },
{ id: 'radius', label: 'Radius', icon: wingElevationIcon },
{ id: 'layout', label: 'Layout', icon: wingLayoutGridIcon },
];
const COMPONENTS_MENU: MenuItem[] = [
{ id: 'buttons', label: 'Buttons', icon: wingFoundationsIcon },
{ id: 'text-inputs', label: 'Text Inputs', icon: wingFoundationsIcon },
{ id: 'controls', label: 'Controls', icon: wingFoundationsIcon },
{ id: 'badge', label: 'Badge', icon: wingColorPaletteIcon },
{ id: 'dialog', label: 'Dialog', icon: wingLayoutGridIcon },
{ id: 'tabs', label: 'Tabs', icon: wingLayoutGridIcon },
{ id: 'popup', label: 'Popup', icon: wingElevationIcon },
{ id: 'navigation', label: 'Navigation', icon: wingTypographyIcon },
];
const SIDEBAR_CONFIG: Record<DesignTab, { title: string; subtitle: string; menu: MenuItem[] }> = {
foundations: { title: 'FOUNDATIONS', subtitle: 'Design Token System', menu: FOUNDATIONS_MENU },
components: { title: 'COMPONENTS', subtitle: 'UI Component Catalog', menu: COMPONENTS_MENU },
};
export interface DesignSidebarProps {
theme: DesignTheme;
activeTab: DesignTab;
activeItem: MenuItemId;
onItemChange: (id: MenuItemId) => void;
}
export function DesignSidebar({ theme, activeTab, activeItem, onItemChange }: DesignSidebarProps) {
const isDark = theme.mode === 'dark';
const { menu } = SIDEBAR_CONFIG[activeTab];
const renderMenuItem = (item: MenuItem) => {
const isActive = activeItem === item.id;
return (
<button
key={item.id}
onClick={() => onItemChange(item.id)}
className="py-3 px-6 flex flex-row gap-3 items-center w-full text-left transition-colors duration-150 border-l-4"
style={{
borderColor: isActive ? theme.textAccent : 'transparent',
color: isActive ? theme.textAccent : theme.textMuted,
background: isActive
? `linear-gradient(90deg, ${isDark ? 'rgba(76,215,246,0.1)' : 'rgba(6,182,212,0.1)'} 0%, transparent 100%)`
: undefined,
}}
>
<img src={item.icon} alt={item.label} className="w-5 h-5 shrink-0" />
<span className="font-sans text-base leading-6">{item.label}</span>
</button>
);
};
return (
<aside
className="w-64 h-full border-r border-solid pt-20 flex flex-col"
style={{
backgroundColor: theme.sidebarBg,
borderColor: theme.sidebarBorder,
boxShadow: `0px 25px 50px -12px ${theme.sidebarShadow}`,
}}
>
{/* 타이틀 영역 */}
{/* <div className="px-6 pb-8">
<p
className="font-sans text-xl leading-7 font-bold"
style={{ letterSpacing: '-1px', color: theme.textPrimary }}
>
{title}
</p>
<p
className="font-sans text-[10px] leading-[15px] font-normal uppercase"
style={{ letterSpacing: '1px', color: theme.textAccent }}
>
{subtitle}
</p>
</div> */}
{/* 메뉴 네비게이션 */}
<nav className="flex-1 flex flex-col">{menu.map(renderMenuItem)}</nav>
</aside>
);
}
export default DesignSidebar;

파일 보기

@ -0,0 +1,567 @@
// LayoutContent.tsx — WING-OPS Layout 콘텐츠 (다크/라이트 테마 지원)
import type { DesignTheme } from './designTheme';
// ---------- 데이터 타입 ----------
interface Breakpoint {
name: string;
prefix: string;
minWidth: string;
inUse: boolean;
note?: string;
}
interface DeviceSpec {
device: string;
width: string;
columns: string;
gutter: string;
margin: string;
supported: boolean;
}
interface SpacingToken {
className: string;
rem: string;
px: string;
usage: string;
}
interface ZLayer {
name: string;
zIndex: number;
description: string;
color: string;
}
interface ShellClass {
className: string;
role: string;
styles: string;
}
// ---------- Breakpoints 데이터 ----------
const BREAKPOINTS: Breakpoint[] = [
{ name: 'sm', prefix: 'sm:', minWidth: '640px', inUse: false },
{ name: 'md', prefix: 'md:', minWidth: '768px', inUse: false },
{ name: 'lg', prefix: 'lg:', minWidth: '1024px', inUse: false },
{ name: 'xl', prefix: 'xl:', minWidth: '1280px', inUse: true, note: 'TopBar 탭 레이블/아이콘 토글' },
{ name: '2xl', prefix: '2xl:', minWidth: '1536px', inUse: false },
];
// ---------- Device Specs ----------
const DEVICE_SPECS: DeviceSpec[] = [
{ device: 'Desktop', width: '≥ 1280px', columns: 'flex 기반 가변', gutter: 'gap-2 ~ gap-6', margin: 'px-5 ~ px-8', supported: true },
{ device: 'Tablet', width: '768px ~ 1279px', columns: '-', gutter: '-', margin: '-', supported: false },
{ device: 'Mobile', width: '< 768px', columns: '-', gutter: '-', margin: '-', supported: false },
];
// ---------- Spacing Scale ----------
const SPACING_TOKENS: SpacingToken[] = [
{ className: '0.5', rem: '0.125rem', px: '2px', usage: '미세 간격' },
{ className: '1', rem: '0.25rem', px: '4px', usage: '최소 간격 (gap-1)' },
{ className: '1.5', rem: '0.375rem', px: '6px', usage: '컴팩트 간격 (gap-1.5)' },
{ className: '2', rem: '0.5rem', px: '8px', usage: '기본 간격 (gap-2, p-2)' },
{ className: '2.5', rem: '0.625rem', px: '10px', usage: '중간 간격' },
{ className: '3', rem: '0.75rem', px: '12px', usage: '표준 간격 (gap-3, p-3)' },
{ className: '4', rem: '1rem', px: '16px', usage: '넓은 간격 (p-4, gap-4)' },
{ className: '5', rem: '1.25rem', px: '20px', usage: '패널 패딩 (px-5, py-5)' },
{ className: '6', rem: '1.5rem', px: '24px', usage: '섹션 간격 (gap-6, p-6)' },
{ className: '8', rem: '2rem', px: '32px', usage: '큰 간격 (px-8, gap-8)' },
{ className: '16', rem: '4rem', px: '64px', usage: '최대 간격' },
];
// ---------- Z-Index Layers ----------
const Z_LAYERS: ZLayer[] = [
{ name: 'Tooltip', zIndex: 60, description: '툴팁, 드롭다운 메뉴', color: '#a855f7' },
{ name: 'Popup', zIndex: 50, description: '팝업, 지도 오버레이', color: '#f97316' },
{ name: 'Modal', zIndex: 40, description: '모달 다이얼로그, 백드롭', color: '#ef4444' },
{ name: 'TopBar', zIndex: 30, description: '상단 네비게이션 바', color: '#3b82f6' },
{ name: 'Sidebar', zIndex: 20, description: '사이드바, 패널', color: '#06b6d4' },
{ name: 'Content', zIndex: 10, description: '메인 콘텐츠 영역', color: '#22c55e' },
{ name: 'Base', zIndex: 0, description: '기본 레이어, 배경', color: '#8690a6' },
];
// ---------- App Shell Classes ----------
const SHELL_CLASSES: ShellClass[] = [
{ className: '.wing-panel', role: '탭 콘텐츠 패널', styles: 'flex flex-col h-full overflow-hidden' },
{ className: '.wing-panel-scroll', role: '패널 내 스크롤 영역', styles: 'flex-1 overflow-y-auto' },
{ className: '.wing-header-bar', role: '패널 헤더', styles: 'flex items-center justify-between shrink-0 px-5 border-b' },
{ className: '.wing-sidebar', role: '사이드바', styles: 'flex flex-col border-r border-border' },
];
// ---------- Props ----------
interface LayoutContentProps {
theme: DesignTheme;
}
// ---------- 컴포넌트 ----------
export const LayoutContent = ({ theme }: LayoutContentProps) => {
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-4"
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 }}
>
Layout
</h1>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
WING-OPS는 . (100vh), flex .
</p>
</div>
<ul
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
style={{ color: t.textSecondary }}
>
<li> : <code style={{ color: t.textAccent, fontSize: '12px' }}>body {'{'} height: 100vh; overflow: hidden {'}'}</code></li>
<li> 수단: flex (2,243) &gt; grid (~120) flex가 </li>
<li>Tailwind CSS breakpoints/spacing을 , </li>
</ul>
</div>
{/* ── 섹션 2: Breakpoints ── */}
<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 }}
>
Breakpoints
</h2>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
Tailwind CSS breakpoints를 . , <code style={{ color: t.textAccent, fontSize: '12px' }}>xl:</code> .
</p>
</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: '100px 100px 120px 100px 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{(['Name', 'Prefix', 'Min Width', 'Status', 'Note'] 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>
{BREAKPOINTS.map((bp, idx) => (
<div
key={bp.name}
className="grid items-center"
style={{
gridTemplateColumns: '100px 100px 120px 100px 1fr',
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="py-3 px-4">
<span className="font-mono text-[11px]" style={{ color: t.textPrimary }}>{bp.name}</span>
</div>
<div className="py-3 px-4">
<span
className="font-mono rounded border border-solid px-2 py-0.5"
style={{ fontSize: '11px', color: t.textAccent, backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
{bp.prefix}
</span>
</div>
<div className="py-3 px-4">
<span className="font-mono text-[11px]" style={{ color: t.textPrimary }}>{bp.minWidth}</span>
</div>
<div className="py-3 px-4">
<span
className="font-mono text-[9px] rounded px-1.5 py-0.5"
style={{
color: bp.inUse ? (isDark ? '#22c55e' : '#047857') : (isDark ? '#8690a6' : '#94a3b8'),
backgroundColor: bp.inUse
? (isDark ? 'rgba(34,197,94,0.10)' : 'rgba(34,197,94,0.08)')
: (isDark ? 'rgba(134,144,166,0.10)' : 'rgba(148,163,184,0.08)'),
}}
>
{bp.inUse ? '사용 중' : '미사용'}
</span>
</div>
<div className="py-3 px-4">
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
{bp.note || '-'}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 섹션 3: Device Grid & Spacing ── */}
<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 }}
>
Device Grid & Spacing
</h2>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
. Desktop만 .
</p>
</div>
<div className="grid grid-cols-3 gap-4">
{DEVICE_SPECS.map((spec) => (
<div
key={spec.device}
className="rounded-lg border border-solid px-5 py-5 flex flex-col gap-4"
style={{
backgroundColor: t.cardBg,
borderColor: spec.supported ? t.cardBorder : (isDark ? 'rgba(66,71,84,0.10)' : '#f1f5f9'),
boxShadow: t.cardShadow,
opacity: spec.supported ? 1 : 0.5,
}}
>
<div className="flex flex-row items-center justify-between">
<span
className="font-sans text-lg font-bold"
style={{ color: t.textPrimary }}
>
{spec.device}
</span>
{!spec.supported && (
<span
className="font-mono text-[9px] rounded px-1.5 py-0.5"
style={{
color: isDark ? '#f97316' : '#c2410c',
backgroundColor: isDark ? 'rgba(249,115,22,0.10)' : 'rgba(249,115,22,0.08)',
}}
>
</span>
)}
</div>
<div className="flex flex-col gap-2">
{[
{ label: 'Width', value: spec.width },
{ label: 'Columns', value: spec.columns },
{ label: 'Gutter', value: spec.gutter },
{ label: 'Margin', value: spec.margin },
].map((row) => (
<div key={row.label} className="flex flex-row justify-between items-center">
<span className="font-mono text-[10px] uppercase" style={{ letterSpacing: '0.5px', color: t.textMuted }}>
{row.label}
</span>
<span className="font-mono text-[11px]" style={{ color: t.textPrimary }}>
{row.value}
</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
{/* ── 섹션 4: Spacing Scale ── */}
<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 }}
>
Spacing Scale
</h2>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
Tailwind CSS spacing . gap, padding, margin에 .
</p>
</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: '100px 120px 80px 1fr 200px',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{(['Scale', 'REM', 'PX', 'Preview', '용도'] 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>
{SPACING_TOKENS.map((token, idx) => (
<div
key={token.className}
className="grid items-center"
style={{
gridTemplateColumns: '100px 120px 80px 1fr 200px',
borderTop: idx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
<div className="py-3 px-4">
<span
className="font-mono rounded border border-solid px-2 py-0.5"
style={{ fontSize: '11px', color: t.textAccent, backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
{token.className}
</span>
</div>
<div className="py-3 px-4">
<span className="font-mono text-[11px]" style={{ color: t.textSecondary }}>{token.rem}</span>
</div>
<div className="py-3 px-4">
<span className="font-mono text-[11px]" style={{ color: t.textPrimary }}>{token.px}</span>
</div>
<div className="py-3 px-4 flex items-center">
<div
style={{
width: token.px,
height: '12px',
backgroundColor: isDark ? 'rgba(6,182,212,0.30)' : 'rgba(6,182,212,0.25)',
borderRadius: '2px',
minWidth: '2px',
}}
/>
</div>
<div className="py-3 px-4">
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>{token.usage}</span>
</div>
</div>
))}
</div>
</div>
{/* ── 섹션 5: Z-Index Layers ── */}
<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 }}
>
Z-Index Layers
</h2>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
UI . z-index가 .
</p>
</div>
<div className="flex flex-col gap-0" style={{ maxWidth: '600px' }}>
{Z_LAYERS.map((layer, idx) => (
<div
key={layer.name}
className="flex flex-row items-stretch"
style={{ marginLeft: `${idx * 16}px` }}
>
{/* z-index 라벨 */}
<div className="w-12 shrink-0 flex items-center justify-end pr-3">
<span className="font-mono text-[10px]" style={{ color: t.textMuted }}>
{layer.zIndex}
</span>
</div>
{/* 레이어 바 */}
<div
className="flex-1 flex flex-row items-center gap-3 px-4 py-3 border border-solid"
style={{
backgroundColor: isDark ? `${layer.color}10` : `${layer.color}08`,
borderColor: isDark ? `${layer.color}30` : `${layer.color}25`,
borderRadius: '6px',
marginBottom: '-1px',
}}
>
<div
className="w-2 h-2 rounded-full shrink-0"
style={{ backgroundColor: layer.color }}
/>
<span className="font-mono text-xs font-bold" style={{ color: layer.color }}>
{layer.name}
</span>
<span className="font-korean text-[11px]" style={{ color: t.textSecondary }}>
{layer.description}
</span>
</div>
</div>
))}
</div>
</div>
{/* ── 섹션 6: App Shell 구조 ── */}
<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 }}
>
App Shell
</h2>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
WING-OPS .
</p>
</div>
{/* 레이아웃 다이어그램 */}
<div
className="rounded-lg border border-solid overflow-hidden"
style={{
backgroundColor: isDark ? '#0a0e1a' : '#f8fafc',
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
maxWidth: '700px',
}}
>
{/* TopBar */}
<div
className="flex items-center justify-between px-4 border-b border-solid"
style={{
height: '36px',
backgroundColor: isDark ? 'rgba(6,182,212,0.08)' : 'rgba(6,182,212,0.06)',
borderColor: isDark ? 'rgba(6,182,212,0.20)' : 'rgba(6,182,212,0.15)',
}}
>
<span className="font-mono text-[10px] font-bold" style={{ color: '#06b6d4' }}>TopBar</span>
<span className="font-mono text-[9px]" style={{ color: t.textMuted }}>h-[52px] / shrink-0</span>
</div>
{/* SubMenuBar */}
<div
className="flex items-center justify-between px-4 border-b border-solid"
style={{
height: '24px',
backgroundColor: isDark ? 'rgba(59,130,246,0.06)' : 'rgba(59,130,246,0.04)',
borderColor: isDark ? 'rgba(59,130,246,0.15)' : 'rgba(59,130,246,0.10)',
}}
>
<span className="font-mono text-[10px] font-bold" style={{ color: '#3b82f6' }}>SubMenuBar</span>
<span className="font-mono text-[9px]" style={{ color: t.textMuted }}>shrink-0 / </span>
</div>
{/* Content Area */}
<div className="flex flex-row" style={{ height: '200px' }}>
{/* Sidebar */}
<div
className="flex flex-col items-center justify-center border-r border-solid"
style={{
width: '120px',
backgroundColor: isDark ? 'rgba(168,85,247,0.06)' : 'rgba(168,85,247,0.04)',
borderColor: isDark ? 'rgba(168,85,247,0.15)' : 'rgba(168,85,247,0.10)',
}}
>
<span className="font-mono text-[10px] font-bold" style={{ color: '#a855f7' }}>Sidebar</span>
<span className="font-mono text-[9px] mt-1" style={{ color: t.textMuted }}> </span>
</div>
{/* Main Content */}
<div
className="flex-1 flex flex-col items-center justify-center"
style={{
backgroundColor: isDark ? 'rgba(34,197,94,0.04)' : 'rgba(34,197,94,0.03)',
}}
>
<span className="font-mono text-[10px] font-bold" style={{ color: '#22c55e' }}>Content</span>
<span className="font-mono text-[9px] mt-1" style={{ color: t.textMuted }}>flex-1 / overflow-y-auto</span>
</div>
</div>
</div>
{/* wing.css 레이아웃 클래스 */}
<div className="flex flex-col gap-3" style={{ maxWidth: '700px' }}>
<h3
className="font-korean text-sm font-bold"
style={{ color: t.textPrimary }}
>
</h3>
{SHELL_CLASSES.map((cls) => (
<div
key={cls.className}
className="rounded-lg border border-solid px-4 py-3 flex flex-row items-center gap-4"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
}}
>
<span
className="font-mono rounded border border-solid px-2 py-0.5 shrink-0"
style={{ fontSize: '11px', color: t.textAccent, backgroundColor: t.cardBg, borderColor: t.cardBorder }}
>
{cls.className}
</span>
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
{cls.role}
</span>
<span className="font-mono text-[10px] ml-auto" style={{ color: t.textMuted }}>
{cls.styles}
</span>
</div>
))}
</div>
</div>
</div>
);
};
export default LayoutContent;

파일 보기

@ -0,0 +1,279 @@
// RadiusContent.tsx — WING-OPS Radius 콘텐츠 (다크/라이트 테마 지원)
import type { DesignTheme } from './designTheme';
// ---------- 데이터 타입 ----------
interface RadiusToken {
name: string;
value: string;
px: number;
isCustom?: boolean;
}
interface ComponentRadius {
className: string;
radius: string;
components: string[];
}
// ---------- Radius Token 데이터 ----------
const RADIUS_TOKENS: RadiusToken[] = [
{ name: 'rounded-sm', value: '6px', px: 6, isCustom: true },
{ name: 'rounded', value: '4px (0.25rem)', px: 4 },
{ name: 'rounded-md', value: '10px', px: 10, isCustom: true },
{ name: 'rounded-lg', value: '8px (0.5rem)', px: 8 },
{ name: 'rounded-xl', value: '12px (0.75rem)', px: 12 },
{ name: 'rounded-2xl', value: '16px (1rem)', px: 16 },
{ name: 'rounded-full', value: '9999px', px: 9999 },
];
// ---------- 컴포넌트 매핑 데이터 ----------
const COMPONENT_RADIUS: ComponentRadius[] = [
{ className: 'rounded-sm (6px)', radius: '6px', components: ['.wing-btn', '.wing-input', '.wing-card-sm'] },
{ className: 'rounded (4px)', radius: '4px', components: ['.wing-badge'] },
{ className: 'rounded-md (10px)', radius: '10px', components: ['.wing-card', '.wing-section', '.wing-tab'] },
{ className: 'rounded-lg (8px)', radius: '8px', components: ['.wing-tab-bar'] },
{ className: 'rounded-xl (12px)', radius: '12px', components: ['.wing-modal'] },
];
// ---------- Props ----------
interface RadiusContentProps {
theme: DesignTheme;
}
// ---------- 컴포넌트 ----------
export const RadiusContent = ({ theme }: RadiusContentProps) => {
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-4"
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 }}
>
Radius
</h1>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
Radius는 .
</p>
</div>
<p
className="font-korean text-sm leading-6"
style={{ color: t.textSecondary }}
>
Radius는 UI . Radius , , .
</p>
<ul
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
style={{ color: t.textSecondary }}
>
<li>
<code style={{ color: t.textAccent, fontSize: '12px' }}>rounded-sm</code>(6px){' '}
<code style={{ color: t.textAccent, fontSize: '12px' }}>rounded-md</code>(10px) Tailwind .
</li>
<li> Tailwind CSS border-radius .</li>
</ul>
</div>
{/* ── 섹션 2: Radius Tokens 테이블 ── */}
<div className="w-full flex flex-col gap-8">
<h2
className="font-sans text-2xl leading-8 font-bold"
style={{ color: t.textPrimary }}
>
Radius Tokens
</h2>
<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: '200px 200px 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{(['이름', '값', 'Preview'] 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>
{/* 데이터 행 */}
{RADIUS_TOKENS.map((token, rowIdx) => (
<div
key={token.name}
className="grid items-center"
style={{
gridTemplateColumns: '200px 200px 1fr',
borderTop: rowIdx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
}}
>
{/* 이름 */}
<div className="py-4 px-4 flex items-center gap-2">
<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.name}
</span>
{token.isCustom && (
<span
className="font-mono text-[9px] rounded px-1.5 py-0.5"
style={{
color: isDark ? '#f97316' : '#c2410c',
backgroundColor: isDark ? 'rgba(249,115,22,0.10)' : 'rgba(249,115,22,0.08)',
}}
>
custom
</span>
)}
</div>
{/* 값 */}
<div className="py-4 px-4">
<span
className="font-mono text-[11px]"
style={{ color: t.textPrimary }}
>
{token.value}
</span>
</div>
{/* Preview */}
<div className="py-4 px-4 flex items-center gap-4">
<div
style={{
width: '80px',
height: '48px',
borderRadius: token.px >= 9999 ? '9999px' : `${token.px}px`,
backgroundColor: isDark ? 'rgba(6,182,212,0.15)' : 'rgba(6,182,212,0.12)',
border: `1.5px solid ${isDark ? 'rgba(6,182,212,0.40)' : 'rgba(6,182,212,0.50)'}`,
}}
/>
<div
style={{
width: '48px',
height: '48px',
borderRadius: token.px >= 9999 ? '9999px' : `${token.px}px`,
backgroundColor: isDark ? 'rgba(6,182,212,0.15)' : 'rgba(6,182,212,0.12)',
border: `1.5px solid ${isDark ? 'rgba(6,182,212,0.40)' : 'rgba(6,182,212,0.50)'}`,
}}
/>
</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>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
wing.css Radius .
</p>
</div>
<div className="grid grid-cols-1 gap-4" style={{ maxWidth: '800px' }}>
{COMPONENT_RADIUS.map((item) => (
<div
key={item.className}
className="rounded-lg border border-solid px-5 py-4 flex flex-row items-center gap-6"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{/* 미리보기 박스 */}
<div
className="shrink-0"
style={{
width: '48px',
height: '48px',
borderRadius: item.radius,
backgroundColor: isDark ? 'rgba(6,182,212,0.12)' : 'rgba(6,182,212,0.10)',
border: `1.5px solid ${isDark ? 'rgba(6,182,212,0.30)' : 'rgba(6,182,212,0.40)'}`,
}}
/>
{/* 정보 */}
<div className="flex flex-col gap-1.5 flex-1">
<span
className="font-mono text-xs font-bold"
style={{ color: t.textPrimary }}
>
{item.className}
</span>
<div className="flex flex-row flex-wrap gap-2">
{item.components.map((comp) => (
<span
key={comp}
className="font-mono rounded border border-solid px-2 py-0.5"
style={{
fontSize: '10px',
lineHeight: '15px',
color: t.textAccent,
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
}}
>
{comp}
</span>
))}
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default RadiusContent;

파일 보기

@ -0,0 +1,462 @@
// 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;

파일 보기

@ -0,0 +1,242 @@
import pdfFileIcon from '../../../assets/icons/wing-pdf-file.svg';
import pdfFileDisabledIcon from '../../../assets/icons/wing-pdf-file-disabled.svg';
interface ButtonRow {
label: string;
defaultBtn: React.ReactNode;
hoverBtn: React.ReactNode;
disabledBtn: React.ReactNode;
}
const buttonRows: ButtonRow[] = [
{
label: '프라이머리 (그라디언트)',
defaultBtn: (
<div
className='rounded-md pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'
style={{
background: 'linear-gradient(120.41deg, rgba(6, 182, 212, 1) 0%, rgba(59, 130, 246, 1) 100%)',
}}
>
<div className='text-white text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
hoverBtn: (
<div
className='rounded-md pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'
style={{
background: 'linear-gradient(120.41deg, rgba(6, 182, 212, 1) 0%, rgba(59, 130, 246, 1) 100%)',
boxShadow: '0px 0px 12px 0px rgba(6, 182, 212, 0.4)',
}}
>
<div className='text-white text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
disabledBtn: (
<div
className='bg-[#334155] rounded-md pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'
style={{ opacity: 0.5 }}
>
<div className='text-[#64748b] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
},
{
label: '세컨더리 (솔리드)',
defaultBtn: (
<div className='bg-[#1a2236] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
hoverBtn: (
<div className='bg-[#1e2844] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
disabledBtn: (
<div className='bg-[rgba(26,34,54,0.50)] rounded-md border border-solid border-[rgba(30,42,66,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[rgba(176,184,204,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
},
{
label: '아웃라인 (고스트)',
defaultBtn: (
<div className='rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
hoverBtn: (
<div className='bg-[#1e2844] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
disabledBtn: (
<div className='rounded-md border border-solid border-[rgba(30,42,66,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[rgba(176,184,204,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
},
{
label: 'PDF 액션',
defaultBtn: (
<div className='bg-[rgba(59,130,246,0.08)] rounded-md border border-solid border-[rgba(59,130,246,0.30)] pt-1.5 pr-3 pb-1.5 pl-3 flex flex-row gap-2 items-center justify-start shrink-0 relative'>
<img
className='shrink-0 relative overflow-visible'
src={pdfFileIcon}
alt='PDF 아이콘'
/>
<div className='text-[#3b82f6] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
PDF
</div>
</div>
),
hoverBtn: (
<div
className='bg-[rgba(59,130,246,0.15)] rounded-md border border-solid border-[rgba(59,130,246,0.50)] pt-1.5 pr-3 pb-1.5 pl-3 flex flex-row gap-2 items-center justify-start shrink-0 relative'
style={{ boxShadow: '0px 0px 8px 0px rgba(59, 130, 246, 0.2)' }}
>
<img
className='shrink-0 relative overflow-visible'
src={pdfFileIcon}
alt='PDF 아이콘'
/>
<div className='text-[#3b82f6] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
PDF
</div>
</div>
),
disabledBtn: (
<div className='bg-[rgba(59,130,246,0.04)] rounded-md border border-solid border-[rgba(59,130,246,0.10)] pt-1.5 pr-3 pb-1.5 pl-3 flex flex-row gap-2 items-center justify-start shrink-0 relative'>
<img
className='shrink-0 relative overflow-visible'
src={pdfFileDisabledIcon}
alt='PDF 아이콘 (비활성)'
/>
<div className='text-[rgba(59,130,246,0.40)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
PDF
</div>
</div>
),
},
{
label: '경고 (삭제)',
defaultBtn: (
<div className='bg-[rgba(239,68,68,0.10)] rounded-md border border-solid border-[rgba(239,68,68,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#ef4444] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
hoverBtn: (
<div
className='bg-[rgba(239,68,68,0.20)] rounded-md border border-solid border-[rgba(239,68,68,0.50)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'
style={{ boxShadow: '0px 0px 8px 0px rgba(239, 68, 68, 0.15)' }}
>
<div className='text-[#ef4444] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
disabledBtn: (
<div className='bg-[rgba(239,68,68,0.05)] rounded-md border border-solid border-[rgba(239,68,68,0.15)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[rgba(239,68,68,0.40)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
},
];
export const ButtonCatalogSection = () => {
return (
<div className='bg-[#1a2236] rounded-[10px] border border-solid border-[#1e2a42] flex flex-col gap-0 items-start justify-start overflow-hidden w-full'>
{/* 카드 헤더 */}
<div className='border-b border-solid border-[#1e2a42] pt-4 pr-6 pb-4 pl-6 flex flex-row gap-3 items-center justify-start self-stretch shrink-0 relative'>
<div className='bg-[#06b6d4] rounded-xl shrink-0 w-1 h-4 relative' />
<div
className='text-[#22d3ee] text-left font-korean text-xs leading-4 font-medium uppercase relative flex items-center justify-start'
style={{ letterSpacing: '1.2px' }}
>
인터페이스: 버튼
</div>
</div>
{/* 테이블 본문 */}
<div className='p-6 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative overflow-hidden'>
<div className='flex flex-col items-start justify-start self-stretch shrink-0 relative'>
{/* 헤더 행 */}
<div className='border-b border-solid border-[#1e2a42] flex flex-row gap-0 items-start justify-center self-stretch shrink-0 relative'>
{['버튼 유형', '기본 상태', '호버 상태', '비활성 상태'].map((header) => (
<div
key={header}
className='pt-px pr-2 pb-[17.5px] pl-2 flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative'
>
<div
className='text-[#64748b] text-left font-korean text-xs font-medium uppercase relative flex items-center justify-start'
style={{ letterSpacing: '-0.55px' }}
>
{header}
</div>
</div>
))}
</div>
{/* 데이터 행 */}
<div
className='flex flex-col items-start justify-start self-stretch shrink-0 relative'
style={{ margin: '-1px 0 0 0' }}
>
{buttonRows.map((row, index) => (
<div
key={row.label}
className='border-t border-solid border-[rgba(30,42,66,0.50)] flex flex-row gap-0 items-start justify-center self-stretch shrink-0 relative'
style={index === 0 ? { borderTopColor: 'transparent' } : { margin: '-1px 0 0 0' }}
>
{/* 버튼 유형 레이블 */}
<div className='pt-[31.5px] pr-2 pb-[31.5px] pl-2 flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative'>
<div className='text-[#bcc9cd] text-left font-korean text-xs font-medium relative flex items-center justify-start'>
{row.label}
</div>
</div>
{/* 기본 상태 */}
<div className='pt-[24.5px] pr-2 pb-[24.5px] pl-2 flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative'>
{row.defaultBtn}
</div>
{/* 호버 상태 */}
<div className='pt-[24.5px] pr-2 pb-[24.5px] pl-2 flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative'>
{row.hoverBtn}
</div>
{/* 비활성 상태 */}
<div className='pt-[24.5px] pr-2 pb-[24.5px] pl-2 flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative'>
{row.disabledBtn}
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
};

파일 보기

@ -0,0 +1,157 @@
import wingAnchorIcon from '../../../assets/icons/wing-anchor.svg';
import wingCargoIcon from '../../../assets/icons/wing-cargo.svg';
import wingAlertTriangleIcon from '../../../assets/icons/wing-alert-triangle.svg';
import wingChartBarIcon from '../../../assets/icons/wing-chart-bar.svg';
import wingWaveGraph from '../../../assets/icons/wing-wave-graph.svg';
interface LogisticsItem {
icon: string;
label: string;
progress: string;
}
const logisticsItems: LogisticsItem[] = [
{ icon: wingAnchorIcon, label: '화물 통관', progress: '진행률: 84%' },
{ icon: wingCargoIcon, label: '화물 통관', progress: '진행률: 84%' },
{ icon: wingAlertTriangleIcon, label: '화물 통관', progress: '진행률: 84%' },
];
export const CardSection = () => {
return (
<div
className='grid gap-6 w-full'
style={{
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
}}
>
{/* col 3: 활성 물류 현황 카드 */}
<div
className='bg-[#1a2236] rounded-[10px] border border-[#1e2a42] p-6 flex flex-col gap-6 items-start justify-start relative'
style={{ gridColumn: '3 / span 1', gridRow: '1 / span 1' }}
>
{/* 카드 헤더 */}
<div className='border-l-2 border-[#06b6d4] pl-3 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'>
<div
className='text-[#64748b] text-left font-korean text-[10px] leading-[15px] font-medium uppercase relative flex items-center justify-start'
style={{ letterSpacing: '1px' }}
>
</div>
</div>
{/* 물류 아이템 목록 */}
<div className='flex flex-col gap-4 items-start justify-start self-stretch shrink-0 relative'>
{logisticsItems.map((item, index) => (
<div
key={index}
className='flex flex-row gap-4 items-center justify-start self-stretch shrink-0 relative'
>
<div className='bg-[#1e293b] rounded-md flex flex-row gap-0 items-center justify-center shrink-0 w-10 h-10 relative'>
<img
className='shrink-0 relative overflow-visible'
src={item.icon}
alt={item.label}
/>
</div>
<div className='flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative'>
<div className='flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'>
<div className='text-[#dfe2f3] text-left font-korean text-[11px] leading-[16.5px] font-medium relative flex items-center justify-start'>
{item.label}
</div>
</div>
<div className='flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'>
<div className='text-[#64748b] text-left font-sans text-[10px] leading-[15px] font-normal relative flex items-center justify-start'>
{item.progress}
</div>
</div>
</div>
</div>
))}
</div>
{/* 대응팀 배치 버튼 */}
<div className='pt-2 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'>
<div
className='rounded-md pt-2 pb-2 flex flex-col gap-0 items-center justify-center self-stretch shrink-0 relative'
style={{
background:
'linear-gradient(97.29deg, rgba(6, 182, 212, 1) 0%, rgba(59, 130, 246, 1) 100%)',
}}
>
<div className='text-white text-center font-korean text-[11px] leading-[16.5px] font-medium relative flex items-center justify-center'>
</div>
</div>
</div>
</div>
{/* col 1-2 span: 실시간 텔레메트리 카드 */}
<div
className='bg-[#1a2236] rounded-[10px] border border-[#1e2a42] p-6 flex flex-col items-start justify-between min-h-[240px] relative overflow-hidden'
style={{ gridColumn: '1 / span 2', gridRow: '1 / span 1' }}
>
{/* 배경 파형 (opacity 0.3) */}
<div
className='flex flex-col gap-0 items-start justify-center shrink-0 h-24 absolute right-px left-px bottom-[1.5px]'
style={{ opacity: 0.3 }}
>
<img
className='self-stretch shrink-0 h-24 relative overflow-visible'
src={wingWaveGraph}
alt='wave graph'
/>
</div>
{/* 상단 콘텐츠 */}
<div className='flex flex-col gap-6 items-start justify-start self-stretch shrink-0 relative'>
{/* 제목 영역 */}
<div className='flex flex-row items-start justify-between self-stretch shrink-0 relative'>
<div className='flex flex-col gap-[4.5px] items-start justify-start shrink-0 relative'>
<div
className='text-[#22d3ee] text-left font-korean text-[10px] leading-[15px] font-medium uppercase relative flex items-center justify-start'
style={{ letterSpacing: '1px' }}
>
</div>
<div className='text-[#dfe2f3] text-left font-korean text-2xl leading-8 font-medium relative flex items-center justify-start'>
</div>
</div>
<img
className='shrink-0 w-[13.5px] h-[13.5px] relative overflow-visible'
src={wingChartBarIcon}
alt='chart bar'
/>
</div>
{/* 속도 수치 */}
<div className='flex flex-row gap-2 justify-start self-stretch shrink-0 relative'>
<div className='text-white text-left font-sans font-bold text-4xl leading-10 relative flex items-center justify-start'>
24.8
</div>
<div className='text-[#64748b] text-left font-sans font-semibold text-sm leading-5 relative flex items-center justify-start'>
(knots)
</div>
</div>
</div>
{/* 하단 뱃지 + 버튼 */}
<div className='flex flex-row items-center justify-between self-stretch shrink-0 relative'>
{/* 정상 가동중 뱃지 */}
<div className='bg-[rgba(34,197,94,0.10)] rounded-xl pt-0.5 pr-2 pb-0.5 pl-2 flex flex-col gap-0 items-start justify-start shrink-0 relative'>
<div className='text-[#22c55e] text-left font-korean text-[9px] leading-[13.5px] font-medium relative flex items-center justify-start'>
</div>
</div>
{/* 대응팀 배치 아웃라인 버튼 */}
<div className='rounded-md border border-[#1e2a42] pt-1 pr-3 pb-1 pl-3 flex flex-col gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[10px] leading-[15px] font-medium relative flex items-center justify-center'>
</div>
</div>
</div>
</div>
</div>
);
};

파일 보기

@ -0,0 +1,193 @@
import wingCompGearIcon from '../../../assets/icons/wing-comp-gear.svg';
import wingCompSearchIcon from '../../../assets/icons/wing-comp-search.svg';
import wingCompCloseIcon from '../../../assets/icons/wing-comp-close.svg';
import wingCompMenuIcon from '../../../assets/icons/wing-comp-menu.svg';
interface IconButtonItem {
icon: string;
label: string;
}
interface StatusBadge {
label: string;
color: string;
bg: string;
}
interface DataTag {
label: string;
color: string;
dotColor: string;
bg: string;
}
const iconButtons: IconButtonItem[] = [
{ icon: wingCompGearIcon, label: 'Settings' },
{ icon: wingCompSearchIcon, label: 'Search' },
{ icon: wingCompCloseIcon, label: 'Close' },
{ icon: wingCompMenuIcon, label: 'Menu' },
];
const statusBadges: StatusBadge[] = [
{ label: '정상', color: '#22c55e', bg: 'rgba(34,197,94,0.10)' },
{ label: '주의', color: '#eab308', bg: 'rgba(234,179,8,0.10)' },
{ label: '위험', color: '#ef4444', bg: 'rgba(239,68,68,0.10)' },
{ label: '진행중', color: '#3b82f6', bg: 'rgba(59,130,246,0.10)' },
{ label: '완료', color: '#8690a6', bg: 'rgba(134,144,166,0.10)' },
];
const dataTags: DataTag[] = [
{ label: 'VESSEL_A', color: '#22c55e', dotColor: '#22c55e', bg: 'rgba(34,197,94,0.10)' },
{ label: 'PRIORITY_H', color: '#eab308', dotColor: '#eab308', bg: 'rgba(234,179,8,0.10)' },
{ label: 'CRITICAL_ERR', color: '#ef4444', dotColor: '#ef4444', bg: 'rgba(239,68,68,0.10)' },
{ label: 'ACTIVE_SYNC', color: '#3b82f6', dotColor: '#3b82f6', bg: 'rgba(59,130,246,0.10)' },
];
export const IconBadgeSection = () => {
return (
<div
className='grid gap-8 w-full'
style={{
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
}}
>
{/* 좌측 카드: 제어 인터페이스 — 아이콘 버튼 */}
<div
className='bg-[#1a2236] rounded-[10px] border border-[#1e2a42] flex flex-col gap-0 items-start justify-start relative overflow-hidden'
style={{ gridColumn: '1 / span 1', gridRow: '1 / span 1' }}
>
{/* 카드 헤더 */}
<div className='border-b border-[#1e2a42] pt-4 pr-6 pb-4 pl-6 flex flex-row gap-3 items-center justify-start self-stretch shrink-0 relative'>
<div className='bg-[#e89337] rounded-xl shrink-0 w-1 h-4 relative'></div>
<div className='flex flex-col gap-0 items-start justify-start shrink-0 relative'>
<div
className='text-[#22d3ee] text-left font-korean text-xs leading-4 font-medium uppercase relative flex items-center justify-start'
style={{ letterSpacing: '1.2px' }}
>
컨트롤: 아이콘
</div>
</div>
</div>
{/* 아이콘 버튼 목록 */}
<div className='p-8 flex flex-row gap-6 items-start justify-evenly self-stretch shrink-0 relative'>
{iconButtons.map((btn) => (
<div
key={btn.label}
className='flex flex-col gap-3 items-center justify-start self-stretch shrink-0 relative'
>
<div className='bg-[#1a2236] rounded-md border border-[#1e2a42] flex flex-row gap-0 items-center justify-center shrink-0 w-9 h-9 relative'>
<img
className='shrink-0 relative overflow-visible'
src={btn.icon}
alt={btn.label}
/>
</div>
<div className='flex flex-col gap-0 items-start justify-start shrink-0 relative'>
<div
className='text-[#64748b] text-left font-sans font-bold text-[10px] leading-[15px] uppercase relative flex items-center justify-start'
style={{ letterSpacing: '0.9px' }}
>
{btn.label}
</div>
</div>
</div>
))}
</div>
{/* 카드 푸터 */}
<div className='bg-[rgba(15,23,42,0.30)] pt-4 pr-6 pb-4 pl-6 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'>
<div className='text-[#64748b] text-left font-sans text-[10px] leading-[15px] font-normal relative flex items-center justify-start'>
Standard dimensions: 36x36px with radius-md (6px)
</div>
</div>
</div>
{/* 우측 카드: 마이크로 컨트롤 — 뱃지 & 태그 */}
<div
className='bg-[#1a2236] rounded-[10px] border border-[#1e2a42] flex flex-col gap-0 items-start justify-start relative overflow-hidden'
style={{ gridColumn: '2 / span 1', gridRow: '1 / span 1' }}
>
{/* 카드 헤더 */}
<div className='border-b border-[#1e2a42] pt-4 pr-6 pb-4 pl-6 flex flex-row gap-3 items-center justify-start self-stretch shrink-0 relative'>
<div className='bg-[#93000a] rounded-xl shrink-0 w-1 h-4 relative'></div>
<div className='flex flex-col gap-0 items-start justify-start shrink-0 relative'>
<div
className='text-[#22d3ee] text-left font-korean text-xs leading-4 font-medium uppercase relative flex items-center justify-start'
style={{ letterSpacing: '1.2px' }}
>
컨트롤: 아이콘
</div>
</div>
</div>
{/* 카드 바디 */}
<div className='p-6 flex flex-col gap-8 items-start justify-start self-stretch shrink-0 relative'>
{/* Operational Status 섹션 */}
<div className='flex flex-col gap-4 items-start justify-start self-stretch shrink-0 relative'>
<div className='border-l-2 border-[#0e7490] pl-3 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'>
<div
className='text-[#64748b] text-left font-sans font-bold text-[10px] leading-[15px] uppercase relative flex items-center justify-start'
style={{ letterSpacing: '1px' }}
>
Operational Status
</div>
</div>
<div className='flex flex-row gap-3 items-start justify-start self-stretch shrink-0 relative'>
{statusBadges.map((badge) => (
<div
key={badge.label}
className='rounded-xl pt-0.5 pr-2 pb-0.5 pl-2 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'
style={{ backgroundColor: badge.bg }}
>
<div
className='text-left font-korean text-[10px] leading-[15px] font-medium relative flex items-center justify-start'
style={{ color: badge.color }}
>
{badge.label}
</div>
</div>
))}
</div>
</div>
{/* Data Classification 섹션 */}
<div className='flex flex-col gap-4 items-start justify-start self-stretch shrink-0 relative'>
<div className='border-l-2 border-[#0e7490] pl-3 flex flex-col gap-0 items-start justify-start self-stretch shrink-0 relative'>
<div
className='text-[#64748b] text-left font-sans font-bold text-[10px] leading-[15px] uppercase relative flex items-center justify-start'
style={{ letterSpacing: '1px' }}
>
Data Classification
</div>
</div>
<div className='flex flex-row gap-4 items-start justify-start self-stretch shrink-0 relative'>
{dataTags.map((tag) => (
<div
key={tag.label}
className='rounded-xl pt-0.5 pr-2 pb-0.5 pl-2 flex flex-row gap-2 items-center justify-start self-stretch shrink-0 relative'
style={{ backgroundColor: tag.bg }}
>
<div
className='rounded-xl shrink-0 w-1.5 h-1.5 relative'
style={{ backgroundColor: tag.dotColor }}
></div>
<div className='flex flex-col gap-0 items-start justify-start shrink-0 relative'>
<div
className='text-left font-sans font-bold text-[10px] leading-[15px] relative flex items-center justify-start'
style={{ color: tag.color }}
>
{tag.label}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};

파일 보기

@ -0,0 +1,380 @@
// designTheme.ts — 디자인 시스템 페이지 다크/라이트 테마 정의
export type ThemeMode = 'dark' | 'light';
// ---------- 토큰 인터페이스 ----------
export interface BgToken {
bg: string;
token: string;
hex: string;
desc: string;
isHover?: boolean;
}
export interface AccentToken {
color: string;
name: string;
token: string;
badge: string;
glow: string;
badgeBg: string;
badgeBorder: string;
badgeText: string;
}
export interface StatusToken {
color: string;
bg: string;
border: string;
label: string;
hex: string;
glow?: string;
}
export interface BorderToken {
token: string;
hex: string;
border: string;
barBg: string;
}
export interface TextTokenItem {
token: string;
sampleText: string;
sampleClass: string;
desc: string;
descColor: string;
}
// ---------- 테마 인터페이스 ----------
export interface DesignTheme {
mode: ThemeMode;
// 레이아웃
pageBg: string;
sidebarBg: string;
sidebarBorder: string;
sidebarShadow: string;
headerBg: string;
headerBorder: string;
// 텍스트
textPrimary: string;
textSecondary: string;
textMuted: string;
textAccent: string;
// 카드
cardBg: string;
cardBorder: string;
cardBorderHover: string;
cardShadow: string;
// 섹션
sectionTitle: string;
sectionSub: string;
sectionSubSpacing: string;
// 테이블
tableContainerBg: string;
tableHeaderBg: string;
tableRowBorder: string;
tableDataRowBg: string;
// 뱃지
badgeRadius: string;
statusBadgeBg: string;
statusBadgeBorder: string;
statusBadgeDot: string;
statusBadgeText: string;
systemActiveBg: string;
systemActiveBorder: string;
systemActiveShadow: string;
// 폰트 뱃지 (06 타이포그래피)
fontBadgePrimaryBg: string;
fontBadgePrimaryText: string;
fontBadgeSecondaryBorder: string;
fontBadgeSecondaryText: string;
// 타이포 샘플 텍스트
typoSampleText: string;
typoSizeText: string;
typoPropertiesText: string;
typoActionBg: string;
typoActionBorder: string;
typoActionText: string;
typoDataText: string;
typoCoordText: string;
// 02 테두리 색상
borderCardBg: string;
borderCardShadow: string;
// 03 텍스트 색상
textSectionBg: string;
textSectionBorder: string;
// 07 radius
radiusSmLabel: string;
radiusMdLabel: string;
radiusCardBg: string;
radiusCardBorder: string;
radiusCardShadow: string;
radiusDescText: string;
// 푸터
footerBorder: string;
footerText: string;
footerAccent: string;
// 01 배경색 카드 스와치 border
swatchBorder: string;
swatchBorderHover: string;
// 데이터 토큰
bgTokens: BgToken[];
accentTokens: AccentToken[];
statusTokens: StatusToken[];
borderTokens: BorderToken[];
textTokens: TextTokenItem[];
}
// ---------- DARK 테마 ----------
export const DARK_THEME: DesignTheme = {
mode: 'dark',
pageBg: '#0a0e1a',
sidebarBg: '#171b28',
sidebarBorder: 'rgba(255,255,255,0.05)',
sidebarShadow: 'rgba(0,0,0,0.4)',
headerBg: '#0a0e1a',
headerBorder: 'rgba(255,255,255,0.05)',
textPrimary: '#dfe2f3',
textSecondary: '#c2c6d6',
textMuted: '#8c909f',
textAccent: '#4cd7f6',
cardBg: '#171b28',
cardBorder: 'rgba(66,71,84,0.10)',
cardBorderHover: 'rgba(66,71,84,0.20)',
cardShadow: 'none',
sectionTitle: '#adc6ff',
sectionSub: '#8c909f',
sectionSubSpacing: '1px',
tableContainerBg: '#171b28',
tableHeaderBg: '#1b1f2c',
tableRowBorder: 'rgba(66,71,84,0.10)',
tableDataRowBg: 'rgba(10,14,26,0.50)',
badgeRadius: 'rounded-xl',
statusBadgeBg: 'transparent',
statusBadgeBorder: 'transparent',
statusBadgeDot: 'transparent',
statusBadgeText: 'transparent',
systemActiveBg: '#171b28',
systemActiveBorder: 'rgba(66,71,84,0.10)',
systemActiveShadow: '0px 0px 8px 0px rgba(76, 215, 246, 0.5)',
fontBadgePrimaryBg: '#edf0f7',
fontBadgePrimaryText: '#0a0e1a',
fontBadgeSecondaryBorder: 'rgba(66,71,84,0.30)',
fontBadgeSecondaryText: '#8c909f',
typoSampleText: '#c2c6d6',
typoSizeText: '#8c909f',
typoPropertiesText: '#c2c6d6',
typoActionBg: 'rgba(76,215,246,0.10)',
typoActionBorder: 'rgba(76,215,246,0.20)',
typoActionText: '#4cd7f6',
typoDataText: '#4cd7f6',
typoCoordText: '#8c909f',
borderCardBg: 'rgba(15,21,36,0.50)',
borderCardShadow: 'none',
textSectionBg: '#0a0e1a',
textSectionBorder: 'rgba(66,71,84,0.10)',
radiusSmLabel: 'radius-sm (6px)',
radiusMdLabel: 'radius-md (10px)',
radiusCardBg: '#171b28',
radiusCardBorder: 'rgba(66,71,84,0.20)',
radiusCardShadow: 'none',
radiusDescText: '#c2c6d6',
footerBorder: 'rgba(66,71,84,0.10)',
footerText: '#8c909f',
footerAccent: '#4cd7f6',
swatchBorder: 'rgba(255,255,255,0.05)',
swatchBorderHover: 'rgba(76,215,246,0.20)',
bgTokens: [
{ bg: '#0a0e1a', token: 'bg-0', hex: '#0a0e1a', desc: 'Primary page canvas, deepest immersion layer.' },
{ bg: '#0f1524', token: 'bg-1', hex: '#0f1524', desc: 'Surface Level 1: Sidebar containers and utility panels.' },
{ bg: '#121929', token: 'bg-2', hex: '#121929', desc: 'Surface Level 2: Table headers and subtle sectional shifts.' },
{ bg: '#1a2236', token: 'bg-3', hex: '#1a2236', desc: 'Surface Level 3: Elevated cards and floating elements.' },
{ bg: '#1e2844', token: 'bg-hover', hex: '#1e2844', desc: 'Interactive states, list item highlighting.', isHover: true },
],
accentTokens: [
{
color: '#06b6d4', name: 'Cyan Accent', token: 'primary', badge: 'Primary Action',
glow: '0px 0px 15px 0px rgba(6,182,212,0.4)',
badgeBg: 'rgba(6,182,212,0.10)', badgeBorder: 'rgba(6,182,212,0.25)', badgeText: '#06b6d4',
},
{
color: '#3b82f6', name: 'Blue Accent', token: 'secondary', badge: 'Information',
glow: '0px 0px 15px 0px rgba(59,130,246,0.3)',
badgeBg: 'rgba(59,130,246,0.10)', badgeBorder: 'rgba(59,130,246,0.25)', badgeText: '#3b82f6',
},
{
color: '#a855f7', name: 'Purple Accent', token: 'tertiary', badge: 'Operations',
glow: '0px 0px 15px 0px rgba(168,85,247,0.3)',
badgeBg: 'rgba(168,85,247,0.10)', badgeBorder: 'rgba(168,85,247,0.25)', badgeText: '#a855f7',
},
],
statusTokens: [
{ color: '#ef4444', bg: 'rgba(239,68,68,0.05)', border: 'rgba(239,68,68,0.20)', label: '위험 Critical', hex: '#ef4444', glow: '0px 0px 8px 0px rgba(239, 68, 68, 0.6)' },
{ color: '#f97316', bg: 'rgba(249,115,22,0.05)', border: 'rgba(249,115,22,0.20)', label: '주의 Warning', hex: '#f97316' },
{ color: '#eab308', bg: 'rgba(234,179,8,0.05)', border: 'rgba(234,179,8,0.20)', label: '경고 Caution', hex: '#eab308' },
{ color: '#22c55e', bg: 'rgba(34,197,94,0.05)', border: 'rgba(34,197,94,0.20)', label: '정상 Normal', hex: '#22c55e' },
],
borderTokens: [
{ token: 'border', hex: '#1e2a42', border: '#1e2a42', barBg: '#1e2a42' },
{ token: 'border-light', hex: '#2a3a5c', border: '#2a3a5c', barBg: '#2a3a5c' },
],
textTokens: [
{ token: 'text-1', sampleText: '주요 텍스트 Primary Text', sampleClass: 'text-[#edf0f7] font-korean text-[15px] font-bold', desc: 'Headings, active values, and primary labels.', descColor: 'rgba(237,240,247,0.60)' },
{ token: 'text-2', sampleText: '보조 텍스트 Secondary Text', sampleClass: 'text-[#b0b8cc] font-korean text-[15px] font-medium', desc: 'Supporting labels and secondary information.', descColor: 'rgba(176,184,204,0.60)' },
{ token: 'text-3', sampleText: '비활성 텍스트 Muted Text', sampleClass: 'text-[#8690a6] font-korean text-[15px]', desc: 'Disabled states, placeholders, and captions.', descColor: 'rgba(134,144,166,0.60)' },
],
};
// ---------- LIGHT 테마 ----------
export const LIGHT_THEME: DesignTheme = {
mode: 'light',
pageBg: '#f8fafc',
sidebarBg: '#ffffff',
sidebarBorder: '#e2e8f0',
sidebarShadow: 'rgba(0,0,0,0.05)',
headerBg: '#ffffff',
headerBorder: '#e2e8f0',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
textAccent: '#06b6d4',
cardBg: '#ffffff',
cardBorder: '#e2e8f0',
cardBorderHover: 'rgba(6,182,212,0.20)',
cardShadow: '0px 1px 2px 0px rgba(0,0,0,0.05)',
sectionTitle: '#1e293b',
sectionSub: '#94a3b8',
sectionSubSpacing: '-0.5px',
tableContainerBg: '#ffffff',
tableHeaderBg: '#f8fafc',
tableRowBorder: '#f1f5f9',
tableDataRowBg: 'rgba(248,250,252,0.50)',
badgeRadius: 'rounded-full',
statusBadgeBg: 'transparent',
statusBadgeBorder: 'transparent',
statusBadgeDot: 'transparent',
statusBadgeText: 'transparent',
systemActiveBg: '#ffffff',
systemActiveBorder: '#e2e8f0',
systemActiveShadow: '0px 1px 2px 0px rgba(0,0,0,0.05)',
fontBadgePrimaryBg: '#0f172a',
fontBadgePrimaryText: '#ffffff',
fontBadgeSecondaryBorder: '#cbd5e1',
fontBadgeSecondaryText: '#64748b',
typoSampleText: '#64748b',
typoSizeText: '#0f172a',
typoPropertiesText: '#94a3b8',
typoActionBg: 'rgba(6,182,212,0.10)',
typoActionBorder: 'rgba(6,182,212,0.20)',
typoActionText: '#06b6d4',
typoDataText: '#06b6d4',
typoCoordText: '#94a3b8',
borderCardBg: '#ffffff',
borderCardShadow: '0px 1px 2px 0px rgba(0,0,0,0.05)',
textSectionBg: '#ffffff',
textSectionBorder: '#e2e8f0',
radiusSmLabel: 'radius-sm (4px)',
radiusMdLabel: 'radius-md (8px)',
radiusCardBg: '#ffffff',
radiusCardBorder: '#e2e8f0',
radiusCardShadow: '0px 1px 2px 0px rgba(0,0,0,0.05)',
radiusDescText: '#475569',
footerBorder: '#e2e8f0',
footerText: '#94a3b8',
footerAccent: '#06b6d4',
swatchBorder: '#e2e8f0',
swatchBorderHover: 'rgba(6,182,212,0.20)',
bgTokens: [
{ bg: '#f8fafc', token: 'bg-0', hex: '#f8fafc', desc: 'Primary page canvas, lightest foundation layer.' },
{ bg: '#ffffff', token: 'bg-1', hex: '#ffffff', desc: 'Surface Level 1: Sidebar containers and utility panels.' },
{ bg: '#f1f5f9', token: 'bg-2', hex: '#f1f5f9', desc: 'Surface Level 2: Table headers and subtle sectional shifts.' },
{ bg: '#e2e8f0', token: 'bg-3', hex: '#e2e8f0', desc: 'Surface Level 3: Elevated cards and floating elements.' },
{ bg: '#cbd5e1', token: 'bg-hover', hex: '#cbd5e1', desc: 'Interactive states, list item highlighting.', isHover: true },
],
accentTokens: [
{
color: '#06b6d4', name: 'Cyan Accent', token: 'primary', badge: 'Primary Action',
glow: '0px 1px 2px 0px rgba(0,0,0,0.05)',
badgeBg: 'rgba(6,182,212,0.10)', badgeBorder: 'rgba(6,182,212,0.25)', badgeText: '#06b6d4',
},
{
color: '#0891b2', name: 'Teal Accent', token: 'secondary', badge: 'Information',
glow: '0px 1px 2px 0px rgba(0,0,0,0.05)',
badgeBg: 'rgba(8,145,178,0.10)', badgeBorder: 'rgba(8,145,178,0.25)', badgeText: '#0891b2',
},
{
color: '#6366f1', name: 'Indigo Accent', token: 'tertiary', badge: 'Operations',
glow: '0px 1px 2px 0px rgba(0,0,0,0.05)',
badgeBg: 'rgba(99,102,241,0.10)', badgeBorder: 'rgba(99,102,241,0.25)', badgeText: '#6366f1',
},
],
statusTokens: [
{ color: '#dc2626', bg: '#fef2f2', border: '#fecaca', label: '위험 Critical', hex: '#f87171' },
{ color: '#c2410c', bg: '#fff7ed', border: '#fed7aa', label: '주의 Warning', hex: '#fb923c' },
{ color: '#b45309', bg: '#fffbeb', border: '#fde68a', label: '경고 Caution', hex: '#fbbf24' },
{ color: '#047857', bg: '#ecfdf5', border: '#a7f3d0', label: '정상 Normal', hex: '#34d399' },
],
borderTokens: [
{ token: 'border', hex: '#cbd5e1', border: '#cbd5e1', barBg: '#cbd5e1' },
{ token: 'border-light', hex: '#e2e8f0', border: '#e2e8f0', barBg: '#e2e8f0' },
],
textTokens: [
{ token: 'text-1', sampleText: '주요 텍스트 Primary Text', sampleClass: 'text-[#0f172a] font-korean text-[15px] font-bold', desc: 'Headings, active values, and primary labels.', descColor: '#64748b' },
{ token: 'text-2', sampleText: '보조 텍스트 Secondary Text', sampleClass: 'text-[#475569] font-korean text-[15px] font-medium', desc: 'Supporting labels and secondary information.', descColor: '#64748b' },
{ token: 'text-3', sampleText: '비활성 텍스트 Muted Text', sampleClass: 'text-[#94a3b8] font-korean text-[15px]', desc: 'Disabled states, placeholders, and captions.', descColor: '#94a3b8' },
],
};
export const getTheme = (mode: ThemeMode): DesignTheme =>
mode === 'dark' ? DARK_THEME : LIGHT_THEME;

파일 보기

@ -6,7 +6,7 @@ import type { StyleSpecification } from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css' import 'maplibre-gl/dist/maplibre-gl.css'
import type { ScatSegment } from './scatTypes' import type { ScatSegment } from './scatTypes'
import type { ApiZoneItem } from '../services/scatApi' import type { ApiZoneItem } from '../services/scatApi'
import { esiColor, jejuCoastCoords } from './scatConstants' import { esiColor } from './scatConstants'
import { hexToRgba } from '@common/components/map/mapUtils' import { hexToRgba } from '@common/components/map/mapUtils'
const BASE_STYLE: StyleSpecification = { const BASE_STYLE: StyleSpecification = {
@ -87,12 +87,17 @@ function getZoomScale(zoom: number) {
} }
// ── 세그먼트 폴리라인 좌표 생성 (lng, lat 순서) ────────── // ── 세그먼트 폴리라인 좌표 생성 (lng, lat 순서) ──────────
function buildSegCoords(seg: ScatSegment, halfLenScale: number): [number, number][] { // 인접 구간 좌표로 해안선 방향을 동적 계산
const coastIdx = seg.id % (jejuCoastCoords.length - 1) function buildSegCoords(
const [clat1, clng1] = jejuCoastCoords[coastIdx] seg: ScatSegment,
const [clat2, clng2] = jejuCoastCoords[(coastIdx + 1) % jejuCoastCoords.length] halfLenScale: number,
const dlat = clat2 - clat1 segments: ScatSegment[],
const dlng = clng2 - clng1 ): [number, number][] {
const idx = segments.indexOf(seg)
const prev = idx > 0 ? segments[idx - 1] : seg
const next = idx < segments.length - 1 ? segments[idx + 1] : seg
const dlat = next.lat - prev.lat
const dlng = next.lng - prev.lng
const dist = Math.sqrt(dlat * dlat + dlng * dlng) const dist = Math.sqrt(dlat * dlat + dlng * dlng)
const nDlat = dist > 0 ? dlat / dist : 0 const nDlat = dist > 0 ? dlat / dist : 0
const nDlng = dist > 0 ? dlng / dist : 1 const nDlng = dist > 0 ? dlng / dist : 1
@ -126,21 +131,21 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg
const zs = useMemo(() => getZoomScale(zoom), [zoom]) const zs = useMemo(() => getZoomScale(zoom), [zoom])
// 제주도 해안선 레퍼런스 라인 // 제주도 해안선 레퍼런스 라인 — 하드코딩 제거, 추후 DB 기반 해안선으로 대체 예정
const coastlineLayer = useMemo( // const coastlineLayer = useMemo(
() => // () =>
new PathLayer({ // new PathLayer({
id: 'jeju-coastline', // id: 'jeju-coastline',
data: [{ path: jejuCoastCoords.map(([lat, lng]) => [lng, lat] as [number, number]) }], // data: [{ path: jejuCoastCoords.map(([lat, lng]) => [lng, lat] as [number, number]) }],
getPath: (d: { path: [number, number][] }) => d.path, // getPath: (d: { path: [number, number][] }) => d.path,
getColor: [6, 182, 212, 46], // getColor: [6, 182, 212, 46],
getWidth: 1.5, // getWidth: 1.5,
getDashArray: [8, 6], // getDashArray: [8, 6],
dashJustified: true, // dashJustified: true,
widthMinPixels: 1, // widthMinPixels: 1,
}), // }),
[], // [],
) // )
// 선택된 구간 글로우 레이어 // 선택된 구간 글로우 레이어
const glowLayer = useMemo( const glowLayer = useMemo(
@ -148,7 +153,7 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg
new PathLayer({ new PathLayer({
id: 'scat-glow', id: 'scat-glow',
data: [selectedSeg], data: [selectedSeg],
getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale), getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale, segments),
getColor: [34, 197, 94, 38], getColor: [34, 197, 94, 38],
getWidth: zs.glowWidth, getWidth: zs.glowWidth,
capRounded: true, capRounded: true,
@ -159,7 +164,7 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg
getWidth: [zs.glowWidth], getWidth: [zs.glowWidth],
}, },
}), }),
[selectedSeg, zs.glowWidth, zs.halfLenScale], [selectedSeg, segments, zs.glowWidth, zs.halfLenScale],
) )
// ESI 색상 세그먼트 폴리라인 // ESI 색상 세그먼트 폴리라인
@ -168,7 +173,7 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg
new PathLayer({ new PathLayer({
id: 'scat-segments', id: 'scat-segments',
data: segments, data: segments,
getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale), getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale, segments),
getColor: (d: ScatSegment) => { getColor: (d: ScatSegment) => {
const isSelected = selectedSeg.id === d.id const isSelected = selectedSeg.id === d.id
const hexCol = isSelected ? '#22c55e' : esiColor(d.esiNum) const hexCol = isSelected ? '#22c55e' : esiColor(d.esiNum)
@ -234,10 +239,10 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const deckLayers: any[] = useMemo(() => { const deckLayers: any[] = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const layers: any[] = [coastlineLayer, glowLayer, segPathLayer] const layers: any[] = [glowLayer, segPathLayer]
if (markerLayer) layers.push(markerLayer) if (markerLayer) layers.push(markerLayer)
return layers return layers
}, [coastlineLayer, glowLayer, segPathLayer, markerLayer]) }, [glowLayer, segPathLayer, markerLayer])
const doneCount = segments.filter(s => s.status === '완료').length const doneCount = segments.filter(s => s.status === '완료').length
const progCount = segments.filter(s => s.status === '진행중').length const progCount = segments.filter(s => s.status === '진행중').length

파일 보기

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Map, useControl } from '@vis.gl/react-maplibre' import { Map, useControl } from '@vis.gl/react-maplibre'
import { MapboxOverlay } from '@deck.gl/mapbox' import { MapboxOverlay } from '@deck.gl/mapbox'
import { PathLayer, ScatterplotLayer } from '@deck.gl/layers' import { ScatterplotLayer } from '@deck.gl/layers'
import type { StyleSpecification } from 'maplibre-gl' import type { StyleSpecification } from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css' import 'maplibre-gl/dist/maplibre-gl.css'
import type { ScatDetail } from './scatTypes' import type { ScatDetail } from './scatTypes'
@ -50,45 +50,22 @@ function PopupMap({
esi: string esi: string
onMapLoad?: () => void onMapLoad?: () => void
}) { }) {
// 해안 구간 라인 (시뮬레이션) — [lng, lat] 순서 // 해안 구간 라인 / 조사 경로 — 하드코딩 방향이라 주석처리, 추후 실제 방향 데이터로 대체
const segLine: [number, number][] = [ // const segLine: [number, number][] = [
[lng - 0.004, lat - 0.002], // [lng - 0.004, lat - 0.002],
[lng - 0.002, lat - 0.001], // [lng - 0.002, lat - 0.001],
[lng, lat], // [lng, lat],
[lng + 0.002, lat + 0.001], // [lng + 0.002, lat + 0.001],
[lng + 0.004, lat + 0.002], // [lng + 0.004, lat + 0.002],
] // ]
// const surveyRoute: [number, number][] = [
// 조사 경로 라인 // [lng - 0.003, lat - 0.0015],
const surveyRoute: [number, number][] = [ // [lng - 0.001, lat - 0.0005],
[lng - 0.003, lat - 0.0015], // [lng + 0.001, lat + 0.0005],
[lng - 0.001, lat - 0.0005], // [lng + 0.003, lat + 0.0015],
[lng + 0.001, lat + 0.0005], // ]
[lng + 0.003, lat + 0.0015],
]
const deckLayers = [ const deckLayers = [
// 조사 경로 (파란 점선)
new PathLayer({
id: 'survey-route',
data: [{ path: surveyRoute }],
getPath: (d: { path: [number, number][] }) => d.path,
getColor: [59, 130, 246, 153],
getWidth: 2,
getDashArray: [6, 4],
dashJustified: true,
widthMinPixels: 1,
}),
// 해안 구간 라인 (ESI 색상)
new PathLayer({
id: 'seg-line',
data: [{ path: segLine }],
getPath: (d: { path: [number, number][] }) => d.path,
getColor: hexToRgba(esiCol, 204),
getWidth: 5,
capRounded: true,
widthMinPixels: 3,
}),
// 접근 포인트 (노란 점) // 접근 포인트 (노란 점)
new ScatterplotLayer({ new ScatterplotLayer({
id: 'access-point', id: 'access-point',

파일 보기

@ -139,7 +139,7 @@ function DetailTab({ detail }: { detail: ScatDetail }) {
/* ═══ 탭 1: 현장 사진 ═══ */ /* ═══ 탭 1: 현장 사진 ═══ */
function PhotoTab({ detail }: { detail: ScatDetail }) { function PhotoTab({ detail }: { detail: ScatDetail }) {
const [imgError, setImgError] = useState(false); const [imgError, setImgError] = useState(false);
const imgSrc = `/scat-img/${detail.code}-1.png`; const imgSrc = `/scat/img/${detail.code}-1.png`;
if (imgError) { if (imgError) {
return ( return (

파일 보기

@ -26,9 +26,9 @@ export const statusColor: Record<string, string> = {
}; };
export const esiLevel = (n: number) => (n >= 8 ? 'h' : n >= 5 ? 'm' : 'l'); export const esiLevel = (n: number) => (n >= 8 ? 'h' : n >= 5 ? 'm' : 'l');
// ═══ 제주도 해안선 좌표 (시계방향) ═══ // ═══ 제주도 해안선 좌표 (시계방향) — 하드코딩 비활성화, 추후 DB 기반으로 대체 ═══
export const jejuCoastCoords: [number, number][] = [ /* export const jejuCoastCoords: [number, number][] = [
// 서부 (대정읍~한경면) // 서부 (대정읍~한경면)
[33.28, 126.16], [33.28, 126.16],
[33.26, 126.18], [33.26, 126.18],
@ -101,4 +101,4 @@ export const jejuCoastCoords: [number, number][] = [
[33.31, 126.19], [33.31, 126.19],
[33.3, 126.175], [33.3, 126.175],
[33.293, 126.162], [33.293, 126.162],
]; ]; */

파일 보기

@ -28,7 +28,9 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@common/*": ["src/common/*"], "@common/*": ["src/common/*"],
"@tabs/*": ["src/tabs/*"] "@tabs/*": ["src/tabs/*"],
"@pages/*": ["src/pages/*"],
"@/*": ["src/*"]
} }
}, },
"include": ["src"] "include": ["src"]

파일 보기

@ -38,6 +38,7 @@ export default defineConfig({
alias: { alias: {
'@common': path.resolve(__dirname, 'src/common'), '@common': path.resolve(__dirname, 'src/common'),
'@tabs': path.resolve(__dirname, 'src/tabs'), '@tabs': path.resolve(__dirname, 'src/tabs'),
'@': path.resolve(__dirname, 'src'),
}, },
}, },
build: { build: {