Merge pull request 'feat(design): 디자인 시스템 폰트 업스케일 및 전체 탭 토큰 적용' (#169) from feature/design-system-etc into develop
This commit is contained in:
커밋
69b01fca9e
22
CLAUDE.md
22
CLAUDE.md
@ -127,6 +127,28 @@ wing/
|
||||
|
||||
## 진행 중 작업 (완료 후 삭제)
|
||||
|
||||
### 폰트 크기 업스케일 작업 (진행 중)
|
||||
|
||||
반드시 `memory/font-upscale-plan.md`를 읽고 Phase 진행 상황을 확인할 것.
|
||||
|
||||
**토큰 변경 매핑 (이름 유지, 값만 변경):**
|
||||
- `caption`/`label-2`/`title-6`: 11px → **12px** (0.75rem)
|
||||
- `label-1`/`title-5`: 12px → **13px** (0.8125rem)
|
||||
- `body-2`/`title-4`: 13px → **14px** (0.875rem)
|
||||
- `body-1`/`title-3`: 14px → **16px** (1rem)
|
||||
|
||||
**네비게이션 클래스 교체:**
|
||||
- TopBar 메인탭: `text-title-4` → `text-title-2` (16px)
|
||||
- SubMenuBar 서브탭: `text-title-5` → `text-title-4` (14px)
|
||||
|
||||
**작업 범위:**
|
||||
- Phase 1: tailwind.config.js + base.css 토큰 값 수정
|
||||
- Phase 2: components.css 하드코딩(27곳) + wing.css `.wing-tab` text-xs→text-caption
|
||||
- Phase 3: TopBar.tsx + SubMenuBar.tsx 클래스 교체
|
||||
- Phase 4: text-xs→text-caption, text-sm→text-body-2 스크립트 교체 (design 페이지 제외, 608건)
|
||||
- Phase 5: prediction 탭 인라인 fontSize 수정
|
||||
- Phase 6 (보류): wing-header-bar 패딩 — 폰트 변경 후 유저 확인 후 진행
|
||||
|
||||
### 디자인 시스템 폰트+색상 통일 작업
|
||||
|
||||
compact 후 반드시 `memory/design-system-work.md`를 읽고 작업 상태(완료/미완료 컴포넌트)를 확인할 것.
|
||||
|
||||
@ -5,9 +5,13 @@
|
||||
## [Unreleased]
|
||||
|
||||
### 추가
|
||||
- 디자인 시스템: HNS·사건사고·확산예측·SCAT·기상 탭 디자인 시스템 토큰 전면 적용
|
||||
- 관리자: 비식별화조치 메뉴 및 패널 추가
|
||||
- 긴급구난/예측도 OSM 지도 적용 및 관리자 패널 추가
|
||||
|
||||
### 변경
|
||||
- 디자인 시스템: 폰트 업스케일 토큰 값 변경 및 전체 탭 색상·폰트 통일
|
||||
|
||||
## [2026-04-13]
|
||||
|
||||
### 추가
|
||||
|
||||
@ -120,7 +120,7 @@ export function LoginPage() {
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span
|
||||
className="absolute text-sm text-fg-disabled pointer-events-none"
|
||||
className="absolute text-body-2 text-fg-disabled pointer-events-none"
|
||||
style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}
|
||||
>
|
||||
<svg
|
||||
@ -174,7 +174,7 @@ export function LoginPage() {
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span
|
||||
className="absolute text-sm text-fg-disabled pointer-events-none"
|
||||
className="absolute text-body-2 text-fg-disabled pointer-events-none"
|
||||
style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}
|
||||
>
|
||||
<svg
|
||||
@ -249,7 +249,7 @@ export function LoginPage() {
|
||||
color: '#67e8f9',
|
||||
}}
|
||||
>
|
||||
<span className="text-sm shrink-0 mt-px">
|
||||
<span className="text-body-2 shrink-0 mt-px">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@ -303,7 +303,7 @@ export function LoginPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full text-color-accent text-sm font-bold rounded-md border"
|
||||
className="w-full text-color-accent text-body-2 font-bold rounded-md border"
|
||||
style={{
|
||||
padding: '12px',
|
||||
background: isLoading
|
||||
|
||||
@ -21,7 +21,7 @@ export function SubMenuBar({ activeMainTab }: SubMenuBarProps) {
|
||||
key={item.id}
|
||||
onClick={() => setActiveSubTab(item.id)}
|
||||
className={`
|
||||
px-4 py-2.5 text-title-5 font-medium transition-all duration-200
|
||||
px-4 py-2.5 text-title-4 font-medium transition-all duration-200
|
||||
font-korean tracking-navigation
|
||||
${activeSubTab === item.id ? 'text-color-accent' : 'text-fg-sub hover:text-fg'}
|
||||
`}
|
||||
|
||||
@ -56,11 +56,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
className="flex items-center hover:opacity-80 transition-opacity cursor-pointer"
|
||||
title="홈으로 이동"
|
||||
>
|
||||
<img
|
||||
src="/wing_logo_white.svg"
|
||||
alt="WING 해양환경 위기대응"
|
||||
className="h-3.5 wing-logo"
|
||||
/>
|
||||
<img src="/wing_logo_white.svg" alt="WING 해양환경 위기대응" className="h-5 wing-logo" />
|
||||
</button>
|
||||
|
||||
{/* Divider */}
|
||||
@ -87,7 +83,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
onClick={handleClick}
|
||||
title={tab.label}
|
||||
className={`
|
||||
px-2.5 xl:px-4 py-2 text-title-4 font-bold transition-all duration-200
|
||||
px-2.5 xl:px-4 py-2 text-title-2 font-bold transition-all duration-200
|
||||
font-korean tracking-navigation border-b-2 border-transparent
|
||||
${isIncident ? 'ml-1' : ''}
|
||||
${isMonitor ? 'ml-1 flex items-center gap-1.5' : ''}
|
||||
@ -127,7 +123,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
{/* Right Section */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Status Badge */}
|
||||
{/* <div className="flex items-center gap-2 px-3 py-1.5 bg-[rgba(239,68,68,0.1)] border border-[rgba(239,68,68,0.2)] rounded-sm text-xs font-medium text-color-danger animate-pulse">
|
||||
{/* <div className="flex items-center gap-2 px-3 py-1.5 bg-[rgba(239,68,68,0.1)] border border-[rgba(239,68,68,0.2)] rounded-sm text-caption font-medium text-color-danger animate-pulse">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-color-danger animate-pulse" />
|
||||
사고 진행중
|
||||
</div> */}
|
||||
|
||||
@ -141,7 +141,7 @@ export function BacktrackReplayBar({
|
||||
className="w-2 h-2 rounded-full bg-color-tertiary"
|
||||
style={{ boxShadow: '0 0 8px rgba(168,85,247,0.5)' }}
|
||||
/>
|
||||
<span className="text-xs font-bold">역추적 리플레이</span>
|
||||
<span className="text-caption font-bold">역추적 리플레이</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
@ -180,7 +180,7 @@ export function BacktrackReplayBar({
|
||||
{/* Play/Pause */}
|
||||
<button
|
||||
onClick={handlePlayClick}
|
||||
className="shrink-0 w-9 h-9 rounded-full flex items-center justify-center text-sm cursor-pointer"
|
||||
className="shrink-0 w-9 h-9 rounded-full flex items-center justify-center text-body-2 cursor-pointer"
|
||||
style={{
|
||||
background: isPlaying ? 'var(--color-tertiary)' : 'rgba(168,85,247,0.15)',
|
||||
border: `2px solid ${isPlaying ? 'var(--color-tertiary)' : 'rgba(168,85,247,0.4)'}`,
|
||||
|
||||
@ -414,7 +414,10 @@ export function MapView({
|
||||
longitude: lng,
|
||||
latitude: lat,
|
||||
content: (
|
||||
<div className="text-xs font-korean" style={{ minWidth: '180px', maxWidth: '260px' }}>
|
||||
<div
|
||||
className="text-caption font-korean"
|
||||
style={{ minWidth: '180px', maxWidth: '260px' }}
|
||||
>
|
||||
<div className="font-semibold mb-1.5 pb-1 border-b border-[rgba(0,0,0,0.12)]">
|
||||
{String(category ?? '민감자원')}
|
||||
</div>
|
||||
@ -535,7 +538,7 @@ export function MapView({
|
||||
longitude: d.lon,
|
||||
latitude: d.lat,
|
||||
content: (
|
||||
<div className="text-xs">
|
||||
<div className="text-caption">
|
||||
<strong>
|
||||
{modelKey} 입자 #{(d.particle ?? 0) + 1}
|
||||
</strong>
|
||||
@ -604,7 +607,7 @@ export function MapView({
|
||||
longitude: info.coordinate?.[0] ?? 0,
|
||||
latitude: info.coordinate?.[1] ?? 0,
|
||||
content: (
|
||||
<div className="text-xs" style={{ minWidth: '140px' }}>
|
||||
<div className="text-caption" style={{ minWidth: '140px' }}>
|
||||
<strong style={{ color: PRIORITY_COLORS[d.priority] }}>{d.name}</strong>
|
||||
<br />
|
||||
우선순위: {PRIORITY_LABELS[d.priority] || d.priority}
|
||||
@ -919,7 +922,7 @@ export function MapView({
|
||||
longitude: info.coordinate[0],
|
||||
latitude: info.coordinate[1],
|
||||
content: (
|
||||
<div className="text-xs leading-relaxed" style={{ minWidth: 180 }}>
|
||||
<div className="text-caption leading-relaxed" style={{ minWidth: 180 }}>
|
||||
<strong className="text-color-warning">
|
||||
{dispersionResult.substance} 대기확산 면적
|
||||
</strong>
|
||||
@ -1009,7 +1012,7 @@ export function MapView({
|
||||
longitude: d.lon,
|
||||
latitude: d.lat,
|
||||
content: (
|
||||
<div className="text-xs" style={{ minWidth: '130px' }}>
|
||||
<div className="text-caption" style={{ minWidth: '130px' }}>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<span>{SENSITIVE_ICONS[d.type]}</span>
|
||||
<strong style={{ color: SENSITIVE_COLORS[d.type] }}>{d.name}</strong>
|
||||
@ -1458,19 +1461,19 @@ function MapControls({ center, zoom }: { center: [number, number]; zoom: number
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
onClick={() => map?.zoomIn()}
|
||||
className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-xs"
|
||||
className="w-[28px] h-[28px] bg-[color-mix(in_srgb,var(--bg-elevated)_85%,transparent)] backdrop-blur-sm border border-stroke rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-caption"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
onClick={() => map?.zoomOut()}
|
||||
className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-xs"
|
||||
className="w-[28px] h-[28px] bg-[color-mix(in_srgb,var(--bg-elevated)_85%,transparent)] backdrop-blur-sm border border-stroke rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-caption"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
onClick={() => map?.flyTo({ center: [center[1], center[0]], zoom, duration: 1000 })}
|
||||
className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:text-fg transition-all text-caption"
|
||||
className="w-[28px] h-[28px] bg-[color-mix(in_srgb,var(--bg-elevated)_85%,transparent)] backdrop-blur-sm border border-stroke rounded-sm text-fg-sub flex items-center justify-center hover:text-fg transition-all text-caption"
|
||||
>
|
||||
🎯
|
||||
</button>
|
||||
@ -1575,7 +1578,7 @@ function MapLegend({
|
||||
className="flex items-center gap-1.5 mt-2 rounded"
|
||||
style={{ padding: '6px', background: 'rgba(168,85,247,0.08)' }}
|
||||
>
|
||||
<div className="text-xs">🧭</div>
|
||||
<div className="text-caption">🧭</div>
|
||||
<span className="text-caption text-fg-disabled">풍향 (방사형)</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -1888,7 +1891,9 @@ function BacktrackReplayBar({
|
||||
minWidth: '340px',
|
||||
}}
|
||||
>
|
||||
<div className="text-sm text-color-tertiary font-mono font-bold">{progress.toFixed(0)}%</div>
|
||||
<div className="text-body-2 text-color-tertiary font-mono font-bold">
|
||||
{progress.toFixed(0)}%
|
||||
</div>
|
||||
<div className="flex-1 h-1 bg-border relative rounded-[2px]">
|
||||
<div
|
||||
className="h-full rounded-[2px]"
|
||||
|
||||
@ -79,15 +79,15 @@
|
||||
--font-size-title-1: 1.125rem;
|
||||
--font-size-subtitle: 0.9375rem;
|
||||
--font-size-title-2: 1rem;
|
||||
--font-size-title-3: 0.875rem;
|
||||
--font-size-title-4: 0.8125rem;
|
||||
--font-size-title-5: 0.75rem;
|
||||
--font-size-title-6: 0.6875rem;
|
||||
--font-size-body-1: 0.875rem;
|
||||
--font-size-body-2: 0.8125rem;
|
||||
--font-size-label-1: 0.75rem;
|
||||
--font-size-label-2: 0.6875rem;
|
||||
--font-size-caption: 0.6875rem;
|
||||
--font-size-title-3: 1rem;
|
||||
--font-size-title-4: 0.875rem;
|
||||
--font-size-title-5: 0.8125rem;
|
||||
--font-size-title-6: 0.75rem;
|
||||
--font-size-body-1: 1rem;
|
||||
--font-size-body-2: 0.875rem;
|
||||
--font-size-label-1: 0.8125rem;
|
||||
--font-size-label-2: 0.75rem;
|
||||
--font-size-caption: 0.75rem;
|
||||
/* typography — font-weight */
|
||||
--font-weight-thin: 300;
|
||||
--font-weight-regular: 400;
|
||||
|
||||
@ -45,6 +45,22 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ═══ Incidents 사고 팝업 ✕ 버튼 — 라이트 지도 기준 검은색 고정 ═══ */
|
||||
.incident-popup .maplibregl-popup-close-button {
|
||||
color: #1a1d21;
|
||||
background: transparent;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
.incident-popup .maplibregl-popup-close-button:hover {
|
||||
color: #000;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ═══ Scrollbar ═══ */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
@ -78,7 +94,7 @@
|
||||
border-radius: 6px;
|
||||
color: var(--fg-default);
|
||||
font-family: var(--font-korean);
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@ -103,7 +119,7 @@
|
||||
|
||||
.prd-date-input,
|
||||
.prd-time-input {
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@ -111,7 +127,7 @@
|
||||
.prd-time-input::-webkit-datetime-edit {
|
||||
color: var(--fg-sub);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
@ -191,7 +207,7 @@
|
||||
background: #1a1f2e;
|
||||
color: var(--fg-default);
|
||||
padding: 10px;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-korean);
|
||||
}
|
||||
|
||||
@ -278,7 +294,7 @@
|
||||
|
||||
.combo-item {
|
||||
padding: 7px 10px;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-korean);
|
||||
color: var(--fg-sub);
|
||||
cursor: pointer;
|
||||
@ -309,7 +325,7 @@
|
||||
gap: 4px;
|
||||
padding: 5px 4px;
|
||||
border-radius: 5px;
|
||||
font-size: 9px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-korean);
|
||||
cursor: pointer;
|
||||
@ -328,7 +344,7 @@
|
||||
|
||||
/* .prd-mc.on::before {
|
||||
content: '✓ ';
|
||||
font-size: 9px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-accent);
|
||||
} */
|
||||
|
||||
@ -370,7 +386,7 @@
|
||||
border: 1px solid rgba(6, 182, 212, 0.2);
|
||||
border-radius: 6px;
|
||||
color: var(--color-accent);
|
||||
font-size: 9px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
@ -551,7 +567,7 @@
|
||||
}
|
||||
|
||||
.tll {
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
@ -620,7 +636,7 @@
|
||||
border: 1px solid var(--color-boom);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-boom);
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-korean);
|
||||
@ -671,7 +687,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.tlsl {
|
||||
@ -699,7 +715,7 @@
|
||||
border: 1px solid var(--stroke-default);
|
||||
background: var(--bg-card);
|
||||
color: var(--fg-sub);
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-korean);
|
||||
@ -750,7 +766,7 @@
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-sub);
|
||||
font-family: var(--font-korean);
|
||||
}
|
||||
@ -835,7 +851,7 @@
|
||||
}
|
||||
|
||||
.layer-count {
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
@ -856,7 +872,7 @@
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-boom);
|
||||
font-family: var(--font-korean);
|
||||
@ -889,7 +905,7 @@
|
||||
border-radius: 4px;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
@ -926,7 +942,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
@ -936,7 +952,7 @@
|
||||
border: 1px solid var(--stroke-default);
|
||||
background: var(--bg-card);
|
||||
color: var(--fg-disabled);
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-mono);
|
||||
@ -1123,7 +1139,7 @@
|
||||
|
||||
.lyr-h1-cnt {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-mono);
|
||||
@ -1152,7 +1168,7 @@
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: background 0.15s;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--fg-sub);
|
||||
font-family: var(--font-korean);
|
||||
@ -1176,7 +1192,7 @@
|
||||
|
||||
.lyr-h2-cnt {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-mono);
|
||||
@ -1199,7 +1215,7 @@
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-sub);
|
||||
transition:
|
||||
color 0.15s,
|
||||
@ -1215,7 +1231,7 @@
|
||||
|
||||
.lyr-cnt {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-mono);
|
||||
@ -1329,7 +1345,7 @@
|
||||
}
|
||||
|
||||
.lyr-ccustom label {
|
||||
font-size: 9px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-korean);
|
||||
}
|
||||
@ -1353,7 +1369,7 @@
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.lyr-style-label {
|
||||
font-size: 9px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-korean);
|
||||
@ -1370,7 +1386,7 @@
|
||||
margin-top: 6px;
|
||||
}
|
||||
.lyr-style-name {
|
||||
font-size: 10px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-korean);
|
||||
min-width: 32px;
|
||||
@ -1395,7 +1411,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.lyr-style-val {
|
||||
font-size: 9px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-mono);
|
||||
min-width: 28px;
|
||||
|
||||
@ -181,7 +181,7 @@
|
||||
}
|
||||
|
||||
.wing-tab {
|
||||
@apply flex-1 py-2 px-1 text-xs font-semibold rounded-md text-center cursor-pointer font-korean;
|
||||
@apply flex-1 py-2 px-1 text-caption font-semibold rounded-md text-center cursor-pointer font-korean;
|
||||
transition: all 0.15s;
|
||||
color: var(--fg-disabled);
|
||||
background: transparent;
|
||||
|
||||
@ -148,7 +148,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
{/* ── 섹션 1: 헤더 ── */}
|
||||
<div className="pb-10 mb-12 border-b border-solid" style={{ borderColor: dividerColor }}>
|
||||
<p
|
||||
className="font-mono text-sm uppercase tracking-widest mb-3"
|
||||
className="font-mono text-body-2 uppercase tracking-widest mb-3"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
Components
|
||||
@ -244,7 +244,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-mono" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textMuted }}>
|
||||
텍스트 + 아이콘 버튼
|
||||
</span>
|
||||
</div>
|
||||
@ -298,7 +298,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-mono" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textMuted }}>
|
||||
아이콘 전용 버튼
|
||||
</span>
|
||||
</div>
|
||||
@ -339,7 +339,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
<div key={size.label} className="flex items-center justify-between gap-8">
|
||||
{/* 라벨 */}
|
||||
<span
|
||||
className="font-mono text-sm w-36 shrink-0"
|
||||
className="font-mono text-body-2 w-36 shrink-0"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
{size.label}
|
||||
@ -349,7 +349,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
<div className="flex-1 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md font-semibold text-sm"
|
||||
className="rounded-md font-semibold text-body-2"
|
||||
style={{
|
||||
height: `${size.heightPx}px`,
|
||||
paddingLeft: `${size.px}px`,
|
||||
@ -380,7 +380,10 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Flexible */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="font-mono text-sm font-bold" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-mono text-body-2 font-bold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
Flexible
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
@ -461,7 +464,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
|
||||
콘텐츠에 맞게 너비가 자동으로 조정됩니다.
|
||||
</span>
|
||||
</div>
|
||||
@ -469,7 +472,10 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
|
||||
{/* Fixed */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="font-mono text-sm font-bold" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-mono text-body-2 font-bold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
Fixed
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
@ -511,7 +517,10 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
<div style={{ height: '1px', flex: 1, backgroundColor: annotationColor }} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-xs ml-4" style={{ color: t.textSecondary }}>
|
||||
<span
|
||||
className="font-mono text-caption ml-4"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
너비가 고정된 버튼입니다.
|
||||
</span>
|
||||
</div>
|
||||
@ -533,7 +542,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
{ resolution: '해상도 320', width: '248px', maxWidth: '248px', padding: 16 },
|
||||
].map((item) => (
|
||||
<div key={item.resolution} className="flex flex-col gap-3">
|
||||
<span className="font-mono text-sm" style={{ color: t.textSecondary }}>
|
||||
<span className="font-mono text-body-2" style={{ color: t.textSecondary }}>
|
||||
{item.resolution}
|
||||
</span>
|
||||
<div className="flex items-center gap-6">
|
||||
@ -597,7 +606,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
{VARIANTS.map((variant) => (
|
||||
<th
|
||||
key={variant}
|
||||
className="font-mono text-xs font-semibold text-center pb-4"
|
||||
className="font-mono text-caption font-semibold text-center pb-4"
|
||||
style={{
|
||||
color: t.textSecondary,
|
||||
padding: '8px 12px',
|
||||
@ -615,7 +624,7 @@ export const ButtonContent = ({ theme }: ButtonContentProps) => {
|
||||
<tr key={row.state}>
|
||||
{/* 상태 라벨 */}
|
||||
<td
|
||||
className="font-mono text-xs font-medium"
|
||||
className="font-mono text-caption font-medium"
|
||||
style={{
|
||||
color: t.textSecondary,
|
||||
padding: rowIdx === 0 ? '8px 12px 8px 0' : '8px 12px 8px 0',
|
||||
|
||||
@ -347,7 +347,7 @@ const TransparencyRow = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-bold text-sm" style={{ color: isDark ? '#dfe2f3' : '#0f172a' }}>
|
||||
<span className="font-bold text-body-2" style={{ color: isDark ? '#dfe2f3' : '#0f172a' }}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="rounded-xl p-4" style={{ backgroundColor: sectionCardBg }}>
|
||||
@ -453,7 +453,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
{/* ── 섹션 1: 헤더 ── */}
|
||||
<div className="pb-10 mb-12 border-b border-solid" style={{ borderColor: dividerColor }}>
|
||||
<p
|
||||
className="font-mono text-sm uppercase tracking-widest mb-3"
|
||||
className="font-mono text-body-2 uppercase tracking-widest mb-3"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
Foundations
|
||||
@ -476,7 +476,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveColorTab(tab)}
|
||||
className="px-5 py-2.5 text-sm font-semibold border-none cursor-pointer bg-transparent"
|
||||
className="px-5 py-2.5 text-body-2 font-semibold border-none cursor-pointer bg-transparent"
|
||||
style={{
|
||||
color: isActive ? t.textAccent : t.textMuted,
|
||||
borderBottom: isActive ? `2px solid ${t.textAccent}` : '2px solid transparent',
|
||||
@ -505,7 +505,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
>
|
||||
--{'{property}'}-{'{role}'}[-{'{variant}'}]
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2 leading-relaxed" style={{ color: t.textSecondary }}>
|
||||
모든 컬러 토큰은{' '}
|
||||
<strong style={{ color: t.textPrimary }}>Property-Role-Variant</strong> 3계층 구조를
|
||||
따릅니다. Property는 색상이 적용되는 CSS 속성, Role은 의미 기반 역할, Variant는 상태
|
||||
@ -551,17 +551,17 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-sm font-bold shrink-0 w-16"
|
||||
className="font-mono text-body-2 font-bold shrink-0 w-16"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
{row.prop}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm" style={{ color: t.textSecondary }}>
|
||||
<div className="text-body-2" style={{ color: t.textSecondary }}>
|
||||
{row.desc}
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
||||
{row.example}
|
||||
</span>
|
||||
</div>
|
||||
@ -581,7 +581,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
style={{ border: `1px solid ${dividerColor}` }}
|
||||
>
|
||||
<div
|
||||
className="px-5 py-3 text-xs font-bold uppercase tracking-wider"
|
||||
className="px-5 py-3 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -608,12 +608,12 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-sm font-semibold"
|
||||
className="font-mono text-body-2 font-semibold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{row.name}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption" style={{ color: t.textMuted }}>
|
||||
{row.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -626,7 +626,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
style={{ border: `1px solid ${dividerColor}` }}
|
||||
>
|
||||
<div
|
||||
className="px-5 py-3 text-xs font-bold uppercase tracking-wider"
|
||||
className="px-5 py-3 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -650,12 +650,12 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-sm font-semibold"
|
||||
className="font-mono text-body-2 font-semibold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{row.name}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption" style={{ color: t.textMuted }}>
|
||||
{row.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -672,7 +672,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
Semantic Tokens
|
||||
</h2>
|
||||
<p className="text-sm mb-6" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2 mb-6" style={{ color: t.textSecondary }}>
|
||||
용도에 따라 의미를 부여한 토큰. 테마 전환 시 값이 변경됩니다.
|
||||
</p>
|
||||
|
||||
@ -745,7 +745,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
},
|
||||
].map((group) => (
|
||||
<div key={group.title} className="mb-8">
|
||||
<h3 className="text-sm font-bold mb-3" style={{ color: t.textAccent }}>
|
||||
<h3 className="text-body-2 font-bold mb-3" style={{ color: t.textAccent }}>
|
||||
{group.title}
|
||||
</h3>
|
||||
<div
|
||||
@ -754,7 +754,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div
|
||||
className="grid grid-cols-[80px_1fr_1fr_80px_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
|
||||
className="grid grid-cols-[80px_1fr_1fr_80px_1fr] gap-2 px-4 py-2.5 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -776,21 +776,21 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-xs line-through"
|
||||
className="font-mono text-caption line-through"
|
||||
style={{ color: t.textMuted }}
|
||||
>
|
||||
{tk.legacy}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{tk.name}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption" style={{ color: t.textMuted }}>
|
||||
{tk.desc}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
|
||||
{tk.value}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -804,7 +804,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span className="font-mono text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
||||
{hexToRgb(tk.value)}
|
||||
</span>
|
||||
</div>
|
||||
@ -823,7 +823,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
Palette Tokens
|
||||
</h2>
|
||||
<p className="text-sm mb-6" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2 mb-6" style={{ color: t.textSecondary }}>
|
||||
fg · bg · stroke 모든 맥락에서 사용되는 색상 원본. Property 접두사 없이{' '}
|
||||
<code className="font-mono" style={{ color: t.textAccent }}>
|
||||
--color-*
|
||||
@ -836,7 +836,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
style={{ border: `1px solid ${dividerColor}` }}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-[80px_1fr_80px_1fr_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
|
||||
className="grid grid-cols-[80px_1fr_80px_1fr_1fr] gap-2 px-4 py-2.5 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -882,16 +882,19 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-xs line-through" style={{ color: t.textMuted }}>
|
||||
<span
|
||||
className="font-mono text-caption line-through"
|
||||
style={{ color: t.textMuted }}
|
||||
>
|
||||
{tk.legacy}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{tk.name}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
|
||||
{tk.value}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -905,11 +908,11 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span className="font-mono text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
||||
{hexToRgb(tk.value)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption" style={{ color: t.textMuted }}>
|
||||
{tk.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -925,7 +928,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
Non-color Tokens
|
||||
</h2>
|
||||
<p className="text-sm mb-6" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2 mb-6" style={{ color: t.textSecondary }}>
|
||||
타이포그래피, 라운딩 등 색상 외 토큰.
|
||||
</p>
|
||||
|
||||
@ -934,7 +937,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
style={{ border: `1px solid ${dividerColor}` }}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-[80px_1fr_1fr_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
|
||||
className="grid grid-cols-[80px_1fr_1fr_1fr] gap-2 px-4 py-2.5 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -979,17 +982,20 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-xs line-through" style={{ color: t.textMuted }}>
|
||||
<span
|
||||
className="font-mono text-caption line-through"
|
||||
style={{ color: t.textMuted }}
|
||||
>
|
||||
{tk.legacy}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{tk.name}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs font-mono px-2 py-0.5 rounded"
|
||||
className="text-caption font-mono px-2 py-0.5 rounded"
|
||||
style={{
|
||||
color: t.textAccent,
|
||||
backgroundColor: isDark ? 'rgba(6,182,212,0.05)' : 'rgba(6,182,212,0.08)',
|
||||
@ -997,7 +1003,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
>
|
||||
{tk.category}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption" style={{ color: t.textMuted }}>
|
||||
{tk.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -1015,7 +1021,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-3" style={{ color: t.textPrimary }}>
|
||||
프라이머리 색상(primary color)
|
||||
</h2>
|
||||
<p className="mb-8 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-8 text-body-2" style={{ color: t.textSecondary }}>
|
||||
Primary 색상은 해양 방제 시스템의 핵심 인터랙션 요소에 사용됩니다. Cyan~Blue
|
||||
그라디언트가 주요 액션 버튼과 강조 요소에 적용됩니다.
|
||||
</p>
|
||||
@ -1023,7 +1029,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Light Mode */}
|
||||
<div className="rounded-xl p-6" style={{ backgroundColor: '#f5f5f5' }}>
|
||||
<p className="font-bold text-sm mb-4" style={{ color: '#1e293b' }}>
|
||||
<p className="font-bold text-body-2 mb-4" style={{ color: '#1e293b' }}>
|
||||
Light Mode
|
||||
</p>
|
||||
<ColorScaleBar
|
||||
@ -1041,7 +1047,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
|
||||
{/* Dark Mode */}
|
||||
<div className="rounded-xl p-6" style={{ backgroundColor: '#1a1a2e' }}>
|
||||
<p className="font-bold text-sm mb-4" style={{ color: '#fff' }}>
|
||||
<p className="font-bold text-body-2 mb-4" style={{ color: '#fff' }}>
|
||||
Dark Mode
|
||||
</p>
|
||||
<ColorScaleBar
|
||||
@ -1067,7 +1073,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-3" style={{ color: t.textPrimary }}>
|
||||
세컨더리 색상(secondary color)
|
||||
</h2>
|
||||
<p className="mb-8 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-8 text-body-2" style={{ color: t.textSecondary }}>
|
||||
Secondary 색상은 UI의 배경과 구조적 요소에 사용됩니다. Navy 계열로 다크 모드의
|
||||
깊이감과 계층 구조를 표현합니다.
|
||||
</p>
|
||||
@ -1075,7 +1081,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Light Mode */}
|
||||
<div className="rounded-xl p-6" style={{ backgroundColor: '#f5f5f5' }}>
|
||||
<p className="font-bold text-sm mb-4" style={{ color: '#1e293b' }}>
|
||||
<p className="font-bold text-body-2 mb-4" style={{ color: '#1e293b' }}>
|
||||
Light Mode
|
||||
</p>
|
||||
<ColorScaleBar
|
||||
@ -1093,7 +1099,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
|
||||
{/* Dark Mode */}
|
||||
<div className="rounded-xl p-6" style={{ backgroundColor: '#1a1a2e' }}>
|
||||
<p className="font-bold text-sm mb-4" style={{ color: '#fff' }}>
|
||||
<p className="font-bold text-body-2 mb-4" style={{ color: '#fff' }}>
|
||||
Dark Mode
|
||||
</p>
|
||||
<ColorScaleBar
|
||||
@ -1119,11 +1125,11 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-3" style={{ color: t.textPrimary }}>
|
||||
그레이 색상(gray color) / 네추럴, 중립 색상
|
||||
</h2>
|
||||
<p className="mb-2 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-2 text-body-2" style={{ color: t.textSecondary }}>
|
||||
Gray 색상은 주로 배경, 텍스트, 구분 선에 사용되며, 시각적 집중을 방해하지 않고
|
||||
콘텐츠에 초점을 맞추도록 도와주는 중립적인 색상이다.
|
||||
</p>
|
||||
<p className="mb-8 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-8 text-body-2" style={{ color: t.textSecondary }}>
|
||||
표준형 스타일의 그레이 색상은 주요 색상과 선명한 모드에서의 조화를 고려해 블루
|
||||
그레이 계열을 사용한다.
|
||||
</p>
|
||||
@ -1141,7 +1147,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<h2 className="text-2xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
Transparent
|
||||
</h2>
|
||||
<p className="mb-8 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-8 text-body-2" style={{ color: t.textSecondary }}>
|
||||
투명도와 음영을 활용하여 정보의 집중도를 조절합니다. 배경의 음영 처리는 투명도 65%를
|
||||
사용합니다.
|
||||
</p>
|
||||
@ -1174,12 +1180,12 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
<div className="mb-16">
|
||||
<div className="mb-6">
|
||||
<p
|
||||
className="font-mono text-sm uppercase tracking-widest mb-1"
|
||||
className="font-mono text-body-2 uppercase tracking-widest mb-1"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
Primitive
|
||||
</p>
|
||||
<p className="text-sm" style={{ color: t.textMuted }}>
|
||||
<p className="text-body-2" style={{ color: t.textMuted }}>
|
||||
UI 전반에서 사용하는 기본 색조 팔레트. 테마와 무관하게 고정된 값입니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -1214,17 +1220,17 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
|
||||
/>
|
||||
{/* 토큰명 */}
|
||||
<span
|
||||
className="font-mono text-sm font-semibold ml-4"
|
||||
className="font-mono text-body-2 font-semibold ml-4"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{token.name}
|
||||
</span>
|
||||
{/* HEX + RGB */}
|
||||
<div className="ml-auto text-right">
|
||||
<div className="font-mono text-sm" style={{ color: t.textPrimary }}>
|
||||
<div className="font-mono text-body-2" style={{ color: t.textPrimary }}>
|
||||
{token.hex}
|
||||
</div>
|
||||
<div className="font-mono text-xs" style={{ color: t.textMuted }}>
|
||||
<div className="font-mono text-caption" style={{ color: t.textMuted }}>
|
||||
{hexToRgb(token.hex)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -13,7 +13,7 @@ export const ComponentsContent = () => {
|
||||
>
|
||||
시스템 컴포넌트 카탈로그
|
||||
</h1>
|
||||
<p className="text-[#bcc9cd] font-korean text-sm leading-5 font-medium max-w-2xl">
|
||||
<p className="text-[#bcc9cd] font-korean text-body-2 leading-5 font-medium max-w-2xl">
|
||||
WING-OPS 해상 물류를 위한 시각적 아이덴티티 시스템입니다. 정밀도와 미션 크리티컬한 운영을
|
||||
위해 설계된 고밀도 산업용 인터페이스입니다.
|
||||
</p>
|
||||
|
||||
@ -256,7 +256,7 @@ const ComponentsOverview = ({ theme, onNavigate }: ComponentsOverviewProps) => {
|
||||
{/* ── 헤더 영역 ── */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<span
|
||||
className="font-mono text-xs font-semibold uppercase"
|
||||
className="font-mono text-caption font-semibold uppercase"
|
||||
style={{ letterSpacing: '1.4px', color: t.textAccent }}
|
||||
>
|
||||
Components
|
||||
@ -264,7 +264,7 @@ const ComponentsOverview = ({ theme, onNavigate }: ComponentsOverviewProps) => {
|
||||
<h1 className="font-sans text-4xl font-bold leading-tight" style={{ color: t.textPrimary }}>
|
||||
Overview
|
||||
</h1>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
재사용 가능한 UI 컴포넌트 카탈로그입니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -307,7 +307,10 @@ const ComponentsOverview = ({ theme, onNavigate }: ComponentsOverviewProps) => {
|
||||
|
||||
{/* 카드 라벨 */}
|
||||
<div className="px-5 py-4">
|
||||
<span className="font-sans text-sm font-semibold" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-sans text-body-2 font-semibold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{card.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -154,7 +154,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
style={{ backgroundColor: t.textAccent, boxShadow: t.systemActiveShadow }}
|
||||
/>
|
||||
<span
|
||||
className="font-mono text-xs leading-4 uppercase"
|
||||
className="font-mono text-caption leading-4 uppercase"
|
||||
style={{ letterSpacing: '1.2px', color: t.textAccent }}
|
||||
>
|
||||
System Active
|
||||
@ -192,11 +192,14 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
/>
|
||||
{/* 정보 */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-mono text-xs leading-4" style={{ color: t.textAccent }}>
|
||||
<span
|
||||
className="font-mono text-caption leading-4"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
{item.token}
|
||||
</span>
|
||||
<span
|
||||
className="font-korean text-sm leading-5 font-bold"
|
||||
className="font-korean text-body-2 leading-5 font-bold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{item.hex}
|
||||
@ -233,7 +236,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
{item.token}
|
||||
</span>
|
||||
<span
|
||||
className="font-korean text-sm font-bold pb-2"
|
||||
className="font-korean text-body-2 font-bold pb-2"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{item.hex}
|
||||
@ -263,7 +266,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
{item.token}
|
||||
</span>
|
||||
<span className={item.sampleClass}>{item.sampleText}</span>
|
||||
<span className="font-korean text-xs" style={{ color: item.descColor }}>
|
||||
<span className="font-korean text-caption" style={{ color: item.descColor }}>
|
||||
{item.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -297,7 +300,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span
|
||||
className="font-korean text-sm font-bold"
|
||||
className="font-korean text-body-2 font-bold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{item.name}
|
||||
@ -435,7 +438,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{/* radius-sm */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-mono text-xs leading-4" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-caption leading-4" style={{ color: t.textMuted }}>
|
||||
{t.radiusSmLabel}
|
||||
</span>
|
||||
<div
|
||||
@ -453,7 +456,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
Small Elements
|
||||
</span>
|
||||
<p
|
||||
className="font-korean text-xs leading-[19.5px] mt-1"
|
||||
className="font-korean text-caption leading-[19.5px] mt-1"
|
||||
style={{ color: t.radiusDescText }}
|
||||
>
|
||||
Applied to tactical buttons, search inputs, and micro-cards for a precise, sharp
|
||||
@ -463,7 +466,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
</div>
|
||||
{/* radius-md */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-mono text-xs leading-4" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-caption leading-4" style={{ color: t.textMuted }}>
|
||||
{t.radiusMdLabel}
|
||||
</span>
|
||||
<div
|
||||
@ -481,7 +484,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
|
||||
Structural Panels
|
||||
</span>
|
||||
<p
|
||||
className="font-korean text-xs leading-[19.5px] mt-1"
|
||||
className="font-korean text-caption leading-[19.5px] mt-1"
|
||||
style={{ color: t.radiusDescText }}
|
||||
>
|
||||
Applied to telemetry cards, floating modals, and primary operational panels to
|
||||
|
||||
@ -50,7 +50,10 @@ export const FloatContent = ({ theme }: FloatContentProps) => {
|
||||
<h1 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
Float
|
||||
</h1>
|
||||
<p className="font-korean text-sm leading-5 mt-1" style={{ color: t.textSecondary }}>
|
||||
<p
|
||||
className="font-korean text-body-2 leading-5 mt-1"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
화면 위에 떠서 표시되는 UI 패턴 카탈로그 — Modal, Dropdown, Overlay, Toast
|
||||
</p>
|
||||
</div>
|
||||
@ -70,7 +73,7 @@ export const FloatContent = ({ theme }: FloatContentProps) => {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-sans text-sm font-bold leading-5"
|
||||
className="font-sans text-body-2 font-bold leading-5"
|
||||
style={{ color: isActive ? t.textAccent : t.textMuted }}
|
||||
>
|
||||
{label}
|
||||
|
||||
@ -174,7 +174,7 @@ const FoundationsOverview = ({ theme, onNavigate }: FoundationsOverviewProps) =>
|
||||
{/* ── 헤더 영역 ── */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<span
|
||||
className="font-mono text-xs font-semibold uppercase"
|
||||
className="font-mono text-caption font-semibold uppercase"
|
||||
style={{ letterSpacing: '1.4px', color: t.textAccent }}
|
||||
>
|
||||
Foundations
|
||||
@ -182,7 +182,7 @@ const FoundationsOverview = ({ theme, onNavigate }: FoundationsOverviewProps) =>
|
||||
<h1 className="font-sans text-4xl font-bold leading-tight" style={{ color: t.textPrimary }}>
|
||||
Overview
|
||||
</h1>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
디자인의 기반이 되는 핵심 요소 사용 기준입니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -225,7 +225,10 @@ const FoundationsOverview = ({ theme, onNavigate }: FoundationsOverviewProps) =>
|
||||
|
||||
{/* 카드 라벨 */}
|
||||
<div className="px-5 py-4">
|
||||
<span className="font-sans text-sm font-semibold" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-sans text-body-2 font-semibold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{card.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -324,7 +324,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<h1 className="font-sans text-3xl leading-9 font-bold" style={{ color: t.textPrimary }}>
|
||||
Layout
|
||||
</h1>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
WING-OPS는 데스크톱 전용 고정 뷰포트 애플리케이션입니다. 화면 전체를 채우는 고정
|
||||
레이아웃(100vh)으로, flex 기반의 패널 구조를 사용합니다. KRDS 가이드라인을 기반으로
|
||||
xlarge / xxlarge 구간에 최적화되어 있습니다.
|
||||
@ -364,7 +364,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
Breakpoint
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
화면 크기에 따라 반응형 레이아웃을 사용하여 환경에 최적화된 구조로 표시됩니다. WING-OPS
|
||||
사용 구간(xl, 2xl)은 cyan으로 강조되어 있습니다.
|
||||
</p>
|
||||
@ -499,7 +499,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
borderColor: isDark ? 'rgba(76,215,246,0.45)' : 'rgba(6,182,212,0.40)',
|
||||
}}
|
||||
/>
|
||||
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
WING-OPS 사용 중
|
||||
</span>
|
||||
</div>
|
||||
@ -511,7 +511,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
borderColor: isDark ? 'rgba(140,144,159,0.25)' : 'rgba(148,163,184,0.30)',
|
||||
}}
|
||||
/>
|
||||
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
미지원 (1280px 미만)
|
||||
</span>
|
||||
</div>
|
||||
@ -525,7 +525,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
Grid
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
컬럼, 마진, 거터로 구성된 그리드 시스템입니다. 데스크톱 전용으로 xl / 2xl 두 구간만
|
||||
지원합니다.
|
||||
</p>
|
||||
@ -548,7 +548,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
style={{ borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0' }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
Breakpoint
|
||||
</span>
|
||||
<span
|
||||
@ -562,10 +562,10 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
Width
|
||||
</span>
|
||||
<span className="font-mono text-sm" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-body-2" style={{ color: t.textPrimary }}>
|
||||
{spec.width}
|
||||
</span>
|
||||
</div>
|
||||
@ -683,7 +683,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<span className="font-mono text-base" style={{ color: '#f97316' }}>
|
||||
⚠
|
||||
</span>
|
||||
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
<strong style={{ color: '#f97316' }}>1280px 미만 미지원</strong> — Mobile / Tablet
|
||||
구간(xs / s / md / lg)은 데스크톱 전용 운영 정책에 따라 지원하지 않습니다.
|
||||
</span>
|
||||
@ -696,7 +696,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
App Shell
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
WING-OPS 애플리케이션의 기본 레이아웃 구조와 KRDS Sub-page 영역 매핑입니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -720,7 +720,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-sm font-bold" style={{ color: '#3b82f6' }}>
|
||||
<span className="font-mono text-body-2 font-bold" style={{ color: '#3b82f6' }}>
|
||||
TopBar
|
||||
</span>
|
||||
<span
|
||||
@ -932,7 +932,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
Spacing
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
UI 요소 간의 간격과 여백을 정의하여 콘텐츠의 위계와 가독성을 조율합니다. Tailwind
|
||||
spacing 토큰과 직결되며, 막대 길이는 실제 px 비율입니다.
|
||||
</p>
|
||||
@ -1000,7 +1000,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
4pt Grid
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
모든 여백과 간격을 4point 단위로 설정해 규칙성을 확보합니다. 컴팩트한 컴포넌트의 경우,
|
||||
2의 배수 단위를 제한적으로 사용합니다.
|
||||
</p>
|
||||
@ -1048,7 +1048,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="font-korean text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textPrimary }}>
|
||||
{item.text}
|
||||
</span>
|
||||
</div>
|
||||
@ -1145,7 +1145,10 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
minWidth: '180px',
|
||||
}}
|
||||
>
|
||||
<span className="font-korean text-xs font-bold" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-korean text-caption font-bold"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
카드 타이틀
|
||||
</span>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
@ -1185,7 +1188,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
디자인 시스템 진실 소스
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
UI 요소의 레이어 스택 순서입니다. 높은 z-index가 위에 표시되며, 이 값은 디자인 시스템의
|
||||
이상적 설계 값으로 실제 코드는 이 값에 맞춰 정정되어야 합니다.
|
||||
</p>
|
||||
@ -1216,7 +1219,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
className="w-2 h-2 rounded-full shrink-0"
|
||||
style={{ backgroundColor: layer.color }}
|
||||
/>
|
||||
<span className="font-mono text-xs font-bold" style={{ color: layer.color }}>
|
||||
<span className="font-mono text-caption font-bold" style={{ color: layer.color }}>
|
||||
{layer.name}
|
||||
</span>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
@ -1234,7 +1237,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
Reference
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
App Shell CSS 클래스와 KRDS Grid 규칙 비교 — 코드 작성 시 참조용입니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -70,17 +70,17 @@ export const RadiusContent = ({ theme }: RadiusContentProps) => {
|
||||
<h1 className="font-sans text-3xl leading-9 font-bold" style={{ color: t.textPrimary }}>
|
||||
Radius
|
||||
</h1>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
Radius는 컴포넌트 혹은 콘텐츠 모서리의 둥글기를 표현합니다.
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
Radius는 UI 구성 요소의 모서리를 둥글게 처리하여 부드럽고 현대적인 느낌을 제공합니다.
|
||||
일관된 Radius 값은 브랜드 아이덴티티를 강화하고, 사용자 경험을 향상시키며, 다양한 화면과
|
||||
컨텍스트에서 시각적 일관성을 유지하는 데 중요한 역할을 합니다.
|
||||
</p>
|
||||
<ul
|
||||
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
|
||||
className="flex flex-col gap-1 list-disc list-inside font-korean text-body-2"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
<li>
|
||||
@ -203,7 +203,7 @@ export const RadiusContent = ({ theme }: RadiusContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
컴포넌트 매핑
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
wing.css 컴포넌트 클래스에 적용된 Radius 토큰입니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -233,7 +233,7 @@ export const RadiusContent = ({ theme }: RadiusContentProps) => {
|
||||
|
||||
{/* 정보 */}
|
||||
<div className="flex flex-col gap-1.5 flex-1">
|
||||
<span className="font-mono text-xs font-bold" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption font-bold" style={{ color: t.textPrimary }}>
|
||||
{item.className}
|
||||
</span>
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
|
||||
@ -207,7 +207,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
{/* ── 섹션 1: 헤더 ── */}
|
||||
<div className="pb-10 mb-12 border-b border-solid" style={{ borderColor: dividerColor }}>
|
||||
<p
|
||||
className="font-mono text-sm uppercase tracking-widest mb-3"
|
||||
className="font-mono text-body-2 uppercase tracking-widest mb-3"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
Components
|
||||
@ -225,7 +225,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
<h2 className="text-2xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
Input Field
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2" style={{ color: t.textSecondary }}>
|
||||
단일 행 텍스트를 입력받는 필드입니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -246,7 +246,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
{/* Prefix label */}
|
||||
<div className="flex flex-col items-center gap-0.5" style={{ width: '70px' }}>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Prefix
|
||||
@ -272,7 +272,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
{/* Input label */}
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Input
|
||||
@ -295,7 +295,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
{/* Suffix label */}
|
||||
<div className="flex flex-col items-center gap-0.5" style={{ width: '70px' }}>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Suffix
|
||||
@ -377,7 +377,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
|
||||
{/* Suffix 텍스트 */}
|
||||
<span
|
||||
className="shrink-0 ml-2 font-mono text-sm"
|
||||
className="shrink-0 ml-2 font-mono text-body-2"
|
||||
style={{ color: fieldPlaceholder }}
|
||||
>
|
||||
원
|
||||
@ -402,7 +402,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Container
|
||||
@ -437,7 +437,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
style={{ width: '1px', height: '10px', backgroundColor: annotationColor }}
|
||||
/>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Clear Button
|
||||
@ -468,7 +468,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Container
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
입력 필드의 외곽 영역입니다. 테두리, 곡률, 내부 여백을 정의합니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
@ -519,13 +519,13 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-mono text-xs" style={{ color: t.textSecondary }}>
|
||||
<p className="font-mono text-caption" style={{ color: t.textSecondary }}>
|
||||
height: 44px (Medium)
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1" style={{ color: t.textSecondary }}>
|
||||
<p className="font-mono text-caption mt-1" style={{ color: t.textSecondary }}>
|
||||
padding: 12px (좌우)
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1" style={{ color: t.textSecondary }}>
|
||||
<p className="font-mono text-caption mt-1" style={{ color: t.textSecondary }}>
|
||||
border-radius: 6px
|
||||
</p>
|
||||
</div>
|
||||
@ -543,14 +543,14 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Placeholder
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
값이 입력되지 않았을 때 표시되는 안내 텍스트입니다. 입력 시 사라집니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
<div className="flex gap-6">
|
||||
{/* 플레이스홀더 있는 필드 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
플레이스홀더 있음
|
||||
</span>
|
||||
<div
|
||||
@ -570,7 +570,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
|
||||
{/* 빈 필드 (플레이스홀더 없음) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
플레이스홀더 없음
|
||||
</span>
|
||||
<div
|
||||
@ -597,14 +597,17 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Label
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
입력 필드의 용도를 설명하는 텍스트입니다. 필수 항목은 * 표시로 구분합니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
<div className="flex gap-8">
|
||||
{/* 일반 라벨 */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-semibold mb-1.5" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="text-body-2 font-semibold mb-1.5"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
이름
|
||||
</span>
|
||||
<div
|
||||
@ -624,7 +627,10 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
|
||||
{/* 필수 라벨 */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-semibold mb-1.5" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="text-body-2 font-semibold mb-1.5"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
이메일 <span style={{ color: '#ef4444' }}>*</span>
|
||||
</span>
|
||||
<div
|
||||
@ -655,7 +661,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Input Text
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
사용자가 실제로 입력한 텍스트입니다. 플레이스홀더보다 진한 색상으로 표시됩니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
@ -673,7 +679,10 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
>
|
||||
홍길동
|
||||
</div>
|
||||
<div className="flex gap-6 text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<div
|
||||
className="flex gap-6 text-caption font-mono"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
<span>font-size: 14px</span>
|
||||
<span>color: textPrimary</span>
|
||||
<span>font-weight: 400</span>
|
||||
@ -692,14 +701,14 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Clear Icon
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
입력값이 있을 때 표시되는 초기화 버튼입니다. 클릭 시 입력값을 삭제합니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
<div className="flex gap-6">
|
||||
{/* 텍스트 입력 + Clear 아이콘 표시 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
입력값 있음 (Clear 표시)
|
||||
</span>
|
||||
<div
|
||||
@ -736,7 +745,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
|
||||
{/* 빈 상태 (Clear 미표시) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
입력값 없음 (Clear 미표시)
|
||||
</span>
|
||||
<div
|
||||
@ -767,7 +776,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Helper Text
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
입력 필드 하단에 표시되는 보조 텍스트입니다. 안내 또는 에러 메시지로 사용됩니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
@ -787,7 +796,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
>
|
||||
비밀번호
|
||||
</div>
|
||||
<span className="text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption" style={{ color: t.textMuted }}>
|
||||
영문, 숫자 포함 8자 이상
|
||||
</span>
|
||||
</div>
|
||||
@ -807,7 +816,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
>
|
||||
비밀번호
|
||||
</div>
|
||||
<span className="text-xs" style={{ color: '#ef4444' }}>
|
||||
<span className="text-caption" style={{ color: '#ef4444' }}>
|
||||
필수 입력 항목입니다.
|
||||
</span>
|
||||
</div>
|
||||
@ -828,7 +837,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
<div key={row.state} className="flex items-center gap-8">
|
||||
{/* 왼쪽: State 라벨 + 뱃지 */}
|
||||
<div className="flex items-center gap-2 shrink-0" style={{ width: '200px' }}>
|
||||
<span className="font-mono text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
|
||||
State
|
||||
</span>
|
||||
<span
|
||||
@ -891,7 +900,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
<h2 className="text-2xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
Text Area
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2" style={{ color: t.textSecondary }}>
|
||||
여러 줄의 텍스트를 입력받는 필드입니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -911,7 +920,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
{/* Input Area label */}
|
||||
<div className="flex flex-col items-center gap-0.5" style={{ width: '80px' }}>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Input Area
|
||||
@ -933,7 +942,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
{/* Placeholder label */}
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Placeholder
|
||||
@ -955,7 +964,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
{/* Character Counter label */}
|
||||
<div className="flex flex-col items-center gap-0.5" style={{ width: '110px' }}>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Character Counter
|
||||
@ -1042,7 +1051,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Container
|
||||
@ -1075,7 +1084,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
style={{ width: '1px', height: '10px', backgroundColor: annotationColor }}
|
||||
/>
|
||||
<span
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: annotationColor }}
|
||||
>
|
||||
Resize Handle
|
||||
@ -1103,7 +1112,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Container
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
텍스트 영역의 외곽 컨테이너입니다. 기본 높이 112px이며 사용자가 리사이즈할 수
|
||||
있습니다.
|
||||
</p>
|
||||
@ -1154,16 +1163,16 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<p className="font-mono text-xs" style={{ color: t.textSecondary }}>
|
||||
<p className="font-mono text-caption" style={{ color: t.textSecondary }}>
|
||||
height: 112px (default)
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1" style={{ color: t.textSecondary }}>
|
||||
<p className="font-mono text-caption mt-1" style={{ color: t.textSecondary }}>
|
||||
padding: 12px
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1" style={{ color: t.textSecondary }}>
|
||||
<p className="font-mono text-caption mt-1" style={{ color: t.textSecondary }}>
|
||||
border-radius: 6px
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1" style={{ color: t.textSecondary }}>
|
||||
<p className="font-mono text-caption mt-1" style={{ color: t.textSecondary }}>
|
||||
resize: vertical
|
||||
</p>
|
||||
</div>
|
||||
@ -1181,14 +1190,14 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Placeholder
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
값이 입력되지 않았을 때 표시되는 안내 텍스트입니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
<div className="flex gap-6">
|
||||
{/* 플레이스홀더 있는 TextArea */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
플레이스홀더 있음
|
||||
</span>
|
||||
<div
|
||||
@ -1209,7 +1218,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
|
||||
{/* 빈 TextArea */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
플레이스홀더 없음
|
||||
</span>
|
||||
<div
|
||||
@ -1236,14 +1245,17 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Label
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
텍스트 영역의 용도를 설명하는 라벨입니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
<div className="flex gap-8">
|
||||
{/* 기본 라벨 */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-semibold mb-1.5" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="text-body-2 font-semibold mb-1.5"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
내용
|
||||
</span>
|
||||
<div
|
||||
@ -1264,7 +1276,10 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
|
||||
{/* 필수(*) 라벨 */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-semibold mb-1.5" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="text-body-2 font-semibold mb-1.5"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
비고 <span style={{ color: '#ef4444' }}>*</span>
|
||||
</span>
|
||||
<div
|
||||
@ -1296,7 +1311,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Input Text
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
사용자가 입력한 여러 줄의 텍스트입니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
@ -1316,7 +1331,10 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
>
|
||||
{'오늘 점검 내용을 기록합니다.\n상세 내용은 아래와 같습니다.'}
|
||||
</div>
|
||||
<div className="flex gap-6 text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<div
|
||||
className="flex gap-6 text-caption font-mono"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
<span>font-size: 14px</span>
|
||||
<span>color: textPrimary</span>
|
||||
<span>line-height: 1.6</span>
|
||||
@ -1335,14 +1353,14 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Clear Icon
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
입력값 초기화 버튼입니다. 텍스트 영역 우상단에 표시됩니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
<div className="flex gap-6">
|
||||
{/* 텍스트 있는 상태 (Clear 표시) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
입력값 있음 (Clear 표시)
|
||||
</span>
|
||||
<div
|
||||
@ -1380,7 +1398,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
|
||||
{/* 빈 상태 (Clear 미표시) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-mono" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textSecondary }}>
|
||||
입력값 없음 (Clear 미표시)
|
||||
</span>
|
||||
<div
|
||||
@ -1412,7 +1430,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
Helper Text
|
||||
</h3>
|
||||
</div>
|
||||
<p className="mb-5 text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="mb-5 text-body-2" style={{ color: t.textSecondary }}>
|
||||
텍스트 영역 하단의 도움말 또는 에러 메시지입니다.
|
||||
</p>
|
||||
<div className="rounded-xl p-8" style={{ backgroundColor: sectionCardBg }}>
|
||||
@ -1434,10 +1452,10 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
내용을 입력하세요
|
||||
</div>
|
||||
<div className="flex items-center justify-between" style={{ width: '240px' }}>
|
||||
<span className="text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption" style={{ color: t.textMuted }}>
|
||||
상세 내용을 입력해 주세요
|
||||
</span>
|
||||
<span className="text-xs font-mono" style={{ color: t.textMuted }}>
|
||||
<span className="text-caption font-mono" style={{ color: t.textMuted }}>
|
||||
0/500
|
||||
</span>
|
||||
</div>
|
||||
@ -1459,7 +1477,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
>
|
||||
내용을 입력하세요
|
||||
</div>
|
||||
<span className="text-xs" style={{ color: '#ef4444' }}>
|
||||
<span className="text-caption" style={{ color: '#ef4444' }}>
|
||||
필수 입력 항목입니다.
|
||||
</span>
|
||||
</div>
|
||||
@ -1480,7 +1498,7 @@ export const TextFieldContent = ({ theme }: TextFieldContentProps) => {
|
||||
<div key={`ta-${row.state}`} className="flex items-start gap-8">
|
||||
{/* 왼쪽: State 라벨 + 뱃지 */}
|
||||
<div className="flex items-center gap-2 shrink-0 pt-3" style={{ width: '200px' }}>
|
||||
<span className="font-mono text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textSecondary }}>
|
||||
State
|
||||
</span>
|
||||
<span
|
||||
|
||||
@ -307,18 +307,18 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
<h1 className="font-sans text-3xl leading-9 font-bold" style={{ color: t.textPrimary }}>
|
||||
Typography
|
||||
</h1>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
WING-OPS 인터페이스에서 사용되는 타이포그래피 체계입니다. 폰트 패밀리, 크기, 두께를
|
||||
토큰과 컴포넌트 클래스로 정의하여 시각적 계층 구조와 일관성을 유지합니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-korean text-sm font-bold" style={{ color: t.textPrimary }}>
|
||||
<h3 className="font-korean text-body-2 font-bold" style={{ color: t.textPrimary }}>
|
||||
개요
|
||||
</h3>
|
||||
<ul
|
||||
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
|
||||
className="flex flex-col gap-1 list-disc list-inside font-korean text-body-2"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
<li>
|
||||
@ -337,7 +337,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
글꼴
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
사용자의 디바이스 환경을 고려하여, 시스템 폰트와 웹 폰트를 조합하여 사용합니다. 한국어
|
||||
UI에 최적화된 폰트 스택으로 다양한 기기에서 일관된 가독성을 보장합니다.
|
||||
</p>
|
||||
@ -352,7 +352,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
className="font-mono text-sm leading-6"
|
||||
className="font-mono text-body-2 leading-6"
|
||||
style={{ color: isDark ? '#c0c8dc' : '#475569' }}
|
||||
>
|
||||
<span style={{ color: t.textAccent }}>font-family</span>
|
||||
@ -390,7 +390,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
</div>
|
||||
<div className="px-5 py-5 flex flex-col gap-4">
|
||||
<div
|
||||
className="font-mono text-xs leading-5 rounded px-3 py-2"
|
||||
className="font-mono text-caption leading-5 rounded px-3 py-2"
|
||||
style={{
|
||||
color: isDark ? '#9ba3b8' : '#64748b',
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)',
|
||||
@ -398,7 +398,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
>
|
||||
{font.stack}
|
||||
</div>
|
||||
<p className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>{font.usage}</p>
|
||||
<p className="font-korean text-caption leading-5" style={{ color: t.textSecondary }}>{font.usage}</p>
|
||||
<div className="flex flex-col gap-3 pt-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-mono text-caption uppercase" style={{ letterSpacing: '1px', color: t.textMuted }}>Regular</span>
|
||||
@ -421,7 +421,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
<h2 className="font-sans text-2xl leading-8 font-bold" style={{ color: t.textPrimary }}>
|
||||
타입 스케일
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-5" style={{ color: t.textSecondary }}>
|
||||
Display, Heading, Body, Navigation, Label의 5가지 용도 카테고리에 맞게 조합하여
|
||||
사용합니다.
|
||||
</p>
|
||||
@ -434,7 +434,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
<h3 className="font-sans text-lg font-bold" style={{ color: t.textPrimary }}>
|
||||
{category.name}
|
||||
</h3>
|
||||
<p className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-caption leading-5" style={{ color: t.textSecondary }}>
|
||||
{category.description}
|
||||
</p>
|
||||
</div>
|
||||
@ -445,7 +445,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
style={{ border: `1px solid ${isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0'}` }}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-[1fr_80px_80px_80px_90px_140px] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
|
||||
className="grid grid-cols-[1fr_80px_80px_80px_90px_140px] gap-2 px-4 py-2.5 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -467,20 +467,23 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>
|
||||
<span
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
{row.token}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
||||
{row.px}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
||||
{row.weight}
|
||||
</span>
|
||||
<span
|
||||
className="font-mono text-xs"
|
||||
className="font-mono text-caption"
|
||||
style={{ color: t.textMuted }}
|
||||
>{`${(row.lineHeight * 100).toFixed(0)}%`}</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
||||
{row.letterSpacing}
|
||||
</span>
|
||||
<span
|
||||
@ -510,7 +513,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
>
|
||||
<div className="shrink-0 w-[100px]">
|
||||
<div
|
||||
className="font-mono text-xs font-semibold"
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
{row.token}
|
||||
@ -533,7 +536,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
{row.sample}
|
||||
</span>
|
||||
</div>
|
||||
<div className="shrink-0 text-xs font-korean" style={{ color: t.textMuted }}>
|
||||
<div className="shrink-0 text-caption font-korean" style={{ color: t.textMuted }}>
|
||||
{row.role}
|
||||
</div>
|
||||
</div>
|
||||
@ -551,7 +554,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
폰트 두께 토큰
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2" style={{ color: t.textSecondary }}>
|
||||
기본 두께 Regular(400), 강조 Bold(700). Medium(500)은 레이블과 소제목, Thin(300)은
|
||||
장식적 대형 텍스트에 사용합니다.
|
||||
</p>
|
||||
@ -561,7 +564,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
style={{ border: `1px solid ${isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0'}` }}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-[1fr_80px_1fr_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
|
||||
className="grid grid-cols-[1fr_80px_1fr_1fr] gap-2 px-4 py-2.5 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -606,17 +609,20 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>
|
||||
<span
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
{row.token}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
||||
{row.value}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption" style={{ color: t.textSecondary }}>
|
||||
{row.name}
|
||||
</span>
|
||||
<span
|
||||
className="font-korean text-sm"
|
||||
className="font-korean text-body-2"
|
||||
style={{ color: t.textPrimary, fontWeight: Number(row.value) }}
|
||||
>
|
||||
{row.preview}
|
||||
@ -632,7 +638,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
줄 높이 토큰
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2" style={{ color: t.textSecondary }}>
|
||||
대형 텍스트는 타이트하게(1.3), 본문은 여유롭게(1.6). 가독성과 공간 효율의 균형을
|
||||
맞춥니다.
|
||||
</p>
|
||||
@ -642,7 +648,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
style={{ border: `1px solid ${isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0'}` }}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-[1fr_80px_100px_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
|
||||
className="grid grid-cols-[1fr_80px_100px_1fr] gap-2 px-4 py-2.5 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -677,16 +683,19 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>
|
||||
<span
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
{row.token}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
||||
{row.value}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
||||
{row.pct}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption" style={{ color: t.textSecondary }}>
|
||||
{row.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -700,7 +709,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}>
|
||||
자간 토큰
|
||||
</h2>
|
||||
<p className="text-sm" style={{ color: t.textSecondary }}>
|
||||
<p className="text-body-2" style={{ color: t.textSecondary }}>
|
||||
카테고리별로 자간을 정의합니다. Display는 넓게, Body는 기본값을 사용합니다.
|
||||
</p>
|
||||
</div>
|
||||
@ -709,7 +718,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
style={{ border: `1px solid ${isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0'}` }}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-[1fr_80px_140px_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
|
||||
className="grid grid-cols-[1fr_80px_140px_1fr] gap-2 px-4 py-2.5 text-caption font-bold uppercase tracking-wider"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9',
|
||||
color: t.textMuted,
|
||||
@ -760,10 +769,13 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<span className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>
|
||||
<span
|
||||
className="font-mono text-caption font-semibold"
|
||||
style={{ color: t.textAccent }}
|
||||
>
|
||||
{row.token}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
||||
{row.value}
|
||||
</span>
|
||||
<span
|
||||
@ -776,7 +788,7 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
|
||||
>
|
||||
{row.tw}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="text-caption" style={{ color: t.textSecondary }}>
|
||||
{row.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -167,7 +167,7 @@ export const ButtonCatalogSection = () => {
|
||||
<div className="border-b border-solid border-[#1e2a42] pt-4 pr-6 pb-4 pl-6 flex flex-row gap-3 items-center justify-start self-stretch shrink-0 relative">
|
||||
<div className="bg-[#06b6d4] rounded-xl shrink-0 w-1 h-4 relative" />
|
||||
<div
|
||||
className="text-[#22d3ee] text-left font-korean text-xs leading-4 font-medium uppercase relative flex items-center justify-start"
|
||||
className="text-[#22d3ee] text-left font-korean text-caption leading-4 font-medium uppercase relative flex items-center justify-start"
|
||||
style={{ letterSpacing: '1.2px' }}
|
||||
>
|
||||
제어 인터페이스: 버튼
|
||||
@ -185,7 +185,7 @@ export const ButtonCatalogSection = () => {
|
||||
className="pt-px pr-2 pb-[17.5px] pl-2 flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative"
|
||||
>
|
||||
<div
|
||||
className="text-[#64748b] text-left font-korean text-xs font-medium uppercase relative flex items-center justify-start"
|
||||
className="text-[#64748b] text-left font-korean text-caption font-medium uppercase relative flex items-center justify-start"
|
||||
style={{ letterSpacing: '-0.55px' }}
|
||||
>
|
||||
{header}
|
||||
@ -207,7 +207,7 @@ export const ButtonCatalogSection = () => {
|
||||
>
|
||||
{/* 버튼 유형 레이블 */}
|
||||
<div className="pt-[31.5px] pr-2 pb-[31.5px] pl-2 flex flex-col gap-0 items-start justify-start flex-1 min-w-0 relative">
|
||||
<div className="text-[#bcc9cd] text-left font-korean text-xs font-medium relative flex items-center justify-start">
|
||||
<div className="text-[#bcc9cd] text-left font-korean text-caption font-medium relative flex items-center justify-start">
|
||||
{row.label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -129,7 +129,7 @@ export const CardSection = () => {
|
||||
<div className="text-white text-left font-sans font-bold text-4xl leading-10 relative flex items-center justify-start">
|
||||
24.8
|
||||
</div>
|
||||
<div className="text-[#64748b] text-left font-sans font-semibold text-sm leading-5 relative flex items-center justify-start">
|
||||
<div className="text-[#64748b] text-left font-sans font-semibold text-body-2 leading-5 relative flex items-center justify-start">
|
||||
노트 (knots)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -61,7 +61,7 @@ export const IconBadgeSection = () => {
|
||||
<div className="bg-[#e89337] rounded-xl shrink-0 w-1 h-4 relative"></div>
|
||||
<div className="flex flex-col gap-0 items-start justify-start shrink-0 relative">
|
||||
<div
|
||||
className="text-[#22d3ee] text-left font-korean text-xs leading-4 font-medium uppercase relative flex items-center justify-start"
|
||||
className="text-[#22d3ee] text-left font-korean text-caption leading-4 font-medium uppercase relative flex items-center justify-start"
|
||||
style={{ letterSpacing: '1.2px' }}
|
||||
>
|
||||
마이크로 컨트롤: 아이콘 버튼
|
||||
@ -113,7 +113,7 @@ export const IconBadgeSection = () => {
|
||||
<div className="bg-[#93000a] rounded-xl shrink-0 w-1 h-4 relative"></div>
|
||||
<div className="flex flex-col gap-0 items-start justify-start shrink-0 relative">
|
||||
<div
|
||||
className="text-[#22d3ee] text-left font-korean text-xs leading-4 font-medium uppercase relative flex items-center justify-start"
|
||||
className="text-[#22d3ee] text-left font-korean text-caption leading-4 font-medium uppercase relative flex items-center justify-start"
|
||||
style={{ letterSpacing: '1.2px' }}
|
||||
>
|
||||
마이크로 컨트롤: 아이콘 버튼
|
||||
|
||||
@ -36,10 +36,10 @@ export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
|
||||
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
|
||||
Dropdown
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
트리거 요소에{' '}
|
||||
<code
|
||||
className="font-mono text-xs px-1.5 py-0.5 rounded"
|
||||
className="font-mono text-caption px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
|
||||
color: t.textAccent,
|
||||
@ -50,7 +50,7 @@ export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
|
||||
로 부착되는 선택 목록. 5개 이상의 선택지가 있는 단일 선택에 사용한다. 프로젝트 공통
|
||||
컴포넌트는{' '}
|
||||
<code
|
||||
className="font-mono text-xs px-1.5 py-0.5 rounded"
|
||||
className="font-mono text-caption px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
|
||||
color: t.textAccent,
|
||||
@ -84,7 +84,7 @@ export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
유출 유형
|
||||
</span>
|
||||
<ComboBox
|
||||
@ -98,7 +98,7 @@ export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
예측 알고리즘
|
||||
</span>
|
||||
<ComboBox
|
||||
@ -112,7 +112,7 @@ export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<p className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
위 컴포넌트는{' '}
|
||||
<code className="font-mono" style={{ color: t.textAccent }}>
|
||||
@common/components/ui/ComboBox
|
||||
@ -373,7 +373,7 @@ export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="py-2.5 px-4">
|
||||
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
{row.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -423,10 +423,16 @@ export const FloatDropdownContent = ({ theme }: FloatDropdownContentProps) => {
|
||||
: t.cardBorder,
|
||||
}}
|
||||
>
|
||||
<span className="font-korean text-sm font-medium" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-korean text-body-2 font-medium"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>
|
||||
<span
|
||||
className="font-korean text-caption leading-5"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
{item.desc}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -88,9 +88,9 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
|
||||
Modal
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
<code
|
||||
className="font-mono text-xs px-1.5 py-0.5 rounded"
|
||||
className="font-mono text-caption px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
|
||||
color: t.textAccent,
|
||||
@ -156,7 +156,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
{SIZE_CONFIG[activeSize].desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -166,7 +166,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-sm font-medium transition-opacity hover:opacity-80"
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-body-2 font-medium transition-opacity hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.15)' : 'rgba(6,182,212,0.12)',
|
||||
borderColor: t.textAccent,
|
||||
@ -178,7 +178,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsConfirmOpen(true)}
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-sm font-medium transition-opacity hover:opacity-80"
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-body-2 font-medium transition-opacity hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(239,68,68,0.10)' : 'rgba(239,68,68,0.06)',
|
||||
borderColor: 'rgba(239,68,68,0.40)',
|
||||
@ -200,7 +200,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
className="rounded-lg border border-solid p-5 flex flex-col gap-3"
|
||||
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
|
||||
>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
Confirm은 독립 컴포넌트가 아닌 <strong>Modal의 variant</strong>다. 타이틀 + 단문 메시지
|
||||
+ 취소/확인 2버튼 구성. 파괴적 작업(삭제, 초기화) 전 사용자 의도를 확인한다.
|
||||
</p>
|
||||
@ -423,7 +423,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
|
||||
>
|
||||
<span
|
||||
className="font-mono text-sm rounded border border-solid px-2 py-0.5 shrink-0"
|
||||
className="font-mono text-body-2 rounded border border-solid px-2 py-0.5 shrink-0"
|
||||
style={{ color: t.textAccent, borderColor: t.cardBorder }}
|
||||
>
|
||||
{row.range}
|
||||
@ -434,7 +434,10 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
>
|
||||
{row.status}
|
||||
</span>
|
||||
<span className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>
|
||||
<span
|
||||
className="font-korean text-caption leading-5"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
{row.desc}
|
||||
</span>
|
||||
</div>
|
||||
@ -480,7 +483,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
}}
|
||||
>
|
||||
<div className="py-2.5 px-4">
|
||||
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
||||
{item.component}
|
||||
</span>
|
||||
</div>
|
||||
@ -496,7 +499,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="py-2.5 px-4">
|
||||
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
{item.trigger}
|
||||
</span>
|
||||
</div>
|
||||
@ -533,7 +536,10 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
className="flex items-center justify-between px-5 py-4 border-b border-solid shrink-0"
|
||||
style={{ borderColor: modalBorder }}
|
||||
>
|
||||
<span className="font-korean text-sm font-medium" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-korean text-body-2 font-medium"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
Modal Preview — {SIZE_CONFIG[activeSize].label} ({SIZE_CONFIG[activeSize].width})
|
||||
</span>
|
||||
<button
|
||||
@ -542,16 +548,16 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
className="w-7 h-7 rounded flex items-center justify-center hover:opacity-70 transition-opacity"
|
||||
style={{ backgroundColor: isDark ? 'rgba(66,71,84,0.25)' : '#f1f5f9' }}
|
||||
>
|
||||
<span className="font-mono text-sm" style={{ color: t.textMuted }}>
|
||||
<span className="font-mono text-body-2" style={{ color: t.textMuted }}>
|
||||
✕
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-5 py-5 flex flex-col gap-3 overflow-y-auto">
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
이 모달은{' '}
|
||||
<code
|
||||
className="font-mono text-xs px-1 rounded"
|
||||
className="font-mono text-caption px-1 rounded"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
|
||||
color: t.textAccent,
|
||||
@ -568,7 +574,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
className="rounded border border-solid px-3 py-2.5"
|
||||
style={{ borderColor: t.cardBorder }}
|
||||
>
|
||||
<span className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
@ -582,7 +588,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-sm transition-opacity hover:opacity-70"
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-body-2 transition-opacity hover:opacity-70"
|
||||
style={{ borderColor: t.cardBorder, color: t.textMuted }}
|
||||
>
|
||||
취소
|
||||
@ -590,7 +596,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-4 py-2 rounded font-korean text-sm font-medium transition-opacity hover:opacity-80"
|
||||
className="px-4 py-2 rounded font-korean text-body-2 font-medium transition-opacity hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.18)' : 'rgba(6,182,212,0.14)',
|
||||
color: t.textAccent,
|
||||
@ -624,13 +630,16 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ color: '#ef4444', fontSize: '16px' }}>⚠</span>
|
||||
<span className="font-korean text-sm font-medium" style={{ color: t.textPrimary }}>
|
||||
<span
|
||||
className="font-korean text-body-2 font-medium"
|
||||
style={{ color: t.textPrimary }}
|
||||
>
|
||||
항목을 삭제하시겠습니까?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
삭제된 데이터는 복구할 수 없습니다. 계속 진행하시겠습니까?
|
||||
</p>
|
||||
</div>
|
||||
@ -641,7 +650,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsConfirmOpen(false)}
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-sm transition-opacity hover:opacity-70"
|
||||
className="px-4 py-2 rounded border border-solid font-korean text-body-2 transition-opacity hover:opacity-70"
|
||||
style={{ borderColor: t.cardBorder, color: t.textMuted }}
|
||||
>
|
||||
취소
|
||||
@ -649,7 +658,7 @@ export const FloatModalContent = ({ theme }: FloatModalContentProps) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsConfirmOpen(false)}
|
||||
className="px-4 py-2 rounded font-korean text-sm font-medium transition-opacity hover:opacity-80"
|
||||
className="px-4 py-2 rounded font-korean text-body-2 font-medium transition-opacity hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.12)',
|
||||
color: '#ef4444',
|
||||
|
||||
@ -63,10 +63,10 @@ export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
||||
<h2 className="font-sans text-xl font-bold" style={{ color: t.textPrimary }}>
|
||||
Overlay
|
||||
</h2>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
지도 컨테이너 위에
|
||||
<code
|
||||
className="font-mono text-xs px-1.5 py-0.5 mx-1 rounded"
|
||||
className="font-mono text-caption px-1.5 py-0.5 mx-1 rounded"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
|
||||
color: t.textAccent,
|
||||
@ -133,12 +133,12 @@ export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="py-2.5 px-4">
|
||||
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
{row.overlay}
|
||||
</span>
|
||||
</div>
|
||||
<div className="py-2.5 px-4">
|
||||
<span className="font-korean text-xs" style={{ color: t.textSecondary }}>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
{row.modal}
|
||||
</span>
|
||||
</div>
|
||||
@ -284,7 +284,7 @@ export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
||||
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
|
||||
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
|
||||
>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
<strong>ScatPopup</strong>은 지도 마커에 앵커된 컨텍스트 팝업이다. Modal(fixed 뷰포트
|
||||
중앙)과 달리 마커 위치에서 동적으로 좌표를 계산하며, 지도 패닝·줌 시 위치가 함께
|
||||
업데이트된다.
|
||||
@ -307,7 +307,7 @@ export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
||||
<span className="font-mono text-caption" style={{ color: t.textMuted }}>
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="font-mono text-xs" style={{ color: t.textAccent }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textAccent }}>
|
||||
{item.value}
|
||||
</span>
|
||||
<span className="font-korean text-caption" style={{ color: t.textSecondary }}>
|
||||
@ -323,7 +323,7 @@ export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
||||
borderColor: 'rgba(234,179,8,0.25)',
|
||||
}}
|
||||
>
|
||||
<span className="font-korean text-xs" style={{ color: '#eab308' }}>
|
||||
<span className="font-korean text-caption" style={{ color: '#eab308' }}>
|
||||
주의: ScatPopup은 MapLibre GL JS의 Popup/Marker 컴포넌트가 아닌 React DOM으로 구현됨.
|
||||
지도 컨테이너 내부에 position: absolute로 렌더링된다.
|
||||
</span>
|
||||
@ -377,7 +377,7 @@ export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
||||
}}
|
||||
>
|
||||
<div className="py-2.5 px-3">
|
||||
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>
|
||||
<span className="font-mono text-caption" style={{ color: t.textPrimary }}>
|
||||
{item.component}
|
||||
</span>
|
||||
</div>
|
||||
@ -413,7 +413,10 @@ export const FloatOverlayContent = ({ theme }: FloatOverlayContentProps) => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="py-2.5 px-3">
|
||||
<span className="font-korean text-xs leading-5" style={{ color: t.textSecondary }}>
|
||||
<span
|
||||
className="font-korean text-caption leading-5"
|
||||
style={{ color: t.textSecondary }}
|
||||
>
|
||||
{item.desc}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -84,10 +84,10 @@ export const FloatToastContent = ({ theme }: FloatToastContentProps) => {
|
||||
미구현 — 설계 사양
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
화면을 차단하지 않는 비파괴적 알림.
|
||||
<code
|
||||
className="font-mono text-xs px-1.5 py-0.5 mx-1 rounded"
|
||||
className="font-mono text-caption px-1.5 py-0.5 mx-1 rounded"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(76,215,246,0.08)' : 'rgba(6,182,212,0.06)',
|
||||
color: t.textAccent,
|
||||
@ -97,7 +97,7 @@ export const FloatToastContent = ({ theme }: FloatToastContentProps) => {
|
||||
</code>
|
||||
에 위치하며 일정 시간 후 자동으로 사라진다. 현재 프로젝트에서는{' '}
|
||||
<code
|
||||
className="font-mono text-xs px-1.5 py-0.5 mx-1 rounded"
|
||||
className="font-mono text-caption px-1.5 py-0.5 mx-1 rounded"
|
||||
style={{
|
||||
backgroundColor: isDark ? 'rgba(239,68,68,0.08)' : 'rgba(239,68,68,0.05)',
|
||||
color: '#ef4444',
|
||||
@ -129,7 +129,7 @@ export const FloatToastContent = ({ theme }: FloatToastContentProps) => {
|
||||
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
|
||||
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
|
||||
>
|
||||
<p className="font-korean text-xs" style={{ color: t.textMuted }}>
|
||||
<p className="font-korean text-caption" style={{ color: t.textMuted }}>
|
||||
버튼 클릭 시 화면 우하단에 Toast가 표시됩니다. 3초 후 자동으로 사라집니다.
|
||||
</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
@ -280,7 +280,7 @@ export const FloatToastContent = ({ theme }: FloatToastContentProps) => {
|
||||
<span className="font-mono text-lg" style={{ color: cfg.color }}>
|
||||
{cfg.icon}
|
||||
</span>
|
||||
<span className="font-mono text-sm font-bold" style={{ color: cfg.color }}>
|
||||
<span className="font-mono text-body-2 font-bold" style={{ color: cfg.color }}>
|
||||
{cfg.label}
|
||||
</span>
|
||||
</div>
|
||||
@ -311,7 +311,7 @@ export const FloatToastContent = ({ theme }: FloatToastContentProps) => {
|
||||
className="rounded-lg border border-solid p-5 flex flex-col gap-4"
|
||||
style={{ backgroundColor: t.cardBg, borderColor: t.cardBorder }}
|
||||
>
|
||||
<p className="font-korean text-sm leading-6" style={{ color: t.textSecondary }}>
|
||||
<p className="font-korean text-body-2 leading-6" style={{ color: t.textSecondary }}>
|
||||
Toast는 앱 어디서든 호출해야 하므로 <strong>Zustand store + useToast hook</strong>{' '}
|
||||
패턴을 권장한다. ToastContainer는 App.tsx 최상위에 한 번만 렌더링한다.
|
||||
</p>
|
||||
@ -387,7 +387,7 @@ export const FloatToastContent = ({ theme }: FloatToastContentProps) => {
|
||||
<span className="font-mono text-base shrink-0" style={{ color: cfg.color }}>
|
||||
{cfg.icon}
|
||||
</span>
|
||||
<span className="font-korean text-sm flex-1" style={{ color: t.textPrimary }}>
|
||||
<span className="font-korean text-body-2 flex-1" style={{ color: t.textPrimary }}>
|
||||
{toast.message}
|
||||
</span>
|
||||
<button
|
||||
|
||||
@ -6,7 +6,7 @@ interface AdminPlaceholderProps {
|
||||
const AdminPlaceholder = ({ label }: AdminPlaceholderProps) => (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||
<div className="text-4xl opacity-20">🚧</div>
|
||||
<div className="text-sm font-korean text-fg-sub font-semibold">{label}</div>
|
||||
<div className="text-body-2 font-korean text-fg-sub font-semibold">{label}</div>
|
||||
<div className="text-label-2 font-korean text-fg-disabled">해당 기능은 준비 중입니다.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -107,7 +107,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="px-4 py-3 border-b border-stroke bg-bg-elevated shrink-0">
|
||||
<div className="text-xs font-bold text-fg font-korean flex items-center gap-1.5">
|
||||
<div className="text-caption font-bold text-fg font-korean flex items-center gap-1.5">
|
||||
<span>⚙️</span> 관리자 설정
|
||||
</div>
|
||||
</div>
|
||||
@ -129,7 +129,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
|
||||
color: hasActiveChild ? 'var(--color-accent)' : 'var(--fg-default)',
|
||||
}}
|
||||
>
|
||||
<span className="text-sm">{section.icon}</span>
|
||||
<span className="text-body-2">{section.icon}</span>
|
||||
<span className="flex-1 text-left">{section.label}</span>
|
||||
<span
|
||||
className="text-caption text-fg-disabled transition-transform"
|
||||
|
||||
@ -103,7 +103,7 @@ function AssetUploadPanel() {
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-stroke flex-shrink-0">
|
||||
<h1 className="text-lg font-bold text-fg font-korean">자산 현행화</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">
|
||||
자산 데이터를 업로드하여 현행화합니다
|
||||
</p>
|
||||
</div>
|
||||
@ -115,7 +115,7 @@ function AssetUploadPanel() {
|
||||
<div className="flex-1 max-w-[560px] space-y-4">
|
||||
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">파일 업로드</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">파일 업로드</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4 space-y-4">
|
||||
{/* 드롭존 */}
|
||||
@ -135,12 +135,12 @@ function AssetUploadPanel() {
|
||||
>
|
||||
<div className="text-3xl mb-2 opacity-40">📁</div>
|
||||
{selectedFile ? (
|
||||
<div className="text-xs font-semibold text-color-accent font-korean mb-1">
|
||||
<div className="text-caption font-semibold text-color-accent font-korean mb-1">
|
||||
{selectedFile.name}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xs font-semibold text-fg-sub font-korean mb-1">
|
||||
<div className="text-caption font-semibold text-fg-sub font-korean mb-1">
|
||||
파일을 드래그하거나 클릭하여 업로드
|
||||
</div>
|
||||
<div className="text-caption text-fg-disabled font-korean mb-3">
|
||||
@ -148,7 +148,7 @@ function AssetUploadPanel() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-1.5 text-xs font-semibold rounded-md bg-color-accent text-bg-0
|
||||
className="px-4 py-1.5 text-caption font-semibold rounded-md bg-color-accent text-bg-0
|
||||
hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@ -176,7 +176,7 @@ function AssetUploadPanel() {
|
||||
<select
|
||||
value={assetCategory}
|
||||
onChange={(e) => setAssetCategory(e.target.value)}
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md
|
||||
text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
{ASSET_CATEGORIES.map((c) => (
|
||||
@ -195,7 +195,7 @@ function AssetUploadPanel() {
|
||||
<select
|
||||
value={jurisdiction}
|
||||
onChange={(e) => setJurisdiction(e.target.value)}
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md
|
||||
text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
{JURISDICTIONS.map((j) => (
|
||||
@ -212,7 +212,7 @@ function AssetUploadPanel() {
|
||||
업로드 방식
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-fg-sub font-korean">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer text-caption text-fg-sub font-korean">
|
||||
<input
|
||||
type="radio"
|
||||
checked={uploadMode === 'add'}
|
||||
@ -221,7 +221,7 @@ function AssetUploadPanel() {
|
||||
/>
|
||||
추가 (기존 + 신규)
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-fg-sub font-korean">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer text-caption text-fg-sub font-korean">
|
||||
<input
|
||||
type="radio"
|
||||
checked={uploadMode === 'replace'}
|
||||
@ -238,7 +238,7 @@ function AssetUploadPanel() {
|
||||
type="button"
|
||||
onClick={handleUpload}
|
||||
disabled={!selectedFile || uploaded}
|
||||
className={`w-full py-2.5 text-xs font-semibold rounded-md transition-all font-korean disabled:opacity-50 ${
|
||||
className={`w-full py-2.5 text-caption font-semibold rounded-md transition-all font-korean disabled:opacity-50 ${
|
||||
uploaded
|
||||
? 'bg-[rgba(34,197,94,0.15)] text-color-success border border-status-green/30'
|
||||
: 'bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
|
||||
@ -255,7 +255,7 @@ function AssetUploadPanel() {
|
||||
{/* 수정 권한 체계 */}
|
||||
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">수정 권한 체계</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">수정 권한 체계</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4 space-y-2">
|
||||
{PERM_ITEMS.map((p) => (
|
||||
@ -264,13 +264,15 @@ function AssetUploadPanel() {
|
||||
className="flex items-center gap-3 px-4 py-3 bg-bg-elevated border border-stroke rounded-md"
|
||||
>
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-sm flex-shrink-0"
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-body-2 flex-shrink-0"
|
||||
style={{ background: p.bg }}
|
||||
>
|
||||
{p.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
|
||||
<div className={`text-caption font-bold font-korean ${p.color}`}>
|
||||
{p.role}
|
||||
</div>
|
||||
<div className="text-caption text-fg-disabled font-korean mt-0.5">
|
||||
{p.desc}
|
||||
</div>
|
||||
@ -283,7 +285,7 @@ function AssetUploadPanel() {
|
||||
{/* 최근 업로드 이력 */}
|
||||
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">최근 업로드 이력</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">최근 업로드 이력</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4 space-y-2">
|
||||
{uploadHistory.length === 0 ? (
|
||||
@ -297,7 +299,9 @@ function AssetUploadPanel() {
|
||||
className="flex justify-between items-center px-4 py-3 bg-bg-elevated border border-stroke rounded-md"
|
||||
>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-fg font-korean">{h.fileNm}</div>
|
||||
<div className="text-caption font-semibold text-fg font-korean">
|
||||
{h.fileNm}
|
||||
</div>
|
||||
<div className="text-caption text-fg-disabled mt-0.5 font-korean">
|
||||
{formatDate(h.regDtm)} · {h.uploaderNm} · {h.uploadCnt.toLocaleString()}건
|
||||
</div>
|
||||
|
||||
@ -119,8 +119,8 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1">
|
||||
<h2 className="text-sm font-semibold text-fg">게시판 관리</h2>
|
||||
<span className="text-xs text-fg-disabled">총 {data?.totalCount ?? 0}건</span>
|
||||
<h2 className="text-body-2 font-semibold text-fg">게시판 관리</h2>
|
||||
<span className="text-caption text-fg-disabled">총 {data?.totalCount ?? 0}건</span>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 탭 + 검색 */}
|
||||
@ -130,7 +130,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
<button
|
||||
key={tab.code}
|
||||
onClick={() => handleCategoryChange(tab.code)}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
className={`px-3 py-1 text-caption rounded-full transition-colors ${
|
||||
activeCategory === tab.code
|
||||
? 'bg-blue-500/20 text-blue-400 font-medium'
|
||||
: 'text-fg-disabled hover:text-fg-sub hover:bg-bg-elevated'
|
||||
@ -146,11 +146,11 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
placeholder="제목/작성자 검색"
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg placeholder:text-text-4 w-48"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-fg placeholder:text-text-4 w-48"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg-sub hover:bg-bg-card"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-fg-sub hover:bg-bg-card"
|
||||
>
|
||||
검색
|
||||
</button>
|
||||
@ -162,7 +162,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={selected.size === 0 || deleting}
|
||||
className="px-3 py-1 text-xs rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 text-caption rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{deleting ? '삭제 중...' : `선택 삭제 (${selected.size})`}
|
||||
</button>
|
||||
@ -170,7 +170,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
|
||||
{/* 테이블 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full text-xs">
|
||||
<table className="w-full text-caption">
|
||||
<thead className="sticky top-0 bg-bg-surface z-10">
|
||||
<tr className="border-b border-stroke-1 text-fg-disabled">
|
||||
<th className="w-8 py-2 text-center">
|
||||
@ -222,7 +222,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
className="px-2 py-1 text-caption rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
@ -234,7 +234,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
className={`w-7 h-7 text-xs rounded ${
|
||||
className={`w-7 h-7 text-caption rounded ${
|
||||
p === page
|
||||
? 'bg-blue-500/20 text-blue-400 font-medium'
|
||||
: 'text-fg-disabled hover:bg-bg-elevated'
|
||||
@ -247,7 +247,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
className="px-2 py-1 text-caption rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
|
||||
@ -99,13 +99,15 @@ function CleanupEquipPanel() {
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-fg font-korean">방제장비 현황</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">총 {filtered.length}개 기관</p>
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">
|
||||
총 {filtered.length}개 기관
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={regionFilter}
|
||||
onChange={handleFilterChange(setRegionFilter)}
|
||||
className="px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="전체">전체 관할청</option>
|
||||
<option value="남해">남해청</option>
|
||||
@ -117,7 +119,7 @@ function CleanupEquipPanel() {
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={handleFilterChange(setTypeFilter)}
|
||||
className="px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="전체">전체 유형</option>
|
||||
{typeOptions.map((t) => (
|
||||
@ -129,7 +131,7 @@ function CleanupEquipPanel() {
|
||||
<select
|
||||
value={equipFilter}
|
||||
onChange={handleFilterChange(setEquipFilter)}
|
||||
className="px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="전체">전체 장비</option>
|
||||
<option value="방제선">방제선</option>
|
||||
@ -146,11 +148,11 @@ function CleanupEquipPanel() {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="w-56 px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-56 px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
<button
|
||||
onClick={load}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md bg-bg-elevated border border-stroke text-fg-sub hover:border-color-accent hover:text-color-accent transition-all font-korean"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md bg-bg-elevated border border-stroke text-fg-sub hover:border-color-accent hover:text-color-accent transition-all font-korean"
|
||||
>
|
||||
새로고침
|
||||
</button>
|
||||
@ -160,7 +162,7 @@ function CleanupEquipPanel() {
|
||||
{/* 테이블 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-body-2 font-korean">
|
||||
불러오는 중...
|
||||
</div>
|
||||
) : (
|
||||
@ -217,7 +219,7 @@ function CleanupEquipPanel() {
|
||||
<tr>
|
||||
<td
|
||||
colSpan={11}
|
||||
className="px-6 py-10 text-center text-xs text-fg-disabled font-korean"
|
||||
className="px-6 py-10 text-center text-caption text-fg-disabled font-korean"
|
||||
>
|
||||
조회된 기관이 없습니다.
|
||||
</td>
|
||||
|
||||
@ -211,7 +211,7 @@ const HEADERS = [
|
||||
function HrTable({ rows, loading }: { rows: HrCollectItem[]; loading: boolean }) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{HEADERS.map((h) => (
|
||||
@ -317,10 +317,10 @@ export default function CollectHrPanel() {
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||||
<h2 className="text-sm font-semibold text-t1">인사정보 수집 현황</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">인사정보 수집 현황</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -332,7 +332,7 @@ export default function CollectHrPanel() {
|
||||
<button
|
||||
onClick={fetchData}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
|
||||
@ -354,11 +354,11 @@ export default function CollectHrPanel() {
|
||||
|
||||
{/* 상태 표시줄 */}
|
||||
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-stroke-1 bg-bg-base">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-emerald-500/10 text-emerald-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-emerald-500/10 text-emerald-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
수집 완료 {completedCount}건
|
||||
</span>
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
전체 {rows.length}건 (활성: {activeCount} / 비활성: {rows.length - activeCount})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -130,7 +130,9 @@ const DispersingZonePanel = () => {
|
||||
onClick={() => handleToggleExpand(zone)}
|
||||
>
|
||||
<span className={`w-3 h-3 rounded-sm shrink-0 ${swatchColor}`} />
|
||||
<span className="flex-1 text-xs font-semibold text-fg font-korean">{info.label}</span>
|
||||
<span className="flex-1 text-caption font-semibold text-fg font-korean">
|
||||
{info.label}
|
||||
</span>
|
||||
{/* 토글 스위치 */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@ -209,7 +211,7 @@ const DispersingZonePanel = () => {
|
||||
<div className="w-[280px] bg-bg-surface border-l border-stroke flex flex-col overflow-hidden shrink-0">
|
||||
{/* 헤더 */}
|
||||
<div className="px-4 py-4 border-b border-stroke shrink-0">
|
||||
<h1 className="text-sm font-bold text-fg font-korean">유처리제 제한구역</h1>
|
||||
<h1 className="text-body-2 font-bold text-fg font-korean">유처리제 제한구역</h1>
|
||||
<p className="text-label-2 text-fg-disabled mt-0.5 font-korean">해양환경관리법 기준</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -186,7 +186,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
|
||||
};
|
||||
|
||||
const inputCls =
|
||||
'w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none';
|
||||
'w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none';
|
||||
const labelCls = 'block text-label-2 font-semibold text-fg-sub font-korean mb-1.5';
|
||||
|
||||
return (
|
||||
@ -194,7 +194,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
|
||||
<div className="bg-bg-surface border border-stroke rounded-lg shadow-lg w-[480px] max-h-[90vh] flex flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke shrink-0">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">
|
||||
{mode === 'create' ? '레이어 등록' : '레이어 수정'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-fg-disabled hover:text-fg transition-colors">
|
||||
@ -214,7 +214,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
|
||||
? handleParentChange(e.target.value)
|
||||
: handleField('upLayerCd', e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none"
|
||||
>
|
||||
<option value="">(없음)</option>
|
||||
{options
|
||||
@ -311,7 +311,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
|
||||
<select
|
||||
value={form.useYn}
|
||||
onChange={(e) => handleField('useYn', e.target.value)}
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none"
|
||||
>
|
||||
<option value="Y">사용</option>
|
||||
<option value="N">미사용</option>
|
||||
@ -329,14 +329,14 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-3 py-1.5 text-xs border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-3 py-1.5 text-caption border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="px-3 py-1.5 text-xs bg-color-accent text-bg-0 rounded hover:opacity-90 disabled:opacity-50 transition-all font-korean"
|
||||
className="px-3 py-1.5 text-caption bg-color-accent text-bg-0 rounded hover:opacity-90 disabled:opacity-50 transition-all font-korean"
|
||||
>
|
||||
{saving ? '저장 중...' : mode === 'create' ? '등록' : '저장'}
|
||||
</button>
|
||||
@ -449,11 +449,11 @@ const LayerPanel = () => {
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-fg font-korean">레이어 관리</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">총 {total}개</p>
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">총 {total}개</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setModal({ mode: 'create' })}
|
||||
className="px-3 py-1.5 text-xs font-semibold bg-color-accent text-bg-0 rounded hover:opacity-90 transition-opacity font-korean"
|
||||
className="px-3 py-1.5 text-caption font-semibold bg-color-accent text-bg-0 rounded hover:opacity-90 transition-opacity font-korean"
|
||||
>
|
||||
신규 등록
|
||||
</button>
|
||||
@ -465,12 +465,12 @@ const LayerPanel = () => {
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="레이어코드 / 레이어명 검색"
|
||||
className="flex-1 px-3 py-1.5 text-xs bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="flex-1 px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
<select
|
||||
value={filterUseYn}
|
||||
onChange={(e) => setFilterUseYn(e.target.value)}
|
||||
className="px-2 py-1.5 text-xs bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-2 py-1.5 text-caption bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
<option value="Y">사용</option>
|
||||
@ -478,7 +478,7 @@ const LayerPanel = () => {
|
||||
</select>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className="px-3 py-1.5 text-xs border border-stroke text-fg-sub rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-3 py-1.5 text-caption border border-stroke text-fg-sub rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
>
|
||||
검색
|
||||
</button>
|
||||
@ -487,7 +487,7 @@ const LayerPanel = () => {
|
||||
|
||||
{/* 오류 메시지 */}
|
||||
{error && (
|
||||
<div className="px-6 py-2 text-xs text-red-400 bg-[rgba(239,68,68,0.05)] border-b border-stroke shrink-0 font-korean">
|
||||
<div className="px-6 py-2 text-caption text-red-400 bg-[rgba(239,68,68,0.05)] border-b border-stroke shrink-0 font-korean">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@ -495,7 +495,7 @@ const LayerPanel = () => {
|
||||
{/* 테이블 영역 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full text-fg-disabled text-sm font-korean">
|
||||
<div className="flex items-center justify-center h-full text-fg-disabled text-body-2 font-korean">
|
||||
불러오는 중...
|
||||
</div>
|
||||
) : (
|
||||
@ -539,7 +539,7 @@ const LayerPanel = () => {
|
||||
<tr>
|
||||
<td
|
||||
colSpan={10}
|
||||
className="px-4 py-12 text-center text-fg-disabled text-sm font-korean"
|
||||
className="px-4 py-12 text-center text-fg-disabled text-body-2 font-korean"
|
||||
>
|
||||
데이터가 없습니다.
|
||||
</td>
|
||||
@ -551,15 +551,15 @@ const LayerPanel = () => {
|
||||
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
|
||||
>
|
||||
{/* 번호 */}
|
||||
<td className="px-4 py-3 text-xs text-fg-disabled font-mono">
|
||||
<td className="px-4 py-3 text-caption text-fg-disabled font-mono">
|
||||
{(page - 1) * PAGE_SIZE + idx + 1}
|
||||
</td>
|
||||
{/* 레이어코드 */}
|
||||
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">{item.layerCd}</td>
|
||||
{/* 레이어명 */}
|
||||
<td className="px-4 py-3 text-xs text-fg font-korean">{item.layerNm}</td>
|
||||
<td className="px-4 py-3 text-caption text-fg font-korean">{item.layerNm}</td>
|
||||
{/* 레이어전체명 */}
|
||||
<td className="px-4 py-3 text-xs text-fg-sub font-korean max-w-[200px]">
|
||||
<td className="px-4 py-3 text-caption text-fg-sub font-korean max-w-[200px]">
|
||||
<span className="block truncate" title={item.layerFullNm}>
|
||||
{item.layerFullNm}
|
||||
</span>
|
||||
@ -575,7 +575,7 @@ const LayerPanel = () => {
|
||||
{item.wmsLayerNm ?? <span className="text-fg-disabled">-</span>}
|
||||
</td>
|
||||
{/* 정렬순서 */}
|
||||
<td className="px-4 py-3 text-xs text-fg-disabled text-center font-mono">
|
||||
<td className="px-4 py-3 text-caption text-fg-disabled text-center font-mono">
|
||||
{item.sortOrd}
|
||||
</td>
|
||||
{/* 등록일시 */}
|
||||
@ -614,13 +614,13 @@ const LayerPanel = () => {
|
||||
<div className="flex items-center justify-center gap-1.5 flex-nowrap">
|
||||
<button
|
||||
onClick={() => setModal({ mode: 'edit', data: item })}
|
||||
className="px-3 py-1 text-xs rounded bg-[rgba(6,182,212,0.15)] text-color-accent hover:bg-[rgba(6,182,212,0.25)] font-korean whitespace-nowrap"
|
||||
className="px-3 py-1 text-caption rounded bg-[rgba(6,182,212,0.15)] text-color-accent hover:bg-[rgba(6,182,212,0.25)] font-korean whitespace-nowrap"
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.layerCd)}
|
||||
className="px-3 py-1 text-xs rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 font-korean whitespace-nowrap"
|
||||
className="px-3 py-1 text-caption rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 font-korean whitespace-nowrap"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
|
||||
@ -78,7 +78,7 @@ function MapBaseModal({
|
||||
<div className="bg-bg-surface border border-stroke rounded-lg shadow-lg w-[520px] max-h-[90vh] flex flex-col">
|
||||
{/* 모달 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">
|
||||
{isEdit ? '지도 수정' : '지도 등록'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-fg-disabled hover:text-fg transition-colors">
|
||||
@ -108,7 +108,7 @@ function MapBaseModal({
|
||||
value={form.mapNm}
|
||||
onChange={(e) => setField('mapNm', e.target.value)}
|
||||
placeholder="지도 이름을 입력하세요"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -123,7 +123,7 @@ function MapBaseModal({
|
||||
onChange={(e) => setField('mapKey', e.target.value)}
|
||||
placeholder="고유 식별 키 (영문/숫자)"
|
||||
disabled={isEdit}
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -135,7 +135,7 @@ function MapBaseModal({
|
||||
<select
|
||||
value={form.mapLevelCd}
|
||||
onChange={(e) => setField('mapLevelCd', e.target.value)}
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
{MAP_LEVEL_OPTIONS.map((opt) => (
|
||||
@ -156,7 +156,7 @@ function MapBaseModal({
|
||||
value={form.mapSrc}
|
||||
onChange={(e) => setField('mapSrc', e.target.value)}
|
||||
placeholder="타일 URL 또는 파일 경로"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -170,7 +170,7 @@ function MapBaseModal({
|
||||
value={form.mapDc}
|
||||
onChange={(e) => setField('mapDc', e.target.value)}
|
||||
placeholder="지도에 대한 설명을 입력하세요"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean resize-none"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -193,7 +193,7 @@ function MapBaseModal({
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-xs text-fg-sub font-korean">
|
||||
<span className="text-caption text-fg-sub font-korean">
|
||||
{form.useYn === 'Y' ? '사용' : '미사용'}
|
||||
</span>
|
||||
</div>
|
||||
@ -208,14 +208,14 @@ function MapBaseModal({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-xs border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-4 py-2 text-caption border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all disabled:opacity-50 font-korean"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all disabled:opacity-50 font-korean"
|
||||
>
|
||||
{saving ? '저장 중...' : isEdit ? '수정' : '등록'}
|
||||
</button>
|
||||
@ -350,11 +350,11 @@ function MapBasePanel() {
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-fg font-korean">지도 관리</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">총 {total}건</p>
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">총 {total}건</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => openModal(null)}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
|
||||
>
|
||||
+ 등록
|
||||
</button>
|
||||
@ -375,7 +375,7 @@ function MapBasePanel() {
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full text-xs">
|
||||
<table className="w-full text-caption">
|
||||
<thead className="sticky top-0 bg-bg-surface z-10">
|
||||
<tr className="border-b border-stroke text-fg-disabled">
|
||||
<th className="w-12 py-3 text-center">번호</th>
|
||||
@ -433,7 +433,7 @@ function MapBasePanel() {
|
||||
<td className="py-3 text-center">
|
||||
<button
|
||||
onClick={() => openModal(item)}
|
||||
className="px-3 py-1 text-xs rounded bg-[rgba(6,182,212,0.15)] text-color-accent hover:bg-[rgba(6,182,212,0.25)]"
|
||||
className="px-3 py-1 text-caption rounded bg-[rgba(6,182,212,0.15)] text-color-accent hover:bg-[rgba(6,182,212,0.25)]"
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
@ -441,7 +441,7 @@ function MapBasePanel() {
|
||||
<td className="py-3 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(item)}
|
||||
className="px-3 py-1 text-xs rounded bg-red-500/20 text-red-400 hover:bg-red-500/30"
|
||||
className="px-3 py-1 text-caption rounded bg-red-500/20 text-red-400 hover:bg-red-500/30"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
@ -452,7 +452,7 @@ function MapBasePanel() {
|
||||
</tbody>
|
||||
</table>
|
||||
{!loading && items.length === 0 && (
|
||||
<div className="flex items-center justify-center h-32 text-xs text-fg-disabled font-korean">
|
||||
<div className="flex items-center justify-center h-32 text-caption text-fg-disabled font-korean">
|
||||
등록된 지도가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
@ -464,7 +464,7 @@ function MapBasePanel() {
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
className="px-2 py-1 text-caption rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
@ -476,7 +476,7 @@ function MapBasePanel() {
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
className={`w-7 h-7 text-xs rounded ${
|
||||
className={`w-7 h-7 text-caption rounded ${
|
||||
p === page
|
||||
? 'bg-blue-500/20 text-blue-400 font-medium'
|
||||
: 'text-fg-disabled hover:bg-bg-elevated'
|
||||
@ -489,7 +489,7 @@ function MapBasePanel() {
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
className="px-2 py-1 text-caption rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
|
||||
@ -124,7 +124,7 @@ function MenusPanel() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-fg-disabled text-sm font-korean">메뉴 설정을 불러오는 중...</div>
|
||||
<div className="text-fg-disabled text-body-2 font-korean">메뉴 설정을 불러오는 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -136,14 +136,14 @@ function MenusPanel() {
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-fg font-korean">메뉴 관리</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">
|
||||
메뉴 표시 여부, 순서, 라벨, 아이콘을 관리합니다
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || saving}
|
||||
className={`px-4 py-2 text-xs font-semibold rounded-md transition-all font-korean ${
|
||||
className={`px-4 py-2 text-caption font-semibold rounded-md transition-all font-korean ${
|
||||
hasChanges && !saving
|
||||
? 'bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
|
||||
: 'bg-bg-card text-fg-disabled cursor-not-allowed'
|
||||
@ -188,7 +188,7 @@ function MenusPanel() {
|
||||
<DragOverlay>
|
||||
{activeMenu ? (
|
||||
<div className="flex items-center gap-3 px-4 py-3 rounded-md border border-color-accent bg-bg-surface shadow-lg opacity-90 max-w-[700px]">
|
||||
<span className="text-fg-disabled text-xs">⠿</span>
|
||||
<span className="text-fg-disabled text-caption">⠿</span>
|
||||
<span className="text-title-2">{activeMenu.icon}</span>
|
||||
<span className="text-title-4 font-semibold text-fg font-korean">
|
||||
{activeMenu.label}
|
||||
|
||||
@ -45,24 +45,24 @@ function formatTime(iso: string | null): string {
|
||||
|
||||
function StatusCell({ row }: { row: NumericalDataStatus }) {
|
||||
if (row.lastStatus === 'COMPLETED') {
|
||||
return <span className="text-emerald-400 text-xs">정상</span>;
|
||||
return <span className="text-emerald-400 text-caption">정상</span>;
|
||||
}
|
||||
if (row.lastStatus === 'FAILED') {
|
||||
return (
|
||||
<span className="text-red-400 text-xs">
|
||||
<span className="text-red-400 text-caption">
|
||||
오류{row.consecutiveFailures > 0 ? ` (${row.consecutiveFailures}회 연속)` : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (row.lastStatus === 'STARTED') {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-cyan-400 text-xs">
|
||||
<span className="inline-flex items-center gap-1 text-cyan-400 text-caption">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||
실행 중
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className="text-t3 text-xs">-</span>;
|
||||
return <span className="text-t3 text-caption">-</span>;
|
||||
}
|
||||
|
||||
function StatusBadge({
|
||||
@ -76,7 +76,7 @@ function StatusBadge({
|
||||
}) {
|
||||
if (loading) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-elevated text-t2">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-bg-elevated text-t2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||
조회 중...
|
||||
</span>
|
||||
@ -84,7 +84,7 @@ function StatusBadge({
|
||||
}
|
||||
if (errorCount === total && total > 0) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-red-500/10 text-red-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-red-500/10 text-red-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-400" />
|
||||
연계 오류
|
||||
</span>
|
||||
@ -92,14 +92,14 @@ function StatusBadge({
|
||||
}
|
||||
if (errorCount > 0) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-yellow-500/10 text-yellow-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-yellow-500/10 text-yellow-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-yellow-400" />
|
||||
일부 오류 ({errorCount}/{total})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-emerald-500/10 text-emerald-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-emerald-500/10 text-emerald-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
정상
|
||||
</span>
|
||||
@ -118,7 +118,7 @@ const TABLE_HEADERS = [
|
||||
function ForecastTable({ rows, loading }: { rows: NumericalDataStatus[]; loading: boolean }) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{TABLE_HEADERS.map((h) => (
|
||||
@ -193,10 +193,10 @@ export default function MonitorForecastPanel() {
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||||
<h2 className="text-sm font-semibold text-t1">수치예측자료 모니터링</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">수치예측자료 모니터링</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -208,7 +208,7 @@ export default function MonitorForecastPanel() {
|
||||
<button
|
||||
onClick={() => void fetchData()}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
|
||||
@ -234,7 +234,7 @@ export default function MonitorForecastPanel() {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2.5 text-xs font-medium border-b-2 transition-colors ${
|
||||
className={`px-4 py-2.5 text-caption font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-cyan-400 text-cyan-400'
|
||||
: 'border-transparent text-t3 hover:text-t2'
|
||||
@ -248,7 +248,9 @@ export default function MonitorForecastPanel() {
|
||||
{/* 상태 표시줄 */}
|
||||
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-stroke-1 bg-bg-base">
|
||||
<StatusBadge loading={loading} errorCount={errorCount} total={totalCount} />
|
||||
{!loading && totalCount > 0 && <span className="text-xs text-t3">모델 {totalCount}개</span>}
|
||||
{!loading && totalCount > 0 && (
|
||||
<span className="text-caption text-t3">모델 {totalCount}개</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 테이블 */}
|
||||
|
||||
@ -84,7 +84,7 @@ function StatusBadge({
|
||||
}) {
|
||||
if (loading) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-elevated text-t2">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-bg-elevated text-t2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||
조회 중...
|
||||
</span>
|
||||
@ -92,7 +92,7 @@ function StatusBadge({
|
||||
}
|
||||
if (errorCount === total) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-red-500/10 text-red-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-red-500/10 text-red-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-400" />
|
||||
연계 오류
|
||||
</span>
|
||||
@ -100,14 +100,14 @@ function StatusBadge({
|
||||
}
|
||||
if (errorCount > 0) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-yellow-500/10 text-yellow-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-yellow-500/10 text-yellow-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-yellow-400" />
|
||||
일부 오류 ({errorCount}/{total})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-emerald-500/10 text-emerald-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-emerald-500/10 text-emerald-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
정상
|
||||
</span>
|
||||
@ -130,7 +130,7 @@ function KhoaTable({ rows, loading }: { rows: KhoaRow[]; loading: boolean }) {
|
||||
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{headers.map((h) => (
|
||||
@ -172,11 +172,11 @@ function KhoaTable({ rows, loading }: { rows: KhoaRow[]; loading: boolean }) {
|
||||
<td className="px-3 py-2 text-t2">{fmt(row.data?.tide_level, 0)}</td>
|
||||
<td className="px-3 py-2">
|
||||
{row.error ? (
|
||||
<span className="text-red-400 text-xs">오류</span>
|
||||
<span className="text-red-400 text-caption">오류</span>
|
||||
) : row.data ? (
|
||||
<span className="text-emerald-400 text-xs">정상</span>
|
||||
<span className="text-emerald-400 text-caption">정상</span>
|
||||
) : (
|
||||
<span className="text-t3 text-xs">-</span>
|
||||
<span className="text-t3 text-caption">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@ -201,7 +201,7 @@ function KmaUltraTable({ rows, loading }: { rows: KmaUltraRow[]; loading: boolea
|
||||
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{headers.map((h) => (
|
||||
@ -241,11 +241,11 @@ function KmaUltraTable({ rows, loading }: { rows: KmaUltraRow[]; loading: boolea
|
||||
<td className="px-3 py-2 text-t2">{fmt(row.data?.humidity, 0)}</td>
|
||||
<td className="px-3 py-2">
|
||||
{row.error ? (
|
||||
<span className="text-red-400 text-xs">오류</span>
|
||||
<span className="text-red-400 text-caption">오류</span>
|
||||
) : row.data ? (
|
||||
<span className="text-emerald-400 text-xs">정상</span>
|
||||
<span className="text-emerald-400 text-caption">정상</span>
|
||||
) : (
|
||||
<span className="text-t3 text-xs">-</span>
|
||||
<span className="text-t3 text-caption">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@ -261,7 +261,7 @@ function MarineTable({ rows, loading }: { rows: MarineRow[]; loading: boolean })
|
||||
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{headers.map((h) => (
|
||||
@ -294,11 +294,11 @@ function MarineTable({ rows, loading }: { rows: MarineRow[]; loading: boolean })
|
||||
<td className="px-3 py-2 text-t2">{fmt(row.data?.temperature)}</td>
|
||||
<td className="px-3 py-2">
|
||||
{row.error ? (
|
||||
<span className="text-red-400 text-xs">오류</span>
|
||||
<span className="text-red-400 text-caption">오류</span>
|
||||
) : row.data ? (
|
||||
<span className="text-emerald-400 text-xs">정상</span>
|
||||
<span className="text-emerald-400 text-caption">정상</span>
|
||||
) : (
|
||||
<span className="text-t3 text-xs">-</span>
|
||||
<span className="text-t3 text-caption">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@ -441,10 +441,10 @@ export default function MonitorRealtimePanel() {
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||||
<h2 className="text-sm font-semibold text-t1">실시간 관측자료 모니터링</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">실시간 관측자료 모니터링</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -456,7 +456,7 @@ export default function MonitorRealtimePanel() {
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${isLoading ? 'animate-spin' : ''}`}
|
||||
@ -482,7 +482,7 @@ export default function MonitorRealtimePanel() {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2.5 text-xs font-medium border-b-2 transition-colors ${
|
||||
className={`px-4 py-2.5 text-caption font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-cyan-400 text-cyan-400'
|
||||
: 'border-transparent text-t3 hover:text-t2'
|
||||
@ -496,7 +496,7 @@ export default function MonitorRealtimePanel() {
|
||||
{/* 상태 표시줄 */}
|
||||
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-stroke-1 bg-bg-base">
|
||||
<StatusBadge loading={isLoading} errorCount={errorCount} total={totalCount} />
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
{activeTab === 'khoa' && `관측소 ${totalCount}개`}
|
||||
{activeTab === 'kma-ultra' && `지점 ${totalCount}개`}
|
||||
{activeTab === 'kma-marine' && `해역 ${totalCount}개`}
|
||||
|
||||
@ -300,7 +300,7 @@ function StatusBadge({
|
||||
}) {
|
||||
if (loading) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-elevated text-t2">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-bg-elevated text-t2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||
조회 중...
|
||||
</span>
|
||||
@ -309,7 +309,7 @@ function StatusBadge({
|
||||
const offCount = total - onCount;
|
||||
if (offCount === total) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-red-500/10 text-red-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-red-500/10 text-red-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-400" />
|
||||
전체 OFF
|
||||
</span>
|
||||
@ -317,14 +317,14 @@ function StatusBadge({
|
||||
}
|
||||
if (offCount > 0) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-yellow-500/10 text-yellow-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-yellow-500/10 text-yellow-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-yellow-400" />
|
||||
일부 OFF ({offCount}/{total})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-emerald-500/10 text-emerald-400">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-emerald-500/10 text-emerald-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
전체 정상
|
||||
</span>
|
||||
@ -376,7 +376,7 @@ const HEADERS = [
|
||||
function VesselTable({ rows, loading }: { rows: VesselMonitorRow[]; loading: boolean }) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{HEADERS.map((h) => (
|
||||
@ -462,10 +462,10 @@ export default function MonitorVesselPanel() {
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||||
<h2 className="text-sm font-semibold text-t1">선박위치정보 모니터링</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">선박위치정보 모니터링</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -477,7 +477,7 @@ export default function MonitorVesselPanel() {
|
||||
<button
|
||||
onClick={fetchData}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
|
||||
@ -500,7 +500,7 @@ export default function MonitorVesselPanel() {
|
||||
{/* 상태 표시줄 */}
|
||||
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-stroke-1 bg-bg-base">
|
||||
<StatusBadge loading={loading} onCount={onCount} total={rows.length} />
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
연계 채널 {rows.length}개 (ON: {onCount} / OFF: {rows.length - onCount})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -557,7 +557,7 @@ function RolePermTab({
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-fg-disabled text-sm font-korean">
|
||||
<div className="flex-1 flex items-center justify-center text-fg-disabled text-body-2 font-korean">
|
||||
역할을 선택하세요
|
||||
</div>
|
||||
)}
|
||||
@ -567,7 +567,7 @@ function RolePermTab({
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="w-[400px] bg-bg-surface rounded-lg border border-stroke shadow-2xl">
|
||||
<div className="px-5 py-4 border-b border-stroke">
|
||||
<h3 className="text-sm font-bold text-fg font-korean">새 역할 추가</h3>
|
||||
<h3 className="text-body-2 font-bold text-fg font-korean">새 역할 추가</h3>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex flex-col gap-3">
|
||||
<div>
|
||||
@ -581,7 +581,7 @@ function RolePermTab({
|
||||
setNewRoleCode(e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g, ''))
|
||||
}
|
||||
placeholder="CUSTOM_ROLE"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
/>
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">
|
||||
영문 대문자, 숫자, 언더스코어만 허용 (생성 후 변경 불가)
|
||||
@ -596,7 +596,7 @@ function RolePermTab({
|
||||
value={newRoleName}
|
||||
onChange={(e) => setNewRoleName(e.target.value)}
|
||||
placeholder="사용자 정의 역할"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -608,7 +608,7 @@ function RolePermTab({
|
||||
value={newRoleDesc}
|
||||
onChange={(e) => setNewRoleDesc(e.target.value)}
|
||||
placeholder="역할에 대한 설명"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
</div>
|
||||
{createError && (
|
||||
@ -620,14 +620,14 @@ function RolePermTab({
|
||||
<div className="px-5 py-3 border-t border-stroke flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setShowCreateForm(false)}
|
||||
className="px-4 py-2 text-xs text-fg-disabled border border-stroke rounded-md hover:bg-bg-surface-hover font-korean"
|
||||
className="px-4 py-2 text-caption text-fg-disabled border border-stroke rounded-md hover:bg-bg-surface-hover font-korean"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreateRole}
|
||||
disabled={!newRoleCode || !newRoleName || creating}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean disabled:opacity-50"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean disabled:opacity-50"
|
||||
>
|
||||
{creating ? '생성 중...' : '생성'}
|
||||
</button>
|
||||
@ -815,7 +815,7 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
|
||||
onFocus={() => setShowDropdown(true)}
|
||||
placeholder={loadingUsers ? '불러오는 중...' : '이름, 계정, 조직으로 검색...'}
|
||||
disabled={loadingUsers}
|
||||
className="w-full max-w-sm px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean disabled:opacity-50"
|
||||
className="w-full max-w-sm px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean disabled:opacity-50"
|
||||
/>
|
||||
{showDropdown && filteredUsers.length > 0 && (
|
||||
<div className="absolute left-0 top-full mt-1 w-full max-w-sm bg-bg-surface border border-stroke rounded-md shadow-xl z-20 overflow-auto max-h-52">
|
||||
@ -826,7 +826,7 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
|
||||
className="w-full px-3 py-2 text-left hover:bg-bg-surface-hover transition-colors flex items-center gap-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-xs font-semibold text-fg font-korean truncate">
|
||||
<div className="text-caption font-semibold text-fg font-korean truncate">
|
||||
{user.name}
|
||||
{user.rank && (
|
||||
<span className="ml-1 text-caption text-fg-disabled font-korean">
|
||||
@ -848,7 +848,7 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
|
||||
</div>
|
||||
)}
|
||||
{showDropdown && !loadingUsers && filteredUsers.length === 0 && searchQuery && (
|
||||
<div className="absolute left-0 top-full mt-1 w-full max-w-sm bg-bg-surface border border-stroke rounded-md shadow-xl z-20 px-3 py-2 text-xs text-fg-disabled font-korean">
|
||||
<div className="absolute left-0 top-full mt-1 w-full max-w-sm bg-bg-surface border border-stroke rounded-md shadow-xl z-20 px-3 py-2 text-caption text-fg-disabled font-korean">
|
||||
검색 결과 없음
|
||||
</div>
|
||||
)}
|
||||
@ -954,13 +954,13 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-fg-disabled text-sm font-korean">
|
||||
<div className="flex-1 flex items-center justify-center text-fg-disabled text-body-2 font-korean">
|
||||
역할을 하나 이상 할당하면 유효 권한이 표시됩니다
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-fg-disabled text-sm font-korean">
|
||||
<div className="flex-1 flex items-center justify-center text-fg-disabled text-body-2 font-korean">
|
||||
사용자를 선택하세요
|
||||
</div>
|
||||
)}
|
||||
@ -1180,7 +1180,7 @@ function PermissionsPanel() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-body-2 font-korean">
|
||||
불러오는 중...
|
||||
</div>
|
||||
);
|
||||
@ -1194,7 +1194,7 @@ function PermissionsPanel() {
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-sm font-bold text-fg font-korean">권한 관리</h1>
|
||||
<h1 className="text-body-2 font-bold text-fg font-korean">권한 관리</h1>
|
||||
<p className="text-caption text-fg-disabled mt-0.5 font-korean">
|
||||
역할별 리소스 × CRUD 권한 설정
|
||||
</p>
|
||||
@ -1203,7 +1203,7 @@ function PermissionsPanel() {
|
||||
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg border border-stroke">
|
||||
<button
|
||||
onClick={() => setActiveTab('role')}
|
||||
className={`px-4 py-1.5 text-xs font-semibold rounded-md transition-all font-korean ${
|
||||
className={`px-4 py-1.5 text-caption font-semibold rounded-md transition-all font-korean ${
|
||||
activeTab === 'role'
|
||||
? 'bg-color-accent text-bg-0 shadow-[0_0_8px_rgba(6,182,212,0.25)]'
|
||||
: 'text-fg-disabled hover:text-fg-sub'
|
||||
@ -1213,7 +1213,7 @@ function PermissionsPanel() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('user')}
|
||||
className={`px-4 py-1.5 text-xs font-semibold rounded-md transition-all font-korean ${
|
||||
className={`px-4 py-1.5 text-caption font-semibold rounded-md transition-all font-korean ${
|
||||
activeTab === 'user'
|
||||
? 'bg-color-accent text-bg-0 shadow-[0_0_8px_rgba(6,182,212,0.25)]'
|
||||
: 'text-fg-disabled hover:text-fg-sub'
|
||||
|
||||
@ -136,7 +136,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-fg font-korean">{title}</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">총 {total}개</p>
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">총 {total}개</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -146,12 +146,12 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="레이어코드 / 레이어명 검색"
|
||||
className="flex-1 px-3 py-1.5 text-xs bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="flex-1 px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
<select
|
||||
value={filterUseYn}
|
||||
onChange={(e) => setFilterUseYn(e.target.value)}
|
||||
className="px-2 py-1.5 text-xs bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-2 py-1.5 text-caption bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
<option value="Y">사용</option>
|
||||
@ -159,7 +159,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
</select>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className="px-3 py-1.5 text-xs border border-stroke text-fg-sub rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-3 py-1.5 text-caption border border-stroke text-fg-sub rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
>
|
||||
검색
|
||||
</button>
|
||||
@ -168,7 +168,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
|
||||
{/* 오류 메시지 */}
|
||||
{error && (
|
||||
<div className="px-6 py-2 text-xs text-red-400 bg-[rgba(239,68,68,0.05)] border-b border-stroke shrink-0 font-korean">
|
||||
<div className="px-6 py-2 text-caption text-red-400 bg-[rgba(239,68,68,0.05)] border-b border-stroke shrink-0 font-korean">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@ -176,7 +176,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
{/* 테이블 영역 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full text-fg-disabled text-sm font-korean">
|
||||
<div className="flex items-center justify-center h-full text-fg-disabled text-body-2 font-korean">
|
||||
불러오는 중...
|
||||
</div>
|
||||
) : (
|
||||
@ -217,7 +217,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
<tr>
|
||||
<td
|
||||
colSpan={9}
|
||||
className="px-4 py-12 text-center text-fg-disabled text-sm font-korean"
|
||||
className="px-4 py-12 text-center text-fg-disabled text-body-2 font-korean"
|
||||
>
|
||||
데이터가 없습니다.
|
||||
</td>
|
||||
@ -228,12 +228,12 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
key={item.layerCd}
|
||||
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3 text-xs text-fg-disabled font-mono">
|
||||
<td className="px-4 py-3 text-caption text-fg-disabled font-mono">
|
||||
{(page - 1) * PAGE_SIZE + idx + 1}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">{item.layerCd}</td>
|
||||
<td className="px-4 py-3 text-xs text-fg font-korean">{item.layerNm}</td>
|
||||
<td className="px-4 py-3 text-xs text-fg-sub font-korean max-w-[200px]">
|
||||
<td className="px-4 py-3 text-caption text-fg font-korean">{item.layerNm}</td>
|
||||
<td className="px-4 py-3 text-caption text-fg-sub font-korean max-w-[200px]">
|
||||
<span className="block truncate" title={item.layerFullNm}>
|
||||
{item.layerFullNm}
|
||||
</span>
|
||||
@ -246,7 +246,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
|
||||
<td className="px-4 py-3 text-label-2 text-fg-sub font-mono">
|
||||
{item.wmsLayerNm ?? <span className="text-fg-disabled">-</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs text-fg-disabled text-center font-mono">
|
||||
<td className="px-4 py-3 text-caption text-fg-disabled text-center font-mono">
|
||||
{item.sortOrd}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono">
|
||||
|
||||
@ -54,7 +54,7 @@ function SettingsPanel() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-body-2 font-korean">
|
||||
불러오는 중...
|
||||
</div>
|
||||
);
|
||||
@ -64,7 +64,7 @@ function SettingsPanel() {
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="px-6 py-4 border-b border-stroke">
|
||||
<h1 className="text-lg font-bold text-fg font-korean">시스템 설정</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">
|
||||
사용자 등록 및 권한 관련 시스템 설정을 관리합니다
|
||||
</p>
|
||||
</div>
|
||||
@ -74,7 +74,7 @@ function SettingsPanel() {
|
||||
{/* 사용자 등록 설정 */}
|
||||
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">사용자 등록 설정</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">사용자 등록 설정</h2>
|
||||
<p className="text-label-2 text-fg-disabled mt-0.5 font-korean">
|
||||
신규 사용자 등록 시 적용되는 정책을 설정합니다
|
||||
</p>
|
||||
@ -140,7 +140,7 @@ function SettingsPanel() {
|
||||
{/* OAuth 설정 */}
|
||||
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">Google OAuth 설정</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">Google OAuth 설정</h2>
|
||||
<p className="text-label-2 text-fg-disabled mt-0.5 font-korean">
|
||||
Google 계정 로그인 시 자동 승인할 이메일 도메인을 설정합니다
|
||||
</p>
|
||||
@ -162,7 +162,7 @@ function SettingsPanel() {
|
||||
value={oauthDomainInput}
|
||||
onChange={(e) => setOauthDomainInput(e.target.value)}
|
||||
placeholder="gcsc.co.kr, example.com"
|
||||
className="flex-1 px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
className="flex-1 px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
@ -183,7 +183,7 @@ function SettingsPanel() {
|
||||
savingOAuth ||
|
||||
oauthDomainInput.trim() === (oauthSettings?.autoApproveDomains || '')
|
||||
}
|
||||
className={`px-4 py-2 text-xs font-semibold rounded-md transition-all font-korean whitespace-nowrap ${
|
||||
className={`px-4 py-2 text-caption font-semibold rounded-md transition-all font-korean whitespace-nowrap ${
|
||||
oauthDomainInput.trim() !== (oauthSettings?.autoApproveDomains || '')
|
||||
? 'bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
|
||||
: 'bg-bg-card text-fg-disabled cursor-not-allowed'
|
||||
@ -220,7 +220,7 @@ function SettingsPanel() {
|
||||
{/* 현재 설정 상태 요약 */}
|
||||
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
|
||||
<div className="px-5 py-3 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">설정 상태 요약</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">설정 상태 요약</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<div className="flex flex-col gap-3 text-label-1 font-korean">
|
||||
|
||||
@ -71,7 +71,7 @@ function SortableMenuItem({
|
||||
<circle cx="9" cy="14" r="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="text-fg-disabled text-xs font-mono w-6 text-center shrink-0">
|
||||
<span className="text-fg-disabled text-caption font-mono w-6 text-center shrink-0">
|
||||
{idx + 1}
|
||||
</span>
|
||||
{isEditing ? (
|
||||
@ -152,14 +152,14 @@ function SortableMenuItem({
|
||||
<button
|
||||
onClick={() => onMove(idx, -1)}
|
||||
disabled={idx === 0}
|
||||
className="w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-xs flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
className="w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-caption flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
▲
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onMove(idx, 1)}
|
||||
disabled={idx === totalCount - 1}
|
||||
className="w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-xs flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
className="w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-caption flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
▼
|
||||
</button>
|
||||
|
||||
@ -87,7 +87,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
<div className="bg-bg-surface border border-stroke rounded-lg shadow-lg w-[480px] max-h-[90vh] flex flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
|
||||
<h2 className="text-sm font-bold text-fg font-korean">사용자 등록</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">사용자 등록</h2>
|
||||
<button onClick={onClose} className="text-fg-disabled hover:text-fg transition-colors">
|
||||
<svg
|
||||
width="16"
|
||||
@ -115,7 +115,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
value={account}
|
||||
onChange={(e) => setAccount(e.target.value)}
|
||||
placeholder="로그인 계정 ID"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -129,7 +129,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="초기 비밀번호"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -143,7 +143,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="실명"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -157,7 +157,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
value={rank}
|
||||
onChange={(e) => setRank(e.target.value)}
|
||||
placeholder="예: 팀장, 주임 등"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -169,7 +169,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
<select
|
||||
value={orgSn}
|
||||
onChange={(e) => setOrgSn(e.target.value !== '' ? Number(e.target.value) : '')}
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="">소속 없음</option>
|
||||
{allOrgs.map((org) => (
|
||||
@ -191,7 +191,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="이메일 주소"
|
||||
className="w-full px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
className="w-full px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -217,7 +217,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
onChange={() => toggleRole(role.sn)}
|
||||
style={{ accentColor: color }}
|
||||
/>
|
||||
<span className="text-xs font-korean" style={{ color }}>
|
||||
<span className="text-caption font-korean" style={{ color }}>
|
||||
{role.name}
|
||||
</span>
|
||||
<span className="text-caption text-fg-disabled font-mono">{role.code}</span>
|
||||
@ -237,14 +237,14 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-xs border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-4 py-2 text-caption border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all disabled:opacity-50 font-korean"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all disabled:opacity-50 font-korean"
|
||||
>
|
||||
{submitting ? '등록 중...' : '등록'}
|
||||
</button>
|
||||
@ -332,7 +332,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-fg font-korean">사용자 정보</h2>
|
||||
<h2 className="text-body-2 font-bold text-fg font-korean">사용자 정보</h2>
|
||||
<p className="text-caption text-fg-disabled font-mono mt-0.5">{user.account}</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-fg-disabled hover:text-fg transition-colors">
|
||||
@ -364,7 +364,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-1.5 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
@ -377,7 +377,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
value={rank}
|
||||
onChange={(e) => setRank(e.target.value)}
|
||||
placeholder="예: 팀장"
|
||||
className="w-full px-3 py-1.5 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -387,7 +387,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
<select
|
||||
value={orgSn}
|
||||
onChange={(e) => setOrgSn(e.target.value !== '' ? Number(e.target.value) : '')}
|
||||
className="w-full px-3 py-1.5 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-full px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="">소속 없음</option>
|
||||
{allOrgs.map((org) => (
|
||||
@ -427,7 +427,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="새 비밀번호 입력"
|
||||
className="w-full px-3 py-1.5 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
className="w-full px-3 py-1.5 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@ -535,7 +535,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
<div className="flex items-center justify-end px-6 py-3 border-t border-stroke">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-xs border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-4 py-2 text-caption border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
@ -680,7 +680,7 @@ function UsersPanel() {
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-fg font-korean">사용자 관리</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">
|
||||
총 {filteredUsers.length}명
|
||||
</p>
|
||||
</div>
|
||||
@ -698,7 +698,7 @@ function UsersPanel() {
|
||||
setOrgFilter(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="">전체 소속</option>
|
||||
{allOrgs.map((org) => (
|
||||
@ -711,7 +711,7 @@ function UsersPanel() {
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="">전체 상태</option>
|
||||
<option value="PENDING">승인대기</option>
|
||||
@ -726,11 +726,11 @@ function UsersPanel() {
|
||||
placeholder="이름, 계정 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-56 px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-56 px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowRegisterModal(true)}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
|
||||
>
|
||||
+ 사용자 등록
|
||||
</button>
|
||||
@ -740,7 +740,7 @@ function UsersPanel() {
|
||||
{/* 테이블 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-body-2 font-korean">
|
||||
불러오는 중...
|
||||
</div>
|
||||
) : (
|
||||
@ -781,7 +781,7 @@ function UsersPanel() {
|
||||
<tr>
|
||||
<td
|
||||
colSpan={9}
|
||||
className="px-6 py-10 text-center text-xs text-fg-disabled font-korean"
|
||||
className="px-6 py-10 text-center text-caption text-fg-disabled font-korean"
|
||||
>
|
||||
조회된 사용자가 없습니다.
|
||||
</td>
|
||||
@ -897,7 +897,7 @@ function UsersPanel() {
|
||||
onChange={() => toggleRoleSelection(role.sn)}
|
||||
style={{ accentColor: color }}
|
||||
/>
|
||||
<span className="text-xs font-korean" style={{ color }}>
|
||||
<span className="text-caption font-korean" style={{ color }}>
|
||||
{role.name}
|
||||
</span>
|
||||
<span className="text-caption text-fg-disabled font-mono">
|
||||
|
||||
@ -90,7 +90,7 @@ function VesselMaterialsPanel() {
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-fg font-korean">방제선 보유자재 현황</h1>
|
||||
<p className="text-xs text-fg-disabled mt-1 font-korean">
|
||||
<p className="text-caption text-fg-disabled mt-1 font-korean">
|
||||
총 {filtered.length}개 기관 (방제선 보유)
|
||||
</p>
|
||||
</div>
|
||||
@ -98,7 +98,7 @@ function VesselMaterialsPanel() {
|
||||
<select
|
||||
value={regionFilter}
|
||||
onChange={handleFilterChange(setRegionFilter)}
|
||||
className="px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="전체">전체 관할청</option>
|
||||
<option value="남해">남해청</option>
|
||||
@ -110,7 +110,7 @@ function VesselMaterialsPanel() {
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={handleFilterChange(setTypeFilter)}
|
||||
className="px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
className="px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg focus:border-color-accent focus:outline-none font-korean"
|
||||
>
|
||||
<option value="전체">전체 유형</option>
|
||||
{typeOptions.map((t) => (
|
||||
@ -127,11 +127,11 @@ function VesselMaterialsPanel() {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="w-56 px-3 py-2 text-xs bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
className="w-56 px-3 py-2 text-caption bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none font-korean"
|
||||
/>
|
||||
<button
|
||||
onClick={load}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md bg-bg-elevated border border-stroke text-fg-sub hover:border-color-accent hover:text-color-accent transition-all font-korean"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md bg-bg-elevated border border-stroke text-fg-sub hover:border-color-accent hover:text-color-accent transition-all font-korean"
|
||||
>
|
||||
새로고침
|
||||
</button>
|
||||
@ -141,7 +141,7 @@ function VesselMaterialsPanel() {
|
||||
{/* 테이블 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
|
||||
<div className="flex items-center justify-center h-32 text-fg-disabled text-body-2 font-korean">
|
||||
불러오는 중...
|
||||
</div>
|
||||
) : (
|
||||
@ -188,7 +188,7 @@ function VesselMaterialsPanel() {
|
||||
<tr>
|
||||
<td
|
||||
colSpan={11}
|
||||
className="px-6 py-10 text-center text-xs text-fg-disabled font-korean"
|
||||
className="px-6 py-10 text-center text-caption text-fg-disabled font-korean"
|
||||
>
|
||||
조회된 기관이 없습니다.
|
||||
</td>
|
||||
|
||||
@ -142,17 +142,17 @@ export default function VesselSignalPanel() {
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-b border-stroke-1">
|
||||
<h2 className="text-sm font-semibold text-fg">선박신호 수신 현황</h2>
|
||||
<h2 className="text-body-2 font-semibold text-fg">선박신호 수신 현황</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-fg"
|
||||
/>
|
||||
<button
|
||||
onClick={load}
|
||||
className="px-3 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg-sub hover:bg-bg-card"
|
||||
className="px-3 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-fg-sub hover:bg-bg-card"
|
||||
>
|
||||
새로고침
|
||||
</button>
|
||||
@ -163,7 +163,7 @@ export default function VesselSignalPanel() {
|
||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<span className="text-xs text-fg-disabled">로딩 중...</span>
|
||||
<span className="text-caption text-fg-disabled">로딩 중...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
|
||||
@ -589,7 +589,7 @@ export function CctvView() {
|
||||
{/* 헤더 */}
|
||||
<div className="p-3 pb-2.5 border-b border-stroke shrink-0 bg-bg-elevated">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs font-bold text-fg font-korean flex items-center gap-1.5">
|
||||
<div className="text-caption font-bold text-fg font-korean flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-[7px] h-[7px] rounded-full inline-block animate-pulse"
|
||||
style={{ background: 'var(--color-danger)' }}
|
||||
@ -776,7 +776,7 @@ export function CctvView() {
|
||||
{/* 뷰어 툴바 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-stroke bg-bg-elevated shrink-0 gap-2.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="text-xs font-bold text-fg font-korean whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<div className="text-caption font-bold text-fg font-korean whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedCamera ? `📹 ${selectedCamera.cameraNm}` : '📹 카메라를 선택하세요'}
|
||||
</div>
|
||||
{selectedCamera?.sttsCd === 'LIVE' && (
|
||||
@ -966,7 +966,7 @@ export function CctvView() {
|
||||
<div key={srcKey} className="mb-5">
|
||||
{/* 출처 헤더 */}
|
||||
<div className="flex items-center gap-2 mb-2 pb-1.5 border-b border-stroke">
|
||||
<span className="text-sm">{group.icon}</span>
|
||||
<span className="text-body-2">{group.icon}</span>
|
||||
<span className="text-label-1 font-bold text-fg font-korean">
|
||||
{group.label}
|
||||
</span>
|
||||
|
||||
@ -421,7 +421,7 @@ export function MediaManagement() {
|
||||
<div className="fixed inset-0 z-[300] bg-black/60 backdrop-blur-sm flex items-center justify-center">
|
||||
<div className="bg-bg-surface border border-stroke rounded-md p-6 w-72 text-center">
|
||||
<div className="text-2xl mb-3">📥</div>
|
||||
<div className="text-sm font-bold font-korean mb-3">다운로드 완료</div>
|
||||
<div className="text-body-2 font-bold font-korean mb-3">다운로드 완료</div>
|
||||
<div className="text-title-4 font-korean text-fg-sub mb-1">
|
||||
총 <span className="text-color-accent font-bold">{downloadResult.total}</span>건 선택
|
||||
</div>
|
||||
@ -441,7 +441,7 @@ export function MediaManagement() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setDownloadResult(null)}
|
||||
className="px-6 py-2 text-sm font-semibold rounded bg-[rgba(6,182,212,0.15)] text-color-accent border border-[rgba(6,182,212,0.3)] hover:bg-[rgba(6,182,212,0.25)] transition-colors font-korean"
|
||||
className="px-6 py-2 text-body-2 font-semibold rounded bg-[rgba(6,182,212,0.15)] text-color-accent border border-[rgba(6,182,212,0.3)] hover:bg-[rgba(6,182,212,0.25)] transition-colors font-korean"
|
||||
>
|
||||
확인
|
||||
</button>
|
||||
@ -475,7 +475,7 @@ export function MediaManagement() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
<label className="block text-caption font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
촬영 장비
|
||||
</label>
|
||||
<select className="prd-i w-full">
|
||||
@ -489,7 +489,7 @@ export function MediaManagement() {
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
<label className="block text-caption font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
연관 사고
|
||||
</label>
|
||||
<select className="prd-i w-full">
|
||||
@ -499,7 +499,7 @@ export function MediaManagement() {
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
<label className="block text-caption font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
메모
|
||||
</label>
|
||||
<textarea
|
||||
@ -508,7 +508,7 @@ export function MediaManagement() {
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="w-full py-3 rounded-sm text-sm font-bold font-korean cursor-pointer hover:brightness-125 transition-all"
|
||||
className="w-full py-3 rounded-sm text-body-2 font-bold font-korean cursor-pointer hover:brightness-125 transition-all"
|
||||
style={{
|
||||
background: 'rgba(6,182,212,0.15)',
|
||||
border: '1px solid rgba(6,182,212,0.3)',
|
||||
|
||||
@ -239,7 +239,7 @@ export function OilAreaAnalysis() {
|
||||
<div className="flex gap-5 h-full overflow-hidden">
|
||||
{/* ── Left Panel ── */}
|
||||
<div className="w-[280px] min-w-[280px] flex flex-col overflow-y-auto scrollbar-thin">
|
||||
<div className="text-sm font-bold mb-1 font-korean">🧩 영상사진합성</div>
|
||||
<div className="text-body-2 font-bold mb-1 font-korean">🧩 영상사진합성</div>
|
||||
<div className="text-label-2 text-fg-disabled mb-4 font-korean">
|
||||
드론 사진을 합성하여 유출유 확산 면적과 기름 양을 산정합니다.
|
||||
</div>
|
||||
@ -256,7 +256,7 @@ export function OilAreaAnalysis() {
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={selectedFiles.length >= MAX_IMAGES || isStitching || isAnalyzing}
|
||||
className="w-full py-2 mb-3 border border-dashed border-stroke rounded-sm text-xs font-korean text-fg-sub
|
||||
className="w-full py-2 mb-3 border border-dashed border-stroke rounded-sm text-caption font-korean text-fg-sub
|
||||
hover:border-color-accent hover:text-color-accent transition-colors cursor-pointer
|
||||
disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
|
||||
@ -177,7 +177,7 @@ export function RealtimeDrone() {
|
||||
{/* 헤더 */}
|
||||
<div className="p-3 pb-2.5 border-b border-stroke shrink-0 bg-bg-elevated">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs font-bold text-fg font-korean flex items-center gap-1.5">
|
||||
<div className="text-caption font-bold text-fg font-korean flex items-center gap-1.5">
|
||||
<span
|
||||
className="w-[7px] h-[7px] rounded-full inline-block"
|
||||
style={{
|
||||
@ -225,7 +225,7 @@ export function RealtimeDrone() {
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm">🚁</div>
|
||||
<div className="text-body-2">🚁</div>
|
||||
<div>
|
||||
<div className="text-label-2 font-normal text-fg font-korean">
|
||||
{stream.shipName}{' '}
|
||||
@ -300,7 +300,7 @@ export function RealtimeDrone() {
|
||||
{/* 툴바 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-stroke bg-bg-elevated shrink-0 gap-2.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="text-xs font-bold text-fg font-korean whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<div className="text-caption font-bold text-fg font-korean whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedStream ? `🚁 ${selectedStream.shipName}` : '🚁 드론 스트림을 선택하세요'}
|
||||
</div>
|
||||
{selectedStream?.status === 'streaming' && (
|
||||
@ -566,7 +566,7 @@ export function RealtimeDrone() {
|
||||
style={{ minWidth: 170, background: 'var(--bg-card)', borderRadius: 6 }}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm">🚁</span>
|
||||
<span className="text-body-2">🚁</span>
|
||||
<div className="text-label-2 font-bold text-fg">{mapPopup.shipName}</div>
|
||||
</div>
|
||||
<div className="text-caption text-fg-disabled mb-0.5">
|
||||
@ -813,7 +813,7 @@ export function RealtimeDrone() {
|
||||
].map((item, i) => (
|
||||
<div key={i} className="px-2 py-1.5 bg-bg-base rounded text-center">
|
||||
<div className="text-caption text-fg-disabled font-korean">{item.label}</div>
|
||||
<div className="text-sm font-mono text-fg">{item.value}</div>
|
||||
<div className="text-body-2 font-mono text-fg">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -485,7 +485,7 @@ export function SatelliteRequest() {
|
||||
<div className="flex items-center gap-3 mb-2 h-9">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div
|
||||
className="w-7 h-7 rounded-md flex items-center justify-center text-sm"
|
||||
className="w-7 h-7 rounded-md flex items-center justify-center text-body-2"
|
||||
style={{
|
||||
background: 'rgba(6,182,212,0.15)',
|
||||
border: '1px solid rgba(6,182,212,0.3)',
|
||||
@ -624,7 +624,7 @@ export function SatelliteRequest() {
|
||||
>
|
||||
<div className="text-label-2 font-mono text-fg-sub">{r.id}</div>
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-fg font-korean">{r.zone}</div>
|
||||
<div className="text-caption font-semibold text-fg font-korean">{r.zone}</div>
|
||||
<div className="text-caption text-fg-disabled font-mono mt-0.5">
|
||||
{r.zoneCoord} · {r.zoneArea}
|
||||
</div>
|
||||
@ -776,7 +776,9 @@ export function SatelliteRequest() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
{/* 가용 위성 현황 */}
|
||||
<div className="bg-bg-elevated border border-stroke rounded-md p-4">
|
||||
<div className="text-xs font-bold text-fg font-korean mb-3">🛰 가용 위성 현황</div>
|
||||
<div className="text-caption font-bold text-fg font-korean mb-3">
|
||||
🛰 가용 위성 현황
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{satellites.map((sat, i) => (
|
||||
<div
|
||||
@ -807,7 +809,7 @@ export function SatelliteRequest() {
|
||||
|
||||
{/* 오늘 촬영 가능 시간 */}
|
||||
<div className="bg-bg-elevated border border-stroke rounded-md p-4">
|
||||
<div className="text-xs font-bold text-fg font-korean mb-3">
|
||||
<div className="text-caption font-bold text-fg font-korean mb-3">
|
||||
⏰ 오늘 촬영 가능 시간 (KST)
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
@ -1133,7 +1135,7 @@ export function SatelliteRequest() {
|
||||
)}
|
||||
<button
|
||||
onClick={() => setMapSelectedItem(null)}
|
||||
className="text-fg-disabled bg-transparent border-none cursor-pointer text-sm"
|
||||
className="text-fg-disabled bg-transparent border-none cursor-pointer text-body-2"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@ -1213,7 +1215,7 @@ export function SatelliteRequest() {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-fg font-korean">BlackSky</div>
|
||||
<div className="text-body-2 font-bold text-fg font-korean">BlackSky</div>
|
||||
<div className="text-caption text-fg-disabled font-korean mt-px">
|
||||
Maxar Electro-Optical API
|
||||
</div>
|
||||
@ -1274,7 +1276,7 @@ export function SatelliteRequest() {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-fg font-korean">
|
||||
<div className="text-body-2 font-bold text-fg font-korean">
|
||||
UP42 — EO + SAR
|
||||
</div>
|
||||
<div className="text-caption text-fg-disabled font-korean mt-px">
|
||||
@ -1703,13 +1705,13 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setModalPhase('provider')}
|
||||
className="px-5 py-2.5 rounded-lg border border-stroke text-xs font-semibold cursor-pointer font-korean text-fg-sub bg-bg-elevated"
|
||||
className="px-5 py-2.5 rounded-lg border border-stroke text-caption font-semibold cursor-pointer font-korean text-fg-sub bg-bg-elevated"
|
||||
>
|
||||
← 뒤로
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModalPhase('none')}
|
||||
className="px-7 py-2.5 rounded-lg text-xs font-bold cursor-pointer font-korean text-color-accent"
|
||||
className="px-7 py-2.5 rounded-lg text-caption font-bold cursor-pointer font-korean text-color-accent"
|
||||
style={{
|
||||
background: 'rgba(6,182,212,0.08)',
|
||||
}}
|
||||
@ -2098,7 +2100,7 @@ export function SatelliteRequest() {
|
||||
{urgency}
|
||||
</span>
|
||||
{up42SelPass === pass.id && (
|
||||
<span className="text-xs text-fg">✓</span>
|
||||
<span className="text-caption text-fg">✓</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -391,7 +391,7 @@ function Vessel3DModel({ viewMode, status }: { viewMode: string; status: string
|
||||
{isProcessing && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-color-accent/40 text-xs font-mono animate-pulse">
|
||||
<div className="text-color-accent/40 text-caption font-mono animate-pulse">
|
||||
재구성 처리중...
|
||||
</div>
|
||||
<div className="w-24 h-0.5 bg-bg-card rounded-full mt-2 mx-auto overflow-hidden">
|
||||
@ -712,7 +712,7 @@ function Pollution3DModel({ viewMode, status }: { viewMode: string; status: stri
|
||||
{isProcessing && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-color-danger/40 text-xs font-mono animate-pulse">
|
||||
<div className="text-color-danger/40 text-caption font-mono animate-pulse">
|
||||
재구성 처리중...
|
||||
</div>
|
||||
<div className="w-24 h-0.5 bg-bg-card rounded-full mt-2 mx-auto overflow-hidden">
|
||||
@ -818,7 +818,7 @@ export function SensorAnalysis() {
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{ background: 'var(--bg-base)' }}
|
||||
>
|
||||
<div className="text-fg-disabled/10 text-xs font-mono">
|
||||
<div className="text-fg-disabled/10 text-caption font-mono">
|
||||
{src.label.split(' ')[0]}
|
||||
</div>
|
||||
</div>
|
||||
@ -916,7 +916,7 @@ export function SensorAnalysis() {
|
||||
{ value: '0.023m', label: 'RMS오차' },
|
||||
].map((s, i) => (
|
||||
<div key={i} className="text-center">
|
||||
<div className="font-mono font-bold text-sm text-color-accent">{s.value}</div>
|
||||
<div className="font-mono font-bold text-body-2 text-color-accent">{s.value}</div>
|
||||
<div className="text-caption text-fg-disabled mt-0.5 font-korean">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -47,7 +47,7 @@ export function WingAI() {
|
||||
<div className="flex items-center gap-3 mb-4 h-9">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div
|
||||
className="w-7 h-7 rounded-md flex items-center justify-center text-sm"
|
||||
className="w-7 h-7 rounded-md flex items-center justify-center text-body-2"
|
||||
style={{
|
||||
background: 'rgba(6,182,212,0.15)',
|
||||
border: '1px solid rgba(6,182,212,0.3)',
|
||||
@ -1757,8 +1757,8 @@ function AoiPanel() {
|
||||
}
|
||||
}
|
||||
>
|
||||
<span className="text-sm shrink-0">{active ? '◉' : '○'}</span>
|
||||
<span className="text-sm shrink-0">{src.icon}</span>
|
||||
<span className="text-body-2 shrink-0">{active ? '◉' : '○'}</span>
|
||||
<span className="text-body-2 shrink-0">{src.icon}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-caption font-bold font-korean">
|
||||
{src.label}
|
||||
|
||||
@ -83,7 +83,9 @@ function AssetManagement() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-fg-disabled text-sm font-korean">방제자산 데이터를 불러오는 중...</div>
|
||||
<div className="text-fg-disabled text-body-2 font-korean">
|
||||
방제자산 데이터를 불러오는 중...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -415,7 +417,7 @@ function AssetManagement() {
|
||||
<aside className="w-[340px] min-w-[340px] bg-bg-surface border-l border-stroke flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-stroke">
|
||||
<div className="text-sm font-bold mb-1 font-korean">{selectedOrg.name}</div>
|
||||
<div className="text-body-2 font-bold mb-1 font-korean">{selectedOrg.name}</div>
|
||||
<div className="text-label-2 text-fg-sub font-semibold font-korean mb-1">
|
||||
{selectedOrg.type} · {regionShort(selectedOrg.jurisdiction)} · {selectedOrg.area}
|
||||
</div>
|
||||
@ -505,7 +507,7 @@ function AssetManagement() {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center text-fg-disabled text-xs py-8 font-korean">
|
||||
<div className="text-center text-fg-disabled text-caption py-8 font-korean">
|
||||
상세 장비 데이터가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
@ -601,10 +603,10 @@ function AssetManagement() {
|
||||
|
||||
{/* Bottom Actions */}
|
||||
{/* <div className="p-3.5 border-t border-stroke flex gap-2">
|
||||
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean text-white bg-color-accent border-none cursor-pointer hover:opacity-90 transition-opacity">
|
||||
<button className="flex-1 py-2.5 rounded-sm text-caption font-semibold font-korean text-white bg-color-accent border-none cursor-pointer hover:opacity-90 transition-opacity">
|
||||
📥 다운로드
|
||||
</button>
|
||||
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean bg-bg-card border border-stroke text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors">
|
||||
<button className="flex-1 py-2.5 rounded-sm text-caption font-semibold font-korean bg-bg-card border border-stroke text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors">
|
||||
✏ 수정
|
||||
</button>
|
||||
</div> */}
|
||||
|
||||
@ -27,7 +27,7 @@ function AssetUpload() {
|
||||
{/* Drop Zone */}
|
||||
<div className="border-2 border-dashed border-stroke-light rounded-md py-10 px-5 text-center mb-5 cursor-pointer hover:border-[rgba(6,182,212,0.4)] transition-colors">
|
||||
<div className="text-4xl mb-2.5 opacity-50">📁</div>
|
||||
<div className="text-sm font-semibold mb-1.5 font-korean">
|
||||
<div className="text-body-2 font-semibold mb-1.5 font-korean">
|
||||
파일을 드래그하거나 클릭하여 업로드
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled mb-4 font-korean">
|
||||
@ -43,7 +43,7 @@ function AssetUpload() {
|
||||
|
||||
{/* Asset Classification */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
<label className="block text-caption font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
자산 분류
|
||||
</label>
|
||||
<select className="prd-i w-full">
|
||||
@ -58,7 +58,7 @@ function AssetUpload() {
|
||||
|
||||
{/* Jurisdiction */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
<label className="block text-caption font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
업로드 대상 관할
|
||||
</label>
|
||||
<select className="prd-i w-full">
|
||||
@ -74,10 +74,10 @@ function AssetUpload() {
|
||||
|
||||
{/* Upload Mode */}
|
||||
<div className="mb-5">
|
||||
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
<label className="block text-caption font-semibold mb-1.5 text-fg-sub font-korean">
|
||||
업로드 방식
|
||||
</label>
|
||||
<div className="flex gap-4 text-xs text-fg-sub font-korean">
|
||||
<div className="flex gap-4 text-caption text-fg-sub font-korean">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
@ -102,7 +102,7 @@ function AssetUpload() {
|
||||
{/* Upload Button */}
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
className={`w-full py-3.5 rounded-sm text-sm font-bold font-korean border-none cursor-pointer transition-all ${
|
||||
className={`w-full py-3.5 rounded-sm text-body-2 font-bold font-korean border-none cursor-pointer transition-all ${
|
||||
uploaded
|
||||
? 'bg-[rgba(34,197,94,0.2)] text-color-success border border-status-green'
|
||||
: 'text-white'
|
||||
@ -163,7 +163,7 @@ function AssetUpload() {
|
||||
{p.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
|
||||
<div className={`text-caption font-bold font-korean ${p.color}`}>{p.role}</div>
|
||||
<div className="text-caption text-fg-disabled font-korean">{p.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -179,7 +179,7 @@ function AssetUpload() {
|
||||
className="flex justify-between items-center p-3.5 px-4 bg-bg-card border border-stroke rounded-sm"
|
||||
>
|
||||
<div>
|
||||
<div className="text-xs font-semibold font-korean">{h.fileNm}</div>
|
||||
<div className="text-caption font-semibold font-korean">{h.fileNm}</div>
|
||||
<div className="text-caption text-fg-disabled mt-0.5 font-korean">
|
||||
{new Date(h.regDtm).toLocaleString('ko-KR')} · {h.uploaderNm} · {h.uploadCnt}건
|
||||
</div>
|
||||
|
||||
@ -27,10 +27,8 @@ export function AssetsView() {
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-5 py-3.5 text-xs font-semibold transition-all font-korean border-b-2 ${
|
||||
activeTab === tab.id
|
||||
? 'text-color-accent border-color-accent'
|
||||
: 'text-fg-disabled border-transparent hover:text-fg-sub'
|
||||
className={`px-4 py-2.5 text-title-4 font-medium transition-all duration-200 font-korean tracking-navigation ${
|
||||
activeTab === tab.id ? 'text-color-accent' : 'text-fg-sub hover:text-fg'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
|
||||
@ -167,7 +167,7 @@ function ShipInsurance() {
|
||||
{total > 0 ? `${total.toLocaleString()}건` : '데이터 없음'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-fg-disabled">
|
||||
<div className="text-caption text-fg-disabled">
|
||||
해양수산부 해운항만물류정보 공공데이터 기반
|
||||
</div>
|
||||
</div>
|
||||
@ -200,7 +200,7 @@ function ShipInsurance() {
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="선박명, 호출부호, IMO, 선주명"
|
||||
className="w-full px-3.5 py-2 bg-bg-base border border-stroke rounded-sm text-xs outline-none box-border"
|
||||
className="w-full px-3.5 py-2 bg-bg-base border border-stroke rounded-sm text-caption outline-none box-border"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -243,20 +243,20 @@ function ShipInsurance() {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className="px-4 py-2 bg-bg-base text-fg border border-stroke rounded-sm text-xs cursor-pointer"
|
||||
className="px-4 py-2 bg-bg-base text-fg border border-stroke rounded-sm text-caption cursor-pointer"
|
||||
>
|
||||
조회
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-4 py-2 bg-bg-base text-fg border border-stroke rounded-sm text-xs cursor-pointer"
|
||||
className="px-4 py-2 bg-bg-base text-fg border border-stroke rounded-sm text-caption cursor-pointer"
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={total === 0}
|
||||
className="px-4 py-2 text-xs cursor-pointer rounded-sm disabled:opacity-30 disabled:cursor-default bg-bg-base border border-stroke"
|
||||
className="px-4 py-2 text-caption cursor-pointer rounded-sm disabled:opacity-30 disabled:cursor-default bg-bg-base border border-stroke"
|
||||
>
|
||||
엑셀 다운로드
|
||||
</button>
|
||||
@ -281,8 +281,8 @@ function ShipInsurance() {
|
||||
{/* 에러 */}
|
||||
{error && !isLoading && (
|
||||
<div className="flex flex-col items-center justify-center py-16 px-5 bg-bg-card border border-stroke rounded-md">
|
||||
<div className="text-sm font-bold text-color-danger mb-2">조회 실패</div>
|
||||
<div className="text-xs text-fg-disabled">{error}</div>
|
||||
<div className="text-body-2 font-bold text-color-danger mb-2">조회 실패</div>
|
||||
<div className="text-caption text-fg-disabled">{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -291,7 +291,7 @@ function ShipInsurance() {
|
||||
<>
|
||||
<div className="border border-stroke rounded-md overflow-hidden mb-3">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-stroke">
|
||||
<div className="text-xs font-bold">
|
||||
<div className="text-caption font-bold">
|
||||
조회 결과 <span className="text-color-accent">{total.toLocaleString()}</span>건
|
||||
{totalPages > 1 && (
|
||||
<span className="text-fg-disabled font-normal ml-2">
|
||||
|
||||
@ -219,7 +219,7 @@ export function HNSLeftPanel({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-80 min-w-[320px] flex flex-col h-full bg-bg-surface border-r border-stroke overflow-hidden">
|
||||
<div className="w-full min-w-0 flex flex-col h-full bg-bg-surface border-r border-stroke overflow-hidden">
|
||||
{/* Scrollable Content */}
|
||||
<div
|
||||
className="flex-1 overflow-y-scroll scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent bg-bg-base"
|
||||
@ -799,15 +799,15 @@ export function HNSLeftPanel({
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
||||
<span className="text-label-2 text-fg-disabled">전체 분석</span>
|
||||
<span className="text-sm font-bold text-color-accent font-mono">8건</span>
|
||||
<span className="text-body-2 font-bold text-color-accent font-mono">8건</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
||||
<span className="text-label-2 text-fg-disabled">고위험 (AEGL-3)</span>
|
||||
<span className="text-sm font-bold text-color-caution font-mono">3건</span>
|
||||
<span className="text-body-2 font-bold text-color-caution font-mono">3건</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
||||
<span className="text-label-2 text-fg-disabled">중위험 (AEGL-2)</span>
|
||||
<span className="text-sm font-bold text-color-accent font-mono">5건</span>
|
||||
<span className="text-body-2 font-bold text-color-accent font-mono">5건</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,8 +35,8 @@ export function HNSRightPanel({
|
||||
}: HNSRightPanelProps) {
|
||||
if (!dispersionResult) {
|
||||
return (
|
||||
<div className="w-[300px] bg-bg-surface border-l border-stroke p-4 overflow-auto">
|
||||
<div className="flex flex-col gap-3 items-center justify-center h-full text-fg-disabled text-label-1">
|
||||
<div className="w-full h-full bg-bg-surface border-l border-stroke p-4 overflow-auto flex items-center justify-center">
|
||||
<div className="flex flex-col gap-3 items-center text-fg-disabled text-label-1">
|
||||
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
|
||||
<div>예측 실행 후 결과가 표시됩니다</div>
|
||||
</div>
|
||||
@ -58,7 +58,7 @@ export function HNSRightPanel({
|
||||
: 'ALOHA';
|
||||
|
||||
return (
|
||||
<div className="w-[300px] bg-bg-surface border-l border-stroke p-4 overflow-auto flex flex-col gap-4">
|
||||
<div className="w-full bg-bg-surface border-l border-stroke p-4 overflow-auto flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
|
||||
@ -792,7 +792,7 @@ ${styles}
|
||||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||||
<div className="flex items-center justify-between mb-[10px]">
|
||||
<div>
|
||||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||||
<span className="text-body-2 font-mono font-extrabold text-color-accent">
|
||||
NH₃
|
||||
</span>{' '}
|
||||
<span className="text-label-1 font-bold">암모니아</span>
|
||||
@ -891,7 +891,7 @@ ${styles}
|
||||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||||
<div className="flex items-center justify-between mb-[10px]">
|
||||
<div>
|
||||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||||
<span className="text-body-2 font-mono font-extrabold text-color-accent">
|
||||
CH₃OH
|
||||
</span>{' '}
|
||||
<span className="text-label-1 font-bold">메탄올</span>
|
||||
@ -990,7 +990,9 @@ ${styles}
|
||||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||||
<div className="flex items-center justify-between mb-[10px]">
|
||||
<div>
|
||||
<span className="text-sm font-mono font-extrabold text-color-accent">H₂</span>{' '}
|
||||
<span className="text-body-2 font-mono font-extrabold text-color-accent">
|
||||
H₂
|
||||
</span>{' '}
|
||||
<span className="text-label-1 font-bold">수소</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
@ -1048,7 +1050,7 @@ ${styles}
|
||||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||||
<div className="flex items-center justify-between mb-[10px]">
|
||||
<div>
|
||||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||||
<span className="text-body-2 font-mono font-extrabold text-color-accent">
|
||||
CH₄
|
||||
</span>{' '}
|
||||
<span className="text-label-1 font-bold">LNG (메탄)</span>
|
||||
@ -1106,7 +1108,7 @@ ${styles}
|
||||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||||
<div className="flex items-center justify-between mb-[10px]">
|
||||
<div>
|
||||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||||
<span className="text-body-2 font-mono font-extrabold text-color-accent">
|
||||
C₆H₅OH
|
||||
</span>{' '}
|
||||
<span className="text-label-1 font-bold">페놀</span>
|
||||
@ -1164,7 +1166,7 @@ ${styles}
|
||||
<div className="rounded-[10px] p-[14px] border border-stroke bg-bg-card">
|
||||
<div className="flex items-center justify-between mb-[10px]">
|
||||
<div>
|
||||
<span className="text-sm font-mono font-extrabold text-color-accent">
|
||||
<span className="text-body-2 font-mono font-extrabold text-color-accent">
|
||||
C₇H₈
|
||||
</span>{' '}
|
||||
<span className="text-label-1 font-bold">톨루엔</span>
|
||||
|
||||
@ -262,6 +262,8 @@ function DispersionTimeSlider({
|
||||
export function HNSView() {
|
||||
const { activeSubTab, setActiveSubTab } = useSubMenu('hns');
|
||||
const { user } = useAuthStore();
|
||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||
const [rightCollapsed, setRightCollapsed] = useState(false);
|
||||
const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null);
|
||||
const [isSelectingLocation, setIsSelectingLocation] = useState(false);
|
||||
const [isRunningPrediction, setIsRunningPrediction] = useState(false);
|
||||
@ -890,22 +892,66 @@ export function HNSView() {
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Left Panel - 분석 목록일 때는 숨김 */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<HNSLeftPanel
|
||||
activeSubTab={activeSubTab}
|
||||
onSubTabChange={setActiveSubTab}
|
||||
incidentCoord={incidentCoord}
|
||||
onCoordChange={setIncidentCoord}
|
||||
onMapSelectClick={() => setIsSelectingLocation(true)}
|
||||
onRunPrediction={handleRunPrediction}
|
||||
isRunningPrediction={isRunningPrediction}
|
||||
onParamsChange={handleParamsChange}
|
||||
onReset={handleReset}
|
||||
loadedParams={loadedParams}
|
||||
/>
|
||||
<div className="shrink-0 overflow-hidden" style={{ width: leftCollapsed ? 0 : 320 }}>
|
||||
<HNSLeftPanel
|
||||
activeSubTab={activeSubTab}
|
||||
onSubTabChange={setActiveSubTab}
|
||||
incidentCoord={incidentCoord}
|
||||
onCoordChange={setIncidentCoord}
|
||||
onMapSelectClick={() => setIsSelectingLocation(true)}
|
||||
onRunPrediction={handleRunPrediction}
|
||||
isRunningPrediction={isRunningPrediction}
|
||||
onParamsChange={handleParamsChange}
|
||||
onReset={handleReset}
|
||||
loadedParams={loadedParams}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Center - Map/Content Area */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{/* Left panel toggle button */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<button
|
||||
onClick={() => setLeftCollapsed((v) => !v)}
|
||||
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
||||
style={{
|
||||
left: 0,
|
||||
width: 18,
|
||||
height: 40,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderLeft: 'none',
|
||||
borderRadius: '0 6px 6px 0',
|
||||
color: 'var(--fg-sub)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{leftCollapsed ? '▶' : '◀'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Right panel toggle button */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<button
|
||||
onClick={() => setRightCollapsed((v) => !v)}
|
||||
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
||||
style={{
|
||||
right: 0,
|
||||
width: 18,
|
||||
height: 40,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderRight: 'none',
|
||||
borderRadius: '6px 0 0 6px',
|
||||
color: 'var(--fg-sub)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{rightCollapsed ? '◀' : '▶'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{activeSubTab === 'list' ? (
|
||||
<HNSAnalysisListTable
|
||||
onTabChange={(v) =>
|
||||
@ -942,14 +988,16 @@ export function HNSView() {
|
||||
|
||||
{/* Right Panel - 분석 목록일 때는 숨김 */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<HNSRightPanel
|
||||
dispersionResult={dispersionResult}
|
||||
computedResult={computedResult}
|
||||
weatherData={inputParams?.weather ?? null}
|
||||
onOpenRecalc={() => setRecalcModalOpen(true)}
|
||||
onOpenReport={handleOpenReport}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
<div className="shrink-0 overflow-hidden" style={{ width: rightCollapsed ? 0 : 300 }}>
|
||||
<HNSRightPanel
|
||||
dispersionResult={dispersionResult}
|
||||
computedResult={computedResult}
|
||||
weatherData={inputParams?.weather ?? null}
|
||||
onOpenRecalc={() => setRecalcModalOpen(true)}
|
||||
onOpenReport={handleOpenReport}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* HNS 재계산 모달 */}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { DndContext } from '@dnd-kit/core';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
|
||||
/**
|
||||
* 해양환경관리법 제22조 기반 선박 발생 오염물 배출 규정
|
||||
@ -100,33 +103,30 @@ const RULES: DischargeRule[] = [
|
||||
];
|
||||
|
||||
const ZONE_LABELS = ['~3해리', '3~12해리', '12~25해리', '25~50해리', '50해리+'];
|
||||
const ZONE_COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#64748b'];
|
||||
const ZONE_COLORS = [
|
||||
'var(--color-danger)',
|
||||
'var(--color-warning)',
|
||||
'var(--color-caution)',
|
||||
'var(--color-success)',
|
||||
'var(--fg-disabled)',
|
||||
];
|
||||
|
||||
function StatusBadge({ status }: { status: Status }) {
|
||||
if (status === 'forbidden')
|
||||
return (
|
||||
<span
|
||||
className="text-caption font-bold px-1.5 py-0.5 rounded"
|
||||
style={{ background: 'rgba(239,68,68,0.15)', color: 'var(--color-danger)' }}
|
||||
className="text-caption px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
background: 'color-mix(in srgb, var(--color-danger) 15%, transparent)',
|
||||
color: 'var(--color-danger)',
|
||||
}}
|
||||
>
|
||||
배출불가
|
||||
</span>
|
||||
);
|
||||
if (status === 'allowed')
|
||||
return (
|
||||
<span
|
||||
className="text-caption font-bold px-1.5 py-0.5 rounded"
|
||||
style={{ background: 'rgba(34,197,94,0.15)', color: 'var(--color-success)' }}
|
||||
>
|
||||
배출가능
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span
|
||||
className="text-caption font-bold px-1.5 py-0.5 rounded"
|
||||
style={{ background: 'rgba(234,179,8,0.15)', color: 'var(--color-caution)' }}
|
||||
>
|
||||
조건부
|
||||
<span className="text-caption px-1.5 py-0.5 rounded text-fg-sub">
|
||||
{status === 'allowed' ? '배출가능' : '조건부'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -139,15 +139,36 @@ interface DischargeZonePanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DischargeZonePanel({
|
||||
export function DischargeZonePanel(props: DischargeZonePanelProps) {
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
setOffset((prev) => ({ x: prev.x + event.delta.x, y: prev.y + event.delta.y }));
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
<DraggablePanel {...props} offset={offset} />
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
function DraggablePanel({
|
||||
lat,
|
||||
lon,
|
||||
distanceNm,
|
||||
zoneIndex,
|
||||
onClose,
|
||||
}: DischargeZonePanelProps) {
|
||||
offset,
|
||||
}: DischargeZonePanelProps & { offset: { x: number; y: number } }) {
|
||||
const zoneIdx = zoneIndex;
|
||||
const [expandedCat, setExpandedCat] = useState<string | null>(null);
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: 'discharge-panel',
|
||||
});
|
||||
|
||||
const tx = offset.x + (transform?.x ?? 0);
|
||||
const ty = offset.y + (transform?.y ?? 0);
|
||||
|
||||
const categories = [...new Set(RULES.map((r) => r.category))];
|
||||
|
||||
@ -161,22 +182,33 @@ export function DischargeZonePanel({
|
||||
border: '1px solid var(--stroke-default)',
|
||||
boxShadow: '0 16px 48px rgba(0,0,0,0.5)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
transform: `translate(${tx}px, ${ty}px)`,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
{/* Header — drag handle */}
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
borderBottom: '1px solid var(--stroke-default)',
|
||||
background: 'var(--bg-elevated)',
|
||||
cursor: 'grab',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="text-label-2 font-bold text-fg font-korean">🚢 오염물 배출 규정</div>
|
||||
<div className="text-label-2 text-fg font-korean">🚢 오염물 배출 규정</div>
|
||||
<div className="text-caption text-fg-sub font-korean">해양환경관리법 제22조</div>
|
||||
</div>
|
||||
<span onClick={onClose} className="text-title-3 cursor-pointer text-fg-sub hover:text-fg">
|
||||
<span
|
||||
onClick={onClose}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className="text-title-3 cursor-pointer text-fg-sub hover:text-fg"
|
||||
style={{ pointerEvents: 'all' }}
|
||||
>
|
||||
✕
|
||||
</span>
|
||||
</div>
|
||||
@ -194,10 +226,7 @@ export function DischargeZonePanel({
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-caption text-fg-sub font-korean">영해기선 거리</span>
|
||||
<span
|
||||
className="text-label-2 font-bold font-mono"
|
||||
style={{ color: ZONE_COLORS[zoneIdx] }}
|
||||
>
|
||||
<span className="text-label-2 font-mono" style={{ color: ZONE_COLORS[zoneIdx] }}>
|
||||
{distanceNm.toFixed(1)} NM
|
||||
</span>
|
||||
</div>
|
||||
@ -206,12 +235,10 @@ export function DischargeZonePanel({
|
||||
{ZONE_LABELS.map((label, i) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex-1 text-center rounded-sm"
|
||||
className="flex-1 text-center rounded-sm text-[10px]"
|
||||
style={{
|
||||
padding: '3px 0',
|
||||
fontSize: 8,
|
||||
fontWeight: i === zoneIdx ? 700 : 400,
|
||||
color: i === zoneIdx ? 'var(--fg-default)' : 'var(--fg-sub)',
|
||||
padding: '2px 0',
|
||||
color: i === zoneIdx ? '#000' : 'var(--fg-sub)',
|
||||
background: i === zoneIdx ? ZONE_COLORS[i] : 'var(--hover-overlay)',
|
||||
border: i === zoneIdx ? 'none' : '1px solid var(--stroke-light)',
|
||||
}}
|
||||
@ -231,8 +258,7 @@ export function DischargeZonePanel({
|
||||
const catRules = RULES.filter((r) => r.category === cat);
|
||||
const isExpanded = expandedCat === cat;
|
||||
const allForbidden = catRules.every((r) => r.zones[zoneIdx] === 'forbidden');
|
||||
const allAllowed = catRules.every((r) => r.zones[zoneIdx] === 'allowed');
|
||||
const summaryColor = allForbidden ? '#ef4444' : allAllowed ? '#22c55e' : '#eab308';
|
||||
const summaryColor = allForbidden ? 'var(--color-danger)' : 'var(--fg-sub)';
|
||||
|
||||
return (
|
||||
<div key={cat} style={{ borderBottom: '1px solid var(--stroke-light)' }}>
|
||||
@ -245,11 +271,11 @@ export function DischargeZonePanel({
|
||||
<div
|
||||
style={{ width: 6, height: 6, borderRadius: '50%', background: summaryColor }}
|
||||
/>
|
||||
<span className="text-caption font-bold text-fg font-korean">{cat}</span>
|
||||
<span className="text-caption text-fg font-korean">{cat}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-caption font-semibold" style={{ color: summaryColor }}>
|
||||
{allForbidden ? '전체 불가' : allAllowed ? '전체 가능' : '항목별 상이'}
|
||||
<span className="text-caption" style={{ color: summaryColor }}>
|
||||
{allForbidden ? '전체 불가' : '허용'}
|
||||
</span>
|
||||
<span className="text-caption text-fg-sub">{isExpanded ? '▾' : '▸'}</span>
|
||||
</div>
|
||||
@ -268,7 +294,7 @@ export function DischargeZonePanel({
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<span className="text-caption text-fg font-korean">{rule.item}</span>
|
||||
<span className="text-caption text-fg-sub font-korean">{rule.item}</span>
|
||||
<StatusBadge status={rule.zones[zoneIdx]} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -23,10 +23,10 @@ export function IncidentTable() {
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-stroke">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-fg">유출유 확산 예측 목록</h1>
|
||||
<p className="text-sm text-fg-disabled mt-1">총 {filteredIncidents.length}건</p>
|
||||
<p className="text-body-2 text-fg-disabled mt-1">총 {filteredIncidents.length}건</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="px-4 py-2 text-sm font-semibold border border-stroke rounded-md bg-bg-card text-fg-sub hover:bg-bg-surface-hover hover:text-fg transition-all">
|
||||
<button className="px-4 py-2 text-body-2 font-semibold border border-stroke rounded-md bg-bg-card text-fg-sub hover:bg-bg-surface-hover hover:text-fg transition-all">
|
||||
시간별 검색
|
||||
</button>
|
||||
<div className="relative">
|
||||
@ -35,10 +35,10 @@ export function IncidentTable() {
|
||||
placeholder="검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-64 px-4 py-2 text-sm bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
|
||||
className="w-64 px-4 py-2 text-body-2 bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button className="px-4 py-2 text-sm font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_16px_rgba(6,182,212,0.3)] transition-all">
|
||||
<button className="px-4 py-2 text-body-2 font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_16px_rgba(6,182,212,0.3)] transition-all">
|
||||
+ 새 분석
|
||||
</button>
|
||||
</div>
|
||||
@ -49,31 +49,31 @@ export function IncidentTable() {
|
||||
<table className="w-full">
|
||||
<thead className="sticky top-0 bg-bg-surface border-b border-stroke z-10">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
번호
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
사고명
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
사고시각
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
선박유형
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
유종
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-right text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
유출량
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
사고유형
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
상태
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-caption font-bold text-fg-disabled uppercase tracking-wider">
|
||||
분석자
|
||||
</th>
|
||||
</tr>
|
||||
@ -84,28 +84,28 @@ export function IncidentTable() {
|
||||
key={incident.acdntSn}
|
||||
className="hover:bg-bg-elevated transition-colors cursor-pointer group"
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-fg-sub font-mono">{incident.acdntSn}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub font-mono">{incident.acdntSn}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-color-danger animate-pulse" />
|
||||
<span className="text-sm font-semibold text-fg group-hover:text-color-accent transition-colors">
|
||||
<span className="text-body-2 font-semibold text-fg group-hover:text-color-accent transition-colors">
|
||||
{incident.acdntNm}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-fg-sub font-mono">{incident.occrnDtm}</td>
|
||||
<td className="px-4 py-3 text-sm text-fg-sub">{incident.vesselTp ?? '—'}</td>
|
||||
<td className="px-4 py-3 text-sm text-fg-sub">{incident.oilTpCd ?? '—'}</td>
|
||||
<td className="px-4 py-3 text-sm text-fg font-mono text-right font-semibold">
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub font-mono">{incident.occrnDtm}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub">{incident.vesselTp ?? '—'}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub">{incident.oilTpCd ?? '—'}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg font-mono text-right font-semibold">
|
||||
{incident.spilQty != null ? incident.spilQty.toFixed(2) : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-fg-sub">{incident.acdntTpCd}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub">{incident.acdntTpCd}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="px-2 py-1 text-xs font-semibold rounded-md bg-[rgba(168,85,247,0.15)] text-purple-400">
|
||||
<span className="px-2 py-1 text-caption font-semibold rounded-md bg-[rgba(168,85,247,0.15)] text-purple-400">
|
||||
{incident.phaseCd}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-fg-sub">{incident.analystNm ?? '—'}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub">{incident.analystNm ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@ -215,7 +215,7 @@ export function IncidentsLeftPanel({
|
||||
{/* Search */}
|
||||
<div className="px-4 py-3 border-b border-stroke shrink-0">
|
||||
<div className="relative">
|
||||
<span className="absolute left-[10px] top-1/2 -translate-y-1/2 text-xs">🔍</span>
|
||||
<span className="absolute left-[10px] top-1/2 -translate-y-1/2 text-caption">🔍</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="사고명, 선박명 검색..."
|
||||
@ -224,7 +224,7 @@ export function IncidentsLeftPanel({
|
||||
setSearchTerm(e.target.value);
|
||||
resetPage();
|
||||
}}
|
||||
className="w-full py-2 pr-3 pl-8 bg-bg-base border border-stroke text-xs outline-none"
|
||||
className="w-full py-2 pr-3 pl-8 bg-bg-base border border-stroke text-caption outline-none"
|
||||
style={{ borderRadius: 'var(--radius-sm)' }}
|
||||
/>
|
||||
</div>
|
||||
@ -257,11 +257,11 @@ export function IncidentsLeftPanel({
|
||||
/>
|
||||
<button
|
||||
onClick={resetPage}
|
||||
className="text-label-2 font-semibold cursor-pointer whitespace-nowrap text-white border-none"
|
||||
className="rounded-sm text-label-2 font-semibold cursor-pointer whitespace-nowrap text-color-accent"
|
||||
style={{
|
||||
padding: '5px 12px',
|
||||
background: 'linear-gradient(135deg,var(--color-accent),var(--color-info))',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid rgba(6,182,212,.3)',
|
||||
background: 'rgba(6,182,212,.08)',
|
||||
}}
|
||||
>
|
||||
조회
|
||||
@ -356,7 +356,7 @@ export function IncidentsLeftPanel({
|
||||
setSelectedStatus(s.id);
|
||||
resetPage();
|
||||
}}
|
||||
className="flex items-center gap-1 text-caption font-semibold cursor-pointer"
|
||||
className="flex items-center gap-1 text-caption cursor-pointer"
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: '12px',
|
||||
@ -442,7 +442,7 @@ export function IncidentsLeftPanel({
|
||||
>
|
||||
{/* Row 1: name + status */}
|
||||
<div className="flex items-center justify-between mb-[5px]">
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold">
|
||||
<div className="flex items-center gap-1.5 text-caption">
|
||||
<span
|
||||
className="shrink-0"
|
||||
style={{
|
||||
@ -456,7 +456,7 @@ export function IncidentsLeftPanel({
|
||||
{inc.name}
|
||||
</div>
|
||||
<span
|
||||
className="shrink-0 text-caption font-semibold"
|
||||
className="shrink-0 text-caption"
|
||||
style={{
|
||||
padding: '2px 10px',
|
||||
borderRadius: '10px',
|
||||
@ -470,9 +470,9 @@ export function IncidentsLeftPanel({
|
||||
{/* Row 2: meta */}
|
||||
<div className="flex items-center gap-2 text-caption text-fg-disabled mb-[5px]">
|
||||
<span>
|
||||
📅 {inc.date} {inc.time}
|
||||
{inc.date} {inc.time}
|
||||
</span>
|
||||
<span>🏛 {inc.office}</span>
|
||||
<span> {inc.office}</span>
|
||||
</div>
|
||||
{/* Row 3: tags + buttons */}
|
||||
<div className="flex items-center justify-between">
|
||||
@ -492,12 +492,12 @@ export function IncidentsLeftPanel({
|
||||
)}
|
||||
{inc.oilType && (
|
||||
<span
|
||||
className="text-caption font-medium text-color-warning"
|
||||
className="text-caption font-medium text-fg-sub"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: '3px',
|
||||
background: 'rgba(249,115,22,0.08)',
|
||||
border: '1px solid rgba(249,115,22,0.2)',
|
||||
background: 'rgba(100,116,139,0.08)',
|
||||
border: '1px solid rgba(100,116,139,0.2)',
|
||||
}}
|
||||
>
|
||||
{inc.oilType}
|
||||
@ -505,12 +505,12 @@ export function IncidentsLeftPanel({
|
||||
)}
|
||||
{inc.prediction && (
|
||||
<span
|
||||
className="text-caption font-medium text-color-success"
|
||||
className="text-caption font-medium text-fg-sub"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: '3px',
|
||||
background: 'rgba(34,197,94,0.08)',
|
||||
border: '1px solid rgba(34,197,94,0.2)',
|
||||
background: 'rgba(100,116,139,0.08)',
|
||||
border: '1px solid rgba(100,116,139,0.2)',
|
||||
}}
|
||||
>
|
||||
{inc.prediction}
|
||||
@ -535,7 +535,7 @@ export function IncidentsLeftPanel({
|
||||
transition: '0.15s',
|
||||
}}
|
||||
>
|
||||
🌤
|
||||
기상정보
|
||||
</button>
|
||||
{(inc.mediaCount ?? 0) > 0 && (
|
||||
<button
|
||||
@ -555,7 +555,7 @@ export function IncidentsLeftPanel({
|
||||
transition: '0.15s',
|
||||
}}
|
||||
>
|
||||
📹 <span className="text-caption">{inc.mediaCount}</span>
|
||||
<span className="text-caption">{inc.mediaCount}</span>
|
||||
</button>
|
||||
)}
|
||||
{inc.hasImgAnalysis && (
|
||||
@ -720,13 +720,13 @@ const WeatherPopup = forwardRef<
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm">🌤</span>
|
||||
<span className="text-body-2">🌤</span>
|
||||
<div>
|
||||
<div className="text-label-2 font-bold">{data?.locNm || '기상정보 없음'}</div>
|
||||
<div className="text-fg-disabled font-mono text-caption">{data?.obsDtm || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span onClick={onClose} className="cursor-pointer text-fg-disabled text-sm p-0.5">
|
||||
<span onClick={onClose} className="cursor-pointer text-fg-disabled text-body-2 p-0.5">
|
||||
✕
|
||||
</span>
|
||||
</div>
|
||||
@ -761,7 +761,7 @@ const WeatherPopup = forwardRef<
|
||||
border: '1px solid rgba(59,130,246,0.1)',
|
||||
}}
|
||||
>
|
||||
<span className="text-xs">⬆</span>
|
||||
<span className="text-caption">⬆</span>
|
||||
<div>
|
||||
<div className="text-fg-disabled text-caption">고조 (만조)</div>
|
||||
<div className="font-bold font-mono text-caption text-color-info">
|
||||
@ -773,7 +773,7 @@ const WeatherPopup = forwardRef<
|
||||
className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md"
|
||||
style={{ background: 'rgba(6,182,212,0.06)', border: '1px solid rgba(6,182,212,0.1)' }}
|
||||
>
|
||||
<span className="text-xs">⬇</span>
|
||||
<span className="text-caption">⬇</span>
|
||||
<div>
|
||||
<div className="text-fg-disabled text-caption">저조 (간조)</div>
|
||||
<div className="text-color-accent font-bold font-mono text-caption">
|
||||
@ -791,7 +791,7 @@ const WeatherPopup = forwardRef<
|
||||
{forecast.map((f, i) => (
|
||||
<div key={i} className="text-center">
|
||||
<div>{f.hour}</div>
|
||||
<div className="text-xs my-0.5">{f.icon}</div>
|
||||
<div className="text-caption my-0.5">{f.icon}</div>
|
||||
<div className="font-semibold">{f.temp}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -50,31 +50,8 @@ interface AnalysisItem {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
/* ── 카테고리별 고유 색상 (목록 순서 인덱스 기반 — 중복 없음) ── */
|
||||
const CATEGORY_PALETTE: [number, number, number][] = [
|
||||
[239, 68, 68], // red
|
||||
[249, 115, 22], // orange
|
||||
[234, 179, 8], // yellow
|
||||
[132, 204, 22], // lime
|
||||
[20, 184, 166], // teal
|
||||
[6, 182, 212], // cyan
|
||||
[59, 130, 246], // blue
|
||||
[99, 102, 241], // indigo
|
||||
[168, 85, 247], // purple
|
||||
[236, 72, 153], // pink
|
||||
[244, 63, 94], // rose
|
||||
[16, 185, 129], // emerald
|
||||
[14, 165, 233], // sky
|
||||
[139, 92, 246], // violet
|
||||
[217, 119, 6], // amber
|
||||
[45, 212, 191], // turquoise
|
||||
];
|
||||
|
||||
function getCategoryColor(index: number): [number, number, number] {
|
||||
return CATEGORY_PALETTE[index % CATEGORY_PALETTE.length];
|
||||
}
|
||||
|
||||
/* ── 카테고리 → 이모지 매핑 (prediction LeftPanel의 CATEGORY_ICON_MAP 기반) ── */
|
||||
/* ── 카테고리 → 이모지 매핑 (prediction LeftPanel의 CATEGORY_ICON_MAP 기반, 미사용 보존) ── */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const CATEGORY_ICON: Record<string, string> = {
|
||||
어장정보: '🐟',
|
||||
양식장: '🦪',
|
||||
@ -140,8 +117,20 @@ function getActiveModels(p: PredictionAnalysis): string {
|
||||
|
||||
/* ── HNS/구난 섹션 (미개발, 고정 구조만 유지) ────── */
|
||||
const STATIC_SECTIONS = [
|
||||
{ key: 'hns', icon: '🧪', title: 'HNS 대기확산', color: '#a855f7', colorRgb: '168,85,247' },
|
||||
{ key: 'rsc', icon: '🚨', title: '긴급구난', color: '#06b6d4', colorRgb: '6,182,212' },
|
||||
{
|
||||
key: 'hns',
|
||||
icon: '🧪',
|
||||
title: 'HNS 대기확산',
|
||||
color: 'var(--color-accent)',
|
||||
colorRgb: '6,182,212',
|
||||
},
|
||||
{
|
||||
key: 'rsc',
|
||||
icon: '🚨',
|
||||
title: '긴급구난',
|
||||
color: 'var(--color-accent)',
|
||||
colorRgb: '6,182,212',
|
||||
},
|
||||
];
|
||||
|
||||
/* ── Component ───────────────────────────────────── */
|
||||
@ -292,7 +281,7 @@ export function IncidentsRightPanel({
|
||||
key: 'oil',
|
||||
icon: '🛢',
|
||||
title: '유출유 확산예측',
|
||||
color: '#f97316',
|
||||
color: 'var(--color-accent)',
|
||||
colorRgb: '249,115,22',
|
||||
totalLabel: `전체 ${predItems.length}건`,
|
||||
items: predItems.map((p) => {
|
||||
@ -310,7 +299,7 @@ export function IncidentsRightPanel({
|
||||
|
||||
if (!incident) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-[280px] min-w-[280px]">
|
||||
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-full h-full">
|
||||
<div className="text-center text-fg-disabled text-label-2">
|
||||
<div className="text-[32px] mb-2 opacity-30">📊</div>
|
||||
좌측에서 사고를 선택하면
|
||||
@ -325,9 +314,9 @@ export function IncidentsRightPanel({
|
||||
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden h-full w-[280px] min-w-[280px]">
|
||||
{/* Header */}
|
||||
<div className="px-[14px] py-2.5 border-b border-stroke shrink-0">
|
||||
<div className="text-xs font-bold mb-0.5">🔬 통합분석 조회</div>
|
||||
<div className="text-caption font-bold mb-0.5">🔬 통합분석 조회</div>
|
||||
<div className="text-caption text-fg-disabled">
|
||||
선택: <b className="text-color-accent">{incident.name}</b>
|
||||
선택: <span className="text-fg-disabled">{incident.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -344,22 +333,19 @@ export function IncidentsRightPanel({
|
||||
<div className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm">{sec.icon}</span>
|
||||
<span className="text-xs font-bold" style={{ color: sec.color }}>
|
||||
{sec.title}
|
||||
</span>
|
||||
{/* <span className="text-body-2">{sec.icon}</span> */}
|
||||
<span className="text-caption">{sec.title}</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-caption font-semibold cursor-pointer"
|
||||
style={{
|
||||
padding: '3px 10px',
|
||||
borderRadius: '4px',
|
||||
background: `rgba(${sec.colorRgb},0.1)`,
|
||||
border: `1px solid rgba(${sec.colorRgb},0.25)`,
|
||||
border: '1px solid var(--stroke-default)',
|
||||
color: sec.color,
|
||||
}}
|
||||
>
|
||||
📋 조회
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
@ -374,8 +360,7 @@ export function IncidentsRightPanel({
|
||||
className="flex items-center gap-1.5"
|
||||
style={{
|
||||
padding: '5px 8px',
|
||||
background: `rgba(${sec.colorRgb},0.06)`,
|
||||
border: `1px solid rgba(${sec.colorRgb},0.15)`,
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
@ -415,22 +400,19 @@ export function IncidentsRightPanel({
|
||||
<div key={sec.key} className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm">{sec.icon}</span>
|
||||
<span className="text-xs font-bold" style={{ color: sec.color }}>
|
||||
{sec.title}
|
||||
</span>
|
||||
{/* <span className="text-body-2">{sec.icon}</span> */}
|
||||
<span className="text-caption">{sec.title}</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-caption font-semibold cursor-pointer"
|
||||
style={{
|
||||
padding: '3px 10px',
|
||||
borderRadius: '4px',
|
||||
background: `rgba(${sec.colorRgb},0.1)`,
|
||||
border: `1px solid rgba(${sec.colorRgb},0.25)`,
|
||||
border: '1px solid var(--stroke-default)',
|
||||
color: sec.color,
|
||||
}}
|
||||
>
|
||||
📋 조회
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-caption text-fg-disabled text-center py-1.5">준비 중입니다</div>
|
||||
@ -443,8 +425,8 @@ export function IncidentsRightPanel({
|
||||
{/* 민감자원 */}
|
||||
<div className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-sm">🐟</span>
|
||||
<span className="text-xs font-bold text-color-success">민감자원</span>
|
||||
{/* <span className="text-body-2">🐟</span> */}
|
||||
<span className="text-caption">민감자원</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[3px]">
|
||||
{sensCategories.length === 0 ? (
|
||||
@ -452,38 +434,33 @@ export function IncidentsRightPanel({
|
||||
해당 사고 영역의 민감자원이 없습니다
|
||||
</div>
|
||||
) : (
|
||||
sensCategories.map((cat, i) => {
|
||||
const icon = CATEGORY_ICON[cat.category] ?? '🌊';
|
||||
sensCategories.map((cat) => {
|
||||
const areaLabel =
|
||||
cat.totalArea != null
|
||||
? `${cat.totalArea.toLocaleString('ko-KR', { maximumFractionDigits: 0 })}ha`
|
||||
: `${cat.count}개소`;
|
||||
const [r, g, b] = getCategoryColor(i);
|
||||
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
return (
|
||||
<label
|
||||
key={cat.category}
|
||||
className="flex items-center cursor-pointer text-caption gap-[5px] rounded-[3px]"
|
||||
style={{ padding: '4px 6px', background: `rgba(${r},${g},${b},0.06)` }}
|
||||
className="flex items-center cursor-pointer text-caption gap-[5px]"
|
||||
style={{ padding: '3px 0' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checkedSensCategories.has(cat.category)}
|
||||
onChange={() => toggleSensCategory(cat.category)}
|
||||
style={{ accentColor: hex }}
|
||||
style={{ accentColor: 'var(--color-accent)' }}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: hex,
|
||||
background: 'var(--color-accent)',
|
||||
flexShrink: 0,
|
||||
display: 'inline-block',
|
||||
border: `1px solid rgba(${r},${g},${b},0.45)`,
|
||||
}}
|
||||
/>
|
||||
<span>{icon}</span>
|
||||
<span className="flex-1">{cat.category}</span>
|
||||
<span className="text-fg-disabled font-mono shrink-0">({areaLabel})</span>
|
||||
</label>
|
||||
@ -496,10 +473,9 @@ export function IncidentsRightPanel({
|
||||
{/* 근처 방제자원 */}
|
||||
<div className="bg-bg-elevated border border-stroke rounded-md p-2.5">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-sm">🛡</span>
|
||||
<span className="text-xs font-bold text-color-boom">근처 방제자원</span>
|
||||
<span className="text-caption font-bold text-color-accent">근처 방제자원</span>
|
||||
{nearbyOrgs.length > 0 && (
|
||||
<span className="ml-auto text-caption font-mono text-color-boom">
|
||||
<span className="ml-auto text-caption font-mono text-color-accent">
|
||||
{nearbyOrgs.length}개
|
||||
</span>
|
||||
)}
|
||||
@ -519,21 +495,18 @@ export function IncidentsRightPanel({
|
||||
반경 내 방제자원 없음
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-[3px] max-h-[200px] overflow-y-auto">
|
||||
<div className="flex flex-col max-h-[200px] overflow-y-auto">
|
||||
{nearbyOrgs.map((org) => (
|
||||
<div
|
||||
key={org.orgSn}
|
||||
className="flex items-start gap-1.5 rounded-[3px] px-[6px] py-[5px]"
|
||||
style={{
|
||||
background: 'rgba(245,158,11,0.05)',
|
||||
border: '1px solid rgba(245,158,11,0.08)',
|
||||
}}
|
||||
className="flex items-start gap-1.5 px-[2px] py-[5px]"
|
||||
style={{ borderBottom: '1px solid var(--stroke-default)' }}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1 mb-[2px]">
|
||||
<span
|
||||
className="text-caption px-[4px] py-[1px] rounded-[2px] font-bold shrink-0"
|
||||
style={{ background: 'rgba(245,158,11,0.15)', color: '#f59e0b' }}
|
||||
className="text-caption px-[4px] py-[1px] rounded-[2px] font-bold shrink-0 text-color-accent"
|
||||
style={{ background: 'rgba(6,182,212,0.1)' }}
|
||||
>
|
||||
{org.orgTp}
|
||||
</span>
|
||||
@ -544,7 +517,7 @@ export function IncidentsRightPanel({
|
||||
{org.totalAssets > 0 ? ` · 장비 ${org.totalAssets}개` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-caption font-mono text-color-boom shrink-0">
|
||||
<span className="text-caption font-mono text-color-accent shrink-0">
|
||||
{org.distanceNm.toFixed(1)} nm
|
||||
</span>
|
||||
</div>
|
||||
@ -553,10 +526,10 @@ export function IncidentsRightPanel({
|
||||
)}
|
||||
|
||||
{/* Radius slider */}
|
||||
<div className="mt-2 pt-2" style={{ borderTop: '1px solid rgba(245,158,11,0.1)' }}>
|
||||
<div className="mt-2 pt-2" style={{ borderTop: '1px solid var(--stroke-default)' }}>
|
||||
<div className="flex items-center justify-between mb-[5px]">
|
||||
<span className="text-caption text-fg-disabled">탐색 반경</span>
|
||||
<span className="text-caption font-bold font-mono text-color-boom">
|
||||
<span className="text-caption font-bold font-mono text-color-accent">
|
||||
{nearbyRadius} nm
|
||||
</span>
|
||||
</div>
|
||||
@ -572,7 +545,7 @@ export function IncidentsRightPanel({
|
||||
height: '4px',
|
||||
background: 'var(--stroke-default)',
|
||||
borderRadius: '2px',
|
||||
accentColor: '#f59e0b',
|
||||
accentColor: 'var(--color-accent)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -605,7 +578,8 @@ export function IncidentsRightPanel({
|
||||
color: isActive ? 'var(--color-accent)' : 'var(--fg-disabled)',
|
||||
}}
|
||||
>
|
||||
{v.icon} {v.label}
|
||||
{/* {v.icon} */}
|
||||
{v.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@ -627,9 +601,7 @@ export function IncidentsRightPanel({
|
||||
className="w-full text-label-2 font-bold cursor-pointer"
|
||||
style={{
|
||||
padding: '8px',
|
||||
background: analysisActive
|
||||
? 'linear-gradient(135deg,rgba(239,68,68,0.15),rgba(239,68,68,0.1))'
|
||||
: 'linear-gradient(135deg,rgba(6,182,212,0.15),rgba(59,130,246,0.1))',
|
||||
background: analysisActive ? 'rgba(239,68,68,0.1)' : 'rgba(6,182,212,0.1)',
|
||||
border: analysisActive
|
||||
? '1px solid rgba(239,68,68,0.3)'
|
||||
: '1px solid rgba(6,182,212,0.3)',
|
||||
|
||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -112,7 +112,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => setCurrentPage(page as number)}
|
||||
className={`px-3 py-1 text-title-3 font-medium rounded transition-colors ${
|
||||
className={`px-3 py-1 text-body-2 font-medium rounded transition-colors ${
|
||||
currentPage === page ? 'bg-color-accent text-bg-0' : 'text-fg-sub hover:bg-bg-elevated'
|
||||
}`}
|
||||
>
|
||||
@ -127,8 +127,8 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-stroke">
|
||||
<div>
|
||||
<h1 className="text-heading-3 font-bold text-fg">유출유 확산 예측 목록</h1>
|
||||
<p className="text-title-3 text-fg-disabled mt-1">총 {analyses.length}건</p>
|
||||
<h1 className="text-heading-3 text-fg">유출유 확산 예측 목록</h1>
|
||||
<p className="text-body-2 text-fg-disabled mt-1">총 {analyses.length}건</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
@ -137,12 +137,12 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
placeholder="검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-64 px-4 py-2 text-title-3 bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
|
||||
className="w-64 px-4 py-2 text-body-2 bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onTabChange('analysis')}
|
||||
className="px-4 py-2 text-title-3 font-semibold rounded-sm cursor-pointer text-color-accent"
|
||||
className="px-4 py-2 text-body-2 font-semibold rounded-sm cursor-pointer text-color-accent"
|
||||
style={{
|
||||
border: '1px solid rgba(6,182,212,.3)',
|
||||
background: 'rgba(6,182,212,.08)',
|
||||
@ -156,7 +156,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
{/* 테이블 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="text-center py-20 text-fg-disabled text-title-3">로딩 중...</div>
|
||||
<div className="text-center py-20 text-fg-disabled text-body-2">로딩 중...</div>
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="sticky top-0 bg-bg-surface border-b border-stroke z-10">
|
||||
@ -208,14 +208,14 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
key={analysis.predRunSn ?? analysis.acdntSn}
|
||||
className="hover:bg-bg-elevated transition-colors cursor-pointer group"
|
||||
>
|
||||
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub font-mono">
|
||||
{analysis.acdntSn}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-color-danger animate-pulse" />
|
||||
<span
|
||||
className="text-title-3 font-medium text-color-accent hover:underline transition-all cursor-pointer"
|
||||
className="text-body-2 font-medium text-color-accent hover:underline transition-all cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onSelectAnalysis) {
|
||||
@ -227,7 +227,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub font-mono">
|
||||
{analysis.occurredAt
|
||||
? new Date(analysis.occurredAt).toLocaleString('ko-KR', {
|
||||
month: '2-digit',
|
||||
@ -237,7 +237,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
})
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub font-mono">
|
||||
{analysis.runDtm
|
||||
? new Date(analysis.runDtm).toLocaleString('ko-KR', {
|
||||
month: '2-digit',
|
||||
@ -247,11 +247,11 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
})
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub font-mono">
|
||||
{analysis.duration}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-title-3 text-fg-sub">{analysis.oilType}</td>
|
||||
<td className="px-4 py-3 text-title-3 text-fg font-mono text-right font-medium">
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub">{analysis.oilType}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg font-mono text-right font-medium">
|
||||
{analysis.volume != null
|
||||
? analysis.volume >= 0.01
|
||||
? analysis.volume.toFixed(2)
|
||||
@ -268,8 +268,8 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
<td className="px-4 py-3 text-center">
|
||||
{getStatusBadge(analysis.backtrackStatus)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-title-3 text-fg-sub">{analysis.analyst}</td>
|
||||
<td className="px-4 py-3 text-title-3 text-fg-sub">{analysis.officeName}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub">{analysis.analyst}</td>
|
||||
<td className="px-4 py-3 text-body-2 text-fg-sub">{analysis.officeName}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@ -277,7 +277,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
)}
|
||||
|
||||
{!loading && analyses.length === 0 && (
|
||||
<div className="text-center py-20 text-fg-disabled text-title-3">
|
||||
<div className="text-center py-20 text-fg-disabled text-body-2">
|
||||
분석 데이터가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
@ -288,14 +288,14 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
<button
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 text-body-2 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
≪
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 text-body-2 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
@ -305,14 +305,14 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
||||
<button
|
||||
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 text-body-2 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1 text-body-2 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
≫
|
||||
</button>
|
||||
|
||||
@ -68,7 +68,7 @@ export function BacktrackModal({
|
||||
border: '1px solid var(--bd)',
|
||||
borderRadius: '6px',
|
||||
color: 'var(--t1)',
|
||||
fontSize: '11px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
fontFamily: 'var(--fM)',
|
||||
outline: 'none',
|
||||
opacity: inputDisabled ? 0.6 : 1,
|
||||
@ -108,9 +108,8 @@ export function BacktrackModal({
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(135deg, rgba(168,85,247,0.2), rgba(6,182,212,0.2))',
|
||||
border: '1px solid rgba(168,85,247,0.3)',
|
||||
fontSize: '18px',
|
||||
}}
|
||||
className="flex items-center justify-center"
|
||||
className="flex items-center justify-center text-title-1"
|
||||
>
|
||||
🔍
|
||||
</div>
|
||||
@ -127,7 +126,7 @@ export function BacktrackModal({
|
||||
height: '32px',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--bg-card)',
|
||||
fontSize: '14px',
|
||||
fontSize: 'var(--font-size-body-2)',
|
||||
}}
|
||||
className="border border-stroke text-fg-disabled cursor-pointer flex items-center justify-center"
|
||||
>
|
||||
@ -245,7 +244,7 @@ export function BacktrackModal({
|
||||
}}
|
||||
>
|
||||
<div className="text-caption text-fg-disabled mb-1">분석 대상 선박</div>
|
||||
<div className="text-sm font-bold text-color-tertiary font-mono">
|
||||
<div className="text-body-2 font-bold text-color-tertiary font-mono">
|
||||
{conditions.totalVessels}척{' '}
|
||||
<span className="text-caption font-medium text-fg-disabled">(AIS 수신)</span>
|
||||
</div>
|
||||
@ -371,7 +370,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
|
||||
borderRadius: '50%',
|
||||
background: `${vessel.color}20`,
|
||||
border: `2px solid ${vessel.color}`,
|
||||
fontSize: '12px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
fontWeight: 800,
|
||||
color: vessel.color,
|
||||
}}
|
||||
|
||||
@ -1222,7 +1222,7 @@ function FieldApplicationPanel() {
|
||||
style={{ background: step.bg, border: `1px solid ${step.bd}` }}
|
||||
>
|
||||
<div
|
||||
className="min-w-[36px] h-[36px] rounded-[9px] flex items-center justify-center font-extrabold text-sm flex-shrink-0"
|
||||
className="min-w-[36px] h-[36px] rounded-[9px] flex items-center justify-center font-extrabold text-body-2 flex-shrink-0"
|
||||
style={{
|
||||
background: step.numBg,
|
||||
border: `1px solid ${step.numBd}`,
|
||||
|
||||
@ -130,7 +130,7 @@ export function LeftPanel({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-80 min-w-[320px] bg-bg-surface border-r border-stroke flex flex-col">
|
||||
<div className="w-full min-w-0 h-full bg-bg-surface border-r border-stroke flex flex-col overflow-hidden">
|
||||
{/* Scrollable Content */}
|
||||
<div
|
||||
className="flex-1 overflow-y-scroll scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent"
|
||||
|
||||
@ -166,6 +166,8 @@ export const ALL_MODELS: PredictionModel[] = ['KOSPS', 'POSEIDON', 'OpenDrift'];
|
||||
|
||||
export function OilSpillView() {
|
||||
const { activeSubTab, setActiveSubTab } = useSubMenu('prediction');
|
||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||
const [rightCollapsed, setRightCollapsed] = useState(false);
|
||||
const [enabledLayers, setEnabledLayers] = useState<Set<string>>(new Set());
|
||||
const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null);
|
||||
const [flyToCoord, setFlyToCoord] = useState<{ lon: number; lat: number } | undefined>(undefined);
|
||||
@ -1129,88 +1131,132 @@ export function OilSpillView() {
|
||||
<div className="relative flex flex-1 overflow-hidden">
|
||||
{/* Left Sidebar */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<LeftPanel
|
||||
selectedAnalysis={selectedAnalysis}
|
||||
enabledLayers={enabledLayers}
|
||||
onToggleLayer={handleToggleLayer}
|
||||
accidentTime={accidentTime}
|
||||
onAccidentTimeChange={(v) => {
|
||||
setAccidentTime(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('accidentTime');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
incidentCoord={incidentCoord}
|
||||
onCoordChange={(v) => {
|
||||
setIncidentCoord(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('coord');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
isSelectingLocation={isSelectingLocation}
|
||||
onMapSelectClick={() => setIsSelectingLocation((prev) => !prev)}
|
||||
onRunSimulation={handleRunSimulation}
|
||||
isRunningSimulation={isRunningSimulation}
|
||||
selectedModels={selectedModels}
|
||||
onModelsChange={(v) => {
|
||||
setSelectedModels(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('models');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
visibleModels={visibleModels}
|
||||
onVisibleModelsChange={setVisibleModels}
|
||||
hasResults={oilTrajectory.length > 0}
|
||||
predictionTime={predictionTime}
|
||||
onPredictionTimeChange={setPredictionTime}
|
||||
spillType={spillType}
|
||||
onSpillTypeChange={setSpillType}
|
||||
oilType={oilType}
|
||||
onOilTypeChange={setOilType}
|
||||
spillAmount={spillAmount}
|
||||
onSpillAmountChange={setSpillAmount}
|
||||
incidentName={incidentName}
|
||||
onIncidentNameChange={(v) => {
|
||||
setIncidentName(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('incidentName');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
spillUnit={spillUnit}
|
||||
onSpillUnitChange={setSpillUnit}
|
||||
boomLines={boomLines}
|
||||
onBoomLinesChange={setBoomLines}
|
||||
oilTrajectory={oilTrajectory}
|
||||
algorithmSettings={algorithmSettings}
|
||||
onAlgorithmSettingsChange={setAlgorithmSettings}
|
||||
isDrawingBoom={isDrawingBoom}
|
||||
onDrawingBoomChange={setIsDrawingBoom}
|
||||
drawingPoints={drawingPoints}
|
||||
onDrawingPointsChange={setDrawingPoints}
|
||||
containmentResult={containmentResult}
|
||||
onContainmentResultChange={setContainmentResult}
|
||||
layerOpacity={layerOpacity}
|
||||
onLayerOpacityChange={setLayerOpacity}
|
||||
layerBrightness={layerBrightness}
|
||||
onLayerBrightnessChange={setLayerBrightness}
|
||||
layerColors={layerColors}
|
||||
onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))}
|
||||
sensitiveResources={sensitiveResourceCategories}
|
||||
onImageAnalysisResult={handleImageAnalysisResult}
|
||||
validationErrors={validationErrors}
|
||||
/>
|
||||
<div className="shrink-0 overflow-hidden" style={{ width: leftCollapsed ? 0 : 320 }}>
|
||||
<LeftPanel
|
||||
selectedAnalysis={selectedAnalysis}
|
||||
enabledLayers={enabledLayers}
|
||||
onToggleLayer={handleToggleLayer}
|
||||
accidentTime={accidentTime}
|
||||
onAccidentTimeChange={(v) => {
|
||||
setAccidentTime(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('accidentTime');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
incidentCoord={incidentCoord}
|
||||
onCoordChange={(v) => {
|
||||
setIncidentCoord(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('coord');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
isSelectingLocation={isSelectingLocation}
|
||||
onMapSelectClick={() => setIsSelectingLocation((prev) => !prev)}
|
||||
onRunSimulation={handleRunSimulation}
|
||||
isRunningSimulation={isRunningSimulation}
|
||||
selectedModels={selectedModels}
|
||||
onModelsChange={(v) => {
|
||||
setSelectedModels(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('models');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
visibleModels={visibleModels}
|
||||
onVisibleModelsChange={setVisibleModels}
|
||||
hasResults={oilTrajectory.length > 0}
|
||||
predictionTime={predictionTime}
|
||||
onPredictionTimeChange={setPredictionTime}
|
||||
spillType={spillType}
|
||||
onSpillTypeChange={setSpillType}
|
||||
oilType={oilType}
|
||||
onOilTypeChange={setOilType}
|
||||
spillAmount={spillAmount}
|
||||
onSpillAmountChange={setSpillAmount}
|
||||
incidentName={incidentName}
|
||||
onIncidentNameChange={(v) => {
|
||||
setIncidentName(v);
|
||||
setValidationErrors((prev) => {
|
||||
const n = new Set(prev);
|
||||
n.delete('incidentName');
|
||||
return n;
|
||||
});
|
||||
}}
|
||||
spillUnit={spillUnit}
|
||||
onSpillUnitChange={setSpillUnit}
|
||||
boomLines={boomLines}
|
||||
onBoomLinesChange={setBoomLines}
|
||||
oilTrajectory={oilTrajectory}
|
||||
algorithmSettings={algorithmSettings}
|
||||
onAlgorithmSettingsChange={setAlgorithmSettings}
|
||||
isDrawingBoom={isDrawingBoom}
|
||||
onDrawingBoomChange={setIsDrawingBoom}
|
||||
drawingPoints={drawingPoints}
|
||||
onDrawingPointsChange={setDrawingPoints}
|
||||
containmentResult={containmentResult}
|
||||
onContainmentResultChange={setContainmentResult}
|
||||
layerOpacity={layerOpacity}
|
||||
onLayerOpacityChange={setLayerOpacity}
|
||||
layerBrightness={layerBrightness}
|
||||
onLayerBrightnessChange={setLayerBrightness}
|
||||
layerColors={layerColors}
|
||||
onLayerColorChange={(id, color) => setLayerColors((prev) => ({ ...prev, [id]: color }))}
|
||||
sensitiveResources={sensitiveResourceCategories}
|
||||
onImageAnalysisResult={handleImageAnalysisResult}
|
||||
validationErrors={validationErrors}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Center - Map/Content Area */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{/* Left panel toggle button */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<button
|
||||
onClick={() => setLeftCollapsed((v) => !v)}
|
||||
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
||||
style={{
|
||||
left: 0,
|
||||
width: 18,
|
||||
height: 40,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderLeft: 'none',
|
||||
borderRadius: '0 6px 6px 0',
|
||||
color: 'var(--fg-sub)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{leftCollapsed ? '▶' : '◀'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Right panel toggle button */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<button
|
||||
onClick={() => setRightCollapsed((v) => !v)}
|
||||
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
||||
style={{
|
||||
right: 0,
|
||||
width: 18,
|
||||
height: 40,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderRight: 'none',
|
||||
borderRadius: '6px 0 0 6px',
|
||||
color: 'var(--fg-sub)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{rightCollapsed ? '◀' : '▶'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{activeSubTab === 'list' ? (
|
||||
<AnalysisListTable
|
||||
onTabChange={setActiveSubTab}
|
||||
@ -1332,7 +1378,7 @@ export function OilSpillView() {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontSize: 'var(--font-size-body-2)',
|
||||
transition: '0.2s',
|
||||
}}
|
||||
>
|
||||
@ -1358,7 +1404,7 @@ export function OilSpillView() {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
fontSize: 'var(--font-size-body-2)',
|
||||
transition: '0.2s',
|
||||
}}
|
||||
>
|
||||
@ -1394,7 +1440,7 @@ export function OilSpillView() {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
transition: '0.2s',
|
||||
}}
|
||||
>
|
||||
@ -1415,7 +1461,7 @@ export function OilSpillView() {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
transition: '0.2s',
|
||||
@ -1439,7 +1485,7 @@ export function OilSpillView() {
|
||||
position: 'absolute',
|
||||
left: `${pos}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: '10px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: isActive ? 'var(--color-accent)' : 'var(--fg-disabled)',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
@ -1526,7 +1572,7 @@ export function OilSpillView() {
|
||||
top: '-18px',
|
||||
left: `${bm.pos}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: '12px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
cursor: 'pointer',
|
||||
filter: 'drop-shadow(0 0 4px rgba(245,158,11,0.5))',
|
||||
}}
|
||||
@ -1569,7 +1615,7 @@ export function OilSpillView() {
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontSize: 'var(--font-size-body-2)',
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-accent)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
@ -1610,7 +1656,7 @@ export function OilSpillView() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
fontSize: '11px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
}}
|
||||
>
|
||||
<span className="text-fg-disabled">{s.label}</span>
|
||||
@ -1655,44 +1701,46 @@ export function OilSpillView() {
|
||||
|
||||
{/* Right Panel */}
|
||||
{activeSubTab === 'analysis' && (
|
||||
<RightPanel
|
||||
onOpenBacktrack={handleOpenBacktrack}
|
||||
onOpenRecalc={() => {
|
||||
if (!selectedAnalysis) {
|
||||
alert('선택된 사고가 없습니다.\n분석 목록에서 사고를 선택해주세요.');
|
||||
return;
|
||||
<div className="shrink-0 overflow-hidden" style={{ width: rightCollapsed ? 0 : 300 }}>
|
||||
<RightPanel
|
||||
onOpenBacktrack={handleOpenBacktrack}
|
||||
onOpenRecalc={() => {
|
||||
if (!selectedAnalysis) {
|
||||
alert('선택된 사고가 없습니다.\n분석 목록에서 사고를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
setRecalcModalOpen(true);
|
||||
}}
|
||||
onOpenReport={handleOpenReport}
|
||||
detail={analysisDetail}
|
||||
summary={
|
||||
stepSummariesByModel[windHydrModel]?.[currentStep] ??
|
||||
summaryByModel[windHydrModel] ??
|
||||
simulationSummary
|
||||
}
|
||||
setRecalcModalOpen(true);
|
||||
}}
|
||||
onOpenReport={handleOpenReport}
|
||||
detail={analysisDetail}
|
||||
summary={
|
||||
stepSummariesByModel[windHydrModel]?.[currentStep] ??
|
||||
summaryByModel[windHydrModel] ??
|
||||
simulationSummary
|
||||
}
|
||||
boomBlockedVolume={boomBlockedVolume}
|
||||
displayControls={displayControls}
|
||||
onDisplayControlsChange={setDisplayControls}
|
||||
windHydrModel={windHydrModel}
|
||||
windHydrModelOptions={windHydrModelOptions}
|
||||
onWindHydrModelChange={setWindHydrModel}
|
||||
analysisTab={analysisTab}
|
||||
onSwitchAnalysisTab={setAnalysisTab}
|
||||
drawAnalysisMode={drawAnalysisMode}
|
||||
analysisPolygonPoints={analysisPolygonPoints}
|
||||
circleRadiusNm={circleRadiusNm}
|
||||
onCircleRadiusChange={setCircleRadiusNm}
|
||||
analysisResult={analysisResult}
|
||||
incidentCoord={incidentCoord}
|
||||
centerPoints={centerPoints}
|
||||
predictionTime={predictionTime}
|
||||
onStartPolygonDraw={handleStartPolygonDraw}
|
||||
onRunPolygonAnalysis={handleRunPolygonAnalysis}
|
||||
onRunCircleAnalysis={handleRunCircleAnalysis}
|
||||
onCancelAnalysis={handleCancelAnalysis}
|
||||
onClearAnalysis={handleClearAnalysis}
|
||||
/>
|
||||
boomBlockedVolume={boomBlockedVolume}
|
||||
displayControls={displayControls}
|
||||
onDisplayControlsChange={setDisplayControls}
|
||||
windHydrModel={windHydrModel}
|
||||
windHydrModelOptions={windHydrModelOptions}
|
||||
onWindHydrModelChange={setWindHydrModel}
|
||||
analysisTab={analysisTab}
|
||||
onSwitchAnalysisTab={setAnalysisTab}
|
||||
drawAnalysisMode={drawAnalysisMode}
|
||||
analysisPolygonPoints={analysisPolygonPoints}
|
||||
circleRadiusNm={circleRadiusNm}
|
||||
onCircleRadiusChange={setCircleRadiusNm}
|
||||
analysisResult={analysisResult}
|
||||
incidentCoord={incidentCoord}
|
||||
centerPoints={centerPoints}
|
||||
predictionTime={predictionTime}
|
||||
onStartPolygonDraw={handleStartPolygonDraw}
|
||||
onRunPolygonAnalysis={handleRunPolygonAnalysis}
|
||||
onRunCircleAnalysis={handleRunCircleAnalysis}
|
||||
onCancelAnalysis={handleCancelAnalysis}
|
||||
onClearAnalysis={handleClearAnalysis}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 확산 예측 실행 중 로딩 오버레이 */}
|
||||
|
||||
@ -159,7 +159,7 @@ export function RecalcModal({
|
||||
borderRadius: '10px',
|
||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(6,182,212,0.2))',
|
||||
border: '1px solid rgba(249,115,22,0.3)',
|
||||
fontSize: '16px',
|
||||
fontSize: 'var(--font-size-body-1)',
|
||||
}}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
@ -178,7 +178,7 @@ export function RecalcModal({
|
||||
height: '28px',
|
||||
borderRadius: '6px',
|
||||
background: 'var(--bg-card)',
|
||||
fontSize: '12px',
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
}}
|
||||
className="border border-stroke text-fg-disabled cursor-pointer flex items-center justify-center"
|
||||
>
|
||||
@ -321,7 +321,7 @@ export function RecalcModal({
|
||||
}
|
||||
toggleModel(model);
|
||||
}}
|
||||
style={{ fontSize: '11px', padding: '5px 10px' }}
|
||||
style={{ fontSize: 'var(--font-size-caption)', padding: '5px 10px' }}
|
||||
>
|
||||
<span className="prd-md" style={{ background: color }} />
|
||||
{model}
|
||||
|
||||
@ -117,7 +117,7 @@ export function RightPanel({
|
||||
}, [incidentCoord, centerPoints, summary, predictionTime]);
|
||||
|
||||
return (
|
||||
<div className="w-[300px] min-w-[300px] bg-bg-surface border-l border-stroke flex flex-col">
|
||||
<div className="w-full min-w-0 h-full bg-bg-surface border-l border-stroke flex flex-col overflow-hidden">
|
||||
{/* Tab Header */}
|
||||
<div className="flex border-b border-stroke">
|
||||
<button className="flex-1 py-3 text-center text-label-1 font-medium text-color-accent border-b-2 border-color-accent transition-all font-korean">
|
||||
@ -580,7 +580,7 @@ export function RightPanel({
|
||||
|
||||
{/* Bottom Action Buttons */}
|
||||
<div className="flex gap-1.5 p-3 border-t border-stroke">
|
||||
<button className="flex-1 py-2 px-1 rounded-sm text-label-2 font-medium border border-color-accent text-color-accent font-korean hover:bg-[rgba(6,182,212,0.08)] transition-colors">
|
||||
<button className="flex-1 py-2 px-1 rounded-sm text-label-2 font-medium border border-stroke font-korean hover:bg-[rgba(6,182,212,0.08)] transition-colors">
|
||||
저장
|
||||
</button>
|
||||
<button
|
||||
@ -597,7 +597,7 @@ export function RightPanel({
|
||||
</button>
|
||||
<button
|
||||
onClick={onOpenBacktrack}
|
||||
className="flex-1 py-2 px-1 rounded-sm text-label-2 font-medium border border-[var(--color-tertiary)] text-[var(--color-tertiary)] font-korean hover:bg-[rgba(168,85,247,0.08)] transition-colors"
|
||||
className="flex-1 py-2 px-1 rounded-sm text-label-2 font-medium border border-stroke font-korean hover:bg-[rgba(168,85,247,0.08)] transition-colors"
|
||||
>
|
||||
역추적
|
||||
</button>
|
||||
|
||||
@ -1120,7 +1120,9 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
{activeSections.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-fg-disabled">
|
||||
<div className="text-4xl mb-4">📋</div>
|
||||
<p className="text-sm font-korean">왼쪽에서 보고서에 포함할 섹션을 선택하세요</p>
|
||||
<p className="text-body-2 font-korean">
|
||||
왼쪽에서 보고서에 포함할 섹션을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -171,13 +171,13 @@ export function ReportsView() {
|
||||
{filteredReports.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-fg-disabled">
|
||||
<div className="text-4xl mb-4">📄</div>
|
||||
<p className="text-sm font-korean mb-2">저장된 보고서가 없습니다</p>
|
||||
<p className="text-body-2 font-korean mb-2">저장된 보고서가 없습니다</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setView({ screen: 'templates' });
|
||||
setActiveSubTab('template');
|
||||
}}
|
||||
className="px-4 py-2 text-xs font-semibold rounded-md border border-color-accent text-color-accent hover:bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)] transition-all font-korean mt-4"
|
||||
className="px-4 py-2 text-caption font-semibold rounded-md border border-color-accent text-color-accent hover:bg-[color-mix(in_srgb,var(--color-accent)_10%,transparent)] transition-all font-korean mt-4"
|
||||
>
|
||||
템플릿에서 작성
|
||||
</button>
|
||||
@ -376,7 +376,7 @@ export function ReportsView() {
|
||||
<td className="px-3 py-3 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(report.id)}
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-color-danger hover:bg-[color-mix(in_srgb,var(--color-danger)_10%,transparent)] transition-all text-sm"
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-color-danger hover:bg-[color-mix(in_srgb,var(--color-danger)_10%,transparent)] transition-all text-body-2"
|
||||
>
|
||||
🗑
|
||||
</button>
|
||||
|
||||
@ -1651,14 +1651,14 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 rounded-md border border-stroke bg-bg-card text-fg-sub text-xs font-semibold cursor-pointer"
|
||||
className="px-5 py-2.5 rounded-md border border-stroke bg-bg-card text-fg-sub text-caption font-semibold cursor-pointer"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
{done ? (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-7 py-2.5 rounded-md border-none text-static-white text-xs font-bold cursor-pointer bg-color-success"
|
||||
className="px-7 py-2.5 rounded-md border-none text-static-white text-caption font-bold cursor-pointer bg-color-success"
|
||||
>
|
||||
✅ 생성 완료 — 닫기
|
||||
</button>
|
||||
@ -1666,7 +1666,7 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
className={`px-7 py-2.5 rounded-md border-none text-xs font-bold ${
|
||||
className={`px-7 py-2.5 rounded-md border-none text-caption font-bold ${
|
||||
submitting
|
||||
? 'bg-bg-card text-fg-disabled cursor-wait'
|
||||
: 'bg-color-navy text-static-white cursor-pointer hover:bg-color-navy-hover'
|
||||
|
||||
@ -3,8 +3,8 @@ function DistributionView() {
|
||||
<div className="flex w-full h-full bg-bg-base items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl opacity-20 mb-3">🗺</div>
|
||||
<div className="text-sm font-bold text-fg-sub font-korean mb-1">해양오염분포도</div>
|
||||
<div className="text-xs text-fg-disabled font-korean">
|
||||
<div className="text-body-2 font-bold text-fg-sub font-korean mb-1">해양오염분포도</div>
|
||||
<div className="text-caption text-fg-disabled font-korean">
|
||||
해양오염 분포도 기능이 준비 중입니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -16,6 +16,8 @@ import ScatRightPanel from './ScatRightPanel';
|
||||
// ═══ Main PreScatView ═══
|
||||
|
||||
export function PreScatView() {
|
||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||
const [rightCollapsed, setRightCollapsed] = useState(false);
|
||||
const [segments, setSegments] = useState<ScatSegment[]>([]);
|
||||
const [zones, setZones] = useState<ApiZoneItem[]>([]);
|
||||
const [jurisdictions, setJurisdictions] = useState<string[]>([]);
|
||||
@ -175,13 +177,13 @@ export function PreScatView() {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex w-full h-full bg-bg-base items-center justify-center flex-col gap-3">
|
||||
<div className="text-color-danger text-sm font-korean">{error}</div>
|
||||
<div className="text-color-danger text-body-2 font-korean">{error}</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
}}
|
||||
className="px-4 py-1.5 bg-color-accent text-white text-xs rounded font-korean"
|
||||
className="px-4 py-1.5 bg-color-accent text-white text-caption rounded font-korean"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
@ -192,36 +194,77 @@ export function PreScatView() {
|
||||
if (loading || !selectedSeg) {
|
||||
return (
|
||||
<div className="flex w-full h-full bg-bg-base items-center justify-center">
|
||||
<div className="text-fg-sub text-sm font-korean">SCAT 데이터 로딩 중...</div>
|
||||
<div className="text-fg-sub text-body-2 font-korean">SCAT 데이터 로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full h-full bg-bg-base overflow-hidden">
|
||||
<ScatLeftPanel
|
||||
segments={segments}
|
||||
zones={zones}
|
||||
jurisdictions={jurisdictions}
|
||||
offices={offices}
|
||||
selectedOffice={selectedOffice}
|
||||
onOfficeChange={setSelectedOffice}
|
||||
selectedSeg={selectedSeg}
|
||||
onSelectSeg={setSelectedSeg}
|
||||
onOpenPopup={handleOpenPopup}
|
||||
jurisdictionFilter={jurisdictionFilter}
|
||||
onJurisdictionChange={setJurisdictionFilter}
|
||||
areaFilter={areaFilter}
|
||||
onAreaChange={setAreaFilter}
|
||||
phaseFilter={phaseFilter}
|
||||
onPhaseChange={setPhaseFilter}
|
||||
statusFilter={statusFilter}
|
||||
onStatusChange={setStatusFilter}
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
{/* Left Panel */}
|
||||
<div className="shrink-0 overflow-hidden" style={{ width: leftCollapsed ? 0 : 340 }}>
|
||||
<ScatLeftPanel
|
||||
segments={segments}
|
||||
zones={zones}
|
||||
jurisdictions={jurisdictions}
|
||||
offices={offices}
|
||||
selectedOffice={selectedOffice}
|
||||
onOfficeChange={setSelectedOffice}
|
||||
selectedSeg={selectedSeg}
|
||||
onSelectSeg={setSelectedSeg}
|
||||
onOpenPopup={handleOpenPopup}
|
||||
jurisdictionFilter={jurisdictionFilter}
|
||||
onJurisdictionChange={setJurisdictionFilter}
|
||||
areaFilter={areaFilter}
|
||||
onAreaChange={setAreaFilter}
|
||||
phaseFilter={phaseFilter}
|
||||
onPhaseChange={setPhaseFilter}
|
||||
statusFilter={statusFilter}
|
||||
onStatusChange={setStatusFilter}
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{/* Left panel toggle button */}
|
||||
<button
|
||||
onClick={() => setLeftCollapsed((v) => !v)}
|
||||
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
||||
style={{
|
||||
left: 0,
|
||||
width: 18,
|
||||
height: 40,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderLeft: 'none',
|
||||
borderRadius: '0 6px 6px 0',
|
||||
color: 'var(--fg-sub)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{leftCollapsed ? '▶' : '◀'}
|
||||
</button>
|
||||
|
||||
{/* Right panel toggle button */}
|
||||
<button
|
||||
onClick={() => setRightCollapsed((v) => !v)}
|
||||
className="absolute z-[500] top-1/2 -translate-y-1/2 flex items-center justify-center text-[10px]"
|
||||
style={{
|
||||
right: 0,
|
||||
width: 18,
|
||||
height: 40,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderRight: 'none',
|
||||
borderRadius: '6px 0 0 6px',
|
||||
color: 'var(--fg-sub)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{rightCollapsed ? '◀' : '▶'}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<ScatMap
|
||||
segments={segments}
|
||||
zones={zones}
|
||||
@ -237,7 +280,10 @@ export function PreScatView() {
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<ScatRightPanel detail={panelDetail} loading={panelLoading} />
|
||||
{/* Right Panel */}
|
||||
<div className="shrink-0 overflow-hidden" style={{ width: rightCollapsed ? 0 : 280 }}>
|
||||
<ScatRightPanel detail={panelDetail} loading={panelLoading} />
|
||||
</div>
|
||||
|
||||
{popupData && (
|
||||
<ScatPopup data={popupData} segCode={selectedSeg.code} onClose={handleClosePopup} />
|
||||
|
||||
@ -156,7 +156,7 @@ function ScatLeftPanel({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-[340px] min-w-[340px] bg-bg-surface border-r border-stroke flex flex-col overflow-hidden">
|
||||
<div className="w-full h-full min-w-0 bg-bg-surface border-r border-stroke flex flex-col overflow-hidden">
|
||||
{/* Filters */}
|
||||
<div className="p-3.5 border-b border-stroke">
|
||||
<div className="flex items-center gap-1.5 text-caption font-bold uppercase tracking-wider text-fg mb-3">
|
||||
@ -269,7 +269,11 @@ function ScatLeftPanel({
|
||||
rowCount={filtered.length}
|
||||
rowHeight={88}
|
||||
overscanCount={5}
|
||||
style={{ height: listHeight }}
|
||||
style={{
|
||||
height: listHeight,
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'var(--stroke-default) transparent',
|
||||
}}
|
||||
rowComponent={SegRow}
|
||||
rowProps={{
|
||||
filtered,
|
||||
|
||||
@ -310,7 +310,7 @@ function ScatMap({
|
||||
</div>
|
||||
|
||||
{/* Right info cards */}
|
||||
<div className="absolute top-3.5 right-3.5 w-[260px] flex flex-col gap-2 z-[1000] max-h-[calc(100%-100px)] overflow-y-auto scrollbar-thin">
|
||||
<div className="absolute top-3.5 right-8 w-[260px] flex flex-col gap-2 z-[1000] max-h-[calc(100%-100px)] overflow-y-auto scrollbar-thin">
|
||||
{/* ESI Legend */}
|
||||
<div className="bg-[color-mix(in_srgb,var(--bg-base)_92%,transparent)] backdrop-blur-xl border border-stroke rounded-md p-3.5 shadow-[0_4px_20px_rgba(0,0,0,0.3)]">
|
||||
<div className="text-caption font-bold uppercase tracking-wider text-fg-disabled mb-2.5">
|
||||
|
||||
@ -182,7 +182,7 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setPopTab(i)}
|
||||
className={`px-5 py-3 text-xs font-semibold font-korean border-b-2 transition-colors cursor-pointer ${
|
||||
className={`px-5 py-3 text-caption font-semibold font-korean border-b-2 transition-colors cursor-pointer ${
|
||||
popTab === i
|
||||
? 'text-color-success border-status-green'
|
||||
: 'text-fg-disabled border-transparent hover:text-fg-sub'
|
||||
@ -204,7 +204,9 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
{/* Skeleton */}
|
||||
{!imgLoaded && !imgError && (
|
||||
<div className="w-full aspect-video bg-bg-card animate-pulse flex items-center justify-center">
|
||||
<span className="text-fg-disabled text-xs font-korean">사진 로딩 중...</span>
|
||||
<span className="text-fg-disabled text-caption font-korean">
|
||||
사진 로딩 중...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
@ -216,7 +218,7 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
{imgError && (
|
||||
<div className="w-full aspect-video flex flex-col items-center justify-center text-fg-disabled text-xs font-korean">
|
||||
<div className="w-full aspect-video flex flex-col items-center justify-center text-fg-disabled text-caption font-korean">
|
||||
{/* <span className="text-[40px]">📷</span> */}
|
||||
<span>사진 없음</span>
|
||||
</div>
|
||||
@ -266,7 +268,7 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
].map(([k, v, cls], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-1.5 text-xs border-b border-stroke last:border-b-0"
|
||||
className="flex justify-between py-1.5 text-caption border-b border-stroke last:border-b-0"
|
||||
>
|
||||
<span className="text-fg-sub font-korean">{k}</span>
|
||||
<span className={`text-fg font-korean ${cls}`}>{v}</span>
|
||||
@ -282,7 +284,7 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
{data.sensitive.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-1.5 text-xs border-b border-stroke last:border-b-0"
|
||||
className="flex justify-between py-1.5 text-caption border-b border-stroke last:border-b-0"
|
||||
>
|
||||
<span className="text-fg-sub font-korean">{s.t}</span>
|
||||
<span className="text-fg font-korean">{s.v}</span>
|
||||
@ -344,7 +346,9 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
<div className="w-full aspect-[4/3] bg-bg-base border border-stroke rounded-md mb-4 overflow-hidden relative">
|
||||
{!mapLoaded && (
|
||||
<div className="absolute inset-0 bg-bg-card animate-pulse flex items-center justify-center z-10">
|
||||
<span className="text-fg-disabled text-xs font-korean">지도 로딩 중...</span>
|
||||
<span className="text-fg-disabled text-caption font-korean">
|
||||
지도 로딩 중...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<PopupMap
|
||||
@ -390,7 +394,7 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
].map(([k, v], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-1.5 text-xs border-b border-stroke last:border-b-0"
|
||||
className="flex justify-between py-1.5 text-caption border-b border-stroke last:border-b-0"
|
||||
>
|
||||
<span className="text-fg-disabled font-korean">{k}</span>
|
||||
<span className="text-color-success font-mono font-medium">{v}</span>
|
||||
@ -413,7 +417,7 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
].map(([k, v], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-1.5 text-xs border-b border-stroke last:border-b-0"
|
||||
className="flex justify-between py-1.5 text-caption border-b border-stroke last:border-b-0"
|
||||
>
|
||||
<span className="text-fg-disabled font-korean">{k}</span>
|
||||
<span className="text-fg font-medium font-korean">{v}</span>
|
||||
@ -441,7 +445,7 @@ function ScatPopup({ data, segCode, onClose }: ScatPopupProps) {
|
||||
].map((h, i) => (
|
||||
<div key={i} className="bg-bg-card border border-stroke rounded-md p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-bold font-mono">{h.date}</span>
|
||||
<span className="text-caption font-bold font-mono">{h.date}</span>
|
||||
<span
|
||||
className={`text-caption font-bold px-2 py-0.5 rounded-lg ${
|
||||
h.type === 'Pre-SCAT'
|
||||
|
||||
@ -23,7 +23,7 @@ export default function ScatRightPanel({
|
||||
|
||||
if (!detail && !loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-[280px] min-w-[280px] h-full">
|
||||
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-full h-full">
|
||||
<div className="text-3xl mb-2">🏖️</div>
|
||||
<div className="text-center text-fg-disabled text-label-2 leading-relaxed">
|
||||
좌측 목록에서 구간을
|
||||
@ -37,7 +37,7 @@ export default function ScatRightPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden h-full w-[280px] min-w-[280px]">
|
||||
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden h-full w-full">
|
||||
{/* 헤더 */}
|
||||
<div className="px-3.5 py-2.5 border-b border-stroke shrink-0">
|
||||
{detail ? (
|
||||
@ -49,12 +49,12 @@ export default function ScatRightPanel({
|
||||
{detail.esi}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-bold truncate">{detail.name}</div>
|
||||
<div className="text-caption font-bold truncate">{detail.name}</div>
|
||||
<div className="text-caption text-fg-disabled">{detail.code}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-fg-disabled">로딩 중...</div>
|
||||
<div className="text-caption text-fg-disabled">로딩 중...</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -67,38 +67,38 @@ function ScatTimeline({ segments, currentIdx, onSeek }: ScatTimelineProps) {
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => onSeek(0)}
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-sm"
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-body-2"
|
||||
>
|
||||
⏮
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSeek(Math.max(0, currentIdx - 1))}
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-sm"
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-body-2"
|
||||
>
|
||||
◀
|
||||
</button>
|
||||
<button
|
||||
onClick={togglePlay}
|
||||
className={`w-[34px] h-[34px] rounded-sm border flex items-center justify-center cursor-pointer text-sm ${playing ? 'bg-color-success text-black border-status-green' : 'border-stroke bg-bg-card text-fg-sub hover:bg-bg-surface-hover'}`}
|
||||
className={`w-[34px] h-[34px] rounded-sm border flex items-center justify-center cursor-pointer text-body-2 ${playing ? 'bg-color-success text-black border-status-green' : 'border-stroke bg-bg-card text-fg-sub hover:bg-bg-surface-hover'}`}
|
||||
>
|
||||
{playing ? '⏸' : '▶'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSeek(Math.min(total - 1, currentIdx + 1))}
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-sm"
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-body-2"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSeek(total - 1)}
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-sm"
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-body-2"
|
||||
>
|
||||
⏭
|
||||
</button>
|
||||
<div className="w-2" />
|
||||
<button
|
||||
onClick={cycleSpeed}
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-xs font-mono font-bold"
|
||||
className="w-[34px] h-[34px] rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center cursor-pointer hover:bg-bg-surface-hover text-caption font-mono font-bold"
|
||||
>
|
||||
{speed}×
|
||||
</button>
|
||||
@ -162,7 +162,7 @@ function ScatTimeline({ segments, currentIdx, onSeek }: ScatTimelineProps) {
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex flex-col items-end gap-1 flex-shrink-0 min-w-[210px]">
|
||||
<span className="text-sm font-semibold text-color-success font-mono">
|
||||
<span className="text-body-2 font-semibold text-color-success font-mono">
|
||||
구간 {displaySegs[currentIdx]?.code || 'S-001'} / {total}개
|
||||
</span>
|
||||
<div className="flex gap-3.5">
|
||||
|
||||
@ -3,8 +3,8 @@ function SurveyView() {
|
||||
<div className="flex w-full h-full bg-bg-base items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl opacity-20 mb-3">📋</div>
|
||||
<div className="text-sm font-bold text-fg-sub font-korean mb-1">해안오염 조사 평가</div>
|
||||
<div className="text-xs text-fg-disabled font-korean">
|
||||
<div className="text-body-2 font-bold text-fg-sub font-korean mb-1">해안오염 조사 평가</div>
|
||||
<div className="text-caption text-fg-disabled font-korean">
|
||||
해안오염 조사 및 평가 기능이 준비 중입니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,11 +33,11 @@ export function WeatherMapControls({ center, zoom }: WeatherMapControlsProps) {
|
||||
<div key={tooltip} className="relative group">
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-[38px] h-[38px] bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-stroke rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-base"
|
||||
className="w-[38px] h-[38px] bg-bg-surface border border-stroke rounded-sm shadow-md text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-base"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-2 px-2 py-1 text-xs bg-bg-base text-fg border border-stroke rounded whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-20">
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 mr-2 px-2 py-1 text-caption bg-bg-base text-fg border border-stroke rounded whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-20">
|
||||
{tooltip}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,26 +33,26 @@ interface WeatherMapOverlayProps {
|
||||
selectedStationId: string | null;
|
||||
}
|
||||
|
||||
// 풍속에 따른 hex 색상 반환
|
||||
// 풍속에 따른 색상 반환
|
||||
function getWindHexColor(speed: number, isSelected: boolean): string {
|
||||
if (isSelected) return '#06b6d4';
|
||||
if (speed > 10) return '#ef4444';
|
||||
if (speed > 7) return '#f59e0b';
|
||||
return '#3b82f6';
|
||||
if (isSelected) return 'var(--color-accent)';
|
||||
if (speed > 10) return 'var(--color-danger)';
|
||||
if (speed > 7) return 'var(--color-caution)';
|
||||
return 'var(--color-info)';
|
||||
}
|
||||
|
||||
// 파고에 따른 hex 색상 반환
|
||||
// 파고에 따른 색상 반환
|
||||
function getWaveHexColor(height: number): string {
|
||||
if (height > 2.5) return '#ef4444';
|
||||
if (height > 1.5) return '#f59e0b';
|
||||
return '#3b82f6';
|
||||
if (height > 2.5) return 'var(--color-danger)';
|
||||
if (height > 1.5) return 'var(--color-caution)';
|
||||
return 'var(--color-info)';
|
||||
}
|
||||
|
||||
// 수온에 따른 hex 색상 반환
|
||||
// 수온에 따른 색상 반환
|
||||
function getTempHexColor(temp: number): string {
|
||||
if (temp > 8) return '#ef4444';
|
||||
if (temp > 6) return '#f59e0b';
|
||||
return '#3b82f6';
|
||||
if (temp > 8) return 'var(--color-danger)';
|
||||
if (temp > 6) return 'var(--color-caution)';
|
||||
return 'var(--color-info)';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,15 +91,17 @@ export function WeatherMapOverlay({
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
style={{ filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))' }}
|
||||
style={{ filter: 'drop-shadow(0 2px 6px rgba(0,0,0,0.5))' }}
|
||||
>
|
||||
{/* 위쪽이 바람 방향을 나타내는 삼각형 */}
|
||||
<polygon points="12,2 4,22 12,16 20,22" fill={color} opacity="0.9" />
|
||||
{/* 흰 외곽선 레이어 */}
|
||||
<polygon points="12,2 4,22 12,16 20,22" fill="white" opacity="0.9" />
|
||||
{/* 색상 레이어 */}
|
||||
<polygon points="12,3 5,21 12,15.5 19,21" fill={color} opacity="0.95" />
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
style={{ color, textShadow: '0 1px 3px rgba(0,0,0,0.8)' }}
|
||||
className="text-xs font-bold leading-none"
|
||||
className="text-caption font-bold leading-none px-1 py-px rounded-sm bg-bg-base/80"
|
||||
style={{ color }}
|
||||
>
|
||||
{station.wind.speed.toFixed(1)}
|
||||
</span>
|
||||
@ -138,7 +140,7 @@ export function WeatherMapOverlay({
|
||||
textShadow: '1px 1px 3px rgba(0,0,0,0.7)',
|
||||
borderBottom: `1px solid ${isSelected ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.3)'}`,
|
||||
}}
|
||||
className="text-center text-xs font-bold pb-1 mb-0.5"
|
||||
className="text-center text-caption font-bold pb-1 mb-0.5"
|
||||
>
|
||||
{station.name}
|
||||
</div>
|
||||
@ -150,7 +152,7 @@ export function WeatherMapOverlay({
|
||||
🌡️
|
||||
</div>
|
||||
<div className="flex items-baseline gap-0.5">
|
||||
<span className="text-sm font-bold text-white" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>
|
||||
<span className="text-body-2 font-bold text-white" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>
|
||||
{station.temperature.current.toFixed(1)}
|
||||
</span>
|
||||
<span className="text-caption text-white opacity-90" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>°C</span>
|
||||
@ -164,7 +166,7 @@ export function WeatherMapOverlay({
|
||||
🌊
|
||||
</div>
|
||||
<div className="flex items-baseline gap-0.5">
|
||||
<span className="text-sm font-bold text-white" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>
|
||||
<span className="text-body-2 font-bold text-white" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>
|
||||
{station.wave.height.toFixed(1)}
|
||||
</span>
|
||||
<span className="text-caption text-white opacity-90" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>m</span>
|
||||
@ -178,7 +180,7 @@ export function WeatherMapOverlay({
|
||||
💨
|
||||
</div>
|
||||
<div className="flex items-baseline gap-0.5">
|
||||
<span className="text-sm font-bold text-white" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>
|
||||
<span className="text-body-2 font-bold text-white" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>
|
||||
{station.wind.speed.toFixed(1)}
|
||||
</span>
|
||||
<span className="text-caption text-white opacity-90" style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.5)' }}>m/s</span>
|
||||
@ -205,7 +207,6 @@ export function useWeatherDeckLayers(
|
||||
onStationClick: (station: WeatherStation) => void,
|
||||
): Layer[] {
|
||||
return useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: Layer[] = [];
|
||||
|
||||
// 파고 분포 ScatterplotLayer (Circle 대체, 반경 = 파고 * 15km)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface WeatherData {
|
||||
stationName: string;
|
||||
location: { lat: number; lon: number };
|
||||
@ -46,20 +48,14 @@ interface WeatherRightPanelProps {
|
||||
weatherData: WeatherData | null;
|
||||
}
|
||||
|
||||
/** 풍속 등급 색상 */
|
||||
function windColor(speed: number): string {
|
||||
if (speed >= 14) return '#ef4444';
|
||||
if (speed >= 10) return '#f97316';
|
||||
if (speed >= 6) return '#eab308';
|
||||
return '#22c55e';
|
||||
/** 풍속 텍스트 색상 (2단계 — danger | accent) */
|
||||
function windTextColor(speed: number): string {
|
||||
return speed >= 10 ? 'var(--color-danger)' : 'var(--color-accent)';
|
||||
}
|
||||
|
||||
/** 파고 등급 색상 */
|
||||
function waveColor(height: number): string {
|
||||
if (height >= 3) return '#ef4444';
|
||||
if (height >= 2) return '#f97316';
|
||||
if (height >= 1) return '#eab308';
|
||||
return '#22c55e';
|
||||
/** 파고 텍스트 색상 (2단계 — danger | accent) */
|
||||
function waveTextColor(height: number): string {
|
||||
return height >= 2 ? 'var(--color-danger)' : 'var(--color-accent)';
|
||||
}
|
||||
|
||||
/** 풍향 텍스트 */
|
||||
@ -86,13 +82,38 @@ function windDirText(deg: number): string {
|
||||
}
|
||||
|
||||
export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<div className="flex flex-col bg-bg-surface border-l border-stroke w-8 shrink-0">
|
||||
<button
|
||||
onClick={() => setCollapsed(false)}
|
||||
className="flex-1 flex flex-col items-center justify-start pt-3 gap-1 text-fg-sub hover:text-fg transition-colors"
|
||||
title="패널 펼치기"
|
||||
>
|
||||
<span className="text-heading-2 leading-none">‹</span>
|
||||
<span className="text-subtitle text-fg-disabled [writing-mode:vertical-rl]">펼치기</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!weatherData) {
|
||||
return (
|
||||
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden w-[320px] shrink-0">
|
||||
<div className="p-6 text-center">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-stroke bg-bg-elevated">
|
||||
<p className="text-fg-disabled text-title-4 font-korean">
|
||||
지도에서 해양 지점을 클릭하세요
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setCollapsed(true)}
|
||||
className="shrink-0 flex items-center gap-0.5 text-fg-sub hover:text-fg transition-colors ml-2"
|
||||
title="패널 접기"
|
||||
>
|
||||
<span className="text-subtitle text-fg-disabled">접기</span>
|
||||
<span className="text-heading-2 leading-none">›</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -109,18 +130,30 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
<div className="flex flex-col bg-bg-surface border-l border-stroke overflow-hidden w-[320px] shrink-0">
|
||||
{/* 헤더 */}
|
||||
<div className="px-4 py-3 border-b border-stroke bg-bg-elevated">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-title-4 font-bold text-color-accent font-korean">
|
||||
📍 {weatherData.stationName}
|
||||
</span>
|
||||
<span className="px-1.5 py-px text-label-2 rounded bg-[rgba(6,182,212,0.15)] text-color-accent font-bold">
|
||||
기상예보관
|
||||
</span>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-title-4 font-bold text-color-accent font-korean truncate">
|
||||
📍 {weatherData.stationName}
|
||||
</span>
|
||||
<span className="px-1.5 py-px text-label-2 rounded bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-color-accent font-bold shrink-0">
|
||||
기상예보관
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-label-2 text-fg-disabled font-mono">
|
||||
{weatherData.location.lat.toFixed(2)}°N, {weatherData.location.lon.toFixed(2)}°E ·{' '}
|
||||
{weatherData.currentTime}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCollapsed(true)}
|
||||
className="shrink-0 flex items-center gap-0.5 text-fg-sub hover:text-fg transition-colors ml-2 mt-0.5"
|
||||
title="패널 접기"
|
||||
>
|
||||
<span className="text-subtitle text-fg-disabled">접기</span>
|
||||
<span className="text-heading-2 leading-none">›</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-label-2 text-fg-disabled font-mono">
|
||||
{weatherData.location.lat.toFixed(2)}°N, {weatherData.location.lon.toFixed(2)}°E ·{' '}
|
||||
{weatherData.currentTime}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 스크롤 콘텐츠 */}
|
||||
@ -131,13 +164,13 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
{/* ── 핵심 지표 3칸 카드 ── */}
|
||||
<div className="grid grid-cols-3 gap-1 px-3 py-2.5">
|
||||
<div className="text-center py-2.5 bg-bg-base border border-stroke rounded-md">
|
||||
<div className="text-[20px] font-bold font-mono" style={{ color: windColor(wSpd) }}>
|
||||
<div className="text-[20px] font-bold font-mono" style={{ color: windTextColor(wSpd) }}>
|
||||
{wSpd.toFixed(1)}
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean">풍속 (m/s)</div>
|
||||
</div>
|
||||
<div className="text-center py-2.5 bg-bg-base border border-stroke rounded-md">
|
||||
<div className="text-[20px] font-bold font-mono" style={{ color: waveColor(wHgt) }}>
|
||||
<div className="text-[20px] font-bold font-mono" style={{ color: waveTextColor(wHgt) }}>
|
||||
{wHgt.toFixed(1)}
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean">파고 (m)</div>
|
||||
@ -152,9 +185,7 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
|
||||
{/* ── 바람 상세 ── */}
|
||||
<div className="px-3 py-2 border-b border-stroke">
|
||||
<div className="text-label-2 font-bold text-fg-disabled font-korean mb-2">
|
||||
🌬️ 바람 현황
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean mb-2">🌬️ 바람 현황</div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{/* 풍향 컴파스 */}
|
||||
<div className="relative w-[50px] h-[50px] shrink-0">
|
||||
@ -202,11 +233,11 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
y1="25"
|
||||
x2={25 + 14 * Math.sin((wind.direction * Math.PI) / 180)}
|
||||
y2={25 - 14 * Math.cos((wind.direction * Math.PI) / 180)}
|
||||
stroke={windColor(wSpd)}
|
||||
stroke="var(--color-accent)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="25" cy="25" r="3" fill={windColor(wSpd)} />
|
||||
<circle cx="25" cy="25" r="3" fill="var(--color-accent)" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 grid grid-cols-2 gap-x-3 gap-y-1.5 text-label-2">
|
||||
@ -222,19 +253,13 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-fg-disabled">1k 최고</span>
|
||||
<span
|
||||
className="font-mono text-title-4"
|
||||
style={{ color: windColor(wind.speed_1k) }}
|
||||
>
|
||||
<span className="font-mono text-title-4 text-fg">
|
||||
{Number(wind.speed_1k).toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-fg-disabled">3k 평균</span>
|
||||
<span
|
||||
className="font-mono text-title-4"
|
||||
style={{ color: windColor(wind.speed_3k) }}
|
||||
>
|
||||
<span className="font-mono text-title-4 text-fg">
|
||||
{Number(wind.speed_3k).toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
@ -248,11 +273,8 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-[5px] bg-bg-card rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${Math.min((wSpd / 20) * 100, 100)}%`,
|
||||
background: windColor(wSpd),
|
||||
}}
|
||||
className="h-full rounded-full transition-all bg-color-accent"
|
||||
style={{ width: `${Math.min((wSpd / 20) * 100, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-label-2 font-mono text-fg-disabled shrink-0">
|
||||
@ -263,24 +285,20 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
|
||||
{/* ── 파도 상세 ── */}
|
||||
<div className="px-3 py-2 border-b border-stroke">
|
||||
<div className="text-label-2 font-bold text-fg-disabled font-korean mb-2">🌊 파도</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean mb-2">🌊 파도</div>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
|
||||
<div className="text-title-3 font-bold font-mono" style={{ color: waveColor(wHgt) }}>
|
||||
{wHgt.toFixed(1)}m
|
||||
</div>
|
||||
<div className="text-title-3 font-bold font-mono text-fg">{wHgt.toFixed(1)}m</div>
|
||||
<div className="text-caption text-fg-disabled">유의파고</div>
|
||||
</div>
|
||||
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
|
||||
<div className="text-title-3 font-bold font-mono text-color-danger">
|
||||
<div className="text-title-3 font-bold font-mono text-fg">
|
||||
{wave.maxHeight.toFixed(1)}m
|
||||
</div>
|
||||
<div className="text-caption text-fg-disabled">최고파고</div>
|
||||
</div>
|
||||
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
|
||||
<div className="text-title-3 font-bold font-mono text-color-accent">
|
||||
{wave.period}s
|
||||
</div>
|
||||
<div className="text-title-3 font-bold font-mono text-fg">{wave.period}s</div>
|
||||
<div className="text-caption text-fg-disabled">주기</div>
|
||||
</div>
|
||||
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
|
||||
@ -295,7 +313,7 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${Math.min((wHgt / 5) * 100, 100)}%`,
|
||||
background: waveColor(wHgt),
|
||||
background: 'var(--color-accent)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -307,14 +325,10 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
|
||||
{/* ── 수온/공기 ── */}
|
||||
<div className="px-3 py-2 border-b border-stroke">
|
||||
<div className="text-label-2 font-bold text-fg-disabled font-korean mb-2">
|
||||
🌡️ 수온 · 공기
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean mb-2">🌡️ 수온 · 공기</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
|
||||
<div className="text-title-3 font-bold font-mono text-color-accent">
|
||||
{wTemp.toFixed(1)}°
|
||||
</div>
|
||||
<div className="text-title-3 font-bold font-mono text-fg">{wTemp.toFixed(1)}°</div>
|
||||
<div className="text-caption text-fg-disabled">수온</div>
|
||||
</div>
|
||||
<div className="text-center py-2 bg-bg-base border border-stroke rounded">
|
||||
@ -332,9 +346,7 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
|
||||
{/* ── 시간별 예보 ── */}
|
||||
<div className="px-3 py-2 border-b border-stroke">
|
||||
<div className="text-label-2 font-bold text-fg-disabled font-korean mb-2">
|
||||
⏰ 시간별 예보
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean mb-2">⏰ 시간별 예보</div>
|
||||
<div className="grid grid-cols-5 gap-1">
|
||||
{forecast.map((f, i) => (
|
||||
<div
|
||||
@ -353,9 +365,7 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
{/* ── 천문/조석 ── */}
|
||||
{astronomy && (
|
||||
<div className="px-3 py-2 border-b border-stroke">
|
||||
<div className="text-label-2 font-bold text-fg-disabled font-korean mb-2">
|
||||
☀️ 천문 · 조석
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean mb-2">☀️ 천문 · 조석</div>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{[
|
||||
{ icon: '🌅', label: '일출', value: astronomy.sunrise },
|
||||
@ -371,7 +381,7 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1.5 px-2 py-1 bg-bg-base border border-stroke rounded text-label-2">
|
||||
<span className="text-sm">🌓</span>
|
||||
<span className="text-body-2">🌓</span>
|
||||
<span className="text-fg-disabled">{astronomy.moonPhase}</span>
|
||||
<span className="ml-auto text-fg font-mono">조차 {astronomy.tidalRange}m</span>
|
||||
</div>
|
||||
@ -381,17 +391,21 @@ export function WeatherRightPanel({ weatherData }: WeatherRightPanelProps) {
|
||||
{/* ── 날씨 특보 ── */}
|
||||
{alert && (
|
||||
<div className="px-3 py-2">
|
||||
<div className="text-label-2 font-bold text-fg-disabled font-korean mb-2">
|
||||
🚨 날씨 특보
|
||||
</div>
|
||||
<div className="text-label-2 text-fg-disabled font-korean mb-2">🚨 날씨 특보</div>
|
||||
<div
|
||||
className="px-2.5 py-2 rounded border"
|
||||
style={{ background: 'rgba(239,68,68,.06)', borderColor: 'rgba(239,68,68,.2)' }}
|
||||
style={{
|
||||
background: 'color-mix(in srgb, var(--color-danger) 6%, transparent)',
|
||||
borderColor: 'color-mix(in srgb, var(--color-danger) 20%, transparent)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-label-2">
|
||||
<span
|
||||
className="px-1.5 py-px rounded text-label-2 font-bold"
|
||||
style={{ background: 'rgba(239,68,68,.15)', color: 'var(--color-danger)' }}
|
||||
style={{
|
||||
background: 'color-mix(in srgb, var(--color-danger) 15%, transparent)',
|
||||
color: 'var(--color-danger)',
|
||||
}}
|
||||
>
|
||||
주의
|
||||
</span>
|
||||
|
||||
@ -90,7 +90,6 @@ const WEATHER_MAP_CENTER: [number, number] = [127.8, 36.5]; // [lng, lat]
|
||||
const WEATHER_MAP_ZOOM = 7;
|
||||
|
||||
// deck.gl 오버레이 컴포넌트 (MapLibre 컨트롤로 등록)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function DeckGLOverlay({ layers }: { layers: Layer[] }) {
|
||||
const overlay = useControl<MapboxOverlay>(() => new MapboxOverlay({ interleaved: true }));
|
||||
overlay.setProps({ layers });
|
||||
@ -178,7 +177,7 @@ function WeatherMapInner({
|
||||
{/* 핀 꼬리 */}
|
||||
<div className="w-px h-3 bg-color-accent" />
|
||||
{/* 좌표 라벨 */}
|
||||
<div className="px-2 py-1 bg-bg-base/90 border border-color-accent rounded text-caption text-color-accent whitespace-nowrap backdrop-blur-sm">
|
||||
<div className="px-2 py-1 bg-bg-base border border-color-accent rounded text-caption text-color-accent whitespace-nowrap shadow-md">
|
||||
{clickedLocation.lat.toFixed(3)}°N {clickedLocation.lon.toFixed(3)}°E
|
||||
</div>
|
||||
</div>
|
||||
@ -295,13 +294,13 @@ export function WeatherView() {
|
||||
{/* Main Map Area */}
|
||||
<div className="flex-1 relative flex flex-col overflow-hidden">
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex items-center border-b border-stroke bg-bg-surface shrink-0">
|
||||
<div className="flex items-center border-b border-stroke bg-bg-surface shrink-0 pt-2 pb-2">
|
||||
<div className="flex items-center gap-2 px-6">
|
||||
{(['0', '3', '6', '9'] as TimeOffset[]).map((offset) => (
|
||||
<button
|
||||
key={offset}
|
||||
onClick={() => setTimeOffset(offset)}
|
||||
className={`px-3 py-2 text-xs font-semibold rounded transition-all ${
|
||||
className={`px-3 py-2 text-caption font-semibold rounded transition-all ${
|
||||
timeOffset === offset
|
||||
? 'bg-color-accent text-bg-0'
|
||||
: 'bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover'
|
||||
@ -312,7 +311,7 @@ export function WeatherView() {
|
||||
))}
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<span className="text-xs text-fg-disabled">
|
||||
<span className="text-caption text-fg-disabled">
|
||||
{lastUpdate
|
||||
? `마지막 업데이트: ${lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -323,7 +322,7 @@ export function WeatherView() {
|
||||
{loading && (
|
||||
<div className="w-4 h-4 border-2 border-color-accent border-t-transparent rounded-full animate-spin" />
|
||||
)}
|
||||
{error && <span className="text-xs text-color-danger">⚠️ {error}</span>}
|
||||
{error && <span className="text-caption text-color-danger">⚠️ {error}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -356,10 +355,7 @@ export function WeatherView() {
|
||||
</Map>
|
||||
|
||||
{/* 레이어 컨트롤 */}
|
||||
<div
|
||||
className="absolute top-4 left-4 bg-bg-surface/85 border border-stroke rounded-md backdrop-blur-sm z-10"
|
||||
style={{ padding: '6px 10px' }}
|
||||
>
|
||||
<div className="absolute top-4 left-4 bg-bg-surface border border-stroke rounded-md shadow-md z-10 px-2.5 py-1.5">
|
||||
<div className="text-caption font-semibold text-fg mb-1.5 font-korean">기상 레이어</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
@ -420,17 +416,12 @@ export function WeatherView() {
|
||||
</div>
|
||||
|
||||
{/* 범례 */}
|
||||
<div
|
||||
className="absolute bottom-4 left-4 bg-bg-surface/85 border border-stroke rounded-md backdrop-blur-sm z-10"
|
||||
style={{ padding: '6px 10px', maxWidth: 180 }}
|
||||
>
|
||||
<div className="text-caption font-semibold text-fg mb-1.5 font-korean">기상 범례</div>
|
||||
<div className="flex flex-col gap-1.5" style={{ fontSize: 8 }}>
|
||||
<div className="absolute bottom-4 left-4 bg-bg-surface border border-stroke rounded-md shadow-md z-10 px-2.5 py-1.5 max-w-[180px]">
|
||||
<div className="text-caption text-fg mb-1.5 font-korean">기상 범례</div>
|
||||
<div className="flex flex-col gap-1.5 text-[8px]">
|
||||
{/* 바람 */}
|
||||
<div>
|
||||
<div className="font-semibold text-fg-sub mb-0.5" style={{ fontSize: 8 }}>
|
||||
바람 (m/s)
|
||||
</div>
|
||||
<div className="text-fg-sub mb-0.5">바람 (m/s)</div>
|
||||
<div className="flex items-center gap-px h-[6px] rounded-sm overflow-hidden mb-0.5">
|
||||
<div className="flex-1 h-full" style={{ background: '#6271b7' }} />
|
||||
<div className="flex-1 h-full" style={{ background: '#39a0f6' }} />
|
||||
@ -441,7 +432,7 @@ export function WeatherView() {
|
||||
<div className="flex-1 h-full" style={{ background: '#f05421' }} />
|
||||
<div className="flex-1 h-full" style={{ background: '#b41e46' }} />
|
||||
</div>
|
||||
<div className="flex justify-between text-fg-disabled" style={{ fontSize: 7 }}>
|
||||
<div className="flex justify-between text-fg-disabled text-[7px]">
|
||||
<span>3</span>
|
||||
<span>5</span>
|
||||
<span>7</span>
|
||||
@ -453,16 +444,14 @@ export function WeatherView() {
|
||||
</div>
|
||||
{/* 해류 */}
|
||||
<div className="pt-1 border-t border-stroke">
|
||||
<div className="font-semibold text-fg-sub mb-0.5" style={{ fontSize: 8 }}>
|
||||
해류 (m/s)
|
||||
</div>
|
||||
<div className="text-fg-sub mb-0.5">해류 (m/s)</div>
|
||||
<div className="flex items-center gap-px h-[6px] rounded-sm overflow-hidden mb-0.5">
|
||||
<div className="flex-1 h-full" style={{ background: 'rgb(59, 130, 246)' }} />
|
||||
<div className="flex-1 h-full" style={{ background: 'rgb(6, 182, 212)' }} />
|
||||
<div className="flex-1 h-full" style={{ background: 'rgb(34, 197, 94)' }} />
|
||||
<div className="flex-1 h-full" style={{ background: 'rgb(249, 115, 22)' }} />
|
||||
</div>
|
||||
<div className="flex justify-between text-fg-disabled" style={{ fontSize: 7 }}>
|
||||
<div className="flex justify-between text-fg-disabled text-[7px]">
|
||||
<span>0.2</span>
|
||||
<span>0.4</span>
|
||||
<span>0.6</span>
|
||||
@ -471,23 +460,18 @@ export function WeatherView() {
|
||||
</div>
|
||||
{/* 파고 */}
|
||||
<div className="pt-1 border-t border-stroke">
|
||||
<div className="font-semibold text-fg-sub mb-0.5" style={{ fontSize: 8 }}>
|
||||
파고 (m)
|
||||
</div>
|
||||
<div className="text-fg-sub mb-0.5">파고 (m)</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<div className="w-2 h-2 rounded-full bg-color-info" />
|
||||
<span className="text-fg-disabled"><1.5 낮음</span>
|
||||
<div className="w-2 h-2 rounded-full bg-orange-500 ml-1" />
|
||||
<div className="w-2 h-2 rounded-full bg-color-warning ml-1" />
|
||||
<span className="text-fg-disabled">~2.5</span>
|
||||
<div className="w-2 h-2 rounded-full bg-red-500 ml-1" />
|
||||
<div className="w-2 h-2 rounded-full bg-color-danger ml-1" />
|
||||
<span className="text-fg-disabled">>2.5</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="mt-1 pt-1 border-t border-stroke text-fg-disabled font-korean"
|
||||
style={{ fontSize: 7 }}
|
||||
>
|
||||
<div className="mt-1 pt-1 border-t border-stroke text-fg-disabled text-[7px] font-korean">
|
||||
💡 지도 클릭 → 기상 예보 확인
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -77,29 +77,29 @@ export default {
|
||||
{ lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-navigation)' },
|
||||
],
|
||||
'title-3': [
|
||||
'0.875rem',
|
||||
'1rem',
|
||||
{ lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-navigation)' },
|
||||
],
|
||||
'title-4': [
|
||||
'0.8125rem',
|
||||
'0.875rem',
|
||||
{ lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-navigation)' },
|
||||
],
|
||||
'title-5': [
|
||||
'0.75rem',
|
||||
'0.8125rem',
|
||||
{ lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-navigation)' },
|
||||
],
|
||||
'title-6': [
|
||||
'0.6875rem',
|
||||
'0.75rem',
|
||||
{ lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-navigation)' },
|
||||
],
|
||||
'body-1': ['0.875rem', { lineHeight: '1.6', letterSpacing: 'var(--letter-spacing-body)' }],
|
||||
'body-2': ['0.8125rem', { lineHeight: '1.6', letterSpacing: 'var(--letter-spacing-body)' }],
|
||||
'label-1': ['0.75rem', { lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-label)' }],
|
||||
'label-2': [
|
||||
'0.6875rem',
|
||||
'body-1': ['1rem', { lineHeight: '1.6', letterSpacing: 'var(--letter-spacing-body)' }],
|
||||
'body-2': ['0.875rem', { lineHeight: '1.6', letterSpacing: 'var(--letter-spacing-body)' }],
|
||||
'label-1': [
|
||||
'0.8125rem',
|
||||
{ lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-label)' },
|
||||
],
|
||||
caption: ['0.6875rem', { lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-body)' }],
|
||||
'label-2': ['0.75rem', { lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-label)' }],
|
||||
caption: ['0.75rem', { lineHeight: '1.5', letterSpacing: 'var(--letter-spacing-body)' }],
|
||||
},
|
||||
letterSpacing: {
|
||||
display: 'var(--letter-spacing-display)',
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user