From d0491c3f0fa29af60326b9b9616395697563b1b4 Mon Sep 17 00:00:00 2001 From: leedano Date: Tue, 24 Mar 2026 16:36:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(design):=20Stitch=20MCP=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=B9=B4=ED=83=88=EB=A1=9C=EA=B7=B8=20+=20SCAT=20?= =?UTF-8?q?=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9=20=ED=95=B4=EC=95=88?= =?UTF-8?q?=EC=84=A0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - react-router-dom 도입, /design 경로에 디자인 토큰/컴포넌트 카탈로그 페이지 추가 - SCAT 지도에서 하드코딩된 제주 해안선 좌표 제거, 인접 구간 기반 동적 방향 계산으로 전환 - @/ path alias 추가, SVG 아이콘 에셋 추가 --- docs/DESIGN-SYSTEM.md | 79 +++ frontend/package-lock.json | 58 ++ frontend/package.json | 1 + frontend/src/App.tsx | 13 +- .../src/assets/icons/wing-alert-triangle.svg | 3 + frontend/src/assets/icons/wing-anchor.svg | 3 + frontend/src/assets/icons/wing-cargo.svg | 3 + frontend/src/assets/icons/wing-chart-bar.svg | 3 + .../src/assets/icons/wing-color-palette.svg | 3 + frontend/src/assets/icons/wing-comp-close.svg | 3 + frontend/src/assets/icons/wing-comp-gear.svg | 3 + frontend/src/assets/icons/wing-comp-menu.svg | 3 + .../src/assets/icons/wing-comp-search.svg | 3 + .../src/assets/icons/wing-documentation.svg | 3 + frontend/src/assets/icons/wing-elevation.svg | 3 + .../src/assets/icons/wing-foundations.svg | 3 + .../src/assets/icons/wing-layout-grid.svg | 3 + .../src/assets/icons/wing-notification.svg | 3 + .../assets/icons/wing-pdf-file-disabled.svg | 3 + frontend/src/assets/icons/wing-pdf-file.svg | 3 + frontend/src/assets/icons/wing-settings.svg | 3 + frontend/src/assets/icons/wing-typography.svg | 3 + frontend/src/assets/icons/wing-wave-graph.svg | 10 + frontend/src/main.tsx | 9 +- .../src/pages/design/ColorPaletteContent.tsx | 491 +++++++++++++++ .../src/pages/design/ComponentsContent.tsx | 48 ++ frontend/src/pages/design/DesignContent.tsx | 489 +++++++++++++++ frontend/src/pages/design/DesignHeader.tsx | 96 +++ frontend/src/pages/design/DesignPage.tsx | 66 ++ frontend/src/pages/design/DesignSidebar.tsx | 107 ++++ frontend/src/pages/design/LayoutContent.tsx | 567 ++++++++++++++++++ frontend/src/pages/design/RadiusContent.tsx | 279 +++++++++ .../src/pages/design/TypographyContent.tsx | 462 ++++++++++++++ .../components/ButtonCatalogSection.tsx | 242 ++++++++ .../pages/design/components/CardSection.tsx | 157 +++++ .../design/components/IconBadgeSection.tsx | 193 ++++++ frontend/src/pages/design/designTheme.ts | 380 ++++++++++++ frontend/src/tabs/scat/components/ScatMap.tsx | 59 +- .../src/tabs/scat/components/ScatPopup.tsx | 53 +- .../tabs/scat/components/ScatRightPanel.tsx | 2 +- .../src/tabs/scat/components/scatConstants.ts | 6 +- frontend/tsconfig.app.json | 4 +- frontend/vite.config.ts | 1 + 43 files changed, 3850 insertions(+), 76 deletions(-) create mode 100644 docs/DESIGN-SYSTEM.md create mode 100644 frontend/src/assets/icons/wing-alert-triangle.svg create mode 100644 frontend/src/assets/icons/wing-anchor.svg create mode 100644 frontend/src/assets/icons/wing-cargo.svg create mode 100644 frontend/src/assets/icons/wing-chart-bar.svg create mode 100644 frontend/src/assets/icons/wing-color-palette.svg create mode 100644 frontend/src/assets/icons/wing-comp-close.svg create mode 100644 frontend/src/assets/icons/wing-comp-gear.svg create mode 100644 frontend/src/assets/icons/wing-comp-menu.svg create mode 100644 frontend/src/assets/icons/wing-comp-search.svg create mode 100644 frontend/src/assets/icons/wing-documentation.svg create mode 100644 frontend/src/assets/icons/wing-elevation.svg create mode 100644 frontend/src/assets/icons/wing-foundations.svg create mode 100644 frontend/src/assets/icons/wing-layout-grid.svg create mode 100644 frontend/src/assets/icons/wing-notification.svg create mode 100644 frontend/src/assets/icons/wing-pdf-file-disabled.svg create mode 100644 frontend/src/assets/icons/wing-pdf-file.svg create mode 100644 frontend/src/assets/icons/wing-settings.svg create mode 100644 frontend/src/assets/icons/wing-typography.svg create mode 100644 frontend/src/assets/icons/wing-wave-graph.svg create mode 100644 frontend/src/pages/design/ColorPaletteContent.tsx create mode 100644 frontend/src/pages/design/ComponentsContent.tsx create mode 100644 frontend/src/pages/design/DesignContent.tsx create mode 100644 frontend/src/pages/design/DesignHeader.tsx create mode 100644 frontend/src/pages/design/DesignPage.tsx create mode 100644 frontend/src/pages/design/DesignSidebar.tsx create mode 100644 frontend/src/pages/design/LayoutContent.tsx create mode 100644 frontend/src/pages/design/RadiusContent.tsx create mode 100644 frontend/src/pages/design/TypographyContent.tsx create mode 100644 frontend/src/pages/design/components/ButtonCatalogSection.tsx create mode 100644 frontend/src/pages/design/components/CardSection.tsx create mode 100644 frontend/src/pages/design/components/IconBadgeSection.tsx create mode 100644 frontend/src/pages/design/designTheme.ts diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md new file mode 100644 index 0000000..13d0ccb --- /dev/null +++ b/docs/DESIGN-SYSTEM.md @@ -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 | 카드, 모달, 패널 | + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5c8fe25..16842e0 100755 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "maplibre-gl": "^5.19.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", "react-window": "^2.2.7", "socket.io-client": "^4.8.3", "xlsx": "^0.18.5", @@ -3366,6 +3367,19 @@ "dev": true, "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": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", @@ -5451,6 +5465,44 @@ "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": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz", @@ -5660,6 +5712,12 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index a1db360..66da675 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "maplibre-gl": "^5.19.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", "react-window": "^2.2.7", "socket.io-client": "^4.8.3", "xlsx": "^0.18.5", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8283fa6..80b677e 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import { Routes, Route } from 'react-router-dom' import { GoogleOAuthProvider } from '@react-oauth/google' import type { MainTab } from '@common/types/navigation' import { MainLayout } from '@common/components/layout/MainLayout' @@ -19,6 +20,7 @@ import { IncidentsView } from '@tabs/incidents' import { AdminView } from '@tabs/admin' import { ScatView } from '@tabs/scat' import { RescueView } from '@tabs/rescue' +import { DesignPage } from '@/pages/design/DesignPage' const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || '' @@ -108,9 +110,14 @@ function App() { } return ( - - {renderView()} - + + } /> + + {renderView()} + + } /> + ) } diff --git a/frontend/src/assets/icons/wing-alert-triangle.svg b/frontend/src/assets/icons/wing-alert-triangle.svg new file mode 100644 index 0000000..848a326 --- /dev/null +++ b/frontend/src/assets/icons/wing-alert-triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-anchor.svg b/frontend/src/assets/icons/wing-anchor.svg new file mode 100644 index 0000000..9dce757 --- /dev/null +++ b/frontend/src/assets/icons/wing-anchor.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-cargo.svg b/frontend/src/assets/icons/wing-cargo.svg new file mode 100644 index 0000000..620297d --- /dev/null +++ b/frontend/src/assets/icons/wing-cargo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-chart-bar.svg b/frontend/src/assets/icons/wing-chart-bar.svg new file mode 100644 index 0000000..2f5a827 --- /dev/null +++ b/frontend/src/assets/icons/wing-chart-bar.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-color-palette.svg b/frontend/src/assets/icons/wing-color-palette.svg new file mode 100644 index 0000000..6e2cd65 --- /dev/null +++ b/frontend/src/assets/icons/wing-color-palette.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-comp-close.svg b/frontend/src/assets/icons/wing-comp-close.svg new file mode 100644 index 0000000..a9f047b --- /dev/null +++ b/frontend/src/assets/icons/wing-comp-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-comp-gear.svg b/frontend/src/assets/icons/wing-comp-gear.svg new file mode 100644 index 0000000..7fc39b2 --- /dev/null +++ b/frontend/src/assets/icons/wing-comp-gear.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-comp-menu.svg b/frontend/src/assets/icons/wing-comp-menu.svg new file mode 100644 index 0000000..43bc70a --- /dev/null +++ b/frontend/src/assets/icons/wing-comp-menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-comp-search.svg b/frontend/src/assets/icons/wing-comp-search.svg new file mode 100644 index 0000000..334b67f --- /dev/null +++ b/frontend/src/assets/icons/wing-comp-search.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-documentation.svg b/frontend/src/assets/icons/wing-documentation.svg new file mode 100644 index 0000000..14c6173 --- /dev/null +++ b/frontend/src/assets/icons/wing-documentation.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-elevation.svg b/frontend/src/assets/icons/wing-elevation.svg new file mode 100644 index 0000000..515b20f --- /dev/null +++ b/frontend/src/assets/icons/wing-elevation.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-foundations.svg b/frontend/src/assets/icons/wing-foundations.svg new file mode 100644 index 0000000..8936146 --- /dev/null +++ b/frontend/src/assets/icons/wing-foundations.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-layout-grid.svg b/frontend/src/assets/icons/wing-layout-grid.svg new file mode 100644 index 0000000..9fee88e --- /dev/null +++ b/frontend/src/assets/icons/wing-layout-grid.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-notification.svg b/frontend/src/assets/icons/wing-notification.svg new file mode 100644 index 0000000..29c2de1 --- /dev/null +++ b/frontend/src/assets/icons/wing-notification.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-pdf-file-disabled.svg b/frontend/src/assets/icons/wing-pdf-file-disabled.svg new file mode 100644 index 0000000..0674110 --- /dev/null +++ b/frontend/src/assets/icons/wing-pdf-file-disabled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-pdf-file.svg b/frontend/src/assets/icons/wing-pdf-file.svg new file mode 100644 index 0000000..16db061 --- /dev/null +++ b/frontend/src/assets/icons/wing-pdf-file.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-settings.svg b/frontend/src/assets/icons/wing-settings.svg new file mode 100644 index 0000000..4bd1896 --- /dev/null +++ b/frontend/src/assets/icons/wing-settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-typography.svg b/frontend/src/assets/icons/wing-typography.svg new file mode 100644 index 0000000..2083125 --- /dev/null +++ b/frontend/src/assets/icons/wing-typography.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/icons/wing-wave-graph.svg b/frontend/src/assets/icons/wing-wave-graph.svg new file mode 100644 index 0000000..7648b6e --- /dev/null +++ b/frontend/src/assets/icons/wing-wave-graph.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index dfc62ce..ea33681 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import './index.css' import App from './App.tsx' @@ -17,8 +18,10 @@ const queryClient = new QueryClient({ createRoot(document.getElementById('root')!).render( - - - + + + + + , ) diff --git a/frontend/src/pages/design/ColorPaletteContent.tsx b/frontend/src/pages/design/ColorPaletteContent.tsx new file mode 100644 index 0000000..b0ac543 --- /dev/null +++ b/frontend/src/pages/design/ColorPaletteContent.tsx @@ -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 ( +
+ + {/* ── 섹션 1: 헤더 ── */} +
+
+

+ Color +

+

+ WING-OPS 인터페이스에서 사용되는 색상 체계입니다. Primitive Token(원시 팔레트)과 Semantic Token(의미 기반 토큰) 두 계층으로 구성됩니다. +

+
+ + {/* 토큰 네이밍 다이어그램 카드 */} +
+ + bg + + + — + + + 0 + +
+
+ + Category + + + 색상 범주 (bg, text, border, status…) + +
+
+
+ + Value + + + 단계 또는 역할 (0–3, hover, red, cyan…) + +
+
+
+
+ + {/* ── 섹션 2: Primitive Tokens ── */} +
+
+

+ Primitive Tokens +

+
    +
  • Tailwind CSS 기반 색상 스케일로 구성된 원시 팔레트입니다.
  • +
  • 각 색조(Hue)는 00(가장 밝음)에서 100(가장 어두움)까지 11단계로 표현됩니다.
  • +
  • Navy는 UI 배경 계층에만 사용되는 특수 팔레트입니다.
  • +
+
+ +
+ {PRIMITIVE_COLORS.map((group) => ( +
+ {/* 그룹명 */} + + {group.name} + + + {/* 가로 컬러 바 */} +
+ {group.steps.map((step) => ( +
+
+
+ + {step.label} + + + {step.hex} + +
+
+ ))} +
+
+ ))} +
+
+ + {/* ── 섹션 3: Semantic Colors ── */} +
+
+

+ Semantic Colors +

+
    +
  • Primitive Token을 역할 기반으로 추상화한 의미 토큰입니다.
  • +
  • 다크/라이트 모드에 따라 실제 색상 값이 달라집니다.
  • +
  • 코드에서는 항상 Semantic Token을 사용하고, Primitive Token을 직접 참조하지 않습니다.
  • +
+
+ +
+ {SEMANTIC_CATEGORIES.map((category) => ( +
+ {/* 카테고리 제목 (좌측 2px cyan accent bar) */} +
+
+ + {category.name} + +
+ + {/* 테이블 */} +
+ {/* 헤더 */} +
+ {(['Token', 'Dark Mode', 'Light Mode', 'Usage'] as const).map((col) => ( +
+ + {col} + +
+ ))} +
+ + {/* 데이터 행 */} + {category.tokens.map((token, rowIdx) => ( +
+ {/* Token 컬럼 */} +
+ + {token.token} + +
+ + {/* Dark Mode 컬럼 */} +
+
+
+ + {token.dark} + +
+
+ + {/* Light Mode 컬럼 */} +
+
+
+ + {token.light} + +
+
+ + {/* Usage 컬럼 */} +
+
    + {token.usage.map((u) => ( +
  • + + + {u} + +
  • + ))} +
+
+
+ ))} +
+
+ ))} +
+
+
+ ); +}; + +export default ColorPaletteContent; diff --git a/frontend/src/pages/design/ComponentsContent.tsx b/frontend/src/pages/design/ComponentsContent.tsx new file mode 100644 index 0000000..6622405 --- /dev/null +++ b/frontend/src/pages/design/ComponentsContent.tsx @@ -0,0 +1,48 @@ +import { ButtonCatalogSection } from './components/ButtonCatalogSection'; +import { IconBadgeSection } from './components/IconBadgeSection'; +import { CardSection } from './components/CardSection'; + +export const ComponentsContent = () => { + return ( +
+ {/* 헤더 */} +
+

+ 시스템 컴포넌트 카탈로그 +

+

+ WING-OPS 해상 물류를 위한 시각적 아이덴티티 시스템입니다. 정밀도와 미션 크리티컬한 운영을 위해 설계된 고밀도 산업용 인터페이스입니다. +

+
+ + {/* 섹션 */} + + + + + {/* 푸터 */} +
+ + © 2024 WING-OPS 해상 시스템 + + + 전술 네비게이터 쉘 v2.4 + +
+
+ ); +}; + +export default ComponentsContent; diff --git a/frontend/src/pages/design/DesignContent.tsx b/frontend/src/pages/design/DesignContent.tsx new file mode 100644 index 0000000..d0bbd66 --- /dev/null +++ b/frontend/src/pages/design/DesignContent.tsx @@ -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) => ( +
+
+

+ {num} {title} +

+ {rightNode} +
+ {sub && ( +

+ {sub} +

+ )} +
+); + +// ---------- 타이포그래피 행 ---------- +const TYPO_ROWS: TypoRow[] = [ + { + size: '9px / Meta', + sampleNode: (t) => ( + 메타정보 Meta info + ), + properties: 'Regular / 400', + }, + { + size: '10px / Table', + sampleNode: (t) => ( + 테이블 데이터 Table data + ), + properties: 'Medium / 500', + }, + { + size: '11px / Action', + sampleNode: (t) => ( + + + 입력/버튼 Input/Button text + + + ), + properties: 'Medium / 500', + }, + { + size: '13px / Header', + sampleNode: (t) => ( + + 섹션 헤더 Section Header + + ), + properties: 'Bold / 700', + }, + { + size: '15px / Title', + sampleNode: (t) => ( + + 패널 타이틀 Panel Title + + ), + properties: 'ExtraBold / 800', + }, + { + size: 'Data / Mono', + sampleNode: (t) => ( +
+ 1,234.56 km² + 35° 06' 12" N +
+ ), + 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 ( +
+ {/* ── 헤더 섹션 ── */} +
+
+

+ 디자인 토큰 +

+

+ Comprehensive design token reference for the WING-OPS operational interface. +

+
+ {/* 상태 뱃지 */} +
+ + + System Active + +
+
+ + {/* ── 2컬럼 그리드 ── */} +
+ {/* ── 행 1 좌측: 01 배경색 ── */} +
+ +
+ {t.bgTokens.map((item) => ( +
+ {/* 색상 스와치 */} +
+ {/* 정보 */} +
+ {item.token} + + {item.hex} + + + {item.desc} + +
+
+ ))} +
+
+ + {/* ── 행 1 우측: 02 테두리 색상 + 03 텍스트 색상 ── */} +
+ {/* 02 테두리 색상 */} +
+ +
+ {t.borderTokens.map((item) => ( +
+ {item.token} + + {item.hex} + +
+
+ ))} +
+
+ + {/* 03 텍스트 색상 */} +
+ +
+ {t.textTokens.map((item) => ( +
+ {item.token} + {item.sampleText} + + {item.desc} + +
+ ))} +
+
+
+ + {/* ── 행 2 좌측: 04 강조 색상 ── */} +
+ +
+ {t.accentTokens.map((item) => ( +
+ {/* 좌측: 색상 원 + 이름/토큰 */} +
+
+
+ {item.name} + + {item.token} / {item.color} + +
+
+ {/* 우측: 뱃지 */} +
+ + {item.badge} + +
+
+ ))} +
+
+ + {/* ── 행 2 우측: 05 상태 표시기 ── */} +
+ +
+ {t.statusTokens.map((item) => ( +
+ + + {item.label} + + + {item.hex} + +
+ ))} +
+
+ + {/* ── 행 3: 06 타이포그래피 스케일 (전체 열 span) ── */} +
+ + + Noto Sans KR + + + JetBrains Mono + +
+ } + /> +
+ {/* 헤더 행 */} +
+ {(['Size / Role', 'Sample String', 'Properties'] as const).map((col, i) => ( +
+ + {col} + +
+ ))} +
+ + {/* 데이터 행 */} + {TYPO_ROWS.map((row) => ( +
+ {/* Size */} +
+ {row.size} +
+ {/* Sample */} +
{row.sampleNode(t)}
+ {/* Properties */} +
+ {row.properties} +
+
+ ))} +
+
+ + {/* ── 행 4: 07 테두리 곡률 (전체 열 span) ── */} +
+ +
+ {/* radius-sm */} +
+ {t.radiusSmLabel} +
+ + Small Elements + +

+ Applied to tactical buttons, search inputs, and micro-cards for a precise, sharp industrial feel. +

+
+
+ {/* radius-md */} +
+ {t.radiusMdLabel} +
+ + Structural Panels + +

+ Applied to telemetry cards, floating modals, and primary operational panels to soften high-density data. +

+
+
+
+
+
+ + {/* ── 푸터 ── */} +
+ {/* 좌측 */} +
+ {['Precision Engineering', 'Safety Compliant', 'Optimized v8.42'].map((label) => ( + + {label} + + ))} +
+ {/* 우측 */} +
+ + Generated for Terminal: + + + 1440x900_PR_MKT + +
+
+
+ ); +}; + +export default DesignContent; diff --git a/frontend/src/pages/design/DesignHeader.tsx b/frontend/src/pages/design/DesignHeader.tsx new file mode 100644 index 0000000..afd37c5 --- /dev/null +++ b/frontend/src/pages/design/DesignHeader.tsx @@ -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 ( +
+ {/* 좌측: 로고 + 버전 뱃지 */} +
+ + WING-OPS + +
+ + Design System v1.0 + +
+
+ + {/* 중앙: 탭 네비게이션 */} + + + {/* 우측: 테마 토글 */} +
+ +
+
+ ); +}; + +export default DesignHeader; diff --git a/frontend/src/pages/design/DesignPage.tsx b/frontend/src/pages/design/DesignPage.tsx new file mode 100644 index 0000000..873f19a --- /dev/null +++ b/frontend/src/pages/design/DesignPage.tsx @@ -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 = { + foundations: 'color', + components: 'buttons', +}; + +export const DesignPage = () => { + const [activeTab, setActiveTab] = useState('foundations'); + const [themeMode, setThemeMode] = useState('dark'); + const [sidebarItem, setSidebarItem] = useState('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 ; + case 'typography': + return ; + case 'radius': + return ; + case 'layout': + return ; + default: + return ; + } + } + return ; + }; + + return ( +
+ setThemeMode(themeMode === 'dark' ? 'light' : 'dark')} /> +
+ +
+ {renderContent()} +
+
+
+ ); +}; + +export default DesignPage; diff --git a/frontend/src/pages/design/DesignSidebar.tsx b/frontend/src/pages/design/DesignSidebar.tsx new file mode 100644 index 0000000..d63daa5 --- /dev/null +++ b/frontend/src/pages/design/DesignSidebar.tsx @@ -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 = { + 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 ( + + ); + }; + + return ( + + ); +} + +export default DesignSidebar; diff --git a/frontend/src/pages/design/LayoutContent.tsx b/frontend/src/pages/design/LayoutContent.tsx new file mode 100644 index 0000000..8ed7166 --- /dev/null +++ b/frontend/src/pages/design/LayoutContent.tsx @@ -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 ( +
+ + {/* ── 섹션 1: 헤더 + 개요 ── */} +
+
+

+ Layout +

+

+ WING-OPS는 데스크톱 전용 고정 뷰포트 애플리케이션입니다. 화면 전체를 채우는 고정 레이아웃(100vh)으로, flex 기반의 패널 구조를 사용합니다. +

+
+
    +
  • 뷰포트 고정: body {'{'} height: 100vh; overflow: hidden {'}'}
  • +
  • 레이아웃 수단: flex (2,243회) > grid (~120회) — flex가 주요 레이아웃 수단
  • +
  • Tailwind CSS 기본 breakpoints/spacing을 사용하며, 커스텀 오버라이드 없음
  • +
+
+ + {/* ── 섹션 2: Breakpoints ── */} +
+
+

+ Breakpoints +

+

+ Tailwind CSS 기본 breakpoints를 사용합니다. 현재 프로젝트는 데스크톱 전용으로, xl: 접두사만 제한적으로 사용합니다. +

+
+ +
+
+ {(['Name', 'Prefix', 'Min Width', 'Status', 'Note'] as const).map((col) => ( +
+ + {col} + +
+ ))} +
+ + {BREAKPOINTS.map((bp, idx) => ( +
+
+ {bp.name} +
+
+ + {bp.prefix} + +
+
+ {bp.minWidth} +
+
+ + {bp.inUse ? '사용 중' : '미사용'} + +
+
+ + {bp.note || '-'} + +
+
+ ))} +
+
+ + {/* ── 섹션 3: Device Grid & Spacing ── */} +
+
+

+ Device Grid & Spacing +

+

+ 디바이스별 그리드 구성과 여백 패턴입니다. 현재 Desktop만 지원합니다. +

+
+ +
+ {DEVICE_SPECS.map((spec) => ( +
+
+ + {spec.device} + + {!spec.supported && ( + + 미지원 + + )} +
+ +
+ {[ + { label: 'Width', value: spec.width }, + { label: 'Columns', value: spec.columns }, + { label: 'Gutter', value: spec.gutter }, + { label: 'Margin', value: spec.margin }, + ].map((row) => ( +
+ + {row.label} + + + {row.value} + +
+ ))} +
+
+ ))} +
+
+ + {/* ── 섹션 4: Spacing Scale ── */} +
+
+

+ Spacing Scale +

+

+ Tailwind CSS 기본 spacing 스케일입니다. gap, padding, margin에 동일하게 적용됩니다. +

+
+ +
+
+ {(['Scale', 'REM', 'PX', 'Preview', '용도'] as const).map((col) => ( +
+ + {col} + +
+ ))} +
+ + {SPACING_TOKENS.map((token, idx) => ( +
+
+ + {token.className} + +
+
+ {token.rem} +
+
+ {token.px} +
+
+
+
+
+ {token.usage} +
+
+ ))} +
+
+ + {/* ── 섹션 5: Z-Index Layers ── */} +
+
+

+ Z-Index Layers +

+

+ UI 요소의 레이어 스택 순서입니다. 높은 z-index가 위에 표시됩니다. +

+
+ +
+ {Z_LAYERS.map((layer, idx) => ( +
+ {/* z-index 라벨 */} +
+ + {layer.zIndex} + +
+ + {/* 레이어 바 */} +
+
+ + {layer.name} + + + {layer.description} + +
+
+ ))} +
+
+ + {/* ── 섹션 6: App Shell 구조 ── */} +
+
+

+ App Shell 구조 +

+

+ WING-OPS 애플리케이션의 기본 레이아웃 구조입니다. +

+
+ + {/* 레이아웃 다이어그램 */} +
+ {/* TopBar */} +
+ TopBar + h-[52px] / shrink-0 +
+ + {/* SubMenuBar */} +
+ SubMenuBar + shrink-0 / 조건부 렌더 +
+ + {/* Content Area */} +
+ {/* Sidebar */} +
+ Sidebar + 가변 너비 +
+ + {/* Main Content */} +
+ Content + flex-1 / overflow-y-auto +
+
+
+ + {/* wing.css 레이아웃 클래스 */} +
+

+ 레이아웃 클래스 +

+ {SHELL_CLASSES.map((cls) => ( +
+ + {cls.className} + + + {cls.role} + + + {cls.styles} + +
+ ))} +
+
+
+ ); +}; + +export default LayoutContent; diff --git a/frontend/src/pages/design/RadiusContent.tsx b/frontend/src/pages/design/RadiusContent.tsx new file mode 100644 index 0000000..5753138 --- /dev/null +++ b/frontend/src/pages/design/RadiusContent.tsx @@ -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 ( +
+ + {/* ── 섹션 1: 헤더 ── */} +
+
+

+ Radius +

+

+ Radius는 컴포넌트 혹은 콘텐츠 모서리의 둥글기를 표현합니다. +

+
+

+ Radius는 UI 구성 요소의 모서리를 둥글게 처리하여 부드럽고 현대적인 느낌을 제공합니다. 일관된 Radius 값은 브랜드 아이덴티티를 강화하고, 사용자 경험을 향상시키며, 다양한 화면과 컨텍스트에서 시각적 일관성을 유지하는 데 중요한 역할을 합니다. +

+
    +
  • + rounded-sm(6px)과{' '} + rounded-md(10px)는 Tailwind 기본값을 오버라이드한 프로젝트 커스텀 값입니다. +
  • +
  • 나머지 토큰은 Tailwind CSS 기본 border-radius 스케일을 따릅니다.
  • +
+
+ + {/* ── 섹션 2: Radius Tokens 테이블 ── */} +
+

+ Radius Tokens +

+ +
+ {/* 헤더 */} +
+ {(['이름', '값', 'Preview'] as const).map((col) => ( +
+ + {col} + +
+ ))} +
+ + {/* 데이터 행 */} + {RADIUS_TOKENS.map((token, rowIdx) => ( +
+ {/* 이름 */} +
+ + {token.name} + + {token.isCustom && ( + + custom + + )} +
+ + {/* 값 */} +
+ + {token.value} + +
+ + {/* Preview */} +
+
= 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)'}`, + }} + /> +
= 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)'}`, + }} + /> +
+
+ ))} +
+
+ + {/* ── 섹션 3: 컴포넌트 매핑 ── */} +
+
+

+ 컴포넌트 매핑 +

+

+ wing.css 컴포넌트 클래스에 적용된 Radius 토큰입니다. +

+
+ +
+ {COMPONENT_RADIUS.map((item) => ( +
+ {/* 미리보기 박스 */} +
+ + {/* 정보 */} +
+ + {item.className} + +
+ {item.components.map((comp) => ( + + {comp} + + ))} +
+
+
+ ))} +
+
+
+ ); +}; + +export default RadiusContent; diff --git a/frontend/src/pages/design/TypographyContent.tsx b/frontend/src/pages/design/TypographyContent.tsx new file mode 100644 index 0000000..7e69fff --- /dev/null +++ b/frontend/src/pages/design/TypographyContent.tsx @@ -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 ( +
+ + {/* ── 섹션 1: 헤더 + 개요 ── */} +
+
+

+ Typography +

+

+ WING-OPS 인터페이스에서 사용되는 타이포그래피 체계입니다. 폰트 패밀리, 크기, 두께를 토큰과 컴포넌트 클래스로 정의하여 시각적 계층 구조와 일관성을 유지합니다. +

+
+ +
+

+ 개요 +

+
    +
  • 폰트 크기, 폰트 두께, 폰트 패밀리를 각각 토큰으로 정의합니다.
  • +
  • 컴포넌트 클래스(.wing-*)로 조합하여 일관된 텍스트 스타일을 적용합니다.
  • +
  • 시스템 폰트를 기반으로 다양한 환경에서 일관된 사용자 경험을 보장합니다.
  • +
+
+
+ + {/* ── 섹션 2: 글꼴 (Font Family) ── */} +
+
+

+ 글꼴 +

+

+ 사용자의 디바이스 환경을 고려하여, 시스템 폰트와 웹 폰트를 조합하여 사용합니다. 한국어 UI에 최적화된 폰트 스택으로 다양한 기기에서 일관된 가독성을 보장합니다. +

+
+ + {/* body 기본 폰트 스택 코드 블록 */} +
+
+            font-family
+            {`: 'Outfit', 'Noto Sans KR', sans-serif;`}
+          
+
+ + {/* 폰트 카드 3종 */} +
+ {FONT_FAMILIES.map((font) => ( +
+ {/* 카드 헤더 */} +
+ + {font.name} + + + {font.className} + +
+ + {/* 카드 본문 */} +
+ {/* 폰트 스택 */} +
+ {font.stack} +
+ + {/* 용도 설명 */} +

+ {font.usage} +

+ + {/* 샘플 렌더 */} +
+ {/* Regular */} +
+ + Regular + + + {font.sampleText} + +
+ {/* Bold */} +
+ + Bold + + + {font.sampleText} + +
+
+
+
+ ))} +
+
+ + {/* ── 섹션 3: 타이포그래피 토큰 ── */} +
+
+

+ 타이포그래피 토큰 +

+
    +
  • Tailwind @apply 기반 컴포넌트 클래스로 정의됩니다 (wing.css).
  • +
  • 크기는 접근성을 위해 px 단위로 명시적으로 지정합니다.
  • +
  • 실제 UI에서는 클래스명을 직접 사용하거나, 동일한 속성 조합으로 적용합니다.
  • +
+
+ + {/* 토큰 테이블 */} +
+ {/* 헤더 */} +
+ {(['Class', 'Size', 'Font', 'Weight', '용도', 'Sample'] as const).map((col) => ( +
+ + {col} + +
+ ))} +
+ + {/* 데이터 행 */} + {TYPOGRAPHY_TOKENS.map((token, rowIdx) => ( +
+ {/* Class */} +
+ + {token.className} + +
+ + {/* Size */} +
+ + {token.size} + +
+ + {/* Font */} +
+ + {token.font} + +
+ + {/* Weight */} +
+ + {token.weight} + +
+ + {/* 용도 */} +
+ + {token.usage} + +
+ + {/* Sample */} +
+ + {token.sampleText} + +
+
+ ))} +
+
+
+ ); +}; + +export default TypographyContent; diff --git a/frontend/src/pages/design/components/ButtonCatalogSection.tsx b/frontend/src/pages/design/components/ButtonCatalogSection.tsx new file mode 100644 index 0000000..5398fa3 --- /dev/null +++ b/frontend/src/pages/design/components/ButtonCatalogSection.tsx @@ -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: ( +
+
+ 실행 +
+
+ ), + hoverBtn: ( +
+
+ 확인 +
+
+ ), + disabledBtn: ( +
+
+ 저장 +
+
+ ), + }, + { + label: '세컨더리 (솔리드)', + defaultBtn: ( +
+
+ 취소 +
+
+ ), + hoverBtn: ( +
+
+ 닫기 +
+
+ ), + disabledBtn: ( +
+
+ 취소 +
+
+ ), + }, + { + label: '아웃라인 (고스트)', + defaultBtn: ( +
+
+ 더보기 +
+
+ ), + hoverBtn: ( +
+
+ 필터 +
+
+ ), + disabledBtn: ( +
+
+ 더보기 +
+
+ ), + }, + { + label: 'PDF 액션', + defaultBtn: ( +
+ PDF 아이콘 +
+ PDF 다운로드 +
+
+ ), + hoverBtn: ( +
+ PDF 아이콘 +
+ PDF 다운로드 +
+
+ ), + disabledBtn: ( +
+ PDF 아이콘 (비활성) +
+ PDF 다운로드 +
+
+ ), + }, + { + label: '경고 (삭제)', + defaultBtn: ( +
+
+ 삭제 +
+
+ ), + hoverBtn: ( +
+
+ 삭제 +
+
+ ), + disabledBtn: ( +
+
+ 초기화 +
+
+ ), + }, +]; + +export const ButtonCatalogSection = () => { + return ( +
+ {/* 카드 헤더 */} +
+
+
+ 제어 인터페이스: 버튼 +
+
+ + {/* 테이블 본문 */} +
+
+ {/* 헤더 행 */} +
+ {['버튼 유형', '기본 상태', '호버 상태', '비활성 상태'].map((header) => ( +
+
+ {header} +
+
+ ))} +
+ + {/* 데이터 행 */} +
+ {buttonRows.map((row, index) => ( +
+ {/* 버튼 유형 레이블 */} +
+
+ {row.label} +
+
+ + {/* 기본 상태 */} +
+ {row.defaultBtn} +
+ + {/* 호버 상태 */} +
+ {row.hoverBtn} +
+ + {/* 비활성 상태 */} +
+ {row.disabledBtn} +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/frontend/src/pages/design/components/CardSection.tsx b/frontend/src/pages/design/components/CardSection.tsx new file mode 100644 index 0000000..5864b6d --- /dev/null +++ b/frontend/src/pages/design/components/CardSection.tsx @@ -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 ( +
+ {/* col 3: 활성 물류 현황 카드 */} +
+ {/* 카드 헤더 */} +
+
+ 활성 물류 현황 +
+
+ + {/* 물류 아이템 목록 */} +
+ {logisticsItems.map((item, index) => ( +
+
+ {item.label} +
+
+
+
+ {item.label} +
+
+
+
+ {item.progress} +
+
+
+
+ ))} +
+ + {/* 대응팀 배치 버튼 */} +
+
+
+ 대응팀 배치 +
+
+
+
+ + {/* col 1-2 span: 실시간 텔레메트리 카드 */} +
+ {/* 배경 파형 (opacity 0.3) */} +
+ wave graph +
+ + {/* 상단 콘텐츠 */} +
+ {/* 제목 영역 */} +
+
+
+ 실시간 텔레메트리 +
+
+ 선박 속도 오버레이 +
+
+ chart bar +
+ + {/* 속도 수치 */} +
+
+ 24.8 +
+
+ 노트 (knots) +
+
+
+ + {/* 하단 뱃지 + 버튼 */} +
+ {/* 정상 가동중 뱃지 */} +
+
+ 정상 가동중 +
+
+ + {/* 대응팀 배치 아웃라인 버튼 */} +
+
+ 대응팀 배치 +
+
+
+
+
+ ); +}; diff --git a/frontend/src/pages/design/components/IconBadgeSection.tsx b/frontend/src/pages/design/components/IconBadgeSection.tsx new file mode 100644 index 0000000..7a2bdbc --- /dev/null +++ b/frontend/src/pages/design/components/IconBadgeSection.tsx @@ -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 ( +
+ {/* 좌측 카드: 제어 인터페이스 — 아이콘 버튼 */} +
+ {/* 카드 헤더 */} +
+
+
+
+ 마이크로 컨트롤: 아이콘 버튼 +
+
+
+ + {/* 아이콘 버튼 목록 */} +
+ {iconButtons.map((btn) => ( +
+
+ {btn.label} +
+
+
+ {btn.label} +
+
+
+ ))} +
+ + {/* 카드 푸터 */} +
+
+ Standard dimensions: 36x36px with radius-md (6px) +
+
+
+ + {/* 우측 카드: 마이크로 컨트롤 — 뱃지 & 태그 */} +
+ {/* 카드 헤더 */} +
+
+
+
+ 마이크로 컨트롤: 아이콘 버튼 +
+
+
+ + {/* 카드 바디 */} +
+ + {/* Operational Status 섹션 */} +
+
+
+ Operational Status +
+
+
+ {statusBadges.map((badge) => ( +
+
+ {badge.label} +
+
+ ))} +
+
+ + {/* Data Classification 섹션 */} +
+
+
+ Data Classification +
+
+
+ {dataTags.map((tag) => ( +
+
+
+
+ {tag.label} +
+
+
+ ))} +
+
+ +
+
+
+ ); +}; diff --git a/frontend/src/pages/design/designTheme.ts b/frontend/src/pages/design/designTheme.ts new file mode 100644 index 0000000..2328c1a --- /dev/null +++ b/frontend/src/pages/design/designTheme.ts @@ -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; diff --git a/frontend/src/tabs/scat/components/ScatMap.tsx b/frontend/src/tabs/scat/components/ScatMap.tsx index 81eeb37..1693a21 100644 --- a/frontend/src/tabs/scat/components/ScatMap.tsx +++ b/frontend/src/tabs/scat/components/ScatMap.tsx @@ -6,7 +6,7 @@ import type { StyleSpecification } from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' import type { ScatSegment } from './scatTypes' import type { ApiZoneItem } from '../services/scatApi' -import { esiColor, jejuCoastCoords } from './scatConstants' +import { esiColor } from './scatConstants' import { hexToRgba } from '@common/components/map/mapUtils' const BASE_STYLE: StyleSpecification = { @@ -87,12 +87,17 @@ function getZoomScale(zoom: number) { } // ── 세그먼트 폴리라인 좌표 생성 (lng, lat 순서) ────────── -function buildSegCoords(seg: ScatSegment, halfLenScale: number): [number, number][] { - const coastIdx = seg.id % (jejuCoastCoords.length - 1) - const [clat1, clng1] = jejuCoastCoords[coastIdx] - const [clat2, clng2] = jejuCoastCoords[(coastIdx + 1) % jejuCoastCoords.length] - const dlat = clat2 - clat1 - const dlng = clng2 - clng1 +// 인접 구간 좌표로 해안선 방향을 동적 계산 +function buildSegCoords( + seg: ScatSegment, + halfLenScale: number, + segments: ScatSegment[], +): [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 nDlat = dist > 0 ? dlat / dist : 0 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 coastlineLayer = useMemo( - () => - new PathLayer({ - id: 'jeju-coastline', - data: [{ path: jejuCoastCoords.map(([lat, lng]) => [lng, lat] as [number, number]) }], - getPath: (d: { path: [number, number][] }) => d.path, - getColor: [6, 182, 212, 46], - getWidth: 1.5, - getDashArray: [8, 6], - dashJustified: true, - widthMinPixels: 1, - }), - [], - ) + // 제주도 해안선 레퍼런스 라인 — 하드코딩 제거, 추후 DB 기반 해안선으로 대체 예정 + // const coastlineLayer = useMemo( + // () => + // new PathLayer({ + // id: 'jeju-coastline', + // data: [{ path: jejuCoastCoords.map(([lat, lng]) => [lng, lat] as [number, number]) }], + // getPath: (d: { path: [number, number][] }) => d.path, + // getColor: [6, 182, 212, 46], + // getWidth: 1.5, + // getDashArray: [8, 6], + // dashJustified: true, + // widthMinPixels: 1, + // }), + // [], + // ) // 선택된 구간 글로우 레이어 const glowLayer = useMemo( @@ -148,7 +153,7 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg new PathLayer({ id: 'scat-glow', data: [selectedSeg], - getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale), + getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale, segments), getColor: [34, 197, 94, 38], getWidth: zs.glowWidth, capRounded: true, @@ -159,7 +164,7 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg getWidth: [zs.glowWidth], }, }), - [selectedSeg, zs.glowWidth, zs.halfLenScale], + [selectedSeg, segments, zs.glowWidth, zs.halfLenScale], ) // ESI 색상 세그먼트 폴리라인 @@ -168,7 +173,7 @@ function ScatMap({ segments, zones, selectedSeg, jurisdictionFilter, onSelectSeg new PathLayer({ id: 'scat-segments', data: segments, - getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale), + getPath: (d: ScatSegment) => buildSegCoords(d, zs.halfLenScale, segments), getColor: (d: ScatSegment) => { const isSelected = selectedSeg.id === d.id 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 const deckLayers: any[] = useMemo(() => { // 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) return layers - }, [coastlineLayer, glowLayer, segPathLayer, markerLayer]) + }, [glowLayer, segPathLayer, markerLayer]) const doneCount = segments.filter(s => s.status === '완료').length const progCount = segments.filter(s => s.status === '진행중').length diff --git a/frontend/src/tabs/scat/components/ScatPopup.tsx b/frontend/src/tabs/scat/components/ScatPopup.tsx index 1677492..5405b07 100644 --- a/frontend/src/tabs/scat/components/ScatPopup.tsx +++ b/frontend/src/tabs/scat/components/ScatPopup.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { Map, useControl } from '@vis.gl/react-maplibre' 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 'maplibre-gl/dist/maplibre-gl.css' import type { ScatDetail } from './scatTypes' @@ -50,45 +50,22 @@ function PopupMap({ esi: string onMapLoad?: () => void }) { - // 해안 구간 라인 (시뮬레이션) — [lng, lat] 순서 - const segLine: [number, number][] = [ - [lng - 0.004, lat - 0.002], - [lng - 0.002, lat - 0.001], - [lng, lat], - [lng + 0.002, lat + 0.001], - [lng + 0.004, lat + 0.002], - ] - - // 조사 경로 라인 - const surveyRoute: [number, number][] = [ - [lng - 0.003, lat - 0.0015], - [lng - 0.001, lat - 0.0005], - [lng + 0.001, lat + 0.0005], - [lng + 0.003, lat + 0.0015], - ] + // 해안 구간 라인 / 조사 경로 — 하드코딩 방향이라 주석처리, 추후 실제 방향 데이터로 대체 + // const segLine: [number, number][] = [ + // [lng - 0.004, lat - 0.002], + // [lng - 0.002, lat - 0.001], + // [lng, lat], + // [lng + 0.002, lat + 0.001], + // [lng + 0.004, lat + 0.002], + // ] + // const surveyRoute: [number, number][] = [ + // [lng - 0.003, lat - 0.0015], + // [lng - 0.001, lat - 0.0005], + // [lng + 0.001, lat + 0.0005], + // [lng + 0.003, lat + 0.0015], + // ] 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({ id: 'access-point', diff --git a/frontend/src/tabs/scat/components/ScatRightPanel.tsx b/frontend/src/tabs/scat/components/ScatRightPanel.tsx index 967108a..a11e0c7 100644 --- a/frontend/src/tabs/scat/components/ScatRightPanel.tsx +++ b/frontend/src/tabs/scat/components/ScatRightPanel.tsx @@ -139,7 +139,7 @@ function DetailTab({ detail }: { detail: ScatDetail }) { /* ═══ 탭 1: 현장 사진 ═══ */ function PhotoTab({ detail }: { detail: ScatDetail }) { const [imgError, setImgError] = useState(false); - const imgSrc = `/scat-img/${detail.code}-1.png`; + const imgSrc = `/scat/img/${detail.code}-1.png`; if (imgError) { return ( diff --git a/frontend/src/tabs/scat/components/scatConstants.ts b/frontend/src/tabs/scat/components/scatConstants.ts index 142a9bc..07d41f1 100644 --- a/frontend/src/tabs/scat/components/scatConstants.ts +++ b/frontend/src/tabs/scat/components/scatConstants.ts @@ -26,9 +26,9 @@ export const statusColor: Record = { }; 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.26, 126.18], @@ -101,4 +101,4 @@ export const jejuCoastCoords: [number, number][] = [ [33.31, 126.19], [33.3, 126.175], [33.293, 126.162], -]; +]; */ diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 016b36c..0f9e9b7 100755 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -28,7 +28,9 @@ "baseUrl": ".", "paths": { "@common/*": ["src/common/*"], - "@tabs/*": ["src/tabs/*"] + "@tabs/*": ["src/tabs/*"], + "@pages/*": ["src/pages/*"], + "@/*": ["src/*"] } }, "include": ["src"] diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index db40627..d29e6f8 100755 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ alias: { '@common': path.resolve(__dirname, 'src/common'), '@tabs': path.resolve(__dirname, 'src/tabs'), + '@': path.resolve(__dirname, 'src'), }, }, build: { From ebe49c7b77d3495257402467baa7fbfcff6a4ac2 Mon Sep 17 00:00:00 2001 From: leedano Date: Tue, 24 Mar 2026 17:23:54 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs(design):=20Foundation=20=ED=83=AD=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=ED=86=A0=ED=81=B0=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Primitive Colors 7그룹 hex 팔레트, Semantic Colors dark/light 병기 - Typography Font Family 3종 + .wing-* 클래스 10종 상세 테이블 - Radius Tokens 7종 + 컴포넌트 매핑 5건 - Layout: Breakpoints, Spacing Scale, Z-Index, App Shell Classes --- docs/DESIGN-SYSTEM.md | 252 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 206 insertions(+), 46 deletions(-) diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md index 13d0ccb..811c957 100644 --- a/docs/DESIGN-SYSTEM.md +++ b/docs/DESIGN-SYSTEM.md @@ -22,58 +22,218 @@ Google Stitch MCP로 생성된 스크린을 기반으로 일관된 UI 구현을 | 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` | 호버 상태 | +## Foundations -### 텍스트 -| 토큰 | Tailwind | 값 | 용도 | -|------|----------|-----|------| -| `--t1` | `text-text-1` | `#edf0f7` | 주요 텍스트 | -| `--t2` | `text-text-2` | `#b0b8cc` | 보조 텍스트 | -| `--t3` | `text-text-3` | `#8690a6` | 비활성, 플레이스홀더 | +### 색상 (Color Palette) -### 보더 -| 토큰 | Tailwind | 값 | -|------|----------|-----| -| `--bd` | `border-border` | `#1e2a42` | -| `--bdL` | `border-border-light` | `#2a3a5c` | +#### Primitive Colors -### 강조색 -| 토큰 | Tailwind | 값 | 용도 | -|------|----------|-----|------| -| `--cyan` | `text-primary-cyan` | `#06b6d4` | Primary accent, 활성 상태 | -| `--blue` | `text-primary-blue` | `#3b82f6` | Secondary accent | -| `--purple` | `text-primary-purple` | `#a855f7` | Tertiary accent | +UI 전반에서 사용하는 기본 색조 팔레트. Navy는 배경 전용 5단계, 나머지는 00~100의 11단계 스케일. -### 상태색 -| 토큰 | 값 | 용도 | -|------|-----|------| -| `--red` | `#ef4444` | 위험, 삭제 | -| `--orange` | `#f97316` | 주의 | -| `--yellow` | `#eab308` | 경고 | -| `--green` | `#22c55e` | 정상, 성공 | +**Navy** (배경 전용) -### 타이포그래피 -| 크기 | 용도 | 폰트 | -|------|------|------| -| 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 | +| Step | Hex | +|------|-----| +| 0 | `#0a0e1a` | +| 1 | `#0f1524` | +| 2 | `#121929` | +| 3 | `#1a2236` | +| hover | `#1e2844` | + +**Cyan** + +| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | +|----|----|----|----|----|----|----|----|----|----|----| +| `#ecfeff` | `#cffafe` | `#a5f3fc` | `#67e8f9` | `#22d3ee` | `#06b6d4` | `#0891b2` | `#0e7490` | `#155e75` | `#164e63` | `#083344` | + +**Blue** + +| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | +|----|----|----|----|----|----|----|----|----|----|----| +| `#eff6ff` | `#dbeafe` | `#bfdbfe` | `#93c5fd` | `#60a5fa` | `#3b82f6` | `#2563eb` | `#1d4ed8` | `#1e40af` | `#1e3a8a` | `#172554` | + +**Red** + +| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | +|----|----|----|----|----|----|----|----|----|----|----| +| `#fef2f2` | `#fee2e2` | `#fecaca` | `#fca5a5` | `#f87171` | `#ef4444` | `#dc2626` | `#b91c1c` | `#991b1b` | `#7f1d1d` | `#450a0a` | + +**Green** + +| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | +|----|----|----|----|----|----|----|----|----|----|----| +| `#f0fdf4` | `#dcfce7` | `#bbf7d0` | `#86efac` | `#4ade80` | `#22c55e` | `#16a34a` | `#15803d` | `#166534` | `#14532d` | `#052e16` | + +**Orange** + +| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | +|----|----|----|----|----|----|----|----|----|----|----| +| `#fff7ed` | `#ffedd5` | `#fed7aa` | `#fdba74` | `#fb923c` | `#f97316` | `#ea580c` | `#c2410c` | `#9a3412` | `#7c2d12` | `#431407` | + +**Yellow** + +| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | +|----|----|----|----|----|----|----|----|----|----|----| +| `#fefce8` | `#fef9c3` | `#fef08a` | `#fde047` | `#facc15` | `#eab308` | `#ca8a04` | `#a16207` | `#854d0e` | `#713f12` | `#422006` | + +#### Semantic Colors + +컨텍스트에 따라 의미를 부여한 토큰. Dark/Light 두 테마 값 병기. + +**Text** + +| 토큰 | Dark | Light | 용도 | +|------|------|-------|------| +| `text-1` | `#edf0f7` | `#0f172a` | 기본 텍스트, 아이콘 기본 | +| `text-2` | `#b0b8cc` | `#475569` | 보조 텍스트 | +| `text-3` | `#8690a6` | `#94a3b8` | 비활성, 플레이스홀더 | + +**Background** + +| 토큰 | Dark | Light | 용도 | +|------|------|-------|------| +| `bg-0` | `#0a0e1a` | `#f8fafc` | 페이지 배경 | +| `bg-1` | `#0f1524` | `#ffffff` | 사이드바, 패널 | +| `bg-2` | `#121929` | `#f1f5f9` | 테이블 헤더 | +| `bg-3` | `#1a2236` | `#e2e8f0` | 카드 배경 | +| `bg-hover` | `#1e2844` | `#cbd5e1` | 호버 상태 | + +**Border** + +| 토큰 | Dark | Light | 용도 | +|------|------|-------|------| +| `border` | `#1e2a42` | `#cbd5e1` | 기본 구분선 | +| `border-light` | `#2a3a5c` | `#e2e8f0` | 연한 구분선 | + +**Accent** + +| 토큰 | Dark | Light | 용도 | +|------|------|-------|------| +| `primary-cyan` | `#06b6d4` | `#06b6d4` | 주요 강조, 활성 상태 | +| `primary-blue` | `#3b82f6` | `#0891b2` | 보조 강조 | +| `primary-purple` | `#a855f7` | `#6366f1` | 3차 강조 | + +**Status** + +| 토큰 | Dark | Light | 용도 | +|------|------|-------|------| +| `status-red` | `#ef4444` | `#dc2626` | 위험, 삭제 | +| `status-orange` | `#f97316` | `#c2410c` | 주의 | +| `status-yellow` | `#eab308` | `#b45309` | 경고 | +| `status-green` | `#22c55e` | `#047857` | 정상, 성공 | + +--- + +### 타이포그래피 (Typography) + +#### Font Family + +| 이름 | className | Font Stack | 용도 | +|------|-----------|------------|------| +| Noto Sans KR | `font-korean` | `'Noto Sans KR', sans-serif` | 기본 UI 텍스트, 한국어 콘텐츠 전반 | +| JetBrains Mono | `font-mono` | `'JetBrains Mono', monospace` | 좌표, 수치, 코드, 토큰 이름 | +| Outfit | `font-sans` | `'Outfit', 'Noto Sans KR', sans-serif` | 영문 헤딩, 브랜드 타이틀 | + +> Body 기본 스택: `font-family: 'Outfit', 'Noto Sans KR', sans-serif` + +#### Typography Tokens (`.wing-*` 클래스) + +| 클래스 | Size | Font | Weight | 용도 | 샘플 | +|--------|------|------|--------|------|------| +| `.wing-title` | 15px | font-korean | Bold (700) | 패널 제목 | 확산 예측 시뮬레이션 | +| `.wing-section-header` | 13px | font-korean | Bold (700) | 섹션 헤더 | 기본 정보 입력 | +| `.wing-label` | 11px | font-korean | Semibold (600) | 필드 레이블 | 유출량 (kL) | +| `.wing-btn` | 11px | font-korean | Semibold (600) | 버튼 텍스트 | 시뮬레이션 실행 | +| `.wing-value` | 11px | font-mono | Semibold (600) | 수치 / 데이터 값 | 35.1284° N, 129.0598° E | +| `.wing-input` | 11px | font-korean | Normal (400) | 입력 필드 | 서해 대산항 인근 해역 | +| `.wing-section-desc` | 10px | font-korean | Normal (400) | 섹션 설명 | 예측 결과는 기상 조건에 따라... | +| `.wing-subtitle` | 10px | font-korean | Normal (400) | 보조 설명 | 최근 업데이트: 2026-03-24 09:00 KST | +| `.wing-meta` | 9px | font-korean | Normal (400) | 메타 정보 | v2.1 \| 해양환경공단 | +| `.wing-badge` | 9px | font-korean | Bold (700) | 뱃지 / 태그 | 진행중 | + +--- ### Border Radius -| 크기 | 값 | 용도 | -|------|-----|------| -| sm | 6px | 버튼, 입력, 소형 카드 | -| md | 10px | 카드, 모달, 패널 | +#### Radius Tokens + +| Tailwind 클래스 | 값 | 비고 | +|-----------------|-----|------| +| `rounded-sm` | 6px | **Custom** (Tailwind 기본값 오버라이드) | +| `rounded` | 4px (0.25rem) | Tailwind 기본 | +| `rounded-md` | 10px | **Custom** (Tailwind 기본값 오버라이드) | +| `rounded-lg` | 8px (0.5rem) | Tailwind 기본 | +| `rounded-xl` | 12px (0.75rem) | Tailwind 기본 | +| `rounded-2xl` | 16px (1rem) | Tailwind 기본 | +| `rounded-full` | 9999px | Tailwind 기본 | + +#### 컴포넌트 매핑 + +| Radius | 값 | 적용 컴포넌트 | +|--------|-----|-------------| +| `rounded-sm` | 6px | `.wing-btn`, `.wing-input`, `.wing-card-sm` | +| `rounded` | 4px | `.wing-badge` | +| `rounded-md` | 10px | `.wing-card`, `.wing-section`, `.wing-tab` | +| `rounded-lg` | 8px | `.wing-tab-bar` | +| `rounded-xl` | 12px | `.wing-modal` | + +--- + +### 레이아웃 (Layout) + +#### Breakpoints + +| Name | Prefix | Min Width | 사용 | 비고 | +|------|--------|-----------|------|------| +| sm | `sm:` | 640px | - | | +| md | `md:` | 768px | - | | +| lg | `lg:` | 1024px | - | | +| xl | `xl:` | 1280px | **사용 중** | TopBar 탭 레이블/아이콘 토글 | +| 2xl | `2xl:` | 1536px | - | | + +> Desktop(≥ 1280px)만 지원. Tablet/Mobile 미지원. + +| Device | Width | Columns | Gutter | Margin | +|--------|-------|---------|--------|--------| +| Desktop | ≥ 1280px | flex 기반 가변 | gap-2 ~ gap-6 | px-5 ~ px-8 | +| Tablet | 768px ~ 1279px | - | - | - | +| Mobile | < 768px | - | - | - | + +#### Spacing Scale + +| Scale | rem | px | 용도 | +|-------|-----|----|------| +| 0.5 | 0.125rem | 2px | 미세 간격 | +| 1 | 0.25rem | 4px | 최소 간격 (gap-1) | +| 1.5 | 0.375rem | 6px | 컴팩트 간격 (gap-1.5) | +| 2 | 0.5rem | 8px | 기본 간격 (gap-2, p-2) | +| 2.5 | 0.625rem | 10px | 중간 간격 | +| 3 | 0.75rem | 12px | 표준 간격 (gap-3, p-3) | +| 4 | 1rem | 16px | 넓은 간격 (p-4, gap-4) | +| 5 | 1.25rem | 20px | 패널 패딩 (px-5, py-5) | +| 6 | 1.5rem | 24px | 섹션 간격 (gap-6, p-6) | +| 8 | 2rem | 32px | 큰 간격 (px-8, gap-8) | +| 16 | 4rem | 64px | 최대 간격 | + +#### Z-Index Layers + +| Layer | z-index | Color | 설명 | +|-------|---------|-------|------| +| Tooltip | 60 | `#a855f7` | 툴팁, 드롭다운 메뉴 | +| Popup | 50 | `#f97316` | 팝업, 지도 오버레이 | +| Modal | 40 | `#ef4444` | 모달 다이얼로그, 백드롭 | +| TopBar | 30 | `#3b82f6` | 상단 네비게이션 바 | +| Sidebar | 20 | `#06b6d4` | 사이드바, 패널 | +| Content | 10 | `#22c55e` | 메인 콘텐츠 영역 | +| Base | 0 | `#8690a6` | 기본 레이어, 배경 | + +#### App Shell Classes + +| 클래스 | 역할 | Tailwind 스타일 | +|--------|------|----------------| +| `.wing-panel` | 탭 콘텐츠 패널 | `flex flex-col h-full overflow-hidden` | +| `.wing-panel-scroll` | 패널 내 스크롤 영역 | `flex-1 overflow-y-auto` | +| `.wing-header-bar` | 패널 헤더 | `flex items-center justify-between shrink-0 px-5 border-b` | +| `.wing-sidebar` | 사이드바 | `flex flex-col border-r border-border` | From 7bbc1479fcb34123d677cafd713244507e007970 Mon Sep 17 00:00:00 2001 From: leedano Date: Tue, 24 Mar 2026 17:43:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 8ca1ce3..1e99d33 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,18 @@ ## [Unreleased] +### 추가 +- Stitch MCP 기반 디자인 시스템 카탈로그 페이지 (/design) +- react-router-dom 도입, BrowserRouter 래핑 +- SVG 아이콘 에셋 19종 추가 +- @/ path alias 추가 + +### 변경 +- SCAT 지도 하드코딩 제주 해안선 제거, 인접 구간 기반 동적 방향 계산으로 전환 + +### 문서 +- Foundation 탭 디자인 토큰 상세 문서화 (DESIGN-SYSTEM.md) + ## [2026-03-20.2] ### 변경