wing-ops/docs/DESIGN-SYSTEM.backup.md

463 lines
14 KiB
Markdown
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (유지)
```