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
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 |
배경 클릭 닫기 |
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
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
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
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 복붙)
{/* 검색 */}
<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 (디자인 시스템)
{/* 검색 */}
<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. 마이그레이션 가이드
작업 순서
- 파일 내 raw Tailwind 문자열을
wing-* 클래스로 교체
- 반복되는 행동 패턴 (모달, 페이지네이션, 테이블)을 React 컴포넌트로 교체
- 의미 없는 컬러 배지를
<Badge> (neutral)로 교체
- 브라우저에서 시각적 동일성 확인
판단 기준
순수 시각 + 5회 이상 반복 → wing-* CSS 클래스
동작 포함 + 5회 이상 반복 → React 컴포넌트
1-2회 사용 → 인라인 Tailwind 유지
도메인 전용 시각 → components.css (유지)