463 lines
14 KiB
Markdown
463 lines
14 KiB
Markdown
# WING-OPS 디자인 시스템 (백업)
|
||
|
||
Tailwind CSS + @apply 기반 디자인 시스템. `wing-*` CSS 클래스와 React UI 컴포넌트로 구성.
|
||
|
||
---
|
||
|
||
## 브랜드 (Brand)
|
||
|
||
### 로고
|
||
|
||
해양 환경 위기대응을 위한 통합 솔루션 **WING**의 로고.
|
||
|
||
- **파일**: `frontend/public/wing_logo_white.svg`
|
||
- **네이티브 크기**: 280 × 20px (비율 14:1)
|
||
- **색상**: 단색 흰색 (다크 배경 전용)
|
||
|
||
#### 로고 규격
|
||
|
||
| 용도 | 높이 | Tailwind | 비고 |
|
||
|------|------|----------|------|
|
||
| Header | 14px | `h-3.5` | TopBar 52px 높이 내 사용 (현재) |
|
||
| Standard | 24px | `h-6` | 일반 UI, 문서 내 |
|
||
| Large | 32px | `h-8` | 로그인, 랜딩 화면 |
|
||
| **Minimum** | **14px** | `h-3.5` | 이보다 작게 사용 금지 |
|
||
|
||
#### 여백 규칙 (Clear Space)
|
||
- 최소 여백: 로고 높이의 **50%** (상하좌우)
|
||
- 로고 주변에 다른 텍스트나 아이콘이 침범하지 않도록 유지
|
||
|
||
### 테마 컬러
|
||
|
||
다크 테마 기반. 게시판(Board) 메뉴에서 추출한 컬러 체계.
|
||
|
||
#### 배경 (Background) — 딥 네이비
|
||
|
||
| 토큰 | CSS 변수 | HEX | 용도 |
|
||
|------|----------|-----|------|
|
||
| `bg-bg-0` | `--bg0` | `#0a0e1a` | 최하위 배경 (body, input) |
|
||
| `bg-bg-1` | `--bg1` | `#0f1524` | 패널, 모달, 푸터 배경 |
|
||
| `bg-bg-2` | `--bg2` | `#121929` | 테이블 헤더, elevated 영역 |
|
||
| `bg-bg-3` | `--bg3` | `#1a2236` | 카드, 보조 버튼, 비활성 요소 |
|
||
| `bg-hover` | `--bgH` | `#1e2844` | 행 hover |
|
||
|
||
#### 텍스트 (Text)
|
||
|
||
| 토큰 | CSS 변수 | HEX | 용도 |
|
||
|------|----------|-----|------|
|
||
| `text-text-1` | `--t1` | `#edf0f7` | 주 텍스트 (제목, 본문) |
|
||
| `text-text-2` | `--t2` | `#b0b8cc` | 보조 텍스트 (라벨, 설명) |
|
||
| `text-text-3` | `--t3` | `#8690a6` | 비활성/메타 (날짜, 조회수) |
|
||
|
||
#### 브랜드 강조색 (Accent)
|
||
|
||
| 토큰 | HEX | 용도 |
|
||
|------|-----|------|
|
||
| `primary-cyan` | `#06b6d4` | 주 강조색 — 활성 상태, 링크, CTA |
|
||
| `primary-blue` | `#3b82f6` | 보조 강조색 — 그라데이션 끝점 |
|
||
| **Primary Gradient** | `linear-gradient(135deg, #06b6d4, #3b82f6)` | Primary 버튼 |
|
||
|
||
#### 시맨틱 (Semantic)
|
||
|
||
| 토큰 | HEX | 용도 |
|
||
|------|-----|------|
|
||
| `red` / `danger` | `#ef4444` | 삭제, 필수 표시(*) |
|
||
| `orange` | `#f97316` | 경고 |
|
||
| `yellow` | `#eab308` | 주의 |
|
||
| `green` | `#22c55e` | 성공, 정상 |
|
||
| `purple` | `#a855f7` | 특수 강조 |
|
||
|
||
#### 테두리 (Border)
|
||
|
||
| 토큰 | CSS 변수 | HEX | 용도 |
|
||
|------|----------|-----|------|
|
||
| `border` | `--bd` | `#1e2a42` | 기본 구분선 |
|
||
| `border-light` | `--bdL` | `#2a3a5c` | 밝은 테두리 |
|
||
|
||
#### 오버레이 (Overlay)
|
||
|
||
| 토큰 | 값 | 용도 |
|
||
|------|-----|------|
|
||
| `overlay` | `rgba(0, 0, 0, 0.55)` | 모달 배경 오버레이 |
|
||
|
||
---
|
||
|
||
## 디자인 원칙
|
||
|
||
### 컬러 사용 규칙
|
||
- **상태 표현** (위험/정상/주의) → 컬러 배지 사용 (red, green, yellow)
|
||
- **단순 분류** (공지사항, 자료실, Q&A) → 기본 텍스트 컬러 또는 neutral 배지
|
||
- **강조** (고정글, 선택 항목) → accent 컬러 (cyan)
|
||
- **원칙**: 색상은 정보를 전달할 때만 사용. 장식 목적 금지.
|
||
|
||
---
|
||
|
||
## 1. 토큰
|
||
|
||
### 컬러 팔레트
|
||
|
||
| CSS 변수 | 값 | 용도 |
|
||
|----------|-----|------|
|
||
| `--bg0` | `#0a0e1a` | 최하위 배경 (body, input) |
|
||
| `--bg1` | `#0f1524` | 기본 패널 배경 |
|
||
| `--bg2` | `#121929` | 테이블 헤더, elevated |
|
||
| `--bg3` | `#1a2236` | 카드, 섹션 배경 |
|
||
| `--bgH` | `#1e2844` | hover 상태 |
|
||
| `--bd` | `#1e2a42` | 기본 테두리 |
|
||
| `--bdL` | `#2a3a5c` | 밝은 테두리 |
|
||
| `--t1` | `#edf0f7` | 기본 텍스트 (밝음) |
|
||
| `--t2` | `#b0b8cc` | 보조 텍스트 |
|
||
| `--t3` | `#8690a6` | 비활성/메타 텍스트 |
|
||
| `--cyan` | `#06b6d4` | Primary accent |
|
||
| `--blue` | `#3b82f6` | Secondary accent |
|
||
| `--purple` | `#a855f7` | 특수 강조 |
|
||
| `--red` | `#ef4444` | 위험/삭제 |
|
||
| `--orange` | `#f97316` | 경고 |
|
||
| `--yellow` | `#eab308` | 주의 |
|
||
| `--green` | `#22c55e` | 성공/정상 |
|
||
|
||
### Z-Index 스케일
|
||
|
||
| 변수 | 값 | 용도 |
|
||
|------|-----|------|
|
||
| `--z-dropdown` | 100 | 드롭다운, 콤보박스 |
|
||
| `--z-sticky` | 200 | sticky 헤더 |
|
||
| `--z-overlay` | 1000 | 오버레이 |
|
||
| `--z-modal` | 10000 | 모달 |
|
||
| `--z-toast` | 10100 | 토스트 알림 |
|
||
|
||
### 패널 너비
|
||
|
||
| 변수 | 값 | 용도 |
|
||
|------|-----|------|
|
||
| `--panel-narrow` | 280px | 좁은 사이드패널 |
|
||
| `--panel-default` | 300px | 기본 사이드패널 |
|
||
| `--panel-wide` | 340px | 넓은 사이드패널 |
|
||
|
||
### 타이포그래피 (Tailwind)
|
||
|
||
| 클래스 | 크기 | 용도 |
|
||
|--------|------|------|
|
||
| `text-wing-meta` | 9px | 메타 텍스트, 날짜, 부가 정보 |
|
||
| `text-wing-caption` | 10px | 캡션, 설명, 라벨 부연 |
|
||
| `text-wing-body` | 11px | 본문, 라벨, 값 (가장 많이 사용) |
|
||
| `text-wing-heading` | 13px | 섹션 헤더 |
|
||
| `text-wing-title` | 15px | 페이지/모달 타이틀 |
|
||
|
||
---
|
||
|
||
## 2. CSS 클래스 (`wing-*`)
|
||
|
||
모든 클래스는 `frontend/src/common/styles/wing.css`에 정의.
|
||
|
||
### Layout
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-panel` | flex column, full height, overflow hidden |
|
||
| `wing-panel-scroll` | flex-1, overflow-y-auto, thin scrollbar |
|
||
| `wing-panel-right` | 우측 사이드패널 (border-l, 300px) |
|
||
| `wing-panel-left` | 좌측 사이드패널 (border-r, 300px) |
|
||
| `wing-header-bar` | 헤더 바 (flex between, border-b, px-5) |
|
||
| `wing-sidebar` | 사이드바 (flex col, border-r, bg1) |
|
||
|
||
### Card / Section
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-card` | 카드 (rounded-md, p-4, border, bg3) |
|
||
| `wing-card-sm` | 작은 카드 (rounded-sm, p-3) |
|
||
| `wing-section` | 섹션 (rounded-md, p-4, mb-3) |
|
||
| `wing-section-header` | 섹션 제목 (13px bold) |
|
||
| `wing-section-desc` | 섹션 설명 (10px, t3 색상) |
|
||
|
||
### Typography
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-title` | 15px bold korean |
|
||
| `wing-subtitle` | 10px korean, t3 색상 |
|
||
| `wing-label` | 11px semibold korean |
|
||
| `wing-value` | 11px semibold mono |
|
||
| `wing-meta` | 9px korean, t3 색상 |
|
||
|
||
### Button
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-btn` | 기본 버튼 (px-3, py-1.5, 11px) |
|
||
| `wing-btn-primary` | cyan→blue gradient, white |
|
||
| `wing-btn-secondary` | bg3, border, t2 색상 |
|
||
| `wing-btn-outline` | transparent, border |
|
||
| `wing-btn-pdf` | blue 테마 |
|
||
| `wing-btn-danger` | red 테마 |
|
||
|
||
### Input / Select / Textarea
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-input` | 기본 입력 (w-full, 11px, cyan focus) |
|
||
| `wing-select` | 셀렉트 (커스텀 화살표 포함) |
|
||
| `wing-textarea` | 텍스트영역 (resize-vertical, min-h 80px) |
|
||
| `wing-input-search` | 검색 입력 (256px 고정) |
|
||
|
||
### Table
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-table` | 테이블 (w-full, 10px, collapse) |
|
||
| `wing-table-head` | 헤더 셀 (bg2, t3, bold) |
|
||
| `wing-table-cell` | 데이터 셀 (t2, border-b) |
|
||
| `wing-table-row` | 행 hover (bgH, cursor-pointer) |
|
||
|
||
### Badge
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-badge` | 기본 배지 (inline-flex, 9px bold) |
|
||
| `wing-badge-neutral` | 회색 (단순 분류용 기본값) |
|
||
| `wing-badge-red` | 위험/삭제 |
|
||
| `wing-badge-blue` | 정보 |
|
||
| `wing-badge-green` | 성공/정상 |
|
||
| `wing-badge-yellow` | 주의 |
|
||
| `wing-badge-purple` | 특수 |
|
||
| `wing-badge-cyan` | 주요 |
|
||
|
||
### Modal
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-overlay` | 오버레이 (fixed, blur, z-modal) |
|
||
| `wing-modal` | 모달 컨테이너 (rounded-xl, bg1) |
|
||
| `wing-modal-header` | 모달 헤더 (flex between, border-b) |
|
||
| `wing-modal-body` | 모달 본문 (flex-1, scroll) |
|
||
| `wing-modal-footer` | 모달 푸터 (flex end, border-t) |
|
||
| `wing-modal-sm` | 400px |
|
||
| `wing-modal-md` | 560px |
|
||
| `wing-modal-lg` | 720px |
|
||
|
||
### Tab
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-tab-bar` | 탭 바 (flex, rounded-lg, border) |
|
||
| `wing-tab` | 탭 아이템 (flex-1, text-center) |
|
||
| `wing-tab.active` | 활성 탭 (cyan border/bg/text) |
|
||
|
||
### Utility
|
||
|
||
| 클래스 | 설명 |
|
||
|--------|------|
|
||
| `wing-divider` | 구분선 (1px, full width) |
|
||
| `wing-info-row` | 키-값 행 (flex between) |
|
||
| `wing-info-label` | 키 라벨 (10px, t3) |
|
||
| `wing-info-value` | 값 (11px, semibold mono) |
|
||
|
||
---
|
||
|
||
## 3. React 컴포넌트
|
||
|
||
위치: `frontend/src/common/components/ui/`
|
||
|
||
### Modal
|
||
|
||
```tsx
|
||
import Modal from '@common/components/ui/Modal';
|
||
|
||
<Modal
|
||
isOpen={isOpen}
|
||
onClose={() => setIsOpen(false)}
|
||
title="제목"
|
||
size="md"
|
||
footer={
|
||
<>
|
||
<button className="wing-btn wing-btn-secondary" onClick={handleCancel}>취소</button>
|
||
<button className="wing-btn wing-btn-primary" onClick={handleConfirm}>확인</button>
|
||
</>
|
||
}
|
||
>
|
||
<p>모달 내용</p>
|
||
</Modal>
|
||
```
|
||
|
||
| Prop | 타입 | 기본값 | 설명 |
|
||
|------|------|--------|------|
|
||
| `isOpen` | `boolean` | 필수 | 표시 여부 |
|
||
| `onClose` | `() => void` | 필수 | 닫기 콜백 |
|
||
| `title` | `string` | 필수 | 헤더 제목 |
|
||
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 너비 (400/560/720px) |
|
||
| `children` | `ReactNode` | 필수 | 본문 |
|
||
| `footer` | `ReactNode` | - | 하단 버튼 영역 |
|
||
| `closeOnBackdrop` | `boolean` | `true` | 배경 클릭 닫기 |
|
||
|
||
### Pagination
|
||
|
||
```tsx
|
||
import Pagination from '@common/components/ui/Pagination';
|
||
|
||
<Pagination
|
||
currentPage={page}
|
||
totalPages={totalPages}
|
||
onPageChange={setPage}
|
||
/>
|
||
```
|
||
|
||
| Prop | 타입 | 기본값 | 설명 |
|
||
|------|------|--------|------|
|
||
| `currentPage` | `number` | 필수 | 현재 페이지 (1-based) |
|
||
| `totalPages` | `number` | 필수 | 전체 페이지 수 |
|
||
| `onPageChange` | `(page: number) => void` | 필수 | 페이지 변경 콜백 |
|
||
| `showFirstLast` | `boolean` | `true` | 처음/끝 버튼 표시 |
|
||
|
||
### DataTable
|
||
|
||
```tsx
|
||
import DataTable, { Column } from '@common/components/ui/DataTable';
|
||
|
||
interface Post {
|
||
id: number;
|
||
title: string;
|
||
author: string;
|
||
createdAt: string;
|
||
}
|
||
|
||
const columns: Column<Post>[] = [
|
||
{ key: 'id', label: '번호', width: '60px', align: 'center' },
|
||
{ key: 'title', label: '제목' },
|
||
{ key: 'author', label: '작성자', width: '100px' },
|
||
{ key: 'createdAt', label: '작성일', width: '120px',
|
||
render: (val) => new Date(val as string).toLocaleDateString() },
|
||
];
|
||
|
||
<DataTable
|
||
columns={columns}
|
||
data={posts}
|
||
onRowClick={(post) => navigate(`/board/${post.id}`)}
|
||
/>
|
||
```
|
||
|
||
| Prop | 타입 | 기본값 | 설명 |
|
||
|------|------|--------|------|
|
||
| `columns` | `Column<T>[]` | 필수 | 컬럼 정의 |
|
||
| `data` | `T[]` | 필수 | 데이터 배열 |
|
||
| `onRowClick` | `(row: T) => void` | - | 행 클릭 콜백 |
|
||
| `stickyHeader` | `boolean` | `true` | 헤더 고정 |
|
||
| `emptyMessage` | `string` | `'데이터가 없습니다.'` | 빈 상태 메시지 |
|
||
|
||
### SidePanel
|
||
|
||
```tsx
|
||
import SidePanel from '@common/components/ui/SidePanel';
|
||
|
||
<SidePanel
|
||
position="right"
|
||
width="default"
|
||
header={<span className="wing-title">상세 정보</span>}
|
||
footer={
|
||
<button className="wing-btn wing-btn-primary w-full">저장</button>
|
||
}
|
||
>
|
||
<div className="p-3">
|
||
<div className="wing-info-row">
|
||
<span className="wing-info-label">상태</span>
|
||
<Badge color="green">정상</Badge>
|
||
</div>
|
||
</div>
|
||
</SidePanel>
|
||
```
|
||
|
||
| Prop | 타입 | 기본값 | 설명 |
|
||
|------|------|--------|------|
|
||
| `position` | `'left' \| 'right'` | 필수 | 배치 방향 |
|
||
| `width` | `'narrow' \| 'default' \| 'wide'` | `'default'` | 너비 (280/300/340px) |
|
||
| `header` | `ReactNode` | - | 헤더 영역 |
|
||
| `footer` | `ReactNode` | - | 하단 영역 |
|
||
|
||
### Badge
|
||
|
||
```tsx
|
||
import Badge from '@common/components/ui/Badge';
|
||
|
||
<Badge color="green">정상</Badge> {/* 상태 표현 */}
|
||
<Badge color="red">위험</Badge> {/* 상태 표현 */}
|
||
<Badge>공지사항</Badge> {/* 단순 분류 → neutral */}
|
||
<Badge>자료실</Badge> {/* 단순 분류 → neutral */}
|
||
```
|
||
|
||
| Prop | 타입 | 기본값 | 설명 |
|
||
|------|------|--------|------|
|
||
| `color` | `'red' \| 'blue' \| 'green' \| 'yellow' \| 'purple' \| 'cyan' \| 'neutral'` | `'neutral'` | 배지 색상 |
|
||
|
||
---
|
||
|
||
## 4. 적용 예시: Before → After
|
||
|
||
### Before (raw Tailwind 복붙)
|
||
```tsx
|
||
{/* 검색 */}
|
||
<input
|
||
type="text"
|
||
placeholder="검색..."
|
||
className="w-64 px-4 py-2 text-sm bg-bg-2 border border-border rounded text-text-1
|
||
placeholder-text-3 focus:border-primary-cyan focus:outline-none"
|
||
/>
|
||
|
||
{/* 테이블 */}
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-border">
|
||
<th className="px-4 py-3 text-left text-xs font-bold text-text-3">제목</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr className="border-b border-border hover:bg-bg-2 cursor-pointer">
|
||
<td className="px-4 py-4 text-sm text-text-2">{item.title}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
{/* 배지 — 의미 없는 컬러 분화 */}
|
||
<span className="px-2.5 py-0.5 rounded text-xs font-semibold bg-red-500/20 text-red-400">
|
||
공지사항
|
||
</span>
|
||
|
||
{/* 페이지네이션 — 직접 구현 */}
|
||
<button className="px-3 py-1.5 text-sm rounded bg-bg-2 text-text-3 ...">이전</button>
|
||
<button className="px-3 py-1.5 text-sm rounded bg-bg-2 text-text-3 ...">다음</button>
|
||
```
|
||
|
||
### After (디자인 시스템)
|
||
```tsx
|
||
{/* 검색 */}
|
||
<input type="text" placeholder="검색..." className="wing-input-search" />
|
||
|
||
{/* 테이블 — React 컴포넌트 */}
|
||
<DataTable columns={columns} data={posts} onRowClick={handleClick} />
|
||
|
||
{/* 배지 — neutral로 통일 */}
|
||
<Badge>공지사항</Badge>
|
||
<Badge>자료실</Badge>
|
||
<Badge color="green">진행중</Badge> {/* 상태만 컬러 */}
|
||
|
||
{/* 페이지네이션 — React 컴포넌트 */}
|
||
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 마이그레이션 가이드
|
||
|
||
### 작업 순서
|
||
1. 파일 내 raw Tailwind 문자열을 `wing-*` 클래스로 교체
|
||
2. 반복되는 행동 패턴 (모달, 페이지네이션, 테이블)을 React 컴포넌트로 교체
|
||
3. 의미 없는 컬러 배지를 `<Badge>` (neutral)로 교체
|
||
4. 브라우저에서 시각적 동일성 확인
|
||
|
||
### 판단 기준
|
||
```
|
||
순수 시각 + 5회 이상 반복 → wing-* CSS 클래스
|
||
동작 포함 + 5회 이상 반복 → React 컴포넌트
|
||
1-2회 사용 → 인라인 Tailwind 유지
|
||
도메인 전용 시각 → components.css (유지)
|
||
```
|