feat(design): 디자인 시스템 시맨틱 토큰 전환 및 다크/라이트 테마 전환 기능 #142

병합
dnlee feature/predict-develop 에서 develop 로 4 commits 를 머지했습니다 2026-03-31 15:09:13 +09:00
126개의 변경된 파일6321개의 추가작업 그리고 6103개의 파일을 삭제
Showing only changes of commit 5e2076647c - Show all commits

파일 보기

@ -3,26 +3,6 @@
## 개요
WING-OPS UI 디자인 시스템의 비주얼 레퍼런스 카탈로그.
Google Stitch MCP로 생성된 스크린을 기반으로 일관된 UI 구현을 유도한다.
## Stitch 프로젝트
- **프로젝트명**: WING-OPS Design System v1
- **프로젝트 ID**: `5453076280291618640`
## 스크린 목록
| # | 스크린 | Screen ID | 용도 |
|---|--------|-----------|------|
| 1 | Design Tokens | `ce520225d85c4c38b2024e93ec6a4fb2` | 색상, 타이포그래피, 간격, 라운딩 토큰 |
| 2 | Component Catalog (Buttons/Badges) | `42fa9cf1a3d341a7972a1bc10ba00a8c` | 버튼 variant, 뱃지, 아이콘 버튼 |
| 3 | Form Components | `7331ad8a598f4cc59f62a14226c1d023` | 입력, 선택, 날짜, 토글, 폼 레이아웃 |
| 4 | Table & List Patterns | `5967382c70f9422ba3a0f4da79922ecf` | 데이터 테이블, 사이드바 리스트, 페이지네이션 |
| 5 | Modal Catalog | `440be91f8db7423cbb5cc89e6dd6f9ca` | 모달 3사이즈, 확인 다이얼로그, 폼 모달 |
| 6 | Operational Shell (Layout) | `86fd57c9f3c749d288f6270838a9387d` | TopBar, SubMenu, 3컬럼 레이아웃 |
| 7 | Container & Navigation | `201c2c0c47b74fcfb3427d029319fa9d` | 카드, 섹션, 탭바, KV 행, 헤더바 |
---
## Foundations
@ -87,8 +67,22 @@ UI 전반에서 사용하는 기본 색조 팔레트. Navy는 배경 전용 5단
| 토큰 | Dark | Light | 용도 |
|------|------|-------|------|
| `text-1` | `#edf0f7` | `#0f172a` | 기본 텍스트, 아이콘 기본 |
| `text-2` | `#b0b8cc` | `#475569` | 보조 텍스트 |
| `text-3` | `#8690a6` | `#94a3b8` | 비활성, 플레이스홀더 |
| `text-2` | `#c0c8dc` | `#475569` | 보조 텍스트 |
| `text-3` | `#9ba3b8` | `#94a3b8` | 비활성, 플레이스홀더 |
> **TODO: 텍스트 토큰 시맨틱 리네이밍**
>
> Tailwind config의 `colors.text` 키를 `colors.color`로 변경하여 클래스명을 직관적으로 개선한다.
>
> | 현재 | 변경 후 | 용도 |
> |------|---------|------|
> | `text-text-1` | `text-color` (DEFAULT) | 기본 텍스트 |
> | `text-text-2` | `text-color-sub` | 보조 텍스트 |
> | `text-text-3` | `text-color-disabled` | 비활성, 플레이스홀더 |
>
> - 영향 범위: 96개 파일, 2,685회 참조
> - CSS 변수(`--t1/2/3`)도 `--color-default/sub/disabled`로 동기화
> - 기계적 find-and-replace로 처리 가능
**Background**
@ -132,11 +126,11 @@ UI 전반에서 사용하는 기본 색조 팔레트. Navy는 배경 전용 5단
| 이름 | className | Font Stack | 용도 |
|------|-----------|------------|------|
| Noto Sans KR | `font-korean` | `'Noto Sans KR', sans-serif` | 기본 UI 텍스트, 한국어 콘텐츠 전반 |
| JetBrains Mono | `font-mono` | `'JetBrains Mono', monospace` | 좌표, 수치, 코드, 토큰 이름 |
| Outfit | `font-sans` | `'Outfit', 'Noto Sans KR', sans-serif` | 영문 헤딩, 브랜드 타이틀 |
| PretendardGOV | `font-korean` | `'PretendardGOV', sans-serif` | 기본 UI 텍스트, 한국어/영문 콘텐츠 전반 |
| PretendardGOV | `font-mono` | `'PretendardGOV', sans-serif` | 좌표, 수치, 데이터 값 |
| PretendardGOV | `font-sans` | `'PretendardGOV', sans-serif` | body 기본 폰트 |
> Body 기본 스택: `font-family: 'Outfit', 'Noto Sans KR', sans-serif`
> Body 기본 스택: `font-family: 'PretendardGOV', sans-serif`
#### Typography Tokens (`.wing-*` 클래스)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

파일 보기

@ -105,7 +105,7 @@ function App() {
case 'monitor':
return null
default:
return <div className="flex items-center justify-center h-full text-text-3"> ...</div>
return <div className="flex items-center justify-center h-full text-fg-disabled"> ...</div>
}
}

파일 보기

@ -98,11 +98,11 @@ export function LoginPage() {
<form onSubmit={handleSubmit}>
{/* User ID */}
<div className="mb-4">
<label className="block text-[10px] font-semibold text-text-3 mb-1.5" style={{ letterSpacing: '0.3px' }}>
<label className="block text-[10px] font-semibold text-fg-disabled mb-1.5" style={{ letterSpacing: '0.3px' }}>
</label>
<div className="relative">
<span className="absolute text-sm text-text-3 pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
<span className="absolute text-sm text-fg-disabled pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<input
@ -112,7 +112,7 @@ export function LoginPage() {
placeholder="사용자 아이디 입력"
autoComplete="username"
autoFocus
className="w-full bg-bg-2 border border-border rounded-md text-[13px] outline-none"
className="w-full bg-bg-elevated border border-stroke rounded-md text-[13px] outline-none"
style={{
padding: '11px 14px 11px 38px',
transition: 'border-color 0.2s, box-shadow 0.2s',
@ -122,7 +122,7 @@ export function LoginPage() {
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(6,182,212,0.08)'
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = 'var(--bd)'
e.currentTarget.style.borderColor = 'var(--stroke-default)'
e.currentTarget.style.boxShadow = 'none'
}}
/>
@ -131,11 +131,11 @@ export function LoginPage() {
{/* Password */}
<div className="mb-5">
<label className="block text-[10px] font-semibold text-text-3 mb-1.5" style={{ letterSpacing: '0.3px' }}>
<label className="block text-[10px] font-semibold text-fg-disabled mb-1.5" style={{ letterSpacing: '0.3px' }}>
</label>
<div className="relative">
<span className="absolute text-sm text-text-3 pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
<span className="absolute text-sm text-fg-disabled pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</span>
<input
@ -144,7 +144,7 @@ export function LoginPage() {
onChange={(e) => { setPassword(e.target.value); clearError() }}
placeholder="비밀번호 입력"
autoComplete="current-password"
className="w-full bg-bg-2 border border-border rounded-md text-[13px] outline-none"
className="w-full bg-bg-elevated border border-stroke rounded-md text-[13px] outline-none"
style={{
padding: '11px 14px 11px 38px',
transition: 'border-color 0.2s, box-shadow 0.2s',
@ -154,7 +154,7 @@ export function LoginPage() {
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(6,182,212,0.08)'
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = 'var(--bd)'
e.currentTarget.style.borderColor = 'var(--stroke-default)'
e.currentTarget.style.boxShadow = 'none'
}}
/>
@ -163,16 +163,16 @@ export function LoginPage() {
{/* Remember + Forgot */}
<div className="flex items-center justify-between mb-5">
<label className="flex items-center gap-1.5 text-[11px] text-text-3 cursor-pointer">
<label className="flex items-center gap-1.5 text-[11px] text-fg-disabled cursor-pointer">
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="accent-[var(--cyan)]"
className="accent-[var(--color-accent)]"
/>
</label>
<button type="button" className="text-[11px] text-primary-cyan cursor-pointer bg-transparent border-none"
<button type="button" className="text-[11px] text-color-accent cursor-pointer bg-transparent border-none"
onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'}
onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
>
@ -209,7 +209,7 @@ export function LoginPage() {
)}
{/* Login button */}
<button type="submit" disabled={isLoading} className="w-full text-primary-cyan text-sm font-bold rounded-md border"
<button type="submit" disabled={isLoading} className="w-full text-color-accent text-sm font-bold rounded-md border"
style={{
padding: '12px',
background: isLoading
@ -237,7 +237,7 @@ export function LoginPage() {
<span className="flex items-center justify-center gap-2">
<span style={{
width: 14, height: 14, border: '2px solid rgba(6,182,212,0.3)',
borderTop: '2px solid var(--cyan)', borderRadius: '50%',
borderTop: '2px solid var(--color-accent)', borderRadius: '50%',
animation: 'loginSpin 0.8s linear infinite', display: 'inline-block',
}} />
...
@ -249,7 +249,7 @@ export function LoginPage() {
{/* Divider */}
<div className="flex items-center gap-3 my-6">
<div className="flex-1 bg-border h-px" />
<span className="text-[9px] text-text-3"></span>
<span className="text-[9px] text-fg-disabled"></span>
<div className="flex-1 bg-border h-px" />
</div>
@ -267,10 +267,10 @@ export function LoginPage() {
/>
</div>
)}
<button type="button" className="w-full rounded-md bg-bg-3 border border-border text-text-2 text-[11px] font-semibold cursor-pointer flex items-center justify-center gap-1.5 px-[10px] py-[10px]"
<button type="button" className="w-full rounded-md bg-bg-card border border-stroke text-fg-sub text-[11px] font-semibold cursor-pointer flex items-center justify-center gap-1.5 px-[10px] py-[10px]"
style={{ transition: 'background 0.15s' }}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bgH)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--bg3)'}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-surface-hover)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--bg-card)'}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
@ -283,7 +283,7 @@ export function LoginPage() {
padding: '10px 12px',
background: 'rgba(6,182,212,0.04)', border: '1px solid rgba(6,182,212,0.08)',
}}>
<div className="text-[9px] font-bold text-primary-cyan mb-1.5">
<div className="text-[9px] font-bold text-color-accent mb-1.5">
</div>
<div className="flex flex-col gap-[3px]">
@ -298,10 +298,10 @@ export function LoginPage() {
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(6,182,212,0.06)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
>
<span className="text-[9px] text-text-2 font-mono">
<span className="text-[9px] text-fg-sub font-mono">
{acc.id} / {acc.password}
</span>
<span className="text-[8px] text-text-3">
<span className="text-[8px] text-fg-disabled">
{acc.label}
</span>
</div>
@ -312,7 +312,7 @@ export function LoginPage() {
</div>{/* end form card */}
{/* Footer */}
<div className="text-center text-[9px] text-text-3 mt-6 leading-[1.6]">
<div className="text-center text-[9px] text-fg-disabled mt-6 leading-[1.6]">
<div>WING V2.0 | </div>
<div className="mt-0.5" style={{ color: 'rgba(134,144,166,0.6)' }}>
&copy; 2026 Korea Coast Guard. All rights reserved.

파일 보기

@ -26,8 +26,8 @@ export function LayerTree({ layers, enabledLayers, onToggleLayer, layerColors =
return (
<div className="px-1">
<div className="flex items-center justify-between px-2 pt-1 pb-2 mb-1 border-b border-border">
<span className="text-[10px] font-semibold text-text-3">
<div className="flex items-center justify-between px-2 pt-1 pb-2 mb-1 border-b border-stroke">
<span className="text-[10px] font-semibold text-fg-disabled">
</span>
<div

파일 보기

@ -11,7 +11,7 @@ interface MainLayoutProps {
export function MainLayout({ children, activeMainTab, onMainTabChange }: MainLayoutProps) {
return (
<div className="h-screen w-screen flex flex-col bg-bg-0 text-text-1 overflow-hidden">
<div className="h-screen w-screen flex flex-col bg-bg-base text-fg overflow-hidden">
{/* Top Navigation - Level 1 */}
<TopBar activeTab={activeMainTab} onTabChange={onMainTabChange} />

파일 보기

@ -14,19 +14,19 @@ export function SubMenuBar({ activeMainTab }: SubMenuBarProps) {
}
return (
<div className="border-b border-border bg-bg-1 shrink-0">
<div className="border-b border-stroke bg-bg-surface shrink-0">
<div className="flex px-5">
{subMenuConfig.map((item) => (
<button
key={item.id}
onClick={() => setActiveSubTab(item.id)}
className={`
px-4 py-2.5 text-[13px] font-bold transition-all duration-200
px-4 py-2.5 text-[0.8125rem] font-bold transition-all duration-200
font-korean tracking-tight
${
activeSubTab === item.id
? 'text-primary-cyan bg-[rgba(6,182,212,0.12)] border-b-2 border-primary-cyan'
: 'text-text-2 hover:text-text-1 hover:bg-[rgba(255,255,255,0.06)]'
? 'text-color-accent bg-[rgba(6,182,212,0.12)] border-b-2 border-color-accent'
: 'text-fg-sub hover:text-fg hover:bg-[rgba(255,255,255,0.06)]'
}
`}
>

파일 보기

@ -44,7 +44,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
}, [showQuickMenu])
return (
<div className="h-[52px] bg-bg-1 border-b border-border flex items-center justify-between px-5 relative z-[100]">
<div className="h-[52px] bg-bg-surface border-b border-stroke flex items-center justify-between px-5 relative z-[100]">
{/* Left Section */}
<div className="flex items-center gap-4">
{/* Logo */}
@ -77,7 +77,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
onClick={handleClick}
title={tab.label}
className={`
px-2.5 xl:px-4 py-2 rounded-sm text-[13px] transition-all duration-200
px-2.5 xl:px-4 py-2 rounded-sm text-[0.8125rem] transition-all duration-200
font-korean tracking-[0.2px]
${isIncident ? 'font-extrabold border-l border-l-[rgba(99,102,241,0.2)] ml-1' : 'font-semibold'}
${isMonitor ? 'border-l border-l-[rgba(239,68,68,0.25)] ml-1 flex items-center gap-1.5' : ''}
@ -100,11 +100,11 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
<span className="w-1.5 h-1.5 rounded-full bg-[#f87171] animate-pulse inline-block" />
{tab.label}
</span>
<span className="xl:hidden text-[16px] leading-none">{tab.icon}</span>
<span className="xl:hidden text-[1rem] leading-none">{tab.icon}</span>
</>
) : (
<>
<span className="xl:hidden text-[16px] leading-none">{tab.icon}</span>
<span className="xl:hidden text-[1rem] leading-none">{tab.icon}</span>
<span className="hidden xl:inline">{tab.label}</span>
</>
)}
@ -117,13 +117,13 @@ 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-status-red animate-pulse">
<div className="w-1.5 h-1.5 rounded-full bg-status-red 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-xs font-medium text-color-danger animate-pulse">
<div className="w-1.5 h-1.5 rounded-full bg-color-danger animate-pulse" />
</div>
</div> */}
{/* Icon Buttons */}
<button className="w-9 h-9 rounded-sm border border-border bg-bg-3 text-text-2 flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all">
<button className="w-9 h-9 rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all">
🔔
</button>
{hasPermission('admin') && (
@ -131,19 +131,19 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
onClick={() => onTabChange('admin')}
className={`w-9 h-9 rounded-sm border flex items-center justify-center transition-all ${
activeTab === 'admin'
? 'border-primary-cyan bg-[rgba(6,182,212,0.15)] text-primary-cyan'
: 'border-border bg-bg-3 text-text-2 hover:bg-bg-hover hover:text-text-1'
? 'border-color-accent bg-[rgba(6,182,212,0.15)] text-color-accent'
: 'border-stroke bg-bg-card text-fg-sub hover:bg-bg-surface-hover hover:text-fg'
}`}
>
</button>
)}
{user && (
<div className="flex items-center gap-2 pl-2 border-l border-border">
<span className="text-[11px] text-text-2 font-korean">{user.name}</span>
<div className="flex items-center gap-2 pl-2 border-l border-stroke">
<span className="text-[0.6875rem] text-fg-sub font-korean">{user.name}</span>
<button
onClick={() => logout()}
className="px-2 py-1 text-[10px] font-semibold text-text-3 border border-border rounded hover:bg-bg-hover hover:text-text-1 transition-all font-korean"
className="px-2 py-1 text-[0.625rem] font-semibold text-fg-disabled border border-stroke rounded hover:bg-bg-surface-hover hover:text-fg transition-all font-korean"
title="로그아웃"
>
@ -157,79 +157,79 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
onClick={() => setShowQuickMenu(!showQuickMenu)}
className={`w-9 h-9 rounded-sm border flex items-center justify-center transition-all ${
showQuickMenu
? 'border-primary-cyan bg-[rgba(6,182,212,0.15)] text-primary-cyan'
: 'border-border bg-bg-3 text-text-2 hover:bg-bg-hover hover:text-text-1'
? 'border-color-accent bg-[rgba(6,182,212,0.15)] text-color-accent'
: 'border-stroke bg-bg-card text-fg-sub hover:bg-bg-surface-hover hover:text-fg'
}`}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><line x1="2" y1="4" x2="14" y2="4" /><line x1="2" y1="8" x2="14" y2="8" /><line x1="2" y1="12" x2="14" y2="12" /></svg>
</button>
{showQuickMenu && (
<div className="absolute top-[44px] right-0 w-[220px] bg-[rgba(18,25,41,0.97)] backdrop-blur-xl border border-border rounded-lg shadow-2xl z-[200] py-2 font-korean">
<div className="absolute top-[44px] right-0 w-[220px] bg-[rgba(18,25,41,0.97)] backdrop-blur-xl border border-stroke rounded-lg shadow-2xl z-[200] py-2 font-korean">
{/* 거리·면적 계산 */}
{/* <div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
{/* <div className="px-3 py-1.5 flex items-center gap-2 text-[0.6875rem] font-bold text-fg-disabled">
<span>📐</span> ·
</div> */}
<button
onClick={() => handleToggleMeasure('distance')}
disabled={!isMapTab}
className={`w-full px-3 py-2 flex items-center gap-2.5 text-[12px] transition-all ${
className={`w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] transition-all ${
!isMapTab
? 'text-text-3 opacity-40 cursor-not-allowed'
? 'text-fg-disabled opacity-40 cursor-not-allowed'
: measureMode === 'distance'
? 'text-primary-cyan bg-[rgba(6,182,212,0.1)]'
: 'text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1'
? 'text-color-accent bg-[rgba(6,182,212,0.1)]'
: 'text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg'
}`}
>
<span className="text-[13px]"></span>
{measureMode === 'distance' && <span className="ml-auto text-[10px] text-primary-cyan"></span>}
<span className="text-[0.8125rem]"></span>
{measureMode === 'distance' && <span className="ml-auto text-[0.625rem] text-color-accent"></span>}
</button>
<button
onClick={() => handleToggleMeasure('area')}
disabled={!isMapTab}
className={`w-full px-3 py-2 flex items-center gap-2.5 text-[12px] transition-all ${
className={`w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] transition-all ${
!isMapTab
? 'text-text-3 opacity-40 cursor-not-allowed'
? 'text-fg-disabled opacity-40 cursor-not-allowed'
: measureMode === 'area'
? 'text-primary-cyan bg-[rgba(6,182,212,0.1)]'
: 'text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1'
? 'text-color-accent bg-[rgba(6,182,212,0.1)]'
: 'text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg'
}`}
>
<span className="text-[13px]"></span>
{measureMode === 'area' && <span className="ml-auto text-[10px] text-primary-cyan"></span>}
<span className="text-[0.8125rem]"></span>
{measureMode === 'area' && <span className="ml-auto text-[0.625rem] text-color-accent"></span>}
</button>
<div className="my-1.5 border-t border-border" />
<div className="my-1.5 border-t border-stroke" />
{/* 출력 */}
<div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
<div className="px-3 py-1.5 flex items-center gap-2 text-[0.6875rem] font-bold text-fg-disabled">
<span>🖨</span>
</div>
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1 transition-all">
<span className="text-[13px]">📸</span>
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg transition-all">
<span className="text-[0.8125rem]">📸</span>
</button>
<button onClick={() => window.print()} className="w-full px-3 py-2 flex items-center gap-2.5 text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1 transition-all">
<span className="text-[13px]">🖨</span>
<button onClick={() => window.print()} className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg transition-all">
<span className="text-[0.8125rem]">🖨</span>
</button>
<div className="my-1.5 border-t border-border" />
<div className="my-1.5 border-t border-stroke" />
{/* 지도 유형 */}
<div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
<div className="px-3 py-1.5 flex items-center gap-2 text-[0.6875rem] font-bold text-fg-disabled">
<span>🗺</span>
</div>
{mapTypes.map(item => (
<button key={item.mapKey} onClick={() => toggleMap(item.mapKey)} className="w-full px-3 py-2 flex items-center justify-between text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] transition-all">
<button key={item.mapKey} onClick={() => toggleMap(item.mapKey)} className="w-full px-3 py-2 flex items-center justify-between text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] transition-all">
<span className="flex items-center gap-2.5">
<span className="text-[13px]">🗺</span> {item.mapNm}
<span className="text-[0.8125rem]">🗺</span> {item.mapNm}
</span>
<div className={`w-[34px] h-[18px] rounded-full transition-all relative ${mapToggles[item.mapKey] ? 'bg-primary-cyan' : 'bg-bg-3 border border-border'}`}>
<div className={`w-[34px] h-[18px] rounded-full transition-all relative ${mapToggles[item.mapKey] ? 'bg-color-accent' : 'bg-bg-card border border-stroke'}`}>
<div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow transition-all ${mapToggles[item.mapKey] ? 'left-[16px]' : 'left-[2px]'}`} />
</div>
</button>
))}
<div className="my-1.5 border-t border-border" />
<div className="my-1.5 border-t border-stroke" />
{/* 매뉴얼 */}
<button
@ -237,9 +237,9 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
setShowManual(true)
setShowQuickMenu(false)
}}
className="w-full px-3 py-2 flex items-center gap-2.5 text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1 transition-all"
className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg transition-all"
>
<span className="text-[13px]">&#x1F4D6;</span>
<span className="text-[0.8125rem]">&#x1F4D6;</span>
</button>
</div>
)}

파일 보기

@ -82,7 +82,7 @@ export function BacktrackReplayBar({
{/* Close button */}
<button
onClick={onClose}
className="text-status-red cursor-pointer font-bold"
className="text-color-danger cursor-pointer font-bold"
style={{
padding: '4px 10px', borderRadius: '6px', fontSize: '10px',
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
@ -100,9 +100,9 @@ export function BacktrackReplayBar({
onClick={onTogglePlay}
className="shrink-0 w-9 h-9 rounded-full flex items-center justify-center text-sm cursor-pointer"
style={{
background: isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.15)',
border: `2px solid ${isPlaying ? 'var(--purple)' : 'rgba(168,85,247,0.4)'}`,
color: isPlaying ? '#fff' : 'var(--purple)',
background: isPlaying ? 'var(--color-tertiary)' : 'rgba(168,85,247,0.15)',
border: `2px solid ${isPlaying ? 'var(--color-tertiary)' : 'rgba(168,85,247,0.4)'}`,
color: isPlaying ? '#fff' : 'var(--color-tertiary)',
}}
>
{isPlaying ? '⏸' : '▶'}
@ -124,7 +124,7 @@ export function BacktrackReplayBar({
className="absolute top-0 left-0 h-full"
style={{
width: `${progress}%`,
background: 'linear-gradient(90deg, var(--purple), var(--cyan))',
background: 'linear-gradient(90deg, var(--color-tertiary), var(--color-accent))',
borderRadius: '2px', transition: 'width 0.05s',
}}
/>
@ -151,7 +151,7 @@ export function BacktrackReplayBar({
style={{
left: `${progress}%`,
transform: 'translate(-50%, -50%)',
border: '3px solid var(--purple)',
border: '3px solid var(--color-tertiary)',
boxShadow: '0 0 8px rgba(168,85,247,0.4)',
zIndex: 2, transition: 'left 0.05s',
}}
@ -160,19 +160,19 @@ export function BacktrackReplayBar({
{/* Time labels */}
<div className="flex justify-between text-[9px] font-mono">
<span className="text-text-3">18:30</span>
<span className="font-semibold text-primary-purple">{currentTimeLabel}</span>
<span className="text-text-3">06:30</span>
<span className="text-fg-disabled">18:30</span>
<span className="font-semibold text-color-tertiary">{currentTimeLabel}</span>
<span className="text-fg-disabled">06:30</span>
</div>
</div>
</div>
{/* Legend row */}
<div className="flex items-center gap-[14px] pt-1 border-t border-border">
<div className="flex items-center gap-[14px] pt-1 border-t border-stroke">
{replayShips.map((ship) => (
<div key={ship.vesselName} className="flex items-center gap-1.5">
<div className="w-4 h-[3px]" style={{ background: ship.color, borderRadius: '1px' }} />
<span className="text-[9px] text-text-2 font-mono">
<span className="text-[9px] text-fg-sub font-mono">
{ship.vesselName}
</span>
</div>

파일 보기

@ -1063,7 +1063,7 @@ export function MapView({
latitude: info.coordinate[1],
content: (
<div className="text-xs leading-relaxed" style={{ minWidth: 180 }}>
<strong className="text-status-orange">{dispersionResult.substance} </strong>
<strong className="text-color-warning">{dispersionResult.substance} </strong>
<table style={{ width: '100%', marginTop: 4, borderCollapse: 'collapse' }}>
<tbody>
{zoneAreas.map(z => (
@ -1404,7 +1404,7 @@ export function MapView({
<Marker longitude={incidentCoord.lon} latitude={incidentCoord.lat} anchor="bottom">
<div
title={`사고 지점\n${incidentCoord.lat.toFixed(4)}°N, ${incidentCoord.lon.toFixed(4)}°E`}
className="w-6 h-6 bg-primary-cyan border-2 border-white"
className="w-6 h-6 bg-color-accent border-2 border-white"
style={{
borderRadius: '50% 50% 50% 0',
transform: 'rotate(-45deg)',
@ -1509,19 +1509,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-text-2 flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all text-xs"
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"
>
+
</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-text-2 flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all text-xs"
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"
>
</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-text-2 flex items-center justify-center hover:text-text-1 transition-all text-[10px]"
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-[10px]"
>
&#x1F3AF;
</button>
@ -1544,49 +1544,49 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], select
if (dispersionResult && incidentCoord) {
return (
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-border rounded-lg min-w-[200px] z-[20]">
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-stroke rounded-lg min-w-[200px] z-[20]">
{/* 헤더 + 최소화 버튼 */}
<div className="flex items-center justify-between px-3.5 pt-3 pb-1 cursor-pointer" onClick={() => setMinimized(!minimized)}>
<span className="text-[10px] font-bold text-text-3 uppercase tracking-wider"></span>
<span className="text-[10px] text-text-3 hover:text-text-1 transition-colors">{minimized ? '▶' : '▼'}</span>
<span className="text-[10px] font-bold text-fg-disabled uppercase tracking-wider"></span>
<span className="text-[10px] text-fg-disabled hover:text-fg transition-colors">{minimized ? '▶' : '▼'}</span>
</div>
{!minimized && (
<div className="px-3.5 pb-3.5">
<div className="flex items-center gap-1.5 mb-2.5">
<div className="text-base">📍</div>
<div>
<h4 className="text-[11px] font-bold text-primary-orange"> </h4>
<div className="text-[8px] text-text-3 font-mono">
<h4 className="text-[11px] font-bold text-color-warning"> </h4>
<div className="text-[8px] text-fg-disabled font-mono">
{incidentCoord.lat.toFixed(4)}°N, {incidentCoord.lon.toFixed(4)}°E
</div>
</div>
</div>
<div className="text-[9px] text-text-2 mb-2 rounded" style={{ background: 'rgba(249,115,22,0.08)', padding: '8px' }}>
<div className="text-[9px] text-fg-sub mb-2 rounded" style={{ background: 'rgba(249,115,22,0.08)', padding: '8px' }}>
<div className="flex justify-between mb-[3px]">
<span className="text-text-3"></span>
<span className="font-semibold text-status-orange">{dispersionResult.substance}</span>
<span className="text-fg-disabled"></span>
<span className="font-semibold text-color-warning">{dispersionResult.substance}</span>
</div>
<div className="flex justify-between mb-[3px]">
<span className="text-text-3"></span>
<span className="text-fg-disabled"></span>
<span className="font-semibold font-mono">SW {dispersionResult.windDirection}°</span>
</div>
<div className="flex justify-between">
<span className="text-text-3"> </span>
<span className="font-semibold text-primary-cyan">{dispersionResult.zones.length}</span>
<span className="text-fg-disabled"> </span>
<span className="font-semibold text-color-accent">{dispersionResult.zones.length}</span>
</div>
</div>
<div>
<h5 className="text-[9px] font-bold text-text-3 mb-2"> </h5>
<h5 className="text-[9px] font-bold text-fg-disabled mb-2"> </h5>
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-3 h-3 rounded-full" style={{ background: 'rgba(239,68,68,0.7)' }} />
<span> (AEGL-3)</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-3 h-3 rounded-full" style={{ background: 'rgba(249,115,22,0.7)' }} />
<span> (AEGL-2)</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-3 h-3 rounded-full" style={{ background: 'rgba(234,179,8,0.7)' }} />
<span> (AEGL-1)</span>
</div>
@ -1594,7 +1594,7 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], select
</div>
<div 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>
<span className="text-[9px] text-text-3"> ()</span>
<span className="text-[9px] text-fg-disabled"> ()</span>
</div>
</div>
)}
@ -1604,34 +1604,34 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], select
if (oilTrajectory.length > 0) {
return (
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.92)] backdrop-blur-xl border border-border rounded-md z-[20]" style={{ minWidth: 155 }}>
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.92)] backdrop-blur-xl border border-stroke rounded-md z-[20]" style={{ minWidth: 155 }}>
{/* 헤더 + 접기/펼치기 */}
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer select-none"
onClick={() => setMinimized(!minimized)}
>
<span className="text-[10px] font-bold text-text-2 font-korean"></span>
<span className="text-[9px] text-text-3 hover:text-text-1 transition-colors ml-3">{minimized ? '▶' : '▼'}</span>
<span className="text-[10px] font-bold text-fg-sub font-korean"></span>
<span className="text-[9px] text-fg-disabled hover:text-fg transition-colors ml-3">{minimized ? '▶' : '▼'}</span>
</div>
{!minimized && (
<div className="px-3 pb-2.5 flex flex-col gap-[5px]">
{/* 모델별 색상 */}
{Array.from(selectedModels).map(model => (
<div key={model} className="flex items-center gap-2 text-[10px] text-text-2">
<div key={model} className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-[14px] h-[3px] rounded-sm" style={{ background: MODEL_COLORS[model] }} />
<span className="font-korean">{model}</span>
</div>
))}
{/* 앙상블 */}
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-[14px] h-[3px] rounded-sm" style={{ background: '#a855f7' }} />
<span className="font-korean"></span>
</div>
{/* 오일펜스 라인 */}
<div className="h-px bg-border my-0.5" />
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="flex gap-px">
<div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
<div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
@ -1642,19 +1642,19 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], select
{/* 도달시간별 선종 */}
<div className="h-px bg-border my-0.5" />
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-[14px] h-[3px] rounded-sm bg-[#ef4444]" />
<span className="font-korean"> (&lt;6h)</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-[14px] h-[3px] rounded-sm bg-[#f97316]" />
<span className="font-korean"> (6~12h)</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-[14px] h-[3px] rounded-sm bg-[#eab308]" />
<span className="font-korean"> (12~24h)</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-text-2">
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
<div className="w-[14px] h-[3px] rounded-sm bg-[#22c55e]" />
<span className="font-korean"></span>
</div>
@ -1829,18 +1829,18 @@ function BacktrackReplayBar({ replayFrame, totalFrames, ships }: { replayFrame:
style={{
bottom: 80, left: '50%', transform: 'translateX(-50%)',
background: 'rgba(10,14,26,0.92)', backdropFilter: 'blur(12px)',
border: '1px solid var(--bdL)', borderRadius: '10px',
border: '1px solid var(--stroke-light)', borderRadius: '10px',
padding: '12px 18px', zIndex: 50,
minWidth: '340px',
}}
>
<div className="text-sm text-primary-purple font-mono font-bold">
<div className="text-sm 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]"
style={{ width: `${progress}%`, background: 'linear-gradient(90deg, var(--purple), var(--cyan))', transition: 'width 0.05s' }}
style={{ width: `${progress}%`, background: 'linear-gradient(90deg, var(--color-tertiary), var(--color-accent))', transition: 'width 0.05s' }}
/>
</div>
<div className="flex gap-1.5">

파일 보기

@ -39,7 +39,7 @@ export function ComboBox({ value, onChange, options, placeholder, className }: C
>
<span>{displayText}</span>
<span
className="text-[8px] text-text-3"
className="text-[8px] text-fg-disabled"
style={{
transition: 'transform 0.2s',
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
@ -51,10 +51,10 @@ export function ComboBox({ value, onChange, options, placeholder, className }: C
{isOpen && (
<div
className="absolute left-0 right-0 bg-bg-0 border border-border overflow-y-auto z-[1000]"
className="absolute left-0 right-0 bg-bg-base border border-stroke overflow-y-auto z-[1000]"
style={{
top: 'calc(100% + 2px)',
borderRadius: 'var(--rS)',
borderRadius: 'var(--radius-sm)',
maxHeight: '200px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
animation: 'fadeSlideDown 0.15s ease-out'
@ -70,10 +70,10 @@ export function ComboBox({ value, onChange, options, placeholder, className }: C
className="text-[11px] cursor-pointer"
style={{
padding: '8px 10px',
color: option.value === String(value) ? 'var(--cyan)' : 'var(--t2)',
color: option.value === String(value) ? 'var(--color-accent)' : 'var(--fg-sub)',
background: option.value === String(value) ? 'rgba(6,182,212,0.1)' : 'transparent',
transition: '0.1s',
borderLeft: option.value === String(value) ? '2px solid var(--cyan)' : '2px solid transparent'
borderLeft: option.value === String(value) ? '2px solid var(--color-accent)' : '2px solid transparent'
}}
onMouseEnter={(e) => {
if (option.value !== String(value)) {

파일 보기

@ -1,28 +1,65 @@
/* ── PretendardGOV @font-face ── */
@font-face { font-family: 'PretendardGOV'; font-weight: 400; font-style: normal; font-display: swap; src: url('/fonts/PretendardGOV-Regular.otf') format('opentype'); }
@font-face { font-family: 'PretendardGOV'; font-weight: 500; font-style: normal; font-display: swap; src: url('/fonts/PretendardGOV-Medium.otf') format('opentype'); }
@font-face { font-family: 'PretendardGOV'; font-weight: 600; font-style: normal; font-display: swap; src: url('/fonts/PretendardGOV-SemiBold.otf') format('opentype'); }
@font-face { font-family: 'PretendardGOV'; font-weight: 700; font-style: normal; font-display: swap; src: url('/fonts/PretendardGOV-Bold.otf') format('opentype'); }
@layer base {
:root {
--bg0: #0a0e1a;
--bg1: #0f1524;
--bg2: #121929;
--bg3: #1a2236;
--bgH: #1e2844;
--bd: #1e2a42;
--bdL: #2a3a5c;
--t1: #edf0f7;
--t2: #b0b8cc;
--t3: #8690a6;
--blue: #3b82f6;
--cyan: #06b6d4;
--red: #ef4444;
--orange: #f97316;
--yellow: #eab308;
--green: #22c55e;
--purple: #a855f7;
--boom: #f59e0b;
--boomH: #fbbf24;
--fK: Noto Sans KR, sans-serif;
--fM: JetBrains Mono, monospace;
--rS: 6px;
--rM: 8px;
/* bg — Background */
--bg-base: #0a0e1a;
--bg-surface: #0f1524;
--bg-elevated: #121929;
--bg-card: #1a2236;
--bg-surface-hover: #1e2844;
/* stroke — Border */
--stroke-default: #1e2a42;
--stroke-light: #2a3a5c;
/* fg — Foreground */
--fg-default: #edf0f7;
--fg-sub: #c0c8dc;
--fg-disabled: #9ba3b8;
/* color — Palette */
--color-info: #3b82f6;
--color-accent: #06b6d4;
--color-danger: #ef4444;
--color-warning: #f97316;
--color-caution: #eab308;
--color-success: #22c55e;
--color-tertiary: #a855f7;
--color-boom: #f59e0b;
--color-boom-hover: #fbbf24;
/* font */
--font-korean: 'PretendardGOV', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Pretendard Variable', Pretendard, Roboto, 'Noto Sans KR', 'Segoe UI', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif;
--font-mono: 'PretendardGOV', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Pretendard Variable', Pretendard, Roboto, 'Noto Sans KR', 'Segoe UI', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif;
/* radius */
--radius-sm: 6px;
--radius-md: 8px;
/* typography — font-size */
--font-size-display-1: 3.75rem;
--font-size-display-2: 2.5rem;
--font-size-display-3: 2.25rem;
--font-size-heading-1: 2rem;
--font-size-heading-2: 1.5rem;
--font-size-heading-3: 1.375rem;
--font-size-title-1: 1.125rem;
--font-size-title-2: 1rem;
--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;
/* typography — font-weight */
--font-weight-thin: 300;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
/* typography — line-height */
--line-height-tight: 1.3;
--line-height-snug: 1.4;
--line-height-normal: 1.5;
--line-height-relaxed: 1.6;
/* === Design Token System === */
@ -112,12 +149,15 @@
}
body {
font-family: 'Outfit', 'Noto Sans KR', sans-serif;
background: var(--bg0);
color: var(--t1);
font-family: 'PretendardGOV', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Pretendard Variable', Pretendard, Roboto, 'Noto Sans KR', 'Segoe UI', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif;
background: var(--bg-base);
color: var(--fg-default);
height: 100vh;
overflow: hidden;
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Date input calendar icon — white for dark theme */

파일 보기

@ -33,7 +33,7 @@
/* ═══ Scrollbar ═══ */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: var(--bdL) transparent;
scrollbar-color: var(--stroke-light) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
@ -46,33 +46,33 @@
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: var(--bdL);
background: var(--stroke-light);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--bd);
background: var(--stroke-default);
}
/* ═══ Prediction Input Form ═══ */
.prd-i {
width: 100%;
padding: 6px 10px;
background: var(--bg0);
border: 1px solid var(--bd);
background: var(--bg-base);
border: 1px solid var(--stroke-default);
border-radius: 6px;
color: var(--t1);
font-family: 'Noto Sans KR', sans-serif;
color: var(--fg-default);
font-family: var(--font-korean);
font-size: 11px;
outline: none;
}
.prd-i:focus {
border-color: var(--cyan);
border-color: var(--color-accent);
}
.prd-i::placeholder {
color: var(--t3);
color: var(--fg-disabled);
}
/* Date/Time picker custom styling */
@ -94,8 +94,8 @@
.prd-date-input::-webkit-datetime-edit,
.prd-time-input::-webkit-datetime-edit {
color: var(--t2);
font-family: var(--fM);
color: var(--fg-sub);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.3px;
}
@ -111,7 +111,7 @@
.prd-time-input::-webkit-datetime-edit-hour-field,
.prd-time-input::-webkit-datetime-edit-minute-field,
.prd-time-input::-webkit-datetime-edit-ampm-field {
color: var(--t2);
color: var(--fg-sub);
background: transparent;
padding: 1px 2px;
border-radius: 2px;
@ -124,12 +124,12 @@
.prd-time-input::-webkit-datetime-edit-minute-field:focus,
.prd-time-input::-webkit-datetime-edit-ampm-field:focus {
background: rgba(6, 182, 212, 0.12);
color: var(--cyan);
color: var(--color-accent);
}
.prd-date-input::-webkit-datetime-edit-text,
.prd-time-input::-webkit-datetime-edit-text {
color: var(--t3);
color: var(--fg-disabled);
padding: 0 1px;
}
@ -138,11 +138,11 @@
color-scheme: dark;
-webkit-appearance: menulist !important;
appearance: menulist !important;
background: var(--bg3) !important;
background: var(--bg-card) !important;
background-image: none !important;
padding-right: 4px;
color: var(--t1);
border-color: var(--bd);
color: var(--fg-default);
border-color: var(--stroke-default);
}
/* Select Dropdown */
@ -152,7 +152,7 @@
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: var(--bg0) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E") no-repeat right 8px center;
background: var(--bg-base) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E") no-repeat right 8px center;
background-size: 10px;
transition: border-color 0.2s, background-image 0.2s;
}
@ -163,27 +163,27 @@
}
select.prd-i:focus {
border-color: var(--cyan);
border-color: var(--color-accent);
outline: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2306b6d4' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
}
select.prd-i option {
background: #1a1f2e;
color: var(--t1);
color: var(--fg-default);
padding: 10px;
font-size: 11px;
font-family: 'Noto Sans KR', sans-serif;
font-family: var(--font-korean);
}
select.prd-i option:checked {
background: linear-gradient(0deg, rgba(6, 182, 212, 0.25) 0%, rgba(30, 58, 138, 0.3) 100%);
color: var(--cyan);
color: var(--color-accent);
font-weight: 500;
}
select.prd-i option:disabled {
color: var(--t3);
color: var(--fg-disabled);
font-style: italic;
opacity: 0.6;
}
@ -200,7 +200,7 @@
}
.combo-input:hover {
border-color: var(--bdL);
border-color: var(--stroke-light);
}
.combo-arrow {
@ -209,19 +209,19 @@
top: 50%;
transform: translateY(-50%);
font-size: 8px;
color: var(--t3);
color: var(--fg-disabled);
pointer-events: none;
transition: 0.2s;
}
.combo-wrap.open .combo-arrow {
transform: translateY(-50%) rotate(180deg);
color: var(--cyan);
color: var(--color-accent);
}
.combo-wrap.open .combo-input {
border-color: var(--cyan);
background: var(--bg2);
border-color: var(--color-accent);
background: var(--bg-elevated);
}
.combo-list {
@ -230,15 +230,15 @@
top: calc(100% + 3px);
left: 0;
right: 0;
background: var(--bg1);
border: 1px solid var(--bdL);
background: var(--bg-surface);
border: 1px solid var(--stroke-light);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
z-index: 200;
max-height: 180px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--bdL) transparent;
scrollbar-color: var(--stroke-light) transparent;
animation: comboIn 0.15s ease;
}
@ -260,8 +260,8 @@
.combo-item {
padding: 7px 10px;
font-size: 11px;
font-family: 'Noto Sans KR', sans-serif;
color: var(--t2);
font-family: var(--font-korean);
color: var(--fg-sub);
cursor: pointer;
transition: 0.12s;
border-bottom: 1px solid rgba(30, 42, 66, 0.5);
@ -273,12 +273,12 @@
.combo-item:hover {
background: rgba(6, 182, 212, 0.08);
color: var(--t1);
color: var(--fg-default);
}
.combo-item.active {
background: rgba(6, 182, 212, 0.12);
color: var(--cyan);
color: var(--color-accent);
font-weight: 600;
}
@ -292,10 +292,10 @@
border-radius: 5px;
font-size: 9px;
font-weight: 600;
font-family: 'Noto Sans KR', sans-serif;
font-family: var(--font-korean);
cursor: pointer;
border: 1px solid var(--bd);
background: var(--bg0);
border: 1px solid var(--stroke-default);
background: var(--bg-base);
color: rgba(255, 255, 255, 0.5);
transition: 0.15s;
user-select: none;
@ -307,11 +307,11 @@
color: rgba(255, 255, 255, 0.9);
}
.prd-mc.on::before {
/* .prd-mc.on::before {
content: '✓ ';
font-size: 9px;
color: var(--cyan);
}
color: var(--color-accent);
} */
.prd-md {
width: 7px;
@ -329,18 +329,19 @@
font-weight: 700;
cursor: pointer;
border: none;
font-family: 'Noto Sans KR', sans-serif;
font-family: var(--font-korean);
}
.prd-btn.pri {
background: linear-gradient(135deg, var(--cyan), var(--blue));
color: #fff;
background: transparent;
border: 1px solid var(--color-accent);
color: var(--color-accent);
}
.prd-btn.sec {
background: var(--bg3);
border: 1px solid var(--bd);
color: var(--t2);
background: var(--bg-card);
border: 1px solid var(--stroke-default);
color: var(--fg-sub);
}
/* Map Button */
@ -349,12 +350,12 @@
background: rgba(6, 182, 212, 0.08);
border: 1px solid rgba(6, 182, 212, 0.2);
border-radius: 6px;
color: var(--cyan);
color: var(--color-accent);
font-size: 9px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
font-family: 'Noto Sans KR', sans-serif;
font-family: var(--font-korean);
}
.prd-map-btn:hover {
@ -373,23 +374,23 @@
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(18, 25, 41, 0.5);
background: rgba(18, 25, 41, 0.7);
backdrop-filter: blur(8px);
border: 1px solid rgba(30, 42, 66, 0.4);
border: none;
border-radius: 6px;
padding: 5px 14px;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: #1a1a2e;
font-weight: 600;
font-family: var(--font-mono);
font-size: 0.6875rem;
color: rgba(255,255,255,0.7);
font-weight: 400;
z-index: 20;
display: flex;
gap: 14px;
}
.cov {
color: var(--cyan);
font-weight: 500;
color: #ffffff;
font-weight: 400;
}
/* ═══ Weather Info Panel ═══ */
@ -397,9 +398,9 @@
position: absolute;
top: 10px;
left: 10px;
background: rgba(18, 25, 41, 0.65);
background: rgba(18, 25, 41, 0.75);
backdrop-filter: blur(10px);
border: 1px solid rgba(30, 42, 66, 0.5);
border: none;
border-radius: 6px;
padding: 6px 10px;
z-index: 20;
@ -415,22 +416,22 @@
}
.wii-icon {
font-size: 12px;
opacity: 0.5;
font-size: 0.75rem;
opacity: 0.7;
}
.wii-value {
font-size: 11px;
font-weight: 700;
color: var(--t1);
font-family: 'JetBrains Mono', monospace;
font-size: 0.6875rem;
font-weight: 400;
color: #ffffff;
font-family: var(--font-mono);
}
.wii-label {
font-size: 7px;
color: #1a1a2e;
font-weight: 700;
font-family: 'Noto Sans KR', sans-serif;
font-size: 0.625rem;
color: rgba(255,255,255,0.55);
font-weight: 400;
font-family: var(--font-korean);
}
/* ═══ Timeline Control Panel ═══ */
@ -442,7 +443,7 @@
height: 72px;
background: rgba(15, 21, 36, 0.95);
backdrop-filter: blur(16px);
border-top: 1px solid var(--bd);
border-top: 1px solid var(--stroke-default);
display: flex;
align-items: center;
padding: 0 20px;
@ -460,9 +461,9 @@
width: 34px;
height: 34px;
border-radius: 6px;
border: 1px solid var(--bd);
background: var(--bg3);
color: var(--t2);
border: 1px solid var(--stroke-default);
background: var(--bg-card);
color: var(--fg-sub);
display: flex;
align-items: center;
justify-content: center;
@ -472,14 +473,14 @@
}
.tb:hover {
background: var(--bgH);
color: var(--t1);
background: var(--bg-surface-hover);
color: var(--fg-default);
}
.tb.on {
background: var(--cyan);
color: var(--bg0);
border-color: var(--cyan);
background: var(--color-accent);
color: var(--bg-base);
border-color: var(--color-accent);
}
.tlt {
@ -497,12 +498,12 @@
.tll {
font-size: 10px;
color: var(--t3);
font-family: 'JetBrains Mono', monospace;
color: var(--fg-disabled);
font-family: var(--font-mono);
}
.tll.on {
color: var(--cyan);
color: var(--color-accent);
font-weight: 600;
}
@ -516,7 +517,7 @@
.tlr {
width: 100%;
height: 4px;
background: var(--bd);
background: var(--stroke-default);
border-radius: 2px;
position: relative;
}
@ -527,7 +528,7 @@
left: 0;
width: 25%;
height: 100%;
background: linear-gradient(90deg, var(--cyan), var(--blue));
background: linear-gradient(90deg, var(--color-accent), var(--color-info));
border-radius: 2px;
transition: width 0.3s;
}
@ -536,14 +537,14 @@
position: absolute;
width: 2px;
height: 10px;
background: var(--bdL);
background: var(--stroke-light);
top: -3px;
}
.tlm.mj {
height: 14px;
top: -5px;
background: var(--t3);
background: var(--fg-disabled);
}
.tlbm {
@ -562,13 +563,13 @@
left: 50%;
transform: translateX(-50%);
background: rgba(18, 25, 41, 0.95);
border: 1px solid var(--boom);
border: 1px solid var(--color-boom);
border-radius: 4px;
padding: 4px 8px;
font-size: 10px;
color: var(--boom);
color: var(--color-boom);
white-space: nowrap;
font-family: 'Noto Sans KR', sans-serif;
font-family: var(--font-korean);
}
.tlbm:hover .tlbt {
@ -582,8 +583,8 @@
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
background: var(--cyan);
border: 3px solid var(--bg0);
background: var(--color-accent);
border: 3px solid var(--bg-base);
border-radius: 50%;
cursor: grab;
box-shadow: 0 0 10px rgba(6, 182, 212, 0.4);
@ -603,8 +604,8 @@
.tlct {
font-size: 14px;
font-weight: 600;
color: var(--cyan);
font-family: 'JetBrains Mono', monospace;
color: var(--color-accent);
font-family: var(--font-mono);
}
.tlss {
@ -620,14 +621,14 @@
}
.tlsl {
color: var(--t3);
font-family: 'Noto Sans KR', sans-serif;
color: var(--fg-disabled);
font-family: var(--font-korean);
}
.tlsv {
color: var(--t1);
color: var(--fg-default);
font-weight: 600;
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
/* Timeline Action Buttons */
@ -641,24 +642,24 @@
.tlab-btn {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid var(--bd);
background: var(--bg3);
color: var(--t2);
border: 1px solid var(--stroke-default);
background: var(--bg-card);
color: var(--fg-sub);
font-size: 11px;
font-weight: 600;
cursor: pointer;
font-family: 'Noto Sans KR', sans-serif;
font-family: var(--font-korean);
transition: 0.2s;
white-space: nowrap;
}
.tlab-btn:hover {
background: var(--bgH);
color: var(--t1);
background: var(--bg-surface-hover);
color: var(--fg-default);
}
.tlab-btn.primary {
background: linear-gradient(135deg, var(--cyan), var(--blue));
background: linear-gradient(135deg, var(--color-accent), var(--color-info));
color: #fff;
border-color: transparent;
}
@ -670,7 +671,7 @@
.tlab-btn.warning {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: var(--red);
color: var(--color-danger);
}
.tlab-btn.warning:hover {
@ -696,8 +697,8 @@
cursor: pointer;
transition: background 0.15s;
font-size: 11px;
color: var(--t2);
font-family: 'Noto Sans KR', sans-serif;
color: var(--fg-sub);
font-family: var(--font-korean);
}
.layer-item:hover {
@ -707,7 +708,7 @@
.layer-arrow {
width: 12px;
font-size: 8px;
color: var(--t3);
color: var(--fg-disabled);
cursor: pointer;
user-select: none;
flex-shrink: 0;
@ -739,7 +740,7 @@
background-color: rgba(90, 100, 120, 0.3);
transition: 0.3s;
border-radius: 18px;
border: 1px solid var(--bd);
border: 1px solid var(--stroke-default);
}
.toggle-slider:before {
@ -749,19 +750,19 @@
width: 12px;
left: 2px;
bottom: 2px;
background-color: var(--t3);
background-color: var(--fg-disabled);
transition: 0.3s;
border-radius: 50%;
}
.layer-toggle input:checked + .toggle-slider {
background-color: rgba(6, 182, 212, 0.3);
border-color: var(--cyan);
border-color: var(--color-accent);
}
.layer-toggle input:checked + .toggle-slider:before {
transform: translateX(14px);
background-color: var(--cyan);
background-color: var(--color-accent);
}
.layer-toggle input:disabled + .toggle-slider {
@ -781,8 +782,8 @@
.layer-count {
font-size: 10px;
color: var(--t3);
font-family: 'JetBrains Mono', monospace;
color: var(--fg-disabled);
font-family: var(--font-mono);
}
.layer-children {
@ -803,8 +804,8 @@
padding: 8px 16px;
font-size: 11px;
font-weight: 600;
color: var(--boom);
font-family: var(--fK);
color: var(--color-boom);
font-family: var(--font-korean);
z-index: 40;
white-space: nowrap;
pointer-events: none;
@ -814,8 +815,8 @@
.boom-eff-bar {
width: 100%;
height: 6px;
background: var(--bg0);
border: 1px solid var(--bd);
background: var(--bg-base);
border: 1px solid var(--stroke-default);
border-radius: 3px;
overflow: hidden;
}
@ -829,11 +830,11 @@
.boom-setting-input {
width: 56px;
padding: 3px 6px;
background: var(--bg2);
border: 1px solid var(--bd);
background: var(--bg-elevated);
border: 1px solid var(--stroke-default);
border-radius: 4px;
color: var(--orange);
font-family: var(--fM);
color: var(--color-warning);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-align: right;
@ -842,7 +843,7 @@
}
.boom-setting-input:focus {
border-color: var(--orange);
border-color: var(--color-accent);
}
.boom-setting-input::-webkit-inner-spin-button,
@ -878,25 +879,25 @@
.bt-spd-btn {
padding: 3px 8px;
border-radius: 4px;
border: 1px solid var(--bd);
background: var(--bg3);
color: var(--t3);
border: 1px solid var(--stroke-default);
background: var(--bg-card);
color: var(--fg-disabled);
font-size: 10px;
font-weight: 700;
cursor: pointer;
font-family: var(--fM);
font-family: var(--font-mono);
transition: 0.15s;
}
.bt-spd-btn:hover {
background: rgba(168, 85, 247, 0.08);
color: var(--t2);
color: var(--fg-sub);
}
.bt-spd-btn.active {
border-color: var(--purple);
border-color: var(--color-tertiary);
background: rgba(168, 85, 247, 0.12);
color: var(--purple);
color: var(--color-tertiary);
}
@keyframes bt-collision-pulse {
@ -913,8 +914,8 @@
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 6px;
border: 1px solid var(--bd);
background: var(--bg3);
border: 1px solid var(--stroke-default);
background: var(--bg-card);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
@ -925,7 +926,7 @@
}
.hns-scn-card.sel {
border-color: var(--cyan);
border-color: var(--color-accent);
background: rgba(6, 182, 212, 0.08);
}
@ -936,7 +937,7 @@
border: none;
background: transparent;
color: #8b949e;
font-family: var(--fK);
font-family: var(--font-korean);
font-size: 9px;
font-weight: 600;
cursor: pointer;
@ -950,19 +951,19 @@
}
.rsc-atab.on {
color: var(--cyan);
border-bottom-color: var(--cyan);
color: var(--color-accent);
border-bottom-color: var(--color-accent);
background: rgba(6, 182, 212, .04);
}
}
@layer utilities {
.font-mono {
font-family: 'JetBrains Mono', monospace;
font-family: var(--font-mono);
}
.font-korean {
font-family: 'Noto Sans KR', sans-serif;
font-family: var(--font-korean);
}
/* ═══ Animations ═══ */
@ -1009,12 +1010,12 @@
gap: 6px;
padding: 7px 8px;
cursor: pointer;
border-radius: var(--rS);
border-radius: var(--radius-sm);
transition: background 0.15s;
font-size: 12px;
font-weight: 700;
color: var(--t1);
font-family: var(--fK);
color: var(--fg-default);
font-family: var(--font-korean);
}
.lyr-h1:hover {
@ -1023,7 +1024,7 @@
.lyr-h1 .lyr-arr {
font-size: 8px;
color: var(--t3);
color: var(--fg-disabled);
transition: transform 0.2s;
width: 12px;
text-align: center;
@ -1037,8 +1038,8 @@
margin-left: auto;
font-size: 10px;
font-weight: 500;
color: var(--t3);
font-family: var(--fM);
color: var(--fg-disabled);
font-family: var(--font-mono);
}
.lyr-c1 {
@ -1066,8 +1067,8 @@
transition: background 0.15s;
font-size: 11px;
font-weight: 600;
color: var(--t2);
font-family: var(--fK);
color: var(--fg-sub);
font-family: var(--font-korean);
}
.lyr-h2:hover {
@ -1076,7 +1077,7 @@
.lyr-h2 .lyr-arr {
font-size: 7px;
color: var(--t3);
color: var(--fg-disabled);
transition: transform 0.2s;
width: 10px;
text-align: center;
@ -1090,8 +1091,8 @@
margin-left: auto;
font-size: 10px;
font-weight: 500;
color: var(--t3);
font-family: var(--fM);
color: var(--fg-disabled);
font-family: var(--font-mono);
}
.lyr-c2 {
@ -1112,14 +1113,14 @@
padding: 4px 8px;
cursor: pointer;
font-size: 11px;
color: var(--t2);
color: var(--fg-sub);
transition: color 0.15s, background 0.15s;
font-family: var(--fK);
font-family: var(--font-korean);
border-radius: 3px;
}
.lyr-t:hover {
color: var(--t1);
color: var(--fg-default);
background: rgba(255, 255, 255, 0.02);
}
@ -1127,8 +1128,8 @@
margin-left: auto;
font-size: 10px;
font-weight: 400;
color: var(--t3);
font-family: var(--fM);
color: var(--fg-disabled);
font-family: var(--font-mono);
flex-shrink: 0;
}
@ -1167,8 +1168,8 @@
right: 20px;
top: -6px;
z-index: 300;
background: var(--bg1);
border: 1px solid var(--bd);
background: var(--bg-surface);
border: 1px solid var(--stroke-default);
border-radius: 8px;
padding: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
@ -1208,7 +1209,7 @@
}
.lyr-cpr-i.sel {
border-color: var(--cyan);
border-color: var(--color-accent);
box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.3);
}
@ -1221,14 +1222,14 @@
.lyr-ccustom label {
font-size: 9px;
color: var(--t3);
font-family: var(--fK);
color: var(--fg-disabled);
font-family: var(--font-korean);
}
.lyr-ccustom input[type="color"] {
width: 24px;
height: 20px;
border: 1px solid var(--bd);
border: 1px solid var(--stroke-default);
border-radius: 3px;
cursor: pointer;
padding: 0;
@ -1239,15 +1240,15 @@
.lyr-style-box {
margin-top: 10px;
padding: 10px 8px;
background: var(--bg0);
border: 1px solid var(--bd);
border-radius: var(--rS);
background: var(--bg-base);
border: 1px solid var(--stroke-default);
border-radius: var(--radius-sm);
}
.lyr-style-label {
font-size: 9px;
font-weight: 700;
color: var(--t3);
font-family: var(--fK);
color: var(--fg-disabled);
font-family: var(--font-korean);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: .3px;
@ -1262,33 +1263,33 @@
}
.lyr-style-name {
font-size: 10px;
color: var(--t3);
font-family: var(--fK);
color: var(--fg-disabled);
font-family: var(--font-korean);
min-width: 32px;
}
.lyr-style-slider {
flex: 1;
height: 4px;
border-radius: 2px;
background: var(--bd);
background: var(--stroke-default);
-webkit-appearance: none;
appearance: none;
outline: none;
cursor: pointer;
accent-color: var(--cyan);
accent-color: var(--color-accent);
}
.lyr-style-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--cyan);
background: var(--color-accent);
cursor: pointer;
}
.lyr-style-val {
font-size: 9px;
color: var(--t3);
font-family: var(--fM);
color: var(--fg-disabled);
font-family: var(--font-mono);
min-width: 28px;
text-align: right;
}
@ -1297,10 +1298,10 @@
.lyr-sw {
width: 28px;
height: 16px;
background: var(--bg0);
background: var(--bg-base);
border-radius: 8px;
position: relative;
border: 1px solid var(--bd);
border: 1px solid var(--stroke-default);
transition: 0.2s;
flex-shrink: 0;
cursor: pointer;
@ -1313,19 +1314,19 @@
left: 2px;
width: 10px;
height: 10px;
background: var(--t3);
background: var(--fg-disabled);
border-radius: 50%;
transition: 0.2s;
}
.lyr-sw.on {
background: rgba(6, 182, 212, 0.3);
border-color: var(--cyan);
border-color: var(--color-accent);
}
.lyr-sw.on::after {
left: 14px;
background: var(--cyan);
background: var(--color-accent);
}
.lyr-sw:disabled {

파일 보기

@ -10,34 +10,34 @@
.wing-panel-scroll {
@apply flex-1 overflow-y-auto;
scrollbar-width: thin;
scrollbar-color: var(--bdL) transparent;
scrollbar-color: var(--stroke-light) transparent;
}
.wing-header-bar {
@apply flex items-center justify-between shrink-0 px-5 border-b border-border;
@apply flex items-center justify-between shrink-0 px-5 border-b border-stroke;
padding-top: 14px;
padding-bottom: 14px;
}
.wing-sidebar {
@apply flex flex-col border-r border-border;
background: var(--bg1);
@apply flex flex-col border-r border-stroke;
background: var(--bg-surface);
}
/* ── Card / Section ── */
.wing-card {
@apply rounded-md p-4 border border-border;
background: var(--bg3);
@apply rounded-md p-4 border border-stroke;
background: var(--bg-card);
}
.wing-card-sm {
@apply rounded-sm p-3 border border-border;
background: var(--bg3);
@apply rounded-sm p-3 border border-stroke;
background: var(--bg-card);
}
.wing-section {
@apply rounded-md p-4 mb-3 border border-border;
background: var(--bg3);
@apply rounded-md p-4 mb-3 border border-stroke;
background: var(--bg-card);
}
.wing-section-header {
@ -46,7 +46,7 @@
.wing-section-desc {
@apply text-[10px] font-korean leading-relaxed;
color: var(--t3);
color: var(--fg-disabled);
}
/* ── Typography ── */
@ -56,7 +56,7 @@
.wing-subtitle {
@apply text-[10px] font-korean mt-0.5;
color: var(--t3);
color: var(--fg-disabled);
}
.wing-label {
@ -69,7 +69,7 @@
.wing-meta {
@apply text-[9px] font-korean;
color: var(--t3);
color: var(--fg-disabled);
}
/* ── Icon Badge ── */
@ -93,7 +93,7 @@
}
.wing-btn-primary {
background: linear-gradient(135deg, var(--cyan), var(--blue));
background: linear-gradient(135deg, var(--color-accent), var(--color-info));
color: #fff;
}
@ -102,51 +102,51 @@
}
.wing-btn-secondary {
@apply border border-border;
background: var(--bg3);
color: var(--t2);
@apply border border-stroke;
background: var(--bg-card);
color: var(--fg-sub);
}
.wing-btn-secondary:hover {
background: var(--bgH);
background: var(--bg-surface-hover);
}
.wing-btn-outline {
@apply bg-transparent border border-border;
color: var(--t2);
@apply bg-transparent border border-stroke;
color: var(--fg-sub);
}
.wing-btn-outline:hover {
background: var(--bgH);
background: var(--bg-surface-hover);
}
.wing-btn-pdf {
border: 1px solid rgba(59, 130, 246, 0.3);
background: rgba(59, 130, 246, 0.08);
color: var(--blue);
color: var(--color-info);
}
.wing-btn-danger {
border: 1px solid rgba(239, 68, 68, 0.3);
background: rgba(239, 68, 68, 0.1);
color: var(--red);
color: var(--color-danger);
}
/* ── Input ── */
.wing-input {
@apply w-full rounded-sm text-[11px] font-korean outline-none;
padding: 6px 10px;
background: var(--bg0);
border: 1px solid var(--bd);
color: var(--t1);
background: var(--bg-base);
border: 1px solid var(--stroke-default);
color: var(--fg-default);
}
.wing-input:focus {
border-color: var(--cyan);
border-color: var(--color-accent);
}
.wing-input::placeholder {
color: var(--t3);
color: var(--fg-disabled);
}
/* ── Table ── */
@ -158,44 +158,44 @@
.wing-th {
@apply text-left font-semibold;
padding: 8px 10px;
color: var(--t3);
background: var(--bg2);
border-bottom: 1px solid var(--bd);
color: var(--fg-disabled);
background: var(--bg-elevated);
border-bottom: 1px solid var(--stroke-default);
}
.wing-td {
padding: 8px 10px;
color: var(--t2);
border-bottom: 1px solid var(--bd);
color: var(--fg-sub);
border-bottom: 1px solid var(--stroke-default);
}
.wing-tr-hover:hover {
background: var(--bgH);
background: var(--bg-surface-hover);
cursor: pointer;
}
/* ── Tab Bar ── */
.wing-tab-bar {
@apply flex gap-0.5 rounded-lg p-1 border border-border;
background: var(--bg3);
@apply flex gap-0.5 rounded-lg p-1 border border-stroke;
background: var(--bg-card);
}
.wing-tab {
@apply flex-1 py-2 px-1 text-xs font-semibold rounded-md text-center cursor-pointer font-korean;
transition: all 0.15s;
color: var(--t3);
color: var(--fg-disabled);
background: transparent;
border: 1px solid transparent;
}
.wing-tab:hover {
color: var(--t2);
color: var(--fg-sub);
}
.wing-tab.active {
border-color: rgba(6, 182, 212, 0.3);
background: rgba(6, 182, 212, 0.08);
color: var(--cyan);
color: var(--color-accent);
}
/* ── Modal ── */
@ -208,13 +208,13 @@
.wing-modal {
@apply rounded-xl overflow-hidden;
background: var(--bg1);
border: 1px solid var(--bd);
background: var(--bg-surface);
border: 1px solid var(--stroke-default);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.wing-modal-header {
@apply flex items-center justify-between px-5 border-b border-border;
@apply flex items-center justify-between px-5 border-b border-stroke;
padding-top: 14px;
padding-bottom: 14px;
}
@ -223,7 +223,7 @@
.wing-divider {
@apply w-full;
height: 1px;
background: var(--bd);
background: var(--stroke-default);
}
.wing-kv-row {
@ -233,7 +233,7 @@
.wing-kv-label {
@apply text-[10px] font-korean;
color: var(--t3);
color: var(--fg-disabled);
}
.wing-kv-value {

파일 보기

@ -441,7 +441,7 @@ interface ColorPaletteContentProps {
export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
const [activeColorTab, setActiveColorTab] = useState<'usage' | 'token'>('usage');
const [activeColorTab, setActiveColorTab] = useState<'rule' | 'definition' | 'token'>('rule');
const sectionCardBg = isDark ? 'rgba(255,255,255,0.03)' : '#f5f5f5';
const dividerColor = isDark ? 'rgba(255,255,255,0.08)' : '#e5e7eb';
@ -477,8 +477,9 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
{/* 탭 전환 */}
<div className="flex gap-1 mb-10" style={{ borderBottom: `1px solid ${dividerColor}` }}>
{(['usage', 'token'] as const).map((tab) => {
{(['rule', 'usage', 'token'] as const).map((tab) => {
const isActive = activeColorTab === tab;
const label = tab === 'rule' ? 'Rule' : tab === 'usage' ? 'Definition' : 'Token';
return (
<button
key={tab}
@ -489,12 +490,284 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
borderBottom: isActive ? `2px solid ${t.textAccent}` : '2px solid transparent',
}}
>
{tab === 'usage' ? 'Usage' : 'Token'}
{label}
</button>
);
})}
</div>
{/* ── Rule 탭 콘텐츠 ── */}
{activeColorTab === 'rule' && (
<div className="mb-16">
{/* 네이밍 공식 */}
<div className="mb-12">
<h2 className="text-xl font-bold mb-3" style={{ color: t.textPrimary }}>
</h2>
<div
className="rounded-lg px-6 py-5 font-mono text-lg font-bold mb-4"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.03)' : '#f5f5f5', color: t.textAccent }}
>
--{'{property}'}-{'{role}'}[-{'{variant}'}]
</div>
<p className="text-sm leading-relaxed" style={{ color: t.textSecondary }}>
<strong style={{ color: t.textPrimary }}>Property-Role-Variant</strong> 3 .
Property는 CSS , Role은 , Variant는 .
</p>
</div>
{/* Property */}
<div className="mb-12">
<h2 className="text-xl font-bold mb-4" style={{ color: t.textPrimary }}>
Property ( )
</h2>
<div className="rounded-lg overflow-hidden" style={{ border: `1px solid ${dividerColor}` }}>
{[
{ prop: 'fg', desc: '텍스트, 아이콘 등 UI의 전경 요소에 사용하는 색상입니다.', css: 'color', example: '--fg-default, --fg-sub' },
{ prop: 'bg', desc: '전체 화면 또는 UI의 배경에 사용하는 색상입니다.', css: 'background', example: '--bg-surface, --bg-card' },
{ prop: 'stroke', desc: '경계를 구분하는 선 또는 UI 요소의 윤곽선에 사용하는 색상입니다.', css: 'border, outline', example: '--stroke-default, --stroke-light' },
].map((row, i) => (
<div
key={row.prop}
className="flex items-center gap-6 px-5 py-4"
style={{
borderTop: i > 0 ? `1px solid ${dividerColor}` : undefined,
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
}}
>
<span className="font-mono text-sm 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 }}>{row.desc}</div>
</div>
<span className="font-mono text-xs" style={{ color: t.textMuted }}>{row.example}</span>
</div>
))}
</div>
</div>
{/* Role & Variant */}
<div className="mb-12">
<h2 className="text-xl font-bold mb-4" style={{ color: t.textPrimary }}>
Role () & Variant ()
</h2>
<div className="grid grid-cols-2 gap-6">
{/* Role */}
<div className="rounded-lg overflow-hidden" style={{ border: `1px solid ${dividerColor}` }}>
<div className="px-5 py-3 text-xs font-bold uppercase tracking-wider" style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}>
Role
</div>
{[
{ name: 'default', desc: '기본 상태' },
{ name: 'sub', desc: '보조, 2차 계층' },
{ name: 'disabled', desc: '비활성, 플레이스홀더' },
{ name: 'base', desc: '최하단 배경' },
{ name: 'surface', desc: '패널, 컨테이너' },
{ name: 'elevated', desc: '부각된 영역' },
{ name: 'card', desc: '카드, 플로팅' },
{ name: 'overlay', desc: '오버레이, 딤' },
].map((row) => (
<div
key={row.name}
className="flex items-center justify-between px-5 py-2.5"
style={{
borderTop: `1px solid ${dividerColor}`,
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
}}
>
<span className="font-mono text-sm font-semibold" style={{ color: t.textPrimary }}>{row.name}</span>
<span className="text-xs" style={{ color: t.textMuted }}>{row.desc}</span>
</div>
))}
</div>
{/* Variant */}
<div className="rounded-lg overflow-hidden" style={{ border: `1px solid ${dividerColor}` }}>
<div className="px-5 py-3 text-xs font-bold uppercase tracking-wider" style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}>
Variant ()
</div>
{[
{ name: 'hover', desc: '호버 상태' },
{ name: 'active', desc: '활성/선택 상태' },
{ name: 'muted', desc: '약한 강도' },
{ name: 'light', desc: '밝은 변형' },
{ name: 'subtle', desc: '미묘한 변형' },
].map((row) => (
<div
key={row.name}
className="flex items-center justify-between px-5 py-2.5"
style={{
borderTop: `1px solid ${dividerColor}`,
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa',
}}
>
<span className="font-mono text-sm font-semibold" style={{ color: t.textPrimary }}>{row.name}</span>
<span className="text-xs" style={{ color: t.textMuted }}>{row.desc}</span>
</div>
))}
</div>
</div>
</div>
{/* Semantic 토큰 매핑 */}
<div
className="mb-12 pt-12 border-t border-solid"
style={{ borderColor: dividerColor }}
>
<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>
{/* bg, fg, stroke 테이블 */}
{[
{
title: 'bg — Background',
tokens: [
{ legacy: '--bg0', name: '--bg-base', value: '#0a0e1a', desc: '페이지 최하단 배경' },
{ legacy: '--bg1', name: '--bg-surface', value: '#0f1524', desc: '패널, 사이드바' },
{ legacy: '--bg2', name: '--bg-elevated', value: '#121929', desc: '테이블 헤더, 섹션' },
{ legacy: '--bg3', name: '--bg-card', value: '#1a2236', desc: '카드, 플로팅 요소' },
{ legacy: '--bgH', name: '--bg-surface-hover', value: '#1e2844', desc: '호버 인터랙션' },
],
},
{
title: 'fg — Foreground',
tokens: [
{ legacy: '--t1', name: '--fg-default', value: '#edf0f7', desc: '기본 텍스트' },
{ legacy: '--t2', name: '--fg-sub', value: '#c0c8dc', desc: '보조 텍스트' },
{ legacy: '--t3', name: '--fg-disabled', value: '#9ba3b8', desc: '비활성, 플레이스홀더' },
],
},
{
title: 'stroke — Border',
tokens: [
{ legacy: '--bd', name: '--stroke-default', value: '#1e2a42', desc: '기본 구분선' },
{ legacy: '--bdL', name: '--stroke-light', value: '#2a3a5c', desc: '연한 구분선' },
],
},
].map((group) => (
<div key={group.title} className="mb-8">
<h3 className="text-sm font-bold mb-3" style={{ color: t.textAccent }}>{group.title}</h3>
<div className="rounded-lg overflow-hidden" style={{ border: `1px solid ${dividerColor}` }}>
{/* 헤더 */}
<div
className="grid grid-cols-[80px_1fr_1fr_80px_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}
>
<span>Legacy</span><span>New</span><span></span><span>Value</span><span>Preview</span>
</div>
{group.tokens.map((tk) => (
<div
key={tk.name}
className="grid grid-cols-[80px_1fr_1fr_80px_1fr] gap-2 items-center px-4 py-3"
style={{ borderTop: `1px solid ${dividerColor}`, backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa' }}
>
<span className="font-mono text-xs line-through" style={{ color: t.textMuted }}>{tk.legacy}</span>
<span className="font-mono text-xs font-semibold" style={{ color: t.textPrimary }}>{tk.name}</span>
<span className="text-xs" style={{ color: t.textMuted }}>{tk.desc}</span>
<span className="font-mono text-xs" style={{ color: t.textSecondary }}>{tk.value}</span>
<div className="flex items-center gap-2">
<div style={{ width: 20, height: 20, borderRadius: 4, backgroundColor: tk.value, border: `1px solid ${dividerColor}`, flexShrink: 0 }} />
<span className="font-mono text-xs" style={{ color: t.textMuted }}>{hexToRgb(tk.value)}</span>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Palette 토큰 매핑 */}
<div
className="mb-12 pt-12 border-t border-solid"
style={{ borderColor: dividerColor }}
>
<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 }}>
fg · bg · stroke . Property <code className="font-mono" style={{ color: t.textAccent }}>--color-*</code> .
</p>
<div className="rounded-lg overflow-hidden" 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"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}
>
<span>Legacy</span><span>New</span><span>Value</span><span>Preview</span><span></span>
</div>
{[
{ legacy: '--cyan', name: '--color-accent', value: '#06b6d4', desc: '주요 강조' },
{ legacy: '--blue', name: '--color-info', value: '#3b82f6', desc: '정보' },
{ legacy: '--red', name: '--color-danger', value: '#ef4444', desc: '위험' },
{ legacy: '--orange', name: '--color-warning', value: '#f97316', desc: '주의' },
{ legacy: '--yellow', name: '--color-caution', value: '#eab308', desc: '경고' },
{ legacy: '--green', name: '--color-success', value: '#22c55e', desc: '성공' },
{ legacy: '--purple', name: '--color-tertiary', value: '#a855f7', desc: '3차 강조' },
{ legacy: '--boom', name: '--color-boom', value: '#f59e0b', desc: '오일붐 (도메인)' },
{ legacy: '--boomH', name: '--color-boom-hover', value: '#fbbf24', desc: '오일붐 호버' },
].map((tk) => (
<div
key={tk.name}
className="grid grid-cols-[80px_1fr_80px_1fr_1fr] gap-2 items-center px-4 py-3"
style={{ borderTop: `1px solid ${dividerColor}`, backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa' }}
>
<span className="font-mono text-xs line-through" style={{ color: t.textMuted }}>{tk.legacy}</span>
<span className="font-mono text-xs font-semibold" style={{ color: t.textPrimary }}>{tk.name}</span>
<span className="font-mono text-xs" style={{ color: t.textSecondary }}>{tk.value}</span>
<div className="flex items-center gap-2">
<div style={{ width: 20, height: 20, borderRadius: 4, backgroundColor: tk.value, border: `1px solid ${dividerColor}`, flexShrink: 0 }} />
<span className="font-mono text-xs" style={{ color: t.textMuted }}>{hexToRgb(tk.value)}</span>
</div>
<span className="text-xs" style={{ color: t.textMuted }}>{tk.desc}</span>
</div>
))}
</div>
</div>
{/* Non-color 토큰 */}
<div
className="mb-12 pt-12 border-t border-solid"
style={{ borderColor: dividerColor }}
>
<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>
<div className="rounded-lg overflow-hidden" 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"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}
>
<span>Legacy</span><span>New</span><span>Category</span><span></span>
</div>
{[
{ legacy: '--fK', name: '--font-korean', category: 'Typography', desc: '한국어 UI 폰트 스택' },
{ legacy: '--fM', name: '--font-mono', category: 'Typography', desc: '수치/데이터 폰트 스택' },
{ legacy: '--rS', name: '--radius-sm', category: 'Radius', desc: '작은 라운딩 (6px)' },
{ legacy: '--rM', name: '--radius-md', category: 'Radius', desc: '중간 라운딩 (8px)' },
].map((tk) => (
<div
key={tk.name}
className="grid grid-cols-[80px_1fr_1fr_1fr] gap-2 items-center px-4 py-3"
style={{ borderTop: `1px solid ${dividerColor}`, backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa' }}
>
<span className="font-mono text-xs line-through" style={{ color: t.textMuted }}>{tk.legacy}</span>
<span className="font-mono text-xs font-semibold" style={{ color: t.textPrimary }}>{tk.name}</span>
<span className="text-xs 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)' }}>{tk.category}</span>
<span className="text-xs" style={{ color: t.textMuted }}>{tk.desc}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* ── Usage 탭 콘텐츠 ── */}
{activeColorTab === 'usage' && (
<>
@ -685,6 +958,17 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
{/* ── Token 탭 콘텐츠 ── */}
{activeColorTab === 'token' && (
<div className="mb-16">
<div className="mb-6">
<p
className="font-mono text-sm uppercase tracking-widest mb-1"
style={{ color: t.textAccent }}
>
Primitive
</p>
<p className="text-sm" style={{ color: t.textMuted }}>
UI . .
</p>
</div>
{COLOR_TOKEN_GROUPS.map((group, groupIdx) => (
<div key={group.title}>
<h2
@ -741,6 +1025,7 @@ export const ColorPaletteContent = ({ theme }: ColorPaletteContentProps) => {
</div>
</div>
))}
</div>
)}

파일 보기

@ -97,7 +97,7 @@ const TYPO_ROWS: TypoRow[] = [
<span className="font-mono text-[11px]" style={{ color: t.typoCoordText }}>35° 06' 12&quot; N</span>
</div>
),
properties: 'JetBrains Mono / 11px',
properties: 'PretendardGOV / 11px',
isData: true,
},
];
@ -339,13 +339,7 @@ export const DesignContent = ({ theme }: DesignContentProps) => {
className="rounded-sm py-0.5 px-2 font-korean text-[10px] font-bold"
style={{ backgroundColor: t.fontBadgePrimaryBg, color: t.fontBadgePrimaryText }}
>
Noto Sans KR
</span>
<span
className="rounded-sm border border-solid py-0.5 px-2 font-korean text-[10px] font-bold"
style={{ borderColor: t.fontBadgeSecondaryBorder, color: t.fontBadgeSecondaryText }}
>
JetBrains Mono
PretendardGOV
</span>
</div>
}

파일 보기

@ -84,7 +84,7 @@ const Z_LAYERS: ZLayer[] = [
{ name: 'TopBar', zIndex: 30, description: '상단 네비게이션 바', color: '#3b82f6' },
{ name: 'Sidebar', zIndex: 20, description: '사이드바, 패널', color: '#06b6d4' },
{ name: 'Content', zIndex: 10, description: '메인 콘텐츠 영역', color: '#22c55e' },
{ name: 'Base', zIndex: 0, description: '기본 레이어, 배경', color: '#8690a6' },
{ name: 'Base', zIndex: 0, description: '기본 레이어, 배경', color: '#9ba3b8' },
];
// ---------- App Shell Classes ----------
@ -93,7 +93,7 @@ const SHELL_CLASSES: ShellClass[] = [
{ className: '.wing-panel', role: '탭 콘텐츠 패널', styles: 'flex flex-col h-full overflow-hidden' },
{ className: '.wing-panel-scroll', role: '패널 내 스크롤 영역', styles: 'flex-1 overflow-y-auto' },
{ className: '.wing-header-bar', role: '패널 헤더', styles: 'flex items-center justify-between shrink-0 px-5 border-b' },
{ className: '.wing-sidebar', role: '사이드바', styles: 'flex flex-col border-r border-border' },
{ className: '.wing-sidebar', role: '사이드바', styles: 'flex flex-col border-r border-stroke' },
];
// ---------- Props ----------
@ -212,7 +212,7 @@ export const LayoutContent = ({ theme }: LayoutContentProps) => {
<span
className="font-mono text-[9px] rounded px-1.5 py-0.5"
style={{
color: bp.inUse ? (isDark ? '#22c55e' : '#047857') : (isDark ? '#8690a6' : '#94a3b8'),
color: bp.inUse ? (isDark ? '#22c55e' : '#047857') : (isDark ? '#9ba3b8' : '#94a3b8'),
backgroundColor: bp.inUse
? (isDark ? 'rgba(34,197,94,0.10)' : 'rgba(34,197,94,0.08)')
: (isDark ? 'rgba(134,144,166,0.10)' : 'rgba(148,163,184,0.08)'),

파일 보기

@ -12,136 +12,19 @@ interface FontFamily {
sampleText: string;
}
interface TypographyToken {
className: string;
size: string;
font: string;
weight: string;
usage: string;
sampleText: string;
sampleStyle: React.CSSProperties;
}
// ---------- Font Family 데이터 ----------
const FONT_FAMILIES: FontFamily[] = [
{
name: 'Noto Sans KR',
className: 'font-korean',
stack: "'Noto Sans KR', sans-serif",
usage: '기본 UI 텍스트, 레이블, 설명 등 한국어 콘텐츠 전반에 사용됩니다. 프로젝트에서 가장 많이 사용되는 폰트입니다.',
sampleText: '해양 방제 운영 지원 시스템 WING-OPS',
},
{
name: 'JetBrains Mono',
className: 'font-mono',
stack: "'JetBrains Mono', monospace",
usage: '좌표, 수치 데이터, 코드, 토큰 이름 등 고정폭이 필요한 콘텐츠에 사용됩니다.',
sampleText: '126.978° E, 37.566° N — #0a0e1a',
},
{
name: 'Outfit',
className: 'font-sans',
stack: "'Outfit', 'Noto Sans KR', sans-serif",
usage: '영문 헤딩과 브랜드 타이틀에 사용됩니다. body 기본 폰트 스택에 포함되어 있습니다.',
sampleText: 'WING-OPS Design System v1.0',
name: 'PretendardGOV',
className: 'font-sans / font-korean / font-mono',
stack: "'PretendardGOV', sans-serif",
usage: '프로젝트 전체 통합 폰트. 한국어/영문 UI 텍스트, 수치 데이터, 좌표 표시 등 모든 콘텐츠에 사용됩니다.',
sampleText: '해양 방제 운영 지원 시스템 WING-OPS 35.1284° N',
},
];
// ---------- Typography Token 데이터 ----------
const TYPOGRAPHY_TOKENS: TypographyToken[] = [
{
className: '.wing-title',
size: '15px',
font: 'font-korean',
weight: 'Bold (700)',
usage: '패널 제목',
sampleText: '확산 예측 시뮬레이션',
sampleStyle: { fontSize: '15px', fontWeight: 700, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-section-header',
size: '13px',
font: 'font-korean',
weight: 'Bold (700)',
usage: '섹션 헤더',
sampleText: '기본 정보 입력',
sampleStyle: { fontSize: '13px', fontWeight: 700, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-label',
size: '11px',
font: 'font-korean',
weight: 'Semibold (600)',
usage: '필드 레이블',
sampleText: '유출량 (kL)',
sampleStyle: { fontSize: '11px', fontWeight: 600, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-btn',
size: '11px',
font: 'font-korean',
weight: 'Semibold (600)',
usage: '버튼 텍스트',
sampleText: '시뮬레이션 실행',
sampleStyle: { fontSize: '11px', fontWeight: 600, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-value',
size: '11px',
font: 'font-mono',
weight: 'Semibold (600)',
usage: '수치 / 데이터 값',
sampleText: '35.1284° N, 129.0598° E',
sampleStyle: { fontSize: '11px', fontWeight: 600, fontFamily: "'JetBrains Mono', monospace" },
},
{
className: '.wing-input',
size: '11px',
font: 'font-korean',
weight: 'Normal (400)',
usage: '입력 필드',
sampleText: '서해 대산항 인근 해역',
sampleStyle: { fontSize: '11px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-section-desc',
size: '10px',
font: 'font-korean',
weight: 'Normal (400)',
usage: '섹션 설명',
sampleText: '예측 결과는 기상 조건에 따라 달라질 수 있습니다.',
sampleStyle: { fontSize: '10px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-subtitle',
size: '10px',
font: 'font-korean',
weight: 'Normal (400)',
usage: '보조 설명',
sampleText: '최근 업데이트: 2026-03-24 09:00 KST',
sampleStyle: { fontSize: '10px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-meta',
size: '9px',
font: 'font-korean',
weight: 'Normal (400)',
usage: '메타 정보',
sampleText: 'v2.1 | 해양환경공단',
sampleStyle: { fontSize: '9px', fontWeight: 400, fontFamily: "'Noto Sans KR', sans-serif" },
},
{
className: '.wing-badge',
size: '9px',
font: 'font-korean',
weight: 'Bold (700)',
usage: '뱃지 / 태그',
sampleText: '진행중',
sampleStyle: { fontSize: '9px', fontWeight: 700, fontFamily: "'Noto Sans KR', sans-serif" },
},
];
// ---------- Props ----------
@ -154,7 +37,6 @@ interface TypographyContentProps {
export const TypographyContent = ({ theme }: TypographyContentProps) => {
const t = theme;
const isDark = t.mode === 'dark';
return (
<div className="pt-24 px-8 pb-16 flex flex-col gap-16 items-start justify-start max-w-[1440px]">
@ -190,271 +72,242 @@ export const TypographyContent = ({ theme }: TypographyContentProps) => {
style={{ color: t.textSecondary }}
>
<li> , , .</li>
<li> (<code style={{ color: t.textAccent, fontSize: '12px' }}>.wing-*</code>) .</li>
<li> .</li>
</ul>
</div>
</div>
{/* ── 섹션 2: 글꼴 (Font Family) ── */}
<div className="w-full flex flex-col gap-8">
<div className="flex flex-col gap-2">
<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 }}
>
, . UI에 .
</p>
</div>
{/* body 기본 폰트 스택 코드 블록 */}
<div
className="rounded-lg border border-solid px-5 py-4 overflow-x-auto"
style={{
backgroundColor: isDark ? '#0f1524' : '#f1f5f9',
borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0',
}}
>
<pre
className="font-mono text-sm leading-6"
style={{ color: isDark ? '#b0b8cc' : '#475569' }}
>
<span style={{ color: t.textAccent }}>font-family</span>
{`: 'Outfit', 'Noto Sans KR', sans-serif;`}
</pre>
</div>
{/* 폰트 카드 3종 */}
<div className="flex flex-col gap-6">
{FONT_FAMILIES.map((font) => (
<div
key={font.name}
className="rounded-lg border border-solid overflow-hidden"
style={{
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{/* 카드 헤더 */}
<div
className="flex flex-row items-center gap-4 px-5 py-4 border-b border-solid"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.15)' : '#e2e8f0' }}
{/* ── 글꼴 (Font Family) ── */}
<div className="w-full flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h2
className="font-sans text-2xl leading-8 font-bold"
style={{ color: t.textPrimary }}
>
<span
className="font-sans text-lg font-bold"
style={{ color: t.textPrimary }}
>
{font.name}
</span>
<span
className="font-mono text-[11px] rounded border border-solid px-2 py-0.5"
style={{
color: t.textAccent,
backgroundColor: isDark ? 'rgba(6,182,212,0.05)' : 'rgba(6,182,212,0.08)',
borderColor: isDark ? 'rgba(6,182,212,0.20)' : 'rgba(6,182,212,0.25)',
}}
>
{font.className}
</span>
</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"
style={{
color: isDark ? '#8690a6' : '#64748b',
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)',
}}
>
{font.stack}
</div>
{/* 용도 설명 */}
<p
className="font-korean text-xs leading-5"
style={{ color: t.textSecondary }}
>
{font.usage}
</p>
{/* 샘플 렌더 */}
<div className="flex flex-col gap-3 pt-2">
{/* Regular */}
<div className="flex flex-col gap-1">
<span
className="font-mono text-[9px] uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Regular
</span>
<span
className={`${font.className} text-xl leading-7`}
style={{ color: t.textPrimary, fontWeight: 400 }}
>
{font.sampleText}
</span>
</div>
{/* Bold */}
<div className="flex flex-col gap-1">
<span
className="font-mono text-[9px] uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
Bold
</span>
<span
className={`${font.className} text-xl leading-7 font-bold`}
style={{ color: t.textPrimary }}
>
{font.sampleText}
</span>
</div>
</div>
</div>
</h2>
<p
className="font-korean text-sm leading-5"
style={{ color: t.textSecondary }}
>
, . UI에 .
</p>
</div>
))}
</div>
</div>
{/* ── 섹션 3: 타이포그래피 토큰 ── */}
<div className="w-full flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h2
className="font-sans text-2xl leading-8 font-bold"
style={{ color: t.textPrimary }}
>
</h2>
<ul
className="flex flex-col gap-1 list-disc list-inside font-korean text-sm"
style={{ color: t.textSecondary }}
>
<li>Tailwind @apply (<code style={{ color: t.textAccent, fontSize: '12px' }}>wing.css</code>).</li>
<li> px .</li>
<li> UI에서는 , .</li>
</ul>
</div>
{/* 토큰 테이블 */}
<div
className="rounded-lg border border-solid overflow-hidden w-full"
style={{
backgroundColor: t.tableContainerBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{/* 헤더 */}
<div
className="grid"
style={{
gridTemplateColumns: '160px 70px 110px 130px 120px 1fr',
backgroundColor: t.tableHeaderBg,
borderBottom: `1px solid ${t.tableRowBorder}`,
}}
>
{(['Class', 'Size', 'Font', 'Weight', '용도', 'Sample'] as const).map((col) => (
<div key={col} className="py-3 px-4">
<span
className="font-mono text-[10px] font-medium uppercase"
style={{ letterSpacing: '1px', color: t.textMuted }}
>
{col}
</span>
</div>
))}
</div>
{/* 데이터 행 */}
{TYPOGRAPHY_TOKENS.map((token, rowIdx) => (
{/* body 기본 폰트 스택 코드 블록 */}
<div
key={token.className}
className="grid items-center"
className="rounded-lg border border-solid px-5 py-4 overflow-x-auto"
style={{
gridTemplateColumns: '160px 70px 110px 130px 120px 1fr',
borderTop: rowIdx === 0 ? 'none' : `1px solid ${t.tableRowBorder}`,
backgroundColor: isDark ? '#0f1524' : '#f1f5f9',
borderColor: isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0',
}}
>
{/* Class */}
<div className="py-3 px-4">
<span
className="font-mono rounded border border-solid px-2 py-0.5"
<pre
className="font-mono text-sm leading-6"
style={{ color: isDark ? '#c0c8dc' : '#475569' }}
>
<span style={{ color: t.textAccent }}>font-family</span>
{`: 'PretendardGOV', -apple-system, BlinkMacSystemFont,\n 'Apple SD Gothic Neo', 'Pretendard Variable', Pretendard,\n Roboto, 'Noto Sans KR', 'Segoe UI', 'Malgun Gothic',\n 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',\n sans-serif;`}
</pre>
</div>
{/* 폰트 카드 */}
<div className="flex flex-col gap-6">
{FONT_FAMILIES.map((font) => (
<div
key={font.name}
className="rounded-lg border border-solid overflow-hidden"
style={{
fontSize: '11px',
lineHeight: '17px',
color: t.textAccent,
backgroundColor: t.cardBg,
borderColor: t.cardBorder,
boxShadow: t.cardShadow,
}}
>
{token.className}
</span>
</div>
{/* Size */}
<div className="py-3 px-4">
<span
className="font-mono text-[11px]"
style={{ color: t.textPrimary }}
>
{token.size}
</span>
</div>
{/* Font */}
<div className="py-3 px-4">
<span
className="font-mono text-[11px]"
style={{ color: t.textSecondary }}
>
{token.font}
</span>
</div>
{/* Weight */}
<div className="py-3 px-4">
<span
className="font-mono text-[11px]"
style={{ color: t.textSecondary }}
>
{token.weight}
</span>
</div>
{/* 용도 */}
<div className="py-3 px-4">
<span
className="font-korean text-xs"
style={{ color: t.textSecondary }}
>
{token.usage}
</span>
</div>
{/* Sample */}
<div className="py-3 px-4">
<span
style={{
...token.sampleStyle,
color: t.textPrimary,
}}
>
{token.sampleText}
</span>
</div>
<div
className="flex flex-row items-center gap-4 px-5 py-4 border-b border-solid"
style={{ borderColor: isDark ? 'rgba(66,71,84,0.15)' : '#e2e8f0' }}
>
<span className="font-sans text-lg font-bold" style={{ color: t.textPrimary }}>{font.name}</span>
<span
className="font-mono text-[11px] rounded border border-solid px-2 py-0.5"
style={{
color: t.textAccent,
backgroundColor: isDark ? 'rgba(6,182,212,0.05)' : 'rgba(6,182,212,0.08)',
borderColor: isDark ? 'rgba(6,182,212,0.20)' : 'rgba(6,182,212,0.25)',
}}
>
{font.className}
</span>
</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"
style={{
color: isDark ? '#9ba3b8' : '#64748b',
backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)',
}}
>
{font.stack}
</div>
<p className="font-korean text-xs 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-[9px] uppercase" style={{ letterSpacing: '1px', color: t.textMuted }}>Regular</span>
<span className="text-xl leading-7" style={{ color: t.textPrimary, fontWeight: 400 }}>{font.sampleText}</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-mono text-[9px] uppercase" style={{ letterSpacing: '1px', color: t.textMuted }}>Bold</span>
<span className="text-xl leading-7 font-bold" style={{ color: t.textPrimary }}>{font.sampleText}</span>
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
<div className="w-full flex flex-col gap-12">
{/* Font Size */}
<div className="flex flex-col gap-4">
<div>
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}> </h2>
<p className="text-sm" style={{ color: t.textSecondary }}>
0.6875rem(11px). rem .
</p>
</div>
<div className="rounded-lg overflow-hidden" style={{ border: `1px solid ${isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0'}` }}>
<div className="grid grid-cols-[1fr_100px_80px_1fr] gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}>
<span>Token</span><span>rem</span><span>px</span><span></span>
</div>
{[
{ token: '--font-size-display-1', rem: '3.75rem', px: '60px', desc: '최상위 헤드라인' },
{ token: '--font-size-display-2', rem: '2.5rem', px: '40px', desc: '메인 타이틀' },
{ token: '--font-size-display-3', rem: '2.25rem', px: '36px', desc: '강조형 헤드라인' },
{ token: '--font-size-heading-1', rem: '2rem', px: '32px', desc: '페이지 상위 제목' },
{ token: '--font-size-heading-2', rem: '1.5rem', px: '24px', desc: '하위 제목' },
{ token: '--font-size-heading-3', rem: '1.375rem', px: '22px', desc: '소제목, 그룹 제목' },
{ token: '--font-size-title-1', rem: '1.125rem', px: '18px', desc: '컴포넌트 제목' },
{ token: '--font-size-title-2', rem: '1rem', px: '16px', desc: '패널 제목' },
{ token: '--font-size-body-1', rem: '0.875rem', px: '14px', desc: '기본 본문' },
{ token: '--font-size-body-2', rem: '0.8125rem', px: '13px', desc: '보조 본문' },
{ token: '--font-size-label-1', rem: '0.75rem', px: '12px', desc: '레이블, 버튼' },
{ token: '--font-size-label-2', rem: '0.6875rem', px: '11px', desc: '소형 레이블, 데이터' },
{ token: '--font-size-caption', rem: '0.6875rem', px: '11px', desc: '캡션, 메타 정보' },
].map((row) => (
<div key={row.token} className="grid grid-cols-[1fr_100px_80px_1fr] gap-2 items-center px-4 py-3"
style={{ borderTop: `1px solid ${isDark ? 'rgba(66,71,84,0.10)' : '#e5e7eb'}`, backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa' }}>
<span className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>{row.token}</span>
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>{row.rem}</span>
<span className="font-mono text-xs" style={{ color: t.textMuted }}>{row.px}</span>
<span className="text-xs" style={{ color: t.textSecondary }}>{row.desc}</span>
</div>
))}
</div>
</div>
{/* Font Weight */}
<div className="flex flex-col gap-4">
<div>
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}> </h2>
<p className="text-sm" style={{ color: t.textSecondary }}>
Regular(400), Bold(700). Medium(500) , Thin(300) .
</p>
</div>
<div className="rounded-lg overflow-hidden" 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"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}>
<span>Token</span><span>Value</span><span>Name</span><span>Preview</span>
</div>
{[
{ token: '--font-weight-thin', value: '300', name: 'Thin', preview: '해양 방제 운영 WING-OPS' },
{ token: '--font-weight-regular', value: '400', name: 'Regular', preview: '해양 방제 운영 WING-OPS' },
{ token: '--font-weight-medium', value: '500', name: 'Medium', preview: '해양 방제 운영 WING-OPS' },
{ token: '--font-weight-bold', value: '700', name: 'Bold', preview: '해양 방제 운영 WING-OPS' },
].map((row) => (
<div key={row.token} className="grid grid-cols-[1fr_80px_1fr_1fr] gap-2 items-center px-4 py-3"
style={{ borderTop: `1px solid ${isDark ? 'rgba(66,71,84,0.10)' : '#e5e7eb'}`, backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa' }}>
<span className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>{row.token}</span>
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>{row.value}</span>
<span className="text-xs" style={{ color: t.textSecondary }}>{row.name}</span>
<span className="font-korean text-sm" style={{ color: t.textPrimary, fontWeight: Number(row.value) }}>{row.preview}</span>
</div>
))}
</div>
</div>
{/* Line Height */}
<div className="flex flex-col gap-4">
<div>
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}> </h2>
<p className="text-sm" style={{ color: t.textSecondary }}>
(1.3), (1.6). .
</p>
</div>
<div className="rounded-lg overflow-hidden" 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"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.04)' : '#f1f5f9', color: t.textMuted }}>
<span>Token</span><span>Value</span><span>Percentage</span><span></span>
</div>
{[
{ token: '--line-height-tight', value: '1.3', pct: '130%', desc: 'Display (대형 텍스트)' },
{ token: '--line-height-snug', value: '1.4', pct: '140%', desc: 'Heading' },
{ token: '--line-height-normal', value: '1.5', pct: '150%', desc: 'Title, Label, Caption' },
{ token: '--line-height-relaxed', value: '1.6', pct: '160%', desc: 'Body (본문)' },
].map((row) => (
<div key={row.token} className="grid grid-cols-[1fr_80px_100px_1fr] gap-2 items-center px-4 py-3"
style={{ borderTop: `1px solid ${isDark ? 'rgba(66,71,84,0.10)' : '#e5e7eb'}`, backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa' }}>
<span className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>{row.token}</span>
<span className="font-mono text-xs" style={{ color: t.textPrimary }}>{row.value}</span>
<span className="font-mono text-xs" style={{ color: t.textMuted }}>{row.pct}</span>
<span className="text-xs" style={{ color: t.textSecondary }}>{row.desc}</span>
</div>
))}
</div>
</div>
{/* Type Scale */}
<div className="flex flex-col gap-4" style={{ borderTop: `1px solid ${isDark ? 'rgba(66,71,84,0.20)' : '#e2e8f0'}`, paddingTop: '3rem' }}>
<div>
<h2 className="text-xl font-bold mb-2" style={{ color: t.textPrimary }}> </h2>
<p className="text-sm" style={{ color: t.textSecondary }}>
Display, Heading, Title, Body, Label, Caption으로 6 .
</p>
</div>
<div className="flex flex-col gap-3">
{[
{ scale: 'Display 1', size: '3.75rem', weight: 700, lh: 1.3, role: '최상위 헤드라인', sample: '해양 방제 운영 지원' },
{ scale: 'Display 2', size: '2.5rem', weight: 700, lh: 1.3, role: '메인 타이틀', sample: '확산 예측 시뮬레이션' },
{ scale: 'Display 3', size: '2.25rem', weight: 500, lh: 1.4, role: '강조형 헤드라인', sample: '오염 종합 상황판' },
{ scale: 'Heading 1', size: '2rem', weight: 700, lh: 1.4, role: '페이지 상위 제목', sample: '사고 현황 분석' },
{ scale: 'Heading 2', size: '1.5rem', weight: 700, lh: 1.4, role: '하위 제목', sample: '유출유 풍화 상태' },
{ scale: 'Heading 3', size: '1.375rem', weight: 500, lh: 1.4, role: '소제목, 그룹 제목', sample: '기상 데이터 요약' },
{ scale: 'Title 1', size: '1.125rem', weight: 700, lh: 1.5, role: '컴포넌트 제목', sample: '확산 예측 시뮬레이션' },
{ scale: 'Title 2', size: '1rem', weight: 500, lh: 1.5, role: '패널 제목', sample: '기본 정보 입력' },
{ scale: 'Body 1', size: '0.875rem', weight: 400, lh: 1.6, role: '기본 본문', sample: '예측 결과는 기상 조건에 따라 달라질 수 있습니다.' },
{ scale: 'Body 2', size: '0.8125rem', weight: 400, lh: 1.6, role: '보조 본문', sample: '최근 업데이트: 2026-03-24 09:00 KST' },
{ scale: 'Label 1', size: '0.75rem', weight: 500, lh: 1.5, role: '레이블, 버튼', sample: '시뮬레이션 실행' },
{ scale: 'Label 2', size: '0.6875rem', weight: 500, lh: 1.5, role: '소형 레이블', sample: '유출량 (kL)' },
{ scale: 'Caption', size: '0.6875rem', weight: 400, lh: 1.5, role: '캡션, 메타', sample: 'v2.1 | 해양환경공단' },
].map((row) => (
<div key={row.scale} className="rounded-lg px-5 py-4 flex items-center gap-6"
style={{ backgroundColor: isDark ? 'rgba(255,255,255,0.02)' : '#fafafa', border: `1px solid ${isDark ? 'rgba(66,71,84,0.10)' : '#e5e7eb'}` }}>
<div className="shrink-0 w-[100px]">
<div className="font-mono text-xs font-semibold" style={{ color: t.textAccent }}>{row.scale}</div>
<div className="font-mono text-[10px] mt-0.5" style={{ color: t.textMuted }}>{row.size} · {row.weight} · {row.lh}</div>
</div>
<div className="flex-1">
<span className="font-korean" style={{ fontSize: row.size, fontWeight: row.weight, lineHeight: row.lh, color: t.textPrimary }}>
{row.sample}
</span>
</div>
<div className="shrink-0 text-xs font-korean" style={{ color: t.textMuted }}>{row.role}</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};

파일 보기

@ -51,21 +51,21 @@ const buttonRows: ButtonRow[] = [
label: '세컨더리 (솔리드)',
defaultBtn: (
<div className='bg-[#1a2236] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
<div className='text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
hoverBtn: (
<div className='bg-[#1e2844] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
<div className='text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
disabledBtn: (
<div className='bg-[rgba(26,34,54,0.50)] rounded-md border border-solid border-[rgba(30,42,66,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[rgba(176,184,204,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
<div className='text-[rgba(192,200,220,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
@ -75,21 +75,21 @@ const buttonRows: ButtonRow[] = [
label: '아웃라인 (고스트)',
defaultBtn: (
<div className='rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
<div className='text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
hoverBtn: (
<div className='bg-[#1e2844] rounded-md border border-solid border-[#1e2a42] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
<div className='text-[#c0c8dc] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>
),
disabledBtn: (
<div className='rounded-md border border-solid border-[rgba(30,42,66,0.30)] pt-1.5 pr-3 pb-[7px] pl-3 flex flex-row gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[rgba(176,184,204,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
<div className='text-[rgba(192,200,220,0.30)] text-center font-korean text-[11px] font-medium relative flex items-center justify-center'>
</div>
</div>

파일 보기

@ -146,7 +146,7 @@ export const CardSection = () => {
{/* 대응팀 배치 아웃라인 버튼 */}
<div className='rounded-md border border-[#1e2a42] pt-1 pr-3 pb-1 pl-3 flex flex-col gap-0 items-center justify-center shrink-0 relative'>
<div className='text-[#b0b8cc] text-center font-korean text-[10px] leading-[15px] font-medium relative flex items-center justify-center'>
<div className='text-[#c0c8dc] text-center font-korean text-[10px] leading-[15px] font-medium relative flex items-center justify-center'>
</div>
</div>

파일 보기

@ -33,7 +33,7 @@ const statusBadges: StatusBadge[] = [
{ label: '주의', color: '#eab308', bg: 'rgba(234,179,8,0.10)' },
{ label: '위험', color: '#ef4444', bg: 'rgba(239,68,68,0.10)' },
{ label: '진행중', color: '#3b82f6', bg: 'rgba(59,130,246,0.10)' },
{ label: '완료', color: '#8690a6', bg: 'rgba(134,144,166,0.10)' },
{ label: '완료', color: '#9ba3b8', bg: 'rgba(155,163,184,0.10)' },
];
const dataTags: DataTag[] = [

파일 보기

@ -253,8 +253,8 @@ export const DARK_THEME: DesignTheme = {
textTokens: [
{ token: 'text-1', sampleText: '주요 텍스트 Primary Text', sampleClass: 'text-[#edf0f7] font-korean text-[15px] font-bold', desc: 'Headings, active values, and primary labels.', descColor: 'rgba(237,240,247,0.60)' },
{ token: 'text-2', sampleText: '보조 텍스트 Secondary Text', sampleClass: 'text-[#b0b8cc] font-korean text-[15px] font-medium', desc: 'Supporting labels and secondary information.', descColor: 'rgba(176,184,204,0.60)' },
{ token: 'text-3', sampleText: '비활성 텍스트 Muted Text', sampleClass: 'text-[#8690a6] font-korean text-[15px]', desc: 'Disabled states, placeholders, and captions.', descColor: 'rgba(134,144,166,0.60)' },
{ token: 'text-2', sampleText: '보조 텍스트 Secondary Text', sampleClass: 'text-[#c0c8dc] font-korean text-[15px] font-medium', desc: 'Supporting labels and secondary information.', descColor: 'rgba(192,200,220,0.60)' },
{ token: 'text-3', sampleText: '비활성 텍스트 Muted Text', sampleClass: 'text-[#9ba3b8] font-korean text-[15px]', desc: 'Disabled states, placeholders, and captions.', descColor: 'rgba(155,163,184,0.60)' },
],
};

파일 보기

@ -6,8 +6,8 @@ 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-text-2 font-semibold">{label}</div>
<div className="text-[11px] font-korean text-text-3"> .</div>
<div className="text-sm font-korean text-fg-sub font-semibold">{label}</div>
<div className="text-[11px] font-korean text-fg-disabled"> .</div>
</div>
);

파일 보기

@ -41,7 +41,7 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
style={{
paddingLeft: `${12 + depth * 14}px`,
background: isActive ? 'rgba(6,182,212,.12)' : 'transparent',
color: isActive ? 'var(--cyan)' : 'var(--t2)',
color: isActive ? 'var(--color-accent)' : 'var(--fg-sub)',
fontWeight: isActive ? 600 : 400,
}}
>
@ -68,12 +68,12 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
className="w-full flex items-center justify-between px-3 py-1.5 text-[11px] font-korean transition-colors cursor-pointer rounded-[3px]"
style={{
paddingLeft: `${12 + depth * 14}px`,
color: hasActiveChild ? 'var(--cyan)' : 'var(--t2)',
color: hasActiveChild ? 'var(--color-accent)' : 'var(--fg-sub)',
fontWeight: hasActiveChild ? 600 : 400,
}}
>
<span>{item.label}</span>
<span className="text-[9px] text-text-3 transition-transform" style={{ transform: isOpen ? 'rotate(90deg)' : 'rotate(0)' }}>
<span className="text-[9px] text-fg-disabled transition-transform" style={{ transform: isOpen ? 'rotate(90deg)' : 'rotate(0)' }}>
</span>
</button>
@ -95,12 +95,12 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
return (
<div
className="flex flex-col bg-bg-1 border-r border-border overflow-y-auto shrink-0"
style={{ width: 240, scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}
className="flex flex-col bg-bg-surface border-r border-stroke overflow-y-auto shrink-0"
style={{ width: 240, scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}
>
{/* 헤더 */}
<div className="px-4 py-3 border-b border-border bg-bg-2 shrink-0">
<div className="text-xs font-bold text-text-1 font-korean flex items-center gap-1.5">
<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">
<span></span>
</div>
</div>
@ -119,12 +119,12 @@ const AdminSidebar = ({ activeMenu, onSelect }: AdminSidebarProps) => {
className="w-full flex items-center gap-2 px-3 py-2 rounded-md text-[11px] font-bold font-korean transition-colors cursor-pointer"
style={{
background: hasActiveChild ? 'rgba(6,182,212,.08)' : 'transparent',
color: hasActiveChild ? 'var(--cyan)' : 'var(--t1)',
color: hasActiveChild ? 'var(--color-accent)' : 'var(--fg-default)',
}}
>
<span className="text-sm">{section.icon}</span>
<span className="flex-1 text-left">{section.label}</span>
<span className="text-[9px] text-text-3 transition-transform" style={{ transform: isOpen ? 'rotate(90deg)' : 'rotate(0)' }}>
<span className="text-[9px] text-fg-disabled transition-transform" style={{ transform: isOpen ? 'rotate(90deg)' : 'rotate(0)' }}>
</span>
</button>

파일 보기

@ -55,7 +55,7 @@ export function AdminView() {
};
return (
<div className="flex flex-1 overflow-hidden bg-bg-0">
<div className="flex flex-1 overflow-hidden bg-bg-base">
<AdminSidebar activeMenu={activeMenu} onSelect={setActiveMenu} />
<div className="flex-1 flex flex-col overflow-hidden">
{renderContent()}

파일 보기

@ -7,8 +7,8 @@ const JURISDICTIONS = ['전체', '남해청', '서해청', '중부청', '동해
const PERM_ITEMS = [
{ icon: '👑', role: '시스템관리자', desc: '전체 자산 업로드/삭제 가능', bg: 'rgba(245,158,11,0.15)', color: 'text-yellow-400' },
{ icon: '🔧', role: '운영관리자', desc: '관할청 내 자산 업로드 가능', bg: 'rgba(6,182,212,0.15)', color: 'text-primary-cyan' },
{ icon: '👁', role: '조회자', desc: '현황 조회만 가능', bg: 'rgba(148,163,184,0.15)', color: 'text-text-2' },
{ icon: '🔧', role: '운영관리자', desc: '관할청 내 자산 업로드 가능', bg: 'rgba(6,182,212,0.15)', color: 'text-color-accent' },
{ icon: '👁', role: '조회자', desc: '현황 조회만 가능', bg: 'rgba(148,163,184,0.15)', color: 'text-fg-sub' },
{ icon: '🚫', role: '게스트', desc: '접근 불가', bg: 'rgba(239,68,68,0.1)', color: 'text-red-400' },
];
@ -67,9 +67,9 @@ function AssetUploadPanel() {
return (
<div className="flex flex-col h-full">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-border flex-shrink-0">
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> </p>
<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>
</div>
{/* 본문 */}
@ -77,9 +77,9 @@ function AssetUploadPanel() {
<div className="flex gap-6 h-full">
{/* 좌측: 파일 업로드 */}
<div className="flex-1 max-w-[560px] space-y-4">
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
<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>
</div>
<div className="px-5 py-4 space-y-4">
{/* 드롭존 */}
@ -90,20 +90,20 @@ function AssetUploadPanel() {
onClick={() => fileInputRef.current?.click()}
className={`rounded-lg border-2 border-dashed py-8 text-center cursor-pointer transition-colors ${
dragging
? 'border-primary-cyan bg-[rgba(6,182,212,0.05)]'
: 'border-border hover:border-primary-cyan/50 bg-bg-2'
? 'border-color-accent bg-[rgba(6,182,212,0.05)]'
: 'border-stroke hover:border-color-accent/50 bg-bg-elevated'
}`}
>
<div className="text-3xl mb-2 opacity-40">📁</div>
{selectedFile ? (
<div className="text-xs font-semibold text-primary-cyan font-korean mb-1">{selectedFile.name}</div>
<div className="text-xs font-semibold text-color-accent font-korean mb-1">{selectedFile.name}</div>
) : (
<>
<div className="text-xs font-semibold text-text-2 font-korean mb-1"> </div>
<div className="text-[10px] text-text-3 font-korean mb-3">(.xlsx), CSV · 10MB</div>
<div className="text-xs font-semibold text-fg-sub font-korean mb-1"> </div>
<div className="text-[10px] text-fg-disabled font-korean mb-3">(.xlsx), CSV · 10MB</div>
<button
type="button"
className="px-4 py-1.5 text-xs font-semibold rounded-md bg-primary-cyan text-bg-0
className="px-4 py-1.5 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"
onClick={e => { e.stopPropagation(); fileInputRef.current?.click(); }}
>
@ -122,12 +122,12 @@ function AssetUploadPanel() {
{/* 자산 분류 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5"> </label>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5"> </label>
<select
value={assetCategory}
onChange={e => setAssetCategory(e.target.value)}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md
text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
{ASSET_CATEGORIES.map(c => (
<option key={c} value={c}>{c}</option>
@ -137,12 +137,12 @@ function AssetUploadPanel() {
{/* 대상 관할 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5"> </label>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5"> </label>
<select
value={jurisdiction}
onChange={e => setJurisdiction(e.target.value)}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md
text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
{JURISDICTIONS.map(j => (
<option key={j} value={j}>{j}</option>
@ -152,9 +152,9 @@ function AssetUploadPanel() {
{/* 업로드 방식 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5"> </label>
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5"> </label>
<div className="flex gap-4">
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-text-2 font-korean">
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-fg-sub font-korean">
<input
type="radio"
checked={uploadMode === 'add'}
@ -163,7 +163,7 @@ function AssetUploadPanel() {
/>
( + )
</label>
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-text-2 font-korean">
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-fg-sub font-korean">
<input
type="radio"
checked={uploadMode === 'replace'}
@ -182,8 +182,8 @@ function AssetUploadPanel() {
disabled={!selectedFile || uploaded}
className={`w-full py-2.5 text-xs font-semibold rounded-md transition-all font-korean disabled:opacity-50 ${
uploaded
? 'bg-[rgba(34,197,94,0.15)] text-status-green border border-status-green/30'
: 'bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
? '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)]'
}`}
>
{uploaded ? '✅ 업로드 완료!' : '📤 업로드 실행'}
@ -195,15 +195,15 @@ function AssetUploadPanel() {
{/* 우측 */}
<div className="w-[400px] space-y-4 flex-shrink-0">
{/* 수정 권한 체계 */}
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
<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>
</div>
<div className="px-5 py-4 space-y-2">
{PERM_ITEMS.map(p => (
<div
key={p.role}
className="flex items-center gap-3 px-4 py-3 bg-bg-2 border border-border rounded-md"
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"
@ -213,7 +213,7 @@ function AssetUploadPanel() {
</div>
<div>
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
<div className="text-[10px] text-text-3 font-korean mt-0.5">{p.desc}</div>
<div className="text-[10px] text-fg-disabled font-korean mt-0.5">{p.desc}</div>
</div>
</div>
))}
@ -221,26 +221,26 @@ function AssetUploadPanel() {
</div>
{/* 최근 업로드 이력 */}
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
<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>
</div>
<div className="px-5 py-4 space-y-2">
{uploadHistory.length === 0 ? (
<div className="text-[11px] text-text-3 font-korean text-center py-4"> .</div>
<div className="text-[11px] text-fg-disabled font-korean text-center py-4"> .</div>
) : uploadHistory.map(h => (
<div
key={h.logSn}
className="flex justify-between items-center px-4 py-3 bg-bg-2 border border-border rounded-md"
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-text-1 font-korean">{h.fileNm}</div>
<div className="text-[10px] text-text-3 mt-0.5 font-korean">
<div className="text-xs font-semibold text-fg font-korean">{h.fileNm}</div>
<div className="text-[10px] text-fg-disabled mt-0.5 font-korean">
{formatDate(h.regDtm)} · {h.uploaderNm} · {h.uploadCnt.toLocaleString()}
</div>
</div>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold
bg-[rgba(34,197,94,0.15)] text-status-green flex-shrink-0">
bg-[rgba(34,197,94,0.15)] text-color-success flex-shrink-0">
</span>
</div>

파일 보기

@ -116,15 +116,15 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="flex items-center justify-between px-5 py-3 border-b border-border-1">
<h2 className="text-sm font-semibold text-text-1"> </h2>
<span className="text-xs text-text-3">
<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>
</div>
{/* 카테고리 탭 + 검색 */}
<div className="flex items-center gap-3 px-5 py-2 border-b border-border-1">
<div className="flex items-center gap-3 px-5 py-2 border-b border-stroke-1">
<div className="flex gap-1">
{CATEGORY_TABS.map(tab => (
<button
@ -133,7 +133,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
className={`px-3 py-1 text-xs rounded-full transition-colors ${
activeCategory === tab.code
? 'bg-blue-500/20 text-blue-400 font-medium'
: 'text-text-3 hover:text-text-2 hover:bg-bg-2'
: 'text-fg-disabled hover:text-fg-sub hover:bg-bg-elevated'
}`}
>
{tab.label}
@ -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-2 border border-border-1 text-text-1 placeholder:text-text-4 w-48"
className="px-2 py-1 text-xs 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-2 border border-border-1 text-text-2 hover:bg-bg-3"
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg-sub hover:bg-bg-card"
>
</button>
@ -158,7 +158,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
</div>
{/* 액션 바 */}
<div className="flex items-center gap-2 px-5 py-2 border-b border-border-1">
<div className="flex items-center gap-2 px-5 py-2 border-b border-stroke-1">
<button
onClick={handleDelete}
disabled={selected.size === 0 || deleting}
@ -171,8 +171,8 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
{/* 테이블 */}
<div className="flex-1 overflow-auto">
<table className="w-full text-xs">
<thead className="sticky top-0 bg-bg-1 z-10">
<tr className="border-b border-border-1 text-text-3">
<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">
<input
type="checkbox"
@ -192,11 +192,11 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
<tbody>
{loading ? (
<tr>
<td colSpan={7} className="py-8 text-center text-text-3"> ...</td>
<td colSpan={7} className="py-8 text-center text-fg-disabled"> ...</td>
</tr>
) : items.length === 0 ? (
<tr>
<td colSpan={7} className="py-8 text-center text-text-3"> .</td>
<td colSpan={7} className="py-8 text-center text-fg-disabled"> .</td>
</tr>
) : (
items.map(post => (
@ -214,11 +214,11 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
{/* 페이지네이션 */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-1 py-2 border-t border-border-1">
<div className="flex items-center justify-center gap-1 py-2 border-t border-stroke-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page <= 1}
className="px-2 py-1 text-xs rounded text-text-3 hover:bg-bg-2 disabled:opacity-30"
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
>
&lt;
</button>
@ -231,7 +231,7 @@ export default function BoardMgmtPanel({ initialCategory = '' }: BoardMgmtPanelP
key={p}
onClick={() => setPage(p)}
className={`w-7 h-7 text-xs rounded ${
p === page ? 'bg-blue-500/20 text-blue-400 font-medium' : 'text-text-3 hover:bg-bg-2'
p === page ? 'bg-blue-500/20 text-blue-400 font-medium' : 'text-fg-disabled hover:bg-bg-elevated'
}`}
>
{p}
@ -241,7 +241,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-text-3 hover:bg-bg-2 disabled:opacity-30"
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
>
&gt;
</button>
@ -260,7 +260,7 @@ interface PostRowProps {
function PostRow({ post, checked, onToggle }: PostRowProps) {
return (
<tr className="border-b border-border-1 hover:bg-bg-1/50 transition-colors">
<tr className="border-b border-stroke-1 hover:bg-bg-surface/50 transition-colors">
<td className="py-2 text-center">
<input
type="checkbox"
@ -269,7 +269,7 @@ function PostRow({ post, checked, onToggle }: PostRowProps) {
className="accent-blue-500"
/>
</td>
<td className="py-2 text-center text-text-3">{post.sn}</td>
<td className="py-2 text-center text-fg-disabled">{post.sn}</td>
<td className="py-2 text-center">
<span className={`inline-block px-2 py-0.5 rounded-full text-[10px] font-medium ${
post.categoryCd === 'NOTICE' ? 'bg-red-500/15 text-red-400' :
@ -279,15 +279,15 @@ function PostRow({ post, checked, onToggle }: PostRowProps) {
{CATEGORY_LABELS[post.categoryCd] ?? post.categoryCd}
</span>
</td>
<td className="py-2 pl-3 text-text-1 truncate max-w-[300px]">
<td className="py-2 pl-3 text-fg truncate max-w-[300px]">
{post.pinnedYn === 'Y' && (
<span className="text-[10px] text-orange-400 mr-1">[]</span>
)}
{post.title}
</td>
<td className="py-2 text-center text-text-2">{post.authorName}</td>
<td className="py-2 text-center text-text-3">{post.viewCnt}</td>
<td className="py-2 text-center text-text-3">{formatDate(post.regDtm)}</td>
<td className="py-2 text-center text-fg-sub">{post.authorName}</td>
<td className="py-2 text-center text-fg-disabled">{post.viewCnt}</td>
<td className="py-2 text-center text-fg-disabled">{formatDate(post.regDtm)}</td>
</tr>
);
}

파일 보기

@ -78,16 +78,16 @@ function CleanupEquipPanel() {
return (
<div className="flex flex-col h-full">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div>
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> {filtered.length} </p>
<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>
</div>
<div className="flex items-center gap-3">
<select
value={regionFilter}
onChange={handleFilterChange(setRegionFilter)}
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value="전체"> </option>
<option value="남해"></option>
@ -99,7 +99,7 @@ function CleanupEquipPanel() {
<select
value={typeFilter}
onChange={handleFilterChange(setTypeFilter)}
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value="전체"> </option>
{typeOptions.map(t => (
@ -109,7 +109,7 @@ function CleanupEquipPanel() {
<select
value={equipFilter}
onChange={handleFilterChange(setEquipFilter)}
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value="전체"> </option>
<option value="방제선"></option>
@ -123,11 +123,11 @@ function CleanupEquipPanel() {
placeholder="기관명, 주소 검색..."
value={searchTerm}
onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }}
className="w-56 px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
<button
onClick={load}
className="px-4 py-2 text-xs font-semibold rounded-md bg-bg-2 border border-border text-text-2 hover:border-primary-cyan hover:text-primary-cyan transition-all font-korean"
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"
>
</button>
@ -137,36 +137,36 @@ function CleanupEquipPanel() {
{/* 테이블 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-32 text-text-3 text-sm font-korean">
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
...
</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-border bg-bg-1">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '방제선' ? 'text-primary-cyan bg-primary-cyan/5' : 'text-text-3'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '유회수기' ? 'text-primary-cyan bg-primary-cyan/5' : 'text-text-3'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '이송펌프' ? 'text-primary-cyan bg-primary-cyan/5' : 'text-text-3'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '방제차량' ? 'text-primary-cyan bg-primary-cyan/5' : 'text-text-3'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '살포장치' ? 'text-primary-cyan bg-primary-cyan/5' : 'text-text-3'}`}></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean"></th>
<tr className="border-b border-stroke bg-bg-surface">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '방제선' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '유회수기' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '이송펌프' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '방제차량' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}></th>
<th className={`px-4 py-3 text-center text-[11px] font-semibold font-korean ${equipFilter === '살포장치' ? 'text-color-accent bg-color-accent/5' : 'text-fg-disabled'}`}></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean"></th>
</tr>
</thead>
<tbody>
{paged.length === 0 ? (
<tr>
<td colSpan={11} className="px-6 py-10 text-center text-xs text-text-3 font-korean">
<td colSpan={11} className="px-6 py-10 text-center text-xs text-fg-disabled font-korean">
.
</td>
</tr>
) : paged.map((org, idx) => (
<tr key={org.id} className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<td className="px-4 py-3 text-[11px] text-text-3 font-mono text-center">
<tr key={org.id} className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono text-center">
{(safePage - 1) * PAGE_SIZE + idx + 1}
</td>
<td className="px-4 py-3">
@ -174,31 +174,31 @@ function CleanupEquipPanel() {
{org.type}
</span>
</td>
<td className="px-4 py-3 text-[11px] text-text-2 font-korean">
<td className="px-4 py-3 text-[11px] text-fg-sub font-korean">
{regionShort(org.jurisdiction)}
</td>
<td className="px-4 py-3 text-[11px] text-text-1 font-korean font-semibold">
<td className="px-4 py-3 text-[11px] text-fg font-korean font-semibold">
{org.name}
</td>
<td className="px-4 py-3 text-[11px] text-text-3 font-korean max-w-[200px] truncate">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-korean max-w-[200px] truncate">
{org.address}
</td>
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '방제선' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : 'text-text-2'}`}>
{org.vessel > 0 ? org.vessel : <span className="text-text-3"></span>}
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '방제선' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}>
{org.vessel > 0 ? org.vessel : <span className="text-fg-disabled"></span>}
</td>
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '유회수기' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : 'text-text-2'}`}>
{org.skimmer > 0 ? org.skimmer : <span className="text-text-3"></span>}
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '유회수기' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}>
{org.skimmer > 0 ? org.skimmer : <span className="text-fg-disabled"></span>}
</td>
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '이송펌프' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : 'text-text-2'}`}>
{org.pump > 0 ? org.pump : <span className="text-text-3"></span>}
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '이송펌프' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}>
{org.pump > 0 ? org.pump : <span className="text-fg-disabled"></span>}
</td>
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '방제차량' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : 'text-text-2'}`}>
{org.vehicle > 0 ? org.vehicle : <span className="text-text-3"></span>}
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '방제차량' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}>
{org.vehicle > 0 ? org.vehicle : <span className="text-fg-disabled"></span>}
</td>
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '살포장치' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : 'text-text-2'}`}>
{org.sprayer > 0 ? org.sprayer : <span className="text-text-3"></span>}
<td className={`px-4 py-3 text-[11px] font-mono text-center ${equipFilter === '살포장치' ? 'text-color-accent font-semibold bg-color-accent/5' : 'text-fg-sub'}`}>
{org.sprayer > 0 ? org.sprayer : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center font-bold text-primary-cyan">
<td className="px-4 py-3 text-[11px] font-mono text-center font-bold text-color-accent">
{org.totalAssets.toLocaleString()}
</td>
</tr>
@ -210,8 +210,8 @@ function CleanupEquipPanel() {
{/* 합계 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center gap-4 px-6 py-2 border-t border-border bg-bg-0/80">
<span className="text-[10px] text-text-3 font-korean font-semibold mr-auto">
<div className="flex items-center gap-4 px-6 py-2 border-t border-stroke bg-bg-base/80">
<span className="text-[10px] text-fg-disabled font-korean font-semibold mr-auto">
({filtered.length} )
</span>
{[
@ -224,9 +224,9 @@ function CleanupEquipPanel() {
].map((t) => {
const isActive = t.label === equipFilter || t.label === '총자산';
return (
<div key={t.label} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${isActive ? 'bg-primary-cyan/10' : ''}`}>
<span className={`text-[9px] font-korean ${isActive ? 'text-primary-cyan' : 'text-text-3'}`}>{t.label}</span>
<span className={`text-[10px] font-mono font-bold ${isActive ? 'text-primary-cyan' : 'text-text-1'}`}>
<div key={t.label} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${isActive ? 'bg-color-accent/10' : ''}`}>
<span className={`text-[9px] font-korean ${isActive ? 'text-color-accent' : 'text-fg-disabled'}`}>{t.label}</span>
<span className={`text-[10px] font-mono font-bold ${isActive ? 'text-color-accent' : 'text-fg'}`}>
{t.value.toLocaleString()}{t.unit}
</span>
</div>
@ -237,15 +237,15 @@ function CleanupEquipPanel() {
{/* 페이지네이션 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-border">
<span className="text-[11px] text-text-3 font-korean">
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke">
<span className="text-[11px] text-fg-disabled font-korean">
{(safePage - 1) * PAGE_SIZE + 1}{Math.min(safePage * PAGE_SIZE, filtered.length)} / {filtered.length}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={safePage === 1}
className="px-2.5 py-1 text-[11px] border border-border rounded text-text-2 hover:border-primary-cyan hover:text-primary-cyan disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&lt;
</button>
@ -255,7 +255,7 @@ function CleanupEquipPanel() {
onClick={() => setCurrentPage(p)}
className="px-2.5 py-1 text-[11px] border rounded transition-colors"
style={p === safePage
? { borderColor: 'var(--cyan)', color: 'var(--cyan)', background: 'rgba(6,182,212,0.1)' }
? { borderColor: 'var(--color-accent)', color: 'var(--color-accent)', background: 'rgba(6,182,212,0.1)' }
: { borderColor: 'var(--border)', color: 'var(--text-2)' }
}
>
@ -265,7 +265,7 @@ function CleanupEquipPanel() {
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={safePage === totalPages}
className="px-2.5 py-1 text-[11px] border border-border rounded text-text-2 hover:border-primary-cyan hover:text-primary-cyan disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&gt;
</button>

파일 보기

@ -179,7 +179,7 @@ function fetchHrCollectData(): Promise<HrCollectItem[]> {
function getCollectStatus(item: HrCollectItem): { label: string; color: string } {
if (item.activeYn !== 'Y') {
return { label: '비활성', color: 'text-t3 bg-bg-2' };
return { label: '비활성', color: 'text-t3 bg-bg-elevated' };
}
if (item.etaClctList.length > 0) {
return { label: '완료', color: 'text-emerald-400 bg-emerald-500/10' };
@ -206,19 +206,19 @@ function HrTable({ rows, loading }: { rows: HrCollectItem[]; loading: boolean })
<div className="overflow-auto">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="bg-bg-2 text-t3 uppercase tracking-wide">
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
{HEADERS.map((h) => (
<th key={h} className="px-3 py-2 text-left font-medium border-b border-border-1 whitespace-nowrap">{h}</th>
<th key={h} className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
{loading && rows.length === 0
? Array.from({ length: 5 }).map((_, i) => (
<tr key={i} className="border-b border-border-1 animate-pulse">
<tr key={i} className="border-b border-stroke-1 animate-pulse">
{HEADERS.map((_, j) => (
<td key={j} className="px-3 py-2">
<div className="h-3 bg-bg-2 rounded w-14" />
<div className="h-3 bg-bg-elevated rounded w-14" />
</td>
))}
</tr>
@ -226,7 +226,7 @@ function HrTable({ rows, loading }: { rows: HrCollectItem[]; loading: boolean })
: rows.map((row, idx) => {
const status = getCollectStatus(row);
return (
<tr key={`${row.seq}`} className="border-b border-border-1 hover:bg-bg-1/50">
<tr key={`${row.seq}`} className="border-b border-stroke-1 hover:bg-bg-surface/50">
<td className="px-3 py-2 text-t2 text-center">{idx + 1}</td>
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.clctName}</td>
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.depth2}</td>
@ -237,7 +237,7 @@ function HrTable({ rows, loading }: { rows: HrCollectItem[]; loading: boolean })
<td className="px-3 py-2 text-t2 font-mono">{row.jobName}</td>
<td className="px-3 py-2 text-center">
<span className={`inline-block px-1.5 py-0.5 rounded text-[11px] font-medium ${
row.activeYn === 'Y' ? 'text-emerald-400 bg-emerald-500/10' : 'text-t3 bg-bg-2'
row.activeYn === 'Y' ? 'text-emerald-400 bg-emerald-500/10' : 'text-t3 bg-bg-elevated'
}`}>
{row.activeYn === 'Y' ? 'Y' : 'N'}
</span>
@ -287,7 +287,7 @@ export default function CollectHrPanel() {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="flex items-center justify-between px-5 py-3 border-b border-border-1 shrink-0">
<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>
<div className="flex items-center gap-3">
{lastUpdate && (
@ -298,7 +298,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-2 hover:bg-bg-3 text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
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"
>
<svg
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
@ -314,7 +314,7 @@ export default function CollectHrPanel() {
</div>
{/* 상태 표시줄 */}
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-border-1 bg-bg-0">
<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="w-1.5 h-1.5 rounded-full bg-emerald-400" />
{completedCount}

파일 보기

@ -143,14 +143,14 @@ const DispersingZonePanel = () => {
const isExpanded = expandedZone === zone;
return (
<div key={zone} className="border border-border rounded-lg overflow-hidden">
<div key={zone} className="border border-stroke rounded-lg overflow-hidden">
{/* 카드 헤더 */}
<div
className="flex items-center gap-2 px-3 py-2.5 cursor-pointer hover:bg-[rgba(255,255,255,0.03)] transition-colors"
onClick={() => handleToggleExpand(zone)}
>
<span className={`w-3 h-3 rounded-sm shrink-0 ${swatchColor}`} />
<span className="flex-1 text-xs font-semibold text-text-1 font-korean">{info.label}</span>
<span className="flex-1 text-xs font-semibold text-fg font-korean">{info.label}</span>
{/* 토글 스위치 */}
<button
onClick={e => {
@ -160,8 +160,8 @@ const DispersingZonePanel = () => {
title={showLayer ? '레이어 숨기기' : '레이어 표시'}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 ${
showLayer
? 'bg-primary-cyan'
: 'bg-[rgba(255,255,255,0.08)] border border-border'
? 'bg-color-accent'
: 'bg-[rgba(255,255,255,0.08)] border border-stroke'
}`}
>
<span
@ -171,20 +171,20 @@ const DispersingZonePanel = () => {
/>
</button>
{/* 펼침 화살표 */}
<span className="text-text-3 text-[10px] shrink-0">{isExpanded ? '▲' : '▼'}</span>
<span className="text-fg-disabled text-[10px] shrink-0">{isExpanded ? '▲' : '▼'}</span>
</div>
{/* 펼침 영역 */}
{isExpanded && (
<div className="border-t border-border px-3 py-3">
<div className="border-t border-stroke px-3 py-3">
<table className="w-full">
<tbody>
{info.rows.map(row => (
<tr key={row.key} className="border-b border-border last:border-0">
<td className="py-2 pr-2 text-[11px] text-text-3 font-korean whitespace-nowrap align-top w-24">
<tr key={row.key} className="border-b border-stroke last:border-0">
<td className="py-2 pr-2 text-[11px] text-fg-disabled font-korean whitespace-nowrap align-top w-24">
{row.key}
</td>
<td className="py-2 text-[11px] text-text-2 font-korean leading-relaxed">
<td className="py-2 text-[11px] text-fg-sub font-korean leading-relaxed">
{row.value}
</td>
</tr>
@ -214,24 +214,24 @@ const DispersingZonePanel = () => {
</Map>
{/* 범례 */}
<div className="absolute bottom-4 left-4 bg-bg-1 border border-border rounded-lg px-3 py-2 flex flex-col gap-1.5">
<div className="absolute bottom-4 left-4 bg-bg-surface border border-stroke rounded-lg px-3 py-2 flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-sm bg-blue-500 opacity-80" />
<span className="text-[11px] text-text-2 font-korean"></span>
<span className="text-[11px] text-fg-sub font-korean"></span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-sm bg-red-500 opacity-80" />
<span className="text-[11px] text-text-2 font-korean"></span>
<span className="text-[11px] text-fg-sub font-korean"></span>
</div>
</div>
</div>
{/* 우측 패널 */}
<div className="w-[280px] bg-bg-1 border-l border-border flex flex-col overflow-hidden shrink-0">
<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-border shrink-0">
<h1 className="text-sm font-bold text-text-1 font-korean"> </h1>
<p className="text-[11px] text-text-3 mt-0.5 font-korean"> </p>
<div className="px-4 py-4 border-b border-stroke shrink-0">
<h1 className="text-sm font-bold text-fg font-korean"> </h1>
<p className="text-[11px] text-fg-disabled mt-0.5 font-korean"> </p>
</div>
{/* 구역 카드 목록 */}

파일 보기

@ -166,18 +166,18 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
}
};
const inputCls = 'w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none';
const labelCls = 'block text-[11px] font-semibold text-text-2 font-korean mb-1.5';
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';
const labelCls = 'block text-[11px] font-semibold text-fg-sub font-korean mb-1.5';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-bg-1 border border-border rounded-lg shadow-lg w-[480px] max-h-[90vh] flex flex-col">
<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-border shrink-0">
<h2 className="text-sm font-bold text-text-1 font-korean">
<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">
{mode === 'create' ? '레이어 등록' : '레이어 수정'}
</h2>
<button onClick={onClose} className="text-text-3 hover:text-text-1 transition-colors"></button>
<button onClick={onClose} className="text-fg-disabled hover:text-fg transition-colors"></button>
</div>
{/* 폼 */}
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto">
@ -188,7 +188,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
<select
value={form.upLayerCd}
onChange={e => mode === 'create' ? handleParentChange(e.target.value) : handleField('upLayerCd', e.target.value)}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none"
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"
>
<option value="">()</option>
{options
@ -210,7 +210,7 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
onChange={e => handleField('layerCd', e.target.value)}
readOnly={mode === 'edit'}
placeholder="예: LAYER_001"
className={`${inputCls} font-mono${mode === 'edit' ? ' bg-bg-0 text-text-3 cursor-not-allowed' : ''}`}
className={`${inputCls} font-mono${mode === 'edit' ? ' bg-bg-base text-fg-disabled cursor-not-allowed' : ''}`}
/>
</div>
{/* 레이어명 */}
@ -276,7 +276,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-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none"
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"
>
<option value="Y"></option>
<option value="N"></option>
@ -290,18 +290,18 @@ const LayerFormModal = ({ mode, initialData, onClose, onSaved }: LayerFormModalP
</div>
)}
{/* 버튼 */}
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-border shrink-0">
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-stroke shrink-0">
<button
type="button"
onClick={onClose}
className="px-3 py-1.5 text-xs border border-border text-text-3 rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
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"
>
</button>
<button
type="submit"
disabled={saving}
className="px-3 py-1.5 text-xs bg-primary-cyan text-bg-0 rounded hover:opacity-90 disabled:opacity-50 transition-all font-korean"
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"
>
{saving ? '저장 중...' : mode === 'create' ? '등록' : '저장'}
</button>
@ -408,15 +408,15 @@ const LayerPanel = () => {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-border shrink-0">
<div className="px-6 py-4 border-b border-stroke shrink-0">
<div className="flex items-center justify-between mb-3">
<div>
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> {total}</p>
<h1 className="text-lg font-bold text-fg font-korean"> </h1>
<p className="text-xs 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-primary-cyan text-bg-0 rounded hover:opacity-90 transition-opacity font-korean"
className="px-3 py-1.5 text-xs font-semibold bg-color-accent text-bg-0 rounded hover:opacity-90 transition-opacity font-korean"
>
</button>
@ -428,12 +428,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-2 border border-border rounded text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
<select
value={filterUseYn}
onChange={e => setFilterUseYn(e.target.value)}
className="px-2 py-1.5 text-xs bg-bg-2 border border-border rounded text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value=""></option>
<option value="Y"></option>
@ -441,7 +441,7 @@ const LayerPanel = () => {
</select>
<button
onClick={handleSearch}
className="px-3 py-1.5 text-xs border border-border text-text-2 rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
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"
>
</button>
@ -450,7 +450,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-border shrink-0 font-korean">
<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">
{error}
</div>
)}
@ -458,29 +458,29 @@ const LayerPanel = () => {
{/* 테이블 영역 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-full text-text-3 text-sm font-korean">
<div className="flex items-center justify-center h-full text-fg-disabled text-sm font-korean">
...
</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-border bg-bg-1 sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-mono"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean w-12 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-mono">WMS레이어명</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean w-16"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-28"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean w-20"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean w-28 whitespace-nowrap"></th>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-12 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">WMS레이어명</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-16"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-28"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-20"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-28 whitespace-nowrap"></th>
</tr>
</thead>
<tbody>
{items.length === 0 ? (
<tr>
<td colSpan={10} className="px-4 py-12 text-center text-text-3 text-sm font-korean">
<td colSpan={10} className="px-4 py-12 text-center text-fg-disabled text-sm font-korean">
.
</td>
</tr>
@ -488,42 +488,42 @@ const LayerPanel = () => {
items.map((item, idx) => (
<tr
key={item.layerCd}
className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors"
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
>
{/* 번호 */}
<td className="px-4 py-3 text-xs text-text-3 font-mono">
<td className="px-4 py-3 text-xs text-fg-disabled font-mono">
{(page - 1) * PAGE_SIZE + idx + 1}
</td>
{/* 레이어코드 */}
<td className="px-4 py-3 text-[11px] text-text-2 font-mono">
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">
{item.layerCd}
</td>
{/* 레이어명 */}
<td className="px-4 py-3 text-xs text-text-1 font-korean">
<td className="px-4 py-3 text-xs text-fg font-korean">
{item.layerNm}
</td>
{/* 레이어전체명 */}
<td className="px-4 py-3 text-xs text-text-2 font-korean max-w-[200px]">
<td className="px-4 py-3 text-xs text-fg-sub font-korean max-w-[200px]">
<span className="block truncate" title={item.layerFullNm}>
{item.layerFullNm}
</span>
</td>
{/* 레벨 */}
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-semibold bg-[rgba(6,182,212,0.1)] text-primary-cyan border border-[rgba(6,182,212,0.3)]">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-semibold bg-[rgba(6,182,212,0.1)] text-color-accent border border-[rgba(6,182,212,0.3)]">
{item.layerLevel}
</span>
</td>
{/* WMS레이어명 */}
<td className="px-4 py-3 text-[11px] text-text-2 font-mono">
{item.wmsLayerNm ?? <span className="text-text-3">-</span>}
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">
{item.wmsLayerNm ?? <span className="text-fg-disabled">-</span>}
</td>
{/* 정렬순서 */}
<td className="px-4 py-3 text-xs text-text-3 text-center font-mono">
<td className="px-4 py-3 text-xs text-fg-disabled text-center font-mono">
{item.sortOrd}
</td>
{/* 등록일시 */}
<td className="px-4 py-3 text-[11px] text-text-3 font-mono">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono">
{item.regDtm ?? '-'}
</td>
{/* 사용여부 토글 */}
@ -540,10 +540,10 @@ const LayerPanel = () => {
}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:opacity-40 ${
item.useYn === 'Y' && item.parentUseYn !== 'N'
? 'bg-primary-cyan'
? 'bg-color-accent'
: item.useYn === 'Y' && item.parentUseYn === 'N'
? 'bg-primary-cyan/40'
: 'bg-[rgba(255,255,255,0.08)] border border-border'
? 'bg-color-accent/40'
: 'bg-[rgba(255,255,255,0.08)] border border-stroke'
}`}
>
<span
@ -558,7 +558,7 @@ 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-primary-cyan hover:bg-[rgba(6,182,212,0.25)] font-korean whitespace-nowrap"
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"
>
</button>
@ -580,29 +580,29 @@ const LayerPanel = () => {
{/* 페이지네이션 */}
{!loading && totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-border bg-bg-1 shrink-0">
<span className="text-[11px] text-text-3 font-korean">
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface shrink-0">
<span className="text-[11px] text-fg-disabled font-korean">
{(page - 1) * PAGE_SIZE + 1}{Math.min(page * PAGE_SIZE, total)} / {total}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="px-2.5 py-1 text-[11px] border border-border text-text-3 rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
{buildPageButtons().map((btn, i) =>
btn === 'ellipsis' ? (
<span key={`e${i}`} className="px-1.5 text-[11px] text-text-3"></span>
<span key={`e${i}`} className="px-1.5 text-[11px] text-fg-disabled"></span>
) : (
<button
key={btn}
onClick={() => setPage(btn)}
className={`px-2.5 py-1 text-[11px] rounded transition-all ${
page === btn
? 'bg-primary-cyan text-bg-0 font-semibold shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'border border-border text-text-3 hover:bg-[rgba(255,255,255,0.04)]'
? 'bg-color-accent text-bg-0 font-semibold shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'border border-stroke text-fg-disabled hover:bg-[rgba(255,255,255,0.04)]'
}`}
>
{btn}
@ -612,7 +612,7 @@ const LayerPanel = () => {
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-2.5 py-1 text-[11px] border border-border text-text-3 rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>

파일 보기

@ -75,15 +75,15 @@ function MapBaseModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-bg-1 border border-border rounded-lg shadow-lg w-[520px] max-h-[90vh] flex flex-col">
<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-border">
<h2 className="text-sm font-bold text-text-1 font-korean">
<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">
{isEdit ? '지도 수정' : '지도 등록'}
</h2>
<button
onClick={onClose}
className="text-text-3 hover:text-text-1 transition-colors"
className="text-fg-disabled hover:text-fg transition-colors"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M18 6L6 18M6 6l12 12" />
@ -96,7 +96,7 @@ function MapBaseModal({
<div className="px-6 py-4 space-y-4">
{/* 지도 이름 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -104,13 +104,13 @@ function MapBaseModal({
value={form.mapNm}
onChange={e => setField('mapNm', e.target.value)}
placeholder="지도 이름을 입력하세요"
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
</div>
{/* 지도 키 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -119,19 +119,19 @@ function MapBaseModal({
onChange={e => setField('mapKey', e.target.value)}
placeholder="고유 식별 키 (영문/숫자)"
disabled={isEdit}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono disabled:opacity-50 disabled:cursor-not-allowed"
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"
/>
</div>
{/* 지도 레벨 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<select
value={form.mapLevelCd}
onChange={e => setField('mapLevelCd', e.target.value)}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value=""></option>
{MAP_LEVEL_OPTIONS.map(opt => (
@ -142,7 +142,7 @@ function MapBaseModal({
{/* 파일 소스 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<input
@ -150,13 +150,13 @@ 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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
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"
/>
</div>
{/* 상세 설명 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<textarea
@ -164,13 +164,13 @@ function MapBaseModal({
value={form.mapDc}
onChange={e => setField('mapDc', e.target.value)}
placeholder="지도에 대한 설명을 입력하세요"
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean resize-none"
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"
/>
</div>
{/* 사용여부 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<div className="flex items-center gap-3">
@ -178,7 +178,7 @@ function MapBaseModal({
type="button"
onClick={() => setField('useYn', form.useYn === 'Y' ? 'N' : 'Y')}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
form.useYn === 'Y' ? 'bg-primary-cyan' : 'bg-border'
form.useYn === 'Y' ? 'bg-color-accent' : 'bg-border'
}`}
>
<span
@ -187,7 +187,7 @@ function MapBaseModal({
}`}
/>
</button>
<span className="text-xs text-text-2 font-korean">
<span className="text-xs text-fg-sub font-korean">
{form.useYn === 'Y' ? '사용' : '미사용'}
</span>
</div>
@ -200,18 +200,18 @@ function MapBaseModal({
</div>
{/* 모달 푸터 */}
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-border">
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-stroke">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-xs border border-border text-text-2 rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
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"
>
</button>
<button
type="submit"
disabled={saving}
className="px-4 py-2 text-xs font-semibold rounded-md bg-primary-cyan 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-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"
>
{saving ? '저장 중...' : isEdit ? '수정' : '등록'}
</button>
@ -339,16 +339,16 @@ function MapBasePanel() {
};
return (
<div className="flex flex-col h-full overflow-hidden bg-bg-0">
<div className="flex flex-col h-full overflow-hidden bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div>
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> {total}</p>
<h1 className="text-lg font-bold text-fg font-korean"> </h1>
<p className="text-xs 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-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
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"
>
+
</button>
@ -370,8 +370,8 @@ function MapBasePanel() {
{/* 테이블 영역 */}
<div className="flex-1 overflow-auto">
<table className="w-full text-xs">
<thead className="sticky top-0 bg-bg-1 z-10">
<tr className="border-b border-border text-text-3">
<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>
<th className="py-3 text-left pl-4"> </th>
<th className="w-20 py-3 text-center"> </th>
@ -385,25 +385,25 @@ function MapBasePanel() {
<tbody>
{loading ? (
<tr>
<td colSpan={8} className="py-8 text-center text-text-3 font-korean">
<td colSpan={8} className="py-8 text-center text-fg-disabled font-korean">
...
</td>
</tr>
) : items.map((item, idx) => (
<tr
key={item.mapSn}
className="border-b border-border hover:bg-bg-1/50 transition-colors"
className="border-b border-stroke hover:bg-bg-surface/50 transition-colors"
>
<td className="py-3 text-center text-text-3">
<td className="py-3 text-center text-fg-disabled">
{(page - 1) * 10 + idx + 1}
</td>
<td className="py-3 pl-4">
<span className="text-text-1 font-korean">{item.mapNm}</span>
<span className="ml-2 text-[10px] text-text-3 font-mono">{item.mapKey}</span>
<span className="text-fg font-korean">{item.mapNm}</span>
<span className="ml-2 text-[10px] text-fg-disabled font-mono">{item.mapKey}</span>
</td>
<td className="py-3 text-center text-text-2">{item.mapLevelCd ?? '-'}</td>
<td className="py-3 text-center text-text-2 font-korean">{item.regNm ?? '-'}</td>
<td className="py-3 text-center text-text-3">{item.regDtm ?? '-'}</td>
<td className="py-3 text-center text-fg-sub">{item.mapLevelCd ?? '-'}</td>
<td className="py-3 text-center text-fg-sub font-korean">{item.regNm ?? '-'}</td>
<td className="py-3 text-center text-fg-disabled">{item.regDtm ?? '-'}</td>
<td
className="py-3 text-center cursor-pointer"
onClick={() => handleToggleUse(item)}
@ -412,7 +412,7 @@ function MapBasePanel() {
<button
type="button"
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
item.useYn === 'Y' ? 'bg-primary-cyan' : 'bg-border'
item.useYn === 'Y' ? 'bg-color-accent' : 'bg-border'
}`}
>
<span
@ -426,7 +426,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-primary-cyan hover:bg-[rgba(6,182,212,0.25)]"
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)]"
>
</button>
@ -444,7 +444,7 @@ function MapBasePanel() {
</tbody>
</table>
{!loading && items.length === 0 && (
<div className="flex items-center justify-center h-32 text-xs text-text-3 font-korean">
<div className="flex items-center justify-center h-32 text-xs text-fg-disabled font-korean">
.
</div>
)}
@ -452,11 +452,11 @@ function MapBasePanel() {
{/* 페이지네이션 */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-1 py-2 border-t border-border">
<div className="flex items-center justify-center gap-1 py-2 border-t border-stroke">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page <= 1}
className="px-2 py-1 text-xs rounded text-text-3 hover:bg-bg-2 disabled:opacity-30"
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
>
&lt;
</button>
@ -469,7 +469,7 @@ function MapBasePanel() {
key={p}
onClick={() => setPage(p)}
className={`w-7 h-7 text-xs rounded ${
p === page ? 'bg-blue-500/20 text-blue-400 font-medium' : 'text-text-3 hover:bg-bg-2'
p === page ? 'bg-blue-500/20 text-blue-400 font-medium' : 'text-fg-disabled hover:bg-bg-elevated'
}`}
>
{p}
@ -479,7 +479,7 @@ function MapBasePanel() {
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
className="px-2 py-1 text-xs rounded text-text-3 hover:bg-bg-2 disabled:opacity-30"
className="px-2 py-1 text-xs rounded text-fg-disabled hover:bg-bg-elevated disabled:opacity-30"
>
&gt;
</button>

파일 보기

@ -124,7 +124,7 @@ function MenusPanel() {
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-text-3 text-sm font-korean"> ...</div>
<div className="text-fg-disabled text-sm font-korean"> ...</div>
</div>
)
}
@ -133,18 +133,18 @@ function MenusPanel() {
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div>
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> , , , </p>
<h1 className="text-lg font-bold text-fg font-korean"> </h1>
<p className="text-xs 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 ${
hasChanges && !saving
? 'bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
: 'bg-bg-3 text-text-3 cursor-not-allowed'
? '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'
}`}
>
{saving ? '저장 중...' : '변경사항 저장'}
@ -182,10 +182,10 @@ function MenusPanel() {
</SortableContext>
<DragOverlay>
{activeMenu ? (
<div className="flex items-center gap-3 px-4 py-3 rounded-md border border-primary-cyan bg-bg-1 shadow-lg opacity-90 max-w-[700px]">
<span className="text-text-3 text-xs"></span>
<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-[16px]">{activeMenu.icon}</span>
<span className="text-[13px] font-semibold text-text-1 font-korean">{activeMenu.label}</span>
<span className="text-[13px] font-semibold text-fg font-korean">{activeMenu.label}</span>
</div>
) : null}
</DragOverlay>

파일 보기

@ -79,7 +79,7 @@ function StatusBadge({
}) {
if (loading) {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-2 text-t2">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-elevated text-t2">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
...
</span>
@ -129,11 +129,11 @@ function ForecastTable({
<div className="overflow-auto">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="bg-bg-2 text-t3 uppercase tracking-wide">
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
{TABLE_HEADERS.map((h) => (
<th
key={h}
className="px-3 py-2 text-left font-medium border-b border-border-1 whitespace-nowrap"
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
>
{h}
</th>
@ -143,16 +143,16 @@ function ForecastTable({
<tbody>
{loading && rows.length === 0
? Array.from({ length: 6 }).map((_, i) => (
<tr key={i} className="border-b border-border-1 animate-pulse">
<tr key={i} className="border-b border-stroke-1 animate-pulse">
{TABLE_HEADERS.map((_, j) => (
<td key={j} className="px-3 py-2">
<div className="h-3 bg-bg-2 rounded w-16" />
<div className="h-3 bg-bg-elevated rounded w-16" />
</td>
))}
</tr>
))
: rows.map((row) => (
<tr key={row.jobName} className="border-b border-border-1 hover:bg-bg-1/50">
<tr key={row.jobName} className="border-b border-stroke-1 hover:bg-bg-surface/50">
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">
{row.modelName}
</td>
@ -203,7 +203,7 @@ export default function MonitorForecastPanel() {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="flex items-center justify-between px-5 py-3 border-b border-border-1 shrink-0">
<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>
<div className="flex items-center gap-3">
{lastUpdate && (
@ -219,7 +219,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-2 hover:bg-bg-3 text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
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"
>
<svg
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
@ -240,7 +240,7 @@ export default function MonitorForecastPanel() {
</div>
{/* 탭 */}
<div className="flex gap-0 border-b border-border-1 shrink-0 px-5">
<div className="flex gap-0 border-b border-stroke-1 shrink-0 px-5">
{TABS.map((tab) => (
<button
key={tab.id}
@ -257,7 +257,7 @@ export default function MonitorForecastPanel() {
</div>
{/* 상태 표시줄 */}
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-border-1 bg-bg-0">
<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>

파일 보기

@ -76,7 +76,7 @@ const fmt = (v: number | null | undefined, digits = 1): string =>
function StatusBadge({ loading, errorCount, total }: { loading: boolean; errorCount: number; total: number }) {
if (loading) {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-2 text-t2">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-elevated text-t2">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
...
</span>
@ -113,25 +113,25 @@ function KhoaTable({ rows, loading }: { rows: KhoaRow[]; loading: boolean }) {
<div className="overflow-auto">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="bg-bg-2 text-t3 uppercase tracking-wide">
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
{headers.map((h) => (
<th key={h} className="px-3 py-2 text-left font-medium border-b border-border-1 whitespace-nowrap">{h}</th>
<th key={h} className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
{loading && rows.length === 0
? Array.from({ length: 5 }).map((_, i) => (
<tr key={i} className="border-b border-border-1 animate-pulse">
<tr key={i} className="border-b border-stroke-1 animate-pulse">
{headers.map((_, j) => (
<td key={j} className="px-3 py-2">
<div className="h-3 bg-bg-2 rounded w-12" />
<div className="h-3 bg-bg-elevated rounded w-12" />
</td>
))}
</tr>
))
: rows.map((row) => (
<tr key={row.stationName} className="border-b border-border-1 hover:bg-bg-1/50">
<tr key={row.stationName} className="border-b border-stroke-1 hover:bg-bg-surface/50">
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.stationName}</td>
<td className="px-3 py-2 text-t2">{fmt(row.data?.water_temp)}</td>
<td className="px-3 py-2 text-t2">{fmt(row.data?.air_temp)}</td>
@ -165,25 +165,25 @@ function KmaUltraTable({ rows, loading }: { rows: KmaUltraRow[]; loading: boolea
<div className="overflow-auto">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="bg-bg-2 text-t3 uppercase tracking-wide">
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
{headers.map((h) => (
<th key={h} className="px-3 py-2 text-left font-medium border-b border-border-1 whitespace-nowrap">{h}</th>
<th key={h} className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
{loading && rows.length === 0
? Array.from({ length: 3 }).map((_, i) => (
<tr key={i} className="border-b border-border-1 animate-pulse">
<tr key={i} className="border-b border-stroke-1 animate-pulse">
{headers.map((_, j) => (
<td key={j} className="px-3 py-2">
<div className="h-3 bg-bg-2 rounded w-12" />
<div className="h-3 bg-bg-elevated rounded w-12" />
</td>
))}
</tr>
))
: rows.map((row) => (
<tr key={row.stationName} className="border-b border-border-1 hover:bg-bg-1/50">
<tr key={row.stationName} className="border-b border-stroke-1 hover:bg-bg-surface/50">
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.stationName}</td>
<td className="px-3 py-2 text-t2">{fmt(row.data?.temperature)}</td>
<td className="px-3 py-2 text-t2">{fmt(row.data?.windSpeed)}</td>
@ -215,25 +215,25 @@ function MarineTable({ rows, loading }: { rows: MarineRow[]; loading: boolean })
<div className="overflow-auto">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="bg-bg-2 text-t3 uppercase tracking-wide">
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
{headers.map((h) => (
<th key={h} className="px-3 py-2 text-left font-medium border-b border-border-1 whitespace-nowrap">{h}</th>
<th key={h} className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
{loading && rows.length === 0
? Array.from({ length: 4 }).map((_, i) => (
<tr key={i} className="border-b border-border-1 animate-pulse">
<tr key={i} className="border-b border-stroke-1 animate-pulse">
{headers.map((_, j) => (
<td key={j} className="px-3 py-2">
<div className="h-3 bg-bg-2 rounded w-14" />
<div className="h-3 bg-bg-elevated rounded w-14" />
</td>
))}
</tr>
))
: rows.map((row) => (
<tr key={row.regId} className="border-b border-border-1 hover:bg-bg-1/50">
<tr key={row.regId} className="border-b border-stroke-1 hover:bg-bg-surface/50">
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.name}</td>
<td className="px-3 py-2 text-t2">{fmt(row.data?.waveHeight)}</td>
<td className="px-3 py-2 text-t2">{fmt(row.data?.windSpeed)}</td>
@ -366,7 +366,7 @@ export default function MonitorRealtimePanel() {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="flex items-center justify-between px-5 py-3 border-b border-border-1 shrink-0">
<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>
<div className="flex items-center gap-3">
{lastUpdate && (
@ -377,7 +377,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-2 hover:bg-bg-3 text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
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"
>
<svg
className={`w-3.5 h-3.5 ${isLoading ? 'animate-spin' : ''}`}
@ -393,7 +393,7 @@ export default function MonitorRealtimePanel() {
</div>
{/* 탭 */}
<div className="flex gap-0 border-b border-border-1 shrink-0 px-5">
<div className="flex gap-0 border-b border-stroke-1 shrink-0 px-5">
{TABS.map((tab) => (
<button
key={tab.id}
@ -410,7 +410,7 @@ export default function MonitorRealtimePanel() {
</div>
{/* 상태 표시줄 */}
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-border-1 bg-bg-0">
<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">
{activeTab === 'khoa' && `관측소 ${totalCount}`}

파일 보기

@ -72,7 +72,7 @@ function fetchVesselMonitorData(): Promise<VesselMonitorRow[]> {
function StatusBadge({ loading, onCount, total }: { loading: boolean; onCount: number; total: number }) {
if (loading) {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-2 text-t2">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-bg-elevated text-t2">
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-pulse" />
...
</span>
@ -137,25 +137,25 @@ function VesselTable({ rows, loading }: { rows: VesselMonitorRow[]; loading: boo
<div className="overflow-auto">
<table className="w-full text-xs border-collapse">
<thead>
<tr className="bg-bg-2 text-t3 uppercase tracking-wide">
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
{HEADERS.map((h) => (
<th key={h} className="px-3 py-2 text-left font-medium border-b border-border-1 whitespace-nowrap">{h}</th>
<th key={h} className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap">{h}</th>
))}
</tr>
</thead>
<tbody>
{loading && rows.length === 0
? Array.from({ length: 8 }).map((_, i) => (
<tr key={i} className="border-b border-border-1 animate-pulse">
<tr key={i} className="border-b border-stroke-1 animate-pulse">
{HEADERS.map((_, j) => (
<td key={j} className="px-3 py-2">
<div className="h-3 bg-bg-2 rounded w-14" />
<div className="h-3 bg-bg-elevated rounded w-14" />
</td>
))}
</tr>
))
: rows.map((row, idx) => (
<tr key={`${row.institutionCode}-${row.systemName}`} className="border-b border-border-1 hover:bg-bg-1/50">
<tr key={`${row.institutionCode}-${row.systemName}`} className="border-b border-stroke-1 hover:bg-bg-surface/50">
<td className="px-3 py-2 text-t2 text-center">{idx + 1}</td>
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.institution}</td>
<td className="px-3 py-2 text-t2">{row.institutionCode}</td>
@ -203,7 +203,7 @@ export default function MonitorVesselPanel() {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="flex items-center justify-between px-5 py-3 border-b border-border-1 shrink-0">
<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>
<div className="flex items-center gap-3">
{lastUpdate && (
@ -214,7 +214,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-2 hover:bg-bg-3 text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
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"
>
<svg
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
@ -230,7 +230,7 @@ export default function MonitorVesselPanel() {
</div>
{/* 상태 표시줄 */}
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-border-1 bg-bg-0">
<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">
{rows.length} (ON: {onCount} / OFF: {rows.length - onCount})

파일 보기

@ -130,14 +130,14 @@ function PermCell({ state, onToggle, label, readOnly = false }: PermCellProps) {
switch (state) {
case 'explicit-granted':
classes = readOnly
? `${baseClasses} bg-[rgba(6,182,212,0.2)] border-primary-cyan text-primary-cyan cursor-default`
: `${baseClasses} bg-[rgba(6,182,212,0.2)] border-primary-cyan text-primary-cyan cursor-pointer hover:bg-[rgba(6,182,212,0.3)]`
? `${baseClasses} bg-[rgba(6,182,212,0.2)] border-color-accent text-color-accent cursor-default`
: `${baseClasses} bg-[rgba(6,182,212,0.2)] border-color-accent text-color-accent cursor-pointer hover:bg-[rgba(6,182,212,0.3)]`
icon = '✓'
break
case 'inherited-granted':
classes = readOnly
? `${baseClasses} bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.3)] text-[rgba(6,182,212,0.5)] cursor-default`
: `${baseClasses} bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.3)] text-[rgba(6,182,212,0.5)] cursor-pointer hover:border-primary-cyan`
: `${baseClasses} bg-[rgba(6,182,212,0.08)] border-[rgba(6,182,212,0.3)] text-[rgba(6,182,212,0.5)] cursor-pointer hover:border-color-accent`
icon = '✓'
break
case 'explicit-denied':
@ -147,7 +147,7 @@ function PermCell({ state, onToggle, label, readOnly = false }: PermCellProps) {
icon = '—'
break
case 'forced-denied':
classes = `${baseClasses} bg-bg-2 border-border text-text-3 opacity-40 cursor-not-allowed`
classes = `${baseClasses} bg-bg-elevated border-stroke text-fg-disabled opacity-40 cursor-not-allowed`
icon = '—'
break
}
@ -195,13 +195,13 @@ function TreeRow({ node, stateMap, expanded, onToggleExpand, onTogglePerm, readO
return (
<>
<tr className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<tr className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<td className="px-3 py-1">
<div className="flex items-center" style={{ paddingLeft: indent }}>
{hasChildren ? (
<button
onClick={() => onToggleExpand(node.code)}
className="w-4 h-4 flex items-center justify-center text-text-3 hover:text-text-1 transition-colors mr-1 flex-shrink-0"
className="w-4 h-4 flex items-center justify-center text-fg-disabled hover:text-fg transition-colors mr-1 flex-shrink-0"
>
<svg
width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
@ -211,13 +211,13 @@ function TreeRow({ node, stateMap, expanded, onToggleExpand, onTogglePerm, readO
</svg>
</button>
) : (
<span className="w-4 mr-1 flex-shrink-0 text-center text-text-3 text-[9px]">
<span className="w-4 mr-1 flex-shrink-0 text-center text-fg-disabled text-[9px]">
{node.level > 0 ? '├' : ''}
</span>
)}
{node.icon && <span className="mr-1 flex-shrink-0 text-[11px]">{node.icon}</span>}
<div className="min-w-0">
<div className={`text-[11px] font-korean truncate ${node.level === 0 ? 'font-bold text-text-1' : 'font-medium text-text-2'}`}>
<div className={`text-[11px] font-korean truncate ${node.level === 0 ? 'font-bold text-fg' : 'font-medium text-fg-sub'}`}>
{node.name}
</div>
</div>
@ -260,9 +260,9 @@ function TreeRow({ node, stateMap, expanded, onToggleExpand, onTogglePerm, readO
// ─── 공통 범례 컴포넌트 ──────────────────────────────
function PermLegend() {
return (
<div className="flex items-center gap-3 px-4 py-1.5 border-b border-border bg-bg-1 text-[9px] text-text-3 font-korean" style={{ flexShrink: 0 }}>
<div className="flex items-center gap-3 px-4 py-1.5 border-b border-stroke bg-bg-surface text-[9px] text-fg-disabled font-korean" style={{ flexShrink: 0 }}>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-3 rounded border bg-[rgba(6,182,212,0.2)] border-primary-cyan text-primary-cyan text-center text-[8px] leading-3"></span>
<span className="inline-block w-3 h-3 rounded border bg-[rgba(6,182,212,0.2)] border-color-accent text-color-accent text-center text-[8px] leading-3"></span>
</span>
<span className="flex items-center gap-1">
@ -274,10 +274,10 @@ function PermLegend() {
</span>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-3 rounded border bg-bg-2 border-border text-text-3 opacity-40 text-center text-[8px] leading-3"></span>
<span className="inline-block w-3 h-3 rounded border bg-bg-elevated border-stroke text-fg-disabled opacity-40 text-center text-[8px] leading-3"></span>
</span>
<span className="ml-2 border-l border-border pl-2 text-text-3">
<span className="ml-2 border-l border-stroke pl-2 text-fg-disabled">
R= C= U= D=
</span>
</div>
@ -364,10 +364,10 @@ function RolePermTab({
return (
<>
{/* 헤더 액션 버튼 */}
<div className="flex items-center gap-2 px-4 py-2 border-b border-border" style={{ flexShrink: 0 }}>
<div className="flex items-center gap-2 px-4 py-2 border-b border-stroke" style={{ flexShrink: 0 }}>
<button
onClick={() => { setShowCreateForm(true); setCreateError('') }}
className="px-3 py-1.5 text-[11px] font-semibold rounded-md border border-primary-cyan text-primary-cyan hover:bg-[rgba(6,182,212,0.08)] transition-all font-korean"
className="px-3 py-1.5 text-[11px] font-semibold rounded-md border border-color-accent text-color-accent hover:bg-[rgba(6,182,212,0.08)] transition-all font-korean"
>
+
</button>
@ -375,18 +375,18 @@ function RolePermTab({
onClick={handleSave}
disabled={!dirty || saving}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all font-korean ${
dirty ? 'bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]' : 'bg-bg-3 text-text-3 cursor-not-allowed'
dirty ? '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'
}`}
>
{saving ? '저장 중...' : '변경사항 저장'}
</button>
{saveError && (
<span className="text-[11px] text-status-red font-korean">{saveError}</span>
<span className="text-[11px] text-color-danger font-korean">{saveError}</span>
)}
</div>
{/* 역할 탭 바 */}
<div className="flex items-center gap-1.5 px-4 py-2 border-b border-border bg-bg-1 overflow-x-auto" style={{ flexShrink: 0 }}>
<div className="flex items-center gap-1.5 px-4 py-2 border-b border-stroke bg-bg-surface overflow-x-auto" style={{ flexShrink: 0 }}>
{roles.map((role, idx) => {
const color = getRoleColor(role.code, idx)
const isSelected = selectedRoleSn === role.sn
@ -397,7 +397,7 @@ function RolePermTab({
className={`px-2.5 py-1 text-[11px] font-semibold rounded-md transition-all font-korean ${
isSelected
? 'border-2 shadow-[0_0_8px_rgba(6,182,212,0.2)]'
: 'border border-border text-text-3 hover:border-border'
: 'border border-stroke text-fg-disabled hover:border-stroke'
}`}
style={isSelected ? { borderColor: color, color } : undefined}
>
@ -413,7 +413,7 @@ function RolePermTab({
onBlur={() => handleSaveRoleName(role.sn)}
onClick={(e) => e.stopPropagation()}
autoFocus
className="w-20 px-1 py-0 text-[11px] font-semibold bg-bg-2 border border-primary-cyan rounded text-center text-text-1 focus:outline-none font-korean"
className="w-20 px-1 py-0 text-[11px] font-semibold bg-bg-elevated border border-color-accent rounded text-center text-fg focus:outline-none font-korean"
/>
) : (
<span onDoubleClick={() => handleStartEditName(role)}>
@ -421,7 +421,7 @@ function RolePermTab({
</span>
)}
<span className="ml-1 text-[9px] font-mono opacity-50">{role.code}</span>
{role.isDefault && <span className="ml-1 text-[9px] text-primary-cyan"></span>}
{role.isDefault && <span className="ml-1 text-[9px] text-color-accent"></span>}
</button>
{isSelected && (
<div className="flex items-center gap-0.5">
@ -429,8 +429,8 @@ function RolePermTab({
onClick={() => toggleDefault(role.sn)}
className={`px-1.5 py-0.5 text-[9px] rounded transition-all font-korean ${
role.isDefault
? 'bg-[rgba(6,182,212,0.15)] text-primary-cyan'
: 'text-text-3 hover:text-text-2'
? 'bg-[rgba(6,182,212,0.15)] text-color-accent'
: 'text-fg-disabled hover:text-fg-sub'
}`}
title="신규 사용자 기본 역할 설정"
>
@ -439,7 +439,7 @@ function RolePermTab({
{role.code !== 'ADMIN' && (
<button
onClick={() => handleDeleteRole(role.sn, role.name)}
className="w-5 h-5 flex items-center justify-center text-text-3 hover:text-red-400 transition-colors"
className="w-5 h-5 flex items-center justify-center text-fg-disabled hover:text-red-400 transition-colors"
title="역할 삭제"
>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
@ -460,12 +460,12 @@ function RolePermTab({
<div className="flex-1 overflow-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border bg-bg-1 sticky top-0 z-10">
<th className="px-3 py-1.5 text-left text-[10px] font-semibold text-text-3 font-korean min-w-[200px]"></th>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-3 py-1.5 text-left text-[10px] font-semibold text-fg-disabled font-korean min-w-[200px]"></th>
{OPER_CODES.map(oper => (
<th key={oper} className="px-1 py-1.5 text-center w-12">
<div className="text-[10px] font-semibold text-text-2">{OPER_LABELS[oper]}</div>
<div className="text-[8px] text-text-3 font-korean">{OPER_FULL_LABELS[oper]}</div>
<div className="text-[10px] font-semibold text-fg-sub">{OPER_LABELS[oper]}</div>
<div className="text-[8px] text-fg-disabled font-korean">{OPER_FULL_LABELS[oper]}</div>
</th>
))}
</tr>
@ -485,7 +485,7 @@ function RolePermTab({
</table>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-text-3 text-sm font-korean">
<div className="flex-1 flex items-center justify-center text-fg-disabled text-sm font-korean">
</div>
)}
@ -493,40 +493,40 @@ function RolePermTab({
{/* 역할 생성 모달 */}
{showCreateForm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-[400px] bg-bg-1 rounded-lg border border-border shadow-2xl">
<div className="px-5 py-4 border-b border-border">
<h3 className="text-sm font-bold text-text-1 font-korean"> </h3>
<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>
</div>
<div className="px-5 py-4 flex flex-col gap-3">
<div>
<label className="text-[11px] text-text-3 font-korean block mb-1"> </label>
<label className="text-[11px] text-fg-disabled font-korean block mb-1"> </label>
<input
type="text"
value={newRoleCode}
onChange={(e) => setNewRoleCode(e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g, ''))}
placeholder="CUSTOM_ROLE"
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
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"
/>
<p className="text-[10px] text-text-3 mt-1 font-korean"> , , ( )</p>
<p className="text-[10px] text-fg-disabled mt-1 font-korean"> , , ( )</p>
</div>
<div>
<label className="text-[11px] text-text-3 font-korean block mb-1"> </label>
<label className="text-[11px] text-fg-disabled font-korean block mb-1"> </label>
<input
type="text"
value={newRoleName}
onChange={(e) => setNewRoleName(e.target.value)}
placeholder="사용자 정의 역할"
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
</div>
<div>
<label className="text-[11px] text-text-3 font-korean block mb-1"> ()</label>
<label className="text-[11px] text-fg-disabled font-korean block mb-1"> ()</label>
<input
type="text"
value={newRoleDesc}
onChange={(e) => setNewRoleDesc(e.target.value)}
placeholder="역할에 대한 설명"
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
</div>
{createError && (
@ -535,17 +535,17 @@ function RolePermTab({
</div>
)}
</div>
<div className="px-5 py-3 border-t border-border flex justify-end gap-2">
<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-text-3 border border-border rounded-md hover:bg-bg-hover font-korean"
className="px-4 py-2 text-xs 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-primary-cyan 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-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"
>
{creating ? '생성 중...' : '생성'}
</button>
@ -710,8 +710,8 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
return (
<div className="flex flex-col flex-1 min-h-0">
{/* 사용자 검색/선택 */}
<div className="px-4 py-2.5 border-b border-border" style={{ flexShrink: 0 }}>
<label className="text-[10px] text-text-3 font-korean block mb-1.5"> </label>
<div className="px-4 py-2.5 border-b border-stroke" style={{ flexShrink: 0 }}>
<label className="text-[10px] text-fg-disabled font-korean block mb-1.5"> </label>
<div className="relative" ref={dropdownRef}>
<input
type="text"
@ -728,32 +728,32 @@ 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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean disabled:opacity-50"
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"
/>
{showDropdown && filteredUsers.length > 0 && (
<div className="absolute left-0 top-full mt-1 w-full max-w-sm bg-bg-1 border border-border rounded-md shadow-xl z-20 overflow-auto max-h-52">
<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">
{filteredUsers.map(user => (
<button
key={user.id}
onClick={() => handleSelectUser(user)}
className="w-full px-3 py-2 text-left hover:bg-bg-hover transition-colors flex items-center gap-2"
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-text-1 font-korean truncate">
<div className="text-xs font-semibold text-fg font-korean truncate">
{user.name}
{user.rank && <span className="ml-1 text-[10px] text-text-3 font-korean">{user.rank}</span>}
{user.rank && <span className="ml-1 text-[10px] text-fg-disabled font-korean">{user.rank}</span>}
</div>
<div className="text-[10px] text-text-3 font-mono truncate">{user.account}</div>
<div className="text-[10px] text-fg-disabled font-mono truncate">{user.account}</div>
</div>
{user.orgName && (
<span className="text-[10px] text-text-3 font-korean flex-shrink-0 truncate max-w-[100px]">{user.orgName}</span>
<span className="text-[10px] text-fg-disabled font-korean flex-shrink-0 truncate max-w-[100px]">{user.orgName}</span>
)}
</button>
))}
</div>
)}
{showDropdown && !loadingUsers && filteredUsers.length === 0 && searchQuery && (
<div className="absolute left-0 top-full mt-1 w-full max-w-sm bg-bg-1 border border-border rounded-md shadow-xl z-20 px-3 py-2 text-xs text-text-3 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-xs text-fg-disabled font-korean">
</div>
)}
@ -763,16 +763,16 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
{selectedUser ? (
<>
{/* 역할 할당 섹션 */}
<div className="px-4 py-2.5 border-b border-border bg-bg-1" style={{ flexShrink: 0 }}>
<div className="px-4 py-2.5 border-b border-stroke bg-bg-surface" style={{ flexShrink: 0 }}>
<div className="flex items-center justify-between mb-2">
<span className="text-[10px] font-semibold text-text-2 font-korean"> </span>
<span className="text-[10px] font-semibold text-fg-sub font-korean"> </span>
<button
onClick={handleSaveRoles}
disabled={!rolesDirty || savingRoles}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all font-korean ${
rolesDirty
? 'bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
: 'bg-bg-3 text-text-3 cursor-not-allowed'
? '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'
}`}
>
{savingRoles ? '저장 중...' : '역할 저장'}
@ -787,7 +787,7 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
key={role.sn}
className={[
'flex items-center gap-1 px-2 py-1 rounded-md border cursor-pointer transition-all font-korean text-[11px] select-none',
isChecked ? '' : 'border-border text-text-3 hover:border-text-2',
isChecked ? '' : 'border-stroke text-fg-disabled hover:border-text-2',
].join(' ')}
style={isChecked ? { borderColor: color, color, backgroundColor: `${color}18` } : undefined}
>
@ -806,8 +806,8 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
</div>
{/* 유효 권한 매트릭스 (읽기 전용) */}
<div className="px-4 py-1.5 border-b border-border bg-bg-1 text-[9px] text-text-3 font-korean" style={{ flexShrink: 0 }}>
<span className="font-semibold text-text-2"> ( )</span>
<div className="px-4 py-1.5 border-b border-stroke bg-bg-surface text-[9px] text-fg-disabled font-korean" style={{ flexShrink: 0 }}>
<span className="font-semibold text-fg-sub"> ( )</span>
<span className="ml-2"> </span>
</div>
@ -817,12 +817,12 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
<div className="flex-1 overflow-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border bg-bg-1 sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean min-w-[240px]"></th>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean min-w-[240px]"></th>
{OPER_CODES.map(oper => (
<th key={oper} className="px-2 py-3 text-center w-16">
<div className="text-[11px] font-semibold text-text-2">{OPER_LABELS[oper]}</div>
<div className="text-[9px] text-text-3 font-korean">{OPER_FULL_LABELS[oper]}</div>
<div className="text-[11px] font-semibold text-fg-sub">{OPER_LABELS[oper]}</div>
<div className="text-[9px] text-fg-disabled font-korean">{OPER_FULL_LABELS[oper]}</div>
</th>
))}
</tr>
@ -843,13 +843,13 @@ function UserPermTab({ roles, permTree, rolePerms }: UserPermTabProps) {
</table>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-text-3 text-sm font-korean">
<div className="flex-1 flex items-center justify-center text-fg-disabled text-sm font-korean">
</div>
)}
</>
) : (
<div className="flex-1 flex items-center justify-center text-text-3 text-sm font-korean">
<div className="flex-1 flex items-center justify-center text-fg-disabled text-sm font-korean">
</div>
)}
@ -1058,25 +1058,25 @@ function PermissionsPanel() {
}
if (loading) {
return <div className="flex items-center justify-center h-32 text-text-3 text-sm font-korean"> ...</div>
return <div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean"> ...</div>
}
return (
<div className="flex flex-col h-full">
{/* 헤더 */}
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border" style={{ flexShrink: 0 }}>
<div className="flex items-center justify-between px-4 py-2.5 border-b border-stroke" style={{ flexShrink: 0 }}>
<div>
<h1 className="text-sm font-bold text-text-1 font-korean"> </h1>
<p className="text-[10px] text-text-3 mt-0.5 font-korean"> × CRUD </p>
<h1 className="text-sm font-bold text-fg font-korean"> </h1>
<p className="text-[10px] text-fg-disabled mt-0.5 font-korean"> × CRUD </p>
</div>
{/* 탭 전환 */}
<div className="flex items-center gap-1 p-1 bg-bg-2 rounded-lg border border-border">
<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 ${
activeTab === 'role'
? 'bg-primary-cyan text-bg-0 shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'text-text-3 hover:text-text-2'
? 'bg-color-accent text-bg-0 shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'text-fg-disabled hover:text-fg-sub'
}`}
>
@ -1085,8 +1085,8 @@ function PermissionsPanel() {
onClick={() => setActiveTab('user')}
className={`px-4 py-1.5 text-xs font-semibold rounded-md transition-all font-korean ${
activeTab === 'user'
? 'bg-primary-cyan text-bg-0 shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'text-text-3 hover:text-text-2'
? 'bg-color-accent text-bg-0 shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'text-fg-disabled hover:text-fg-sub'
}`}
>

파일 보기

@ -127,11 +127,11 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-border shrink-0">
<div className="px-6 py-4 border-b border-stroke shrink-0">
<div className="flex items-center justify-between mb-3">
<div>
<h1 className="text-lg font-bold text-text-1 font-korean">{title}</h1>
<p className="text-xs text-text-3 mt-1 font-korean"> {total}</p>
<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>
</div>
</div>
<div className="flex items-center gap-2">
@ -141,12 +141,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-2 border border-border rounded text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
<select
value={filterUseYn}
onChange={e => setFilterUseYn(e.target.value)}
className="px-2 py-1.5 text-xs bg-bg-2 border border-border rounded text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value=""></option>
<option value="Y"></option>
@ -154,7 +154,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
</select>
<button
onClick={handleSearch}
className="px-3 py-1.5 text-xs border border-border text-text-2 rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
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"
>
</button>
@ -163,7 +163,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-border shrink-0 font-korean">
<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">
{error}
</div>
)}
@ -171,28 +171,28 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
{/* 테이블 영역 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-full text-text-3 text-sm font-korean">
<div className="flex items-center justify-center h-full text-fg-disabled text-sm font-korean">
...
</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-border bg-bg-1 sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-mono"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean w-12 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-mono">WMS레이어명</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean w-16"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-28"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean w-20"></th>
<tr className="border-b border-stroke bg-bg-surface sticky top-0 z-10">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-12 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">WMS레이어명</th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-16"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-28"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean w-20"></th>
</tr>
</thead>
<tbody>
{items.length === 0 ? (
<tr>
<td colSpan={9} className="px-4 py-12 text-center text-text-3 text-sm font-korean">
<td colSpan={9} className="px-4 py-12 text-center text-fg-disabled text-sm font-korean">
.
</td>
</tr>
@ -200,34 +200,34 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
items.map((item, idx) => (
<tr
key={item.layerCd}
className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors"
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
>
<td className="px-4 py-3 text-xs text-text-3 font-mono">
<td className="px-4 py-3 text-xs text-fg-disabled font-mono">
{(page - 1) * PAGE_SIZE + idx + 1}
</td>
<td className="px-4 py-3 text-[11px] text-text-2 font-mono">
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">
{item.layerCd}
</td>
<td className="px-4 py-3 text-xs text-text-1 font-korean">
<td className="px-4 py-3 text-xs text-fg font-korean">
{item.layerNm}
</td>
<td className="px-4 py-3 text-xs text-text-2 font-korean max-w-[200px]">
<td className="px-4 py-3 text-xs text-fg-sub font-korean max-w-[200px]">
<span className="block truncate" title={item.layerFullNm}>
{item.layerFullNm}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-semibold bg-[rgba(6,182,212,0.1)] text-primary-cyan border border-[rgba(6,182,212,0.3)]">
<span className="inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-semibold bg-[rgba(6,182,212,0.1)] text-color-accent border border-[rgba(6,182,212,0.3)]">
{item.layerLevel}
</span>
</td>
<td className="px-4 py-3 text-[11px] text-text-2 font-mono">
{item.wmsLayerNm ?? <span className="text-text-3">-</span>}
<td className="px-4 py-3 text-[11px] text-fg-sub font-mono">
{item.wmsLayerNm ?? <span className="text-fg-disabled">-</span>}
</td>
<td className="px-4 py-3 text-xs text-text-3 text-center font-mono">
<td className="px-4 py-3 text-xs text-fg-disabled text-center font-mono">
{item.sortOrd}
</td>
<td className="px-4 py-3 text-[11px] text-text-3 font-mono">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono">
{item.regDtm ?? '-'}
</td>
<td className="px-4 py-3 text-center">
@ -237,8 +237,8 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
title={item.useYn === 'Y' ? '사용 중 (클릭하여 비활성화)' : '미사용 (클릭하여 활성화)'}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:opacity-50 ${
item.useYn === 'Y'
? 'bg-primary-cyan'
: 'bg-[rgba(255,255,255,0.08)] border border-border'
? 'bg-color-accent'
: 'bg-[rgba(255,255,255,0.08)] border border-stroke'
}`}
>
<span
@ -258,29 +258,29 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
{/* 페이지네이션 */}
{!loading && totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-border bg-bg-1 shrink-0">
<span className="text-[11px] text-text-3 font-korean">
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface shrink-0">
<span className="text-[11px] text-fg-disabled font-korean">
{(page - 1) * PAGE_SIZE + 1}{Math.min(page * PAGE_SIZE, total)} / {total}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
className="px-2.5 py-1 text-[11px] border border-border text-text-3 rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
{buildPageButtons().map((btn, i) =>
btn === 'ellipsis' ? (
<span key={`e${i}`} className="px-1.5 text-[11px] text-text-3"></span>
<span key={`e${i}`} className="px-1.5 text-[11px] text-fg-disabled"></span>
) : (
<button
key={btn}
onClick={() => setPage(btn)}
className={`px-2.5 py-1 text-[11px] rounded transition-all ${
page === btn
? 'bg-primary-cyan text-bg-0 font-semibold shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'border border-border text-text-3 hover:bg-[rgba(255,255,255,0.04)]'
? 'bg-color-accent text-bg-0 font-semibold shadow-[0_0_8px_rgba(6,182,212,0.25)]'
: 'border border-stroke text-fg-disabled hover:bg-[rgba(255,255,255,0.04)]'
}`}
>
{btn}
@ -290,7 +290,7 @@ const SensitiveLayerPanel = ({ categoryCode, title }: SensitiveLayerPanelProps)
<button
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="px-2.5 py-1 text-[11px] border border-border text-text-3 rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>

파일 보기

@ -53,31 +53,31 @@ function SettingsPanel() {
}
if (loading) {
return <div className="flex items-center justify-center h-32 text-text-3 text-sm font-korean"> ...</div>
return <div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean"> ...</div>
}
return (
<div className="flex flex-col h-full">
<div className="px-6 py-4 border-b border-border">
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> </p>
<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>
</div>
<div className="flex-1 overflow-auto px-6 py-6">
<div className="max-w-[640px] flex flex-col gap-6">
{/* 사용자 등록 설정 */}
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
<p className="text-[11px] text-text-3 mt-0.5 font-korean"> </p>
<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>
<p className="text-[11px] text-fg-disabled mt-0.5 font-korean"> </p>
</div>
<div className="divide-y divide-border">
{/* 자동 승인 */}
<div className="px-5 py-4 flex items-center justify-between">
<div className="flex-1 mr-4">
<div className="text-[13px] font-semibold text-text-1 font-korean"> </div>
<p className="text-[11px] text-text-3 mt-1 font-korean leading-relaxed">
<div className="text-[13px] font-semibold text-fg font-korean"> </div>
<p className="text-[11px] text-fg-disabled mt-1 font-korean leading-relaxed">
<span className="text-green-400 font-semibold">ACTIVE</span> .
<span className="text-yellow-400 font-semibold">PENDING</span> .
</p>
@ -86,7 +86,7 @@ function SettingsPanel() {
onClick={() => handleToggle('autoApprove')}
disabled={saving}
className={`relative w-12 h-6 rounded-full transition-all flex-shrink-0 ${
settings?.autoApprove ? 'bg-primary-cyan' : 'bg-bg-3 border border-border'
settings?.autoApprove ? 'bg-color-accent' : 'bg-bg-card border border-stroke'
} ${saving ? 'opacity-50' : ''}`}
>
<span
@ -100,9 +100,9 @@ function SettingsPanel() {
{/* 기본 역할 자동 할당 */}
<div className="px-5 py-4 flex items-center justify-between">
<div className="flex-1 mr-4">
<div className="text-[13px] font-semibold text-text-1 font-korean"> </div>
<p className="text-[11px] text-text-3 mt-1 font-korean leading-relaxed">
<span className="text-primary-cyan font-semibold"> </span> .
<div className="text-[13px] font-semibold text-fg font-korean"> </div>
<p className="text-[11px] text-fg-disabled mt-1 font-korean leading-relaxed">
<span className="text-color-accent font-semibold"> </span> .
.
</p>
</div>
@ -110,7 +110,7 @@ function SettingsPanel() {
onClick={() => handleToggle('defaultRole')}
disabled={saving}
className={`relative w-12 h-6 rounded-full transition-all flex-shrink-0 ${
settings?.defaultRole ? 'bg-primary-cyan' : 'bg-bg-3 border border-border'
settings?.defaultRole ? 'bg-color-accent' : 'bg-bg-card border border-stroke'
} ${saving ? 'opacity-50' : ''}`}
>
<span
@ -124,15 +124,15 @@ function SettingsPanel() {
</div>
{/* OAuth 설정 */}
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean">Google OAuth </h2>
<p className="text-[11px] text-text-3 mt-0.5 font-korean">Google </p>
<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>
<p className="text-[11px] text-fg-disabled mt-0.5 font-korean">Google </p>
</div>
<div className="px-5 py-4">
<div className="flex-1 mr-4 mb-3">
<div className="text-[13px] font-semibold text-text-1 font-korean mb-1"> </div>
<p className="text-[11px] text-text-3 font-korean leading-relaxed mb-3">
<div className="text-[13px] font-semibold text-fg font-korean mb-1"> </div>
<p className="text-[11px] text-fg-disabled font-korean leading-relaxed mb-3">
Google <span className="text-green-400 font-semibold">ACTIVE</span> .
<span className="text-yellow-400 font-semibold">PENDING</span> .
(,) .
@ -143,7 +143,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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
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"
/>
<button
onClick={async () => {
@ -161,8 +161,8 @@ function SettingsPanel() {
disabled={savingOAuth || oauthDomainInput.trim() === (oauthSettings?.autoApproveDomains || '')}
className={`px-4 py-2 text-xs font-semibold rounded-md transition-all font-korean whitespace-nowrap ${
oauthDomainInput.trim() !== (oauthSettings?.autoApproveDomains || '')
? 'bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
: 'bg-bg-3 text-text-3 cursor-not-allowed'
? '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'
}`}
>
{savingOAuth ? '저장 중...' : '저장'}
@ -175,7 +175,7 @@ function SettingsPanel() {
<span
key={domain}
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-mono rounded-md"
style={{ background: 'rgba(6,182,212,0.1)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,0.25)' }}
style={{ background: 'rgba(6,182,212,0.1)', color: 'var(--color-accent)', border: '1px solid rgba(6,182,212,0.25)' }}
>
@{domain}
</span>
@ -186,15 +186,15 @@ function SettingsPanel() {
</div>
{/* 현재 설정 상태 요약 */}
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
<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>
</div>
<div className="px-5 py-4">
<div className="flex flex-col gap-3 text-[12px] font-korean">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${settings?.autoApprove ? 'bg-green-400' : 'bg-yellow-400'}`} />
<span className="text-text-2">
<span className="text-fg-sub">
{' '}
{settings?.autoApprove ? (
<span className="text-green-400 font-semibold"> </span>
@ -204,24 +204,24 @@ function SettingsPanel() {
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${settings?.defaultRole ? 'bg-green-400' : 'bg-text-3'}`} />
<span className="text-text-2">
<span className={`w-2 h-2 rounded-full ${settings?.defaultRole ? 'bg-green-400' : 'bg-fg-disabled'}`} />
<span className="text-fg-sub">
{' '}
{settings?.defaultRole ? (
<span className="text-green-400 font-semibold"></span>
) : (
<span className="text-text-3 font-semibold"></span>
<span className="text-fg-disabled font-semibold"></span>
)}
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${oauthSettings?.autoApproveDomains ? 'bg-blue-400' : 'bg-text-3'}`} />
<span className="text-text-2">
<span className={`w-2 h-2 rounded-full ${oauthSettings?.autoApproveDomains ? 'bg-blue-400' : 'bg-fg-disabled'}`} />
<span className="text-fg-sub">
Google OAuth {' '}
{oauthSettings?.autoApproveDomains ? (
<span className="text-blue-400 font-semibold font-mono">{oauthSettings.autoApproveDomains}</span>
) : (
<span className="text-text-3 font-semibold"></span>
<span className="text-fg-disabled font-semibold"></span>
)}
</span>
</div>

파일 보기

@ -47,15 +47,15 @@ function SortableMenuItem({
style={style}
className={`flex items-center justify-between px-4 py-3 rounded-md border transition-all ${
menu.enabled
? 'bg-bg-1 border-border'
: 'bg-bg-0 border-border opacity-50'
? 'bg-bg-surface border-stroke'
: 'bg-bg-base border-stroke opacity-50'
}`}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<button
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing w-6 h-7 flex items-center justify-center text-text-3 hover:text-text-1 transition-all shrink-0"
className="cursor-grab active:cursor-grabbing w-6 h-7 flex items-center justify-center text-fg-disabled hover:text-fg transition-all shrink-0"
title="드래그하여 순서 변경"
>
<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
@ -64,13 +64,13 @@ function SortableMenuItem({
<circle cx="3" cy="14" r="1.5" /><circle cx="9" cy="14" r="1.5" />
</svg>
</button>
<span className="text-text-3 text-xs font-mono w-6 text-center shrink-0">{idx + 1}</span>
<span className="text-fg-disabled text-xs font-mono w-6 text-center shrink-0">{idx + 1}</span>
{isEditing ? (
<>
<div className="relative shrink-0">
<button
onClick={() => onEmojiPickerToggle(emojiPickerId === menu.id ? null : menu.id)}
className="w-10 h-10 text-[20px] bg-bg-2 border border-border rounded flex items-center justify-center hover:border-primary-cyan transition-all"
className="w-10 h-10 text-[20px] bg-bg-elevated border border-stroke rounded flex items-center justify-center hover:border-color-accent transition-all"
title="아이콘 변경"
>
{menu.icon}
@ -94,13 +94,13 @@ function SortableMenuItem({
type="text"
value={menu.label}
onChange={(e) => onLabelChange(menu.id, e.target.value)}
className="w-full h-8 text-[13px] font-semibold font-korean bg-bg-2 border border-border rounded px-2 text-text-1 focus:border-primary-cyan focus:outline-none"
className="w-full h-8 text-[13px] font-semibold font-korean bg-bg-elevated border border-stroke rounded px-2 text-fg focus:border-color-accent focus:outline-none"
/>
<div className="text-[10px] text-text-3 font-mono mt-0.5">{menu.id}</div>
<div className="text-[10px] text-fg-disabled font-mono mt-0.5">{menu.id}</div>
</div>
<button
onClick={onEditEnd}
className="shrink-0 px-2 py-1 text-[10px] font-semibold text-primary-cyan border border-primary-cyan rounded hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
className="shrink-0 px-2 py-1 text-[10px] font-semibold text-color-accent border border-color-accent rounded hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
>
</button>
@ -109,14 +109,14 @@ function SortableMenuItem({
<>
<span className="text-[16px] shrink-0">{menu.icon}</span>
<div className="flex-1 min-w-0">
<div className={`text-[13px] font-semibold font-korean ${menu.enabled ? 'text-text-1' : 'text-text-3'}`}>
<div className={`text-[13px] font-semibold font-korean ${menu.enabled ? 'text-fg' : 'text-fg-disabled'}`}>
{menu.label}
</div>
<div className="text-[10px] text-text-3 font-mono">{menu.id}</div>
<div className="text-[10px] text-fg-disabled font-mono">{menu.id}</div>
</div>
<button
onClick={() => onEditStart(menu.id)}
className="shrink-0 w-7 h-7 rounded border border-border bg-bg-2 text-text-3 text-[11px] flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all"
className="shrink-0 w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-[11px] flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all"
title="라벨/아이콘 편집"
>
@ -128,7 +128,7 @@ function SortableMenuItem({
<button
onClick={() => onToggle(menu.id)}
className={`relative w-10 h-5 rounded-full transition-all ${
menu.enabled ? 'bg-primary-cyan' : 'bg-bg-3 border border-border'
menu.enabled ? 'bg-color-accent' : 'bg-bg-card border border-stroke'
}`}
>
<span
@ -141,14 +141,14 @@ function SortableMenuItem({
<button
onClick={() => onMove(idx, -1)}
disabled={idx === 0}
className="w-7 h-7 rounded border border-border bg-bg-2 text-text-3 text-xs flex items-center justify-center hover:bg-bg-hover hover:text-text-1 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-xs 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-border bg-bg-2 text-text-3 text-xs flex items-center justify-center hover:bg-bg-hover hover:text-text-1 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-xs flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
>
</button>

파일 보기

@ -84,13 +84,13 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-bg-1 border border-border rounded-lg shadow-lg w-[480px] max-h-[90vh] flex flex-col">
<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-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
<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>
<button
onClick={onClose}
className="text-text-3 hover:text-text-1 transition-colors"
className="text-fg-disabled hover:text-fg transition-colors"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M18 6L6 18M6 6l12 12" />
@ -103,7 +103,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
<div className="px-6 py-4 space-y-4">
{/* 계정 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -111,13 +111,13 @@ 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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
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"
/>
</div>
{/* 비밀번호 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -125,13 +125,13 @@ 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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
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"
/>
</div>
{/* 사용자명 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
<span className="text-red-400">*</span>
</label>
<input
@ -139,13 +139,13 @@ 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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
</div>
{/* 직급 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<input
@ -153,19 +153,19 @@ 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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
</div>
{/* 소속 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<select
value={orgSn}
onChange={e => setOrgSn(e.target.value !== '' ? Number(e.target.value) : '')}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value=""> </option>
{allOrgs.map(org => (
@ -178,7 +178,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
{/* 이메일 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<input
@ -186,18 +186,18 @@ 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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
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"
/>
</div>
{/* 역할 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5">
<label className="block text-[11px] font-semibold text-fg-sub font-korean mb-1.5">
</label>
<div className="bg-bg-2 border border-border rounded-md p-2 space-y-1 max-h-[120px] overflow-y-auto">
<div className="bg-bg-elevated border border-stroke rounded-md p-2 space-y-1 max-h-[120px] overflow-y-auto">
{allRoles.length === 0 ? (
<p className="text-[10px] text-text-3 font-korean px-1 py-1"> </p>
<p className="text-[10px] text-fg-disabled font-korean px-1 py-1"> </p>
) : allRoles.map((role, idx) => {
const color = getRoleColor(role.code, idx)
return (
@ -212,7 +212,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
style={{ accentColor: color }}
/>
<span className="text-xs font-korean" style={{ color }}>{role.name}</span>
<span className="text-[10px] text-text-3 font-mono">{role.code}</span>
<span className="text-[10px] text-fg-disabled font-mono">{role.code}</span>
</label>
)
})}
@ -226,18 +226,18 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
</div>
{/* 푸터 */}
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-border">
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-stroke">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-xs border border-border text-text-2 rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
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"
>
</button>
<button
type="submit"
disabled={submitting}
className="px-4 py-2 text-xs font-semibold rounded-md bg-primary-cyan 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-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"
>
{submitting ? '등록 중...' : '등록'}
</button>
@ -321,14 +321,14 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-bg-1 border border-border rounded-lg shadow-lg w-[480px] max-h-[90vh] flex flex-col">
<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-border">
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div>
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
<p className="text-[10px] text-text-3 font-mono mt-0.5">{user.account}</p>
<h2 className="text-sm font-bold text-fg font-korean"> </h2>
<p className="text-[10px] text-fg-disabled font-mono mt-0.5">{user.account}</p>
</div>
<button onClick={onClose} className="text-text-3 hover:text-text-1 transition-colors">
<button onClick={onClose} className="text-fg-disabled hover:text-fg transition-colors">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M18 6L6 18M6 6l12 12" />
</svg>
@ -338,34 +338,34 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-5">
{/* 기본 정보 수정 */}
<div>
<h3 className="text-[11px] font-semibold text-text-2 font-korean mb-3"> </h3>
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-3"> </h3>
<div className="space-y-3">
<div>
<label className="block text-[10px] text-text-3 font-korean mb-1"></label>
<label className="block text-[10px] text-fg-disabled font-korean mb-1"></label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="w-full px-3 py-1.5 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-[10px] text-text-3 font-korean mb-1"></label>
<label className="block text-[10px] text-fg-disabled font-korean mb-1"></label>
<input
type="text"
value={rank}
onChange={e => setRank(e.target.value)}
placeholder="예: 팀장"
className="w-full px-3 py-1.5 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
</div>
<div>
<label className="block text-[10px] text-text-3 font-korean mb-1"></label>
<label className="block text-[10px] text-fg-disabled font-korean mb-1"></label>
<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-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value=""> </option>
{allOrgs.map(org => (
@ -379,7 +379,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
<button
onClick={handleSaveInfo}
disabled={saving || !name.trim()}
className="px-4 py-1.5 text-[11px] font-semibold rounded-md bg-primary-cyan 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-1.5 text-[11px] 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 ? '저장 중...' : '정보 저장'}
</button>
@ -387,20 +387,20 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
</div>
{/* 구분선 */}
<div className="border-t border-border" />
<div className="border-t border-stroke" />
{/* 비밀번호 초기화 */}
<div>
<h3 className="text-[11px] font-semibold text-text-2 font-korean mb-3"> </h3>
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-3"> </h3>
<div className="flex items-end gap-2">
<div className="flex-1">
<label className="block text-[10px] text-text-3 font-korean mb-1"> </label>
<label className="block text-[10px] text-fg-disabled font-korean mb-1"> </label>
<input
type="password"
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
placeholder="새 비밀번호 입력"
className="w-full px-3 py-1.5 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-mono"
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"
/>
</div>
<button
@ -419,16 +419,16 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
{unlockLoading ? '해제 중...' : '패스워드잠금해제'}
</button>
</div>
<p className="text-[9px] text-text-3 font-korean mt-1.5"> .</p>
<p className="text-[9px] text-fg-disabled font-korean mt-1.5"> .</p>
</div>
{/* 구분선 */}
<div className="border-t border-border" />
<div className="border-t border-stroke" />
{/* 계정 잠금 해제 */}
<div>
<h3 className="text-[11px] font-semibold text-text-2 font-korean mb-2"> </h3>
<div className="flex items-center justify-between bg-bg-2 border border-border rounded-md px-4 py-3">
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-2"> </h3>
<div className="flex items-center justify-between bg-bg-elevated border border-stroke rounded-md px-4 py-3">
<div>
<div className="flex items-center gap-2">
<span className={`inline-flex items-center gap-1.5 text-[11px] font-semibold font-korean ${(statusLabels[user.status] || statusLabels.INACTIVE).color}`}>
@ -442,7 +442,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
)}
</div>
{user.status === 'LOCKED' && (
<p className="text-[9px] text-text-3 font-korean mt-1">
<p className="text-[9px] text-fg-disabled font-korean mt-1">
5
</p>
)}
@ -461,23 +461,23 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
{/* 기타 정보 (읽기 전용) */}
<div>
<h3 className="text-[11px] font-semibold text-text-2 font-korean mb-2"> </h3>
<h3 className="text-[11px] font-semibold text-fg-sub font-korean mb-2"> </h3>
<div className="grid grid-cols-2 gap-2 text-[10px] font-korean">
<div className="bg-bg-2 border border-border rounded px-3 py-2">
<span className="text-text-3">: </span>
<span className="text-text-2 font-mono">{user.email || '-'}</span>
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2">
<span className="text-fg-disabled">: </span>
<span className="text-fg-sub font-mono">{user.email || '-'}</span>
</div>
<div className="bg-bg-2 border border-border rounded px-3 py-2">
<span className="text-text-3">OAuth: </span>
<span className="text-text-2">{user.oauthProvider || '-'}</span>
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2">
<span className="text-fg-disabled">OAuth: </span>
<span className="text-fg-sub">{user.oauthProvider || '-'}</span>
</div>
<div className="bg-bg-2 border border-border rounded px-3 py-2">
<span className="text-text-3"> : </span>
<span className="text-text-2">{user.lastLogin ? formatDate(user.lastLogin) : '-'}</span>
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2">
<span className="text-fg-disabled"> : </span>
<span className="text-fg-sub">{user.lastLogin ? formatDate(user.lastLogin) : '-'}</span>
</div>
<div className="bg-bg-2 border border-border rounded px-3 py-2">
<span className="text-text-3">: </span>
<span className="text-text-2">{formatDate(user.regDtm)}</span>
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2">
<span className="text-fg-disabled">: </span>
<span className="text-fg-sub">{formatDate(user.regDtm)}</span>
</div>
</div>
</div>
@ -495,10 +495,10 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
</div>
{/* 푸터 */}
<div className="flex items-center justify-end px-6 py-3 border-t border-border">
<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-border text-text-2 rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
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"
>
</button>
@ -641,11 +641,11 @@ function UsersPanel() {
<>
<div className="flex flex-col h-full">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div className="flex items-center gap-3">
<div>
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> {filteredUsers.length}</p>
<h1 className="text-lg font-bold text-fg font-korean"> </h1>
<p className="text-xs text-fg-disabled mt-1 font-korean"> {filteredUsers.length}</p>
</div>
{pendingCount > 0 && (
<span className="px-2.5 py-1 text-[10px] font-bold rounded-full bg-[rgba(250,204,21,0.15)] text-yellow-400 border border-[rgba(250,204,21,0.3)] animate-pulse font-korean">
@ -658,7 +658,7 @@ function UsersPanel() {
<select
value={orgFilter}
onChange={e => { setOrgFilter(e.target.value); setCurrentPage(1) }}
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value=""> </option>
{allOrgs.map(org => (
@ -671,7 +671,7 @@ function UsersPanel() {
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value)}
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value=""> </option>
<option value="PENDING"></option>
@ -686,11 +686,11 @@ function UsersPanel() {
placeholder="이름, 계정 검색..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="w-56 px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
<button
onClick={() => setShowRegisterModal(true)}
className="px-4 py-2 text-xs font-semibold rounded-md bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
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"
>
+
</button>
@ -700,28 +700,28 @@ function UsersPanel() {
{/* 테이블 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-32 text-text-3 text-sm font-korean">
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
...
</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-border bg-bg-1">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-10"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-mono">ID</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-right text-[11px] font-semibold text-text-3 font-korean"></th>
<tr className="border-b border-stroke bg-bg-surface">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-mono">ID</th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-right text-[11px] font-semibold text-fg-disabled font-korean"></th>
</tr>
</thead>
<tbody>
{pagedUsers.length === 0 ? (
<tr>
<td colSpan={9} className="px-6 py-10 text-center text-xs text-text-3 font-korean">
<td colSpan={9} className="px-6 py-10 text-center text-xs text-fg-disabled font-korean">
.
</td>
</tr>
@ -731,34 +731,34 @@ function UsersPanel() {
return (
<tr
key={user.id}
className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors"
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
>
{/* 번호 */}
<td className="px-4 py-3 text-[11px] text-text-3 font-mono text-center">{rowNum}</td>
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono text-center">{rowNum}</td>
{/* ID(account) */}
<td className="px-4 py-3 text-[12px] text-text-2 font-mono">{user.account}</td>
<td className="px-4 py-3 text-[12px] text-fg-sub font-mono">{user.account}</td>
{/* 사용자명 */}
<td className="px-4 py-3">
<button
onClick={() => setDetailUser(user)}
className="text-[12px] text-primary-cyan font-semibold font-korean hover:underline"
className="text-[12px] text-color-accent font-semibold font-korean hover:underline"
>
{user.name}
</button>
</td>
{/* 직급 */}
<td className="px-4 py-3 text-[12px] text-text-2 font-korean">{user.rank || '-'}</td>
<td className="px-4 py-3 text-[12px] text-fg-sub font-korean">{user.rank || '-'}</td>
{/* 소속 */}
<td className="px-4 py-3 text-[12px] text-text-2 font-korean">
<td className="px-4 py-3 text-[12px] text-fg-sub font-korean">
{user.orgAbbr || user.orgName || '-'}
</td>
{/* 이메일 */}
<td className="px-4 py-3 text-[11px] text-text-3 font-mono">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono">
{user.email || '-'}
</td>
@ -787,9 +787,9 @@ function UsersPanel() {
</span>
)
}) : (
<span className="text-[10px] text-text-3 font-korean"> </span>
<span className="text-[10px] text-fg-disabled font-korean"> </span>
)}
<span className="text-[10px] text-text-3 ml-0.5">
<span className="text-[10px] text-fg-disabled ml-0.5">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
@ -799,9 +799,9 @@ function UsersPanel() {
{roleEditUserId === user.id && (
<div
ref={roleDropdownRef}
className="absolute z-20 top-full left-0 mt-1 p-2 bg-bg-1 border border-border rounded-lg shadow-lg min-w-[200px]"
className="absolute z-20 top-full left-0 mt-1 p-2 bg-bg-surface border border-stroke rounded-lg shadow-lg min-w-[200px]"
>
<div className="text-[10px] text-text-3 font-korean font-semibold mb-1.5 px-1"> </div>
<div className="text-[10px] text-fg-disabled font-korean font-semibold mb-1.5 px-1"> </div>
{allRoles.map((role, roleIdx) => {
const color = getRoleColor(role.code, roleIdx)
return (
@ -816,21 +816,21 @@ function UsersPanel() {
style={{ accentColor: color }}
/>
<span className="text-xs font-korean" style={{ color }}>{role.name}</span>
<span className="text-[10px] text-text-3 font-mono">{role.code}</span>
<span className="text-[10px] text-fg-disabled font-mono">{role.code}</span>
</label>
)
})}
<div className="flex justify-end gap-2 mt-2 pt-2 border-t border-border">
<div className="flex justify-end gap-2 mt-2 pt-2 border-t border-stroke">
<button
onClick={() => setRoleEditUserId(null)}
className="px-3 py-1 text-[10px] text-text-3 border border-border rounded hover:bg-bg-hover font-korean"
className="px-3 py-1 text-[10px] text-fg-disabled border border-stroke rounded hover:bg-bg-surface-hover font-korean"
>
</button>
<button
onClick={() => handleSaveRoles(user.id)}
disabled={selectedRoleSns.length === 0}
className="px-3 py-1 text-[10px] font-semibold rounded bg-primary-cyan text-bg-0 hover:shadow-[0_0_8px_rgba(6,182,212,0.3)] disabled:opacity-50 font-korean"
className="px-3 py-1 text-[10px] font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_8px_rgba(6,182,212,0.3)] disabled:opacity-50 font-korean"
>
</button>
@ -878,7 +878,7 @@ function UsersPanel() {
{user.status === 'ACTIVE' && (
<button
onClick={() => handleDeactivate(user.id)}
className="px-2 py-1 text-[10px] font-semibold text-text-3 border border-border rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
className="px-2 py-1 text-[10px] font-semibold text-fg-disabled border border-stroke rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
>
</button>
@ -903,15 +903,15 @@ function UsersPanel() {
{/* 페이지네이션 */}
{!loading && totalPages > 1 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-border bg-bg-1">
<span className="text-[11px] text-text-3 font-korean">
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface">
<span className="text-[11px] text-fg-disabled font-korean">
{(currentPage - 1) * PAGE_SIZE + 1}{Math.min(currentPage * PAGE_SIZE, totalCount)} / {totalCount}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-2.5 py-1 text-[11px] border border-border text-text-3 rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>
@ -926,7 +926,7 @@ function UsersPanel() {
}, [])
.map((item, i) =>
item === '...' ? (
<span key={`ellipsis-${i}`} className="px-2 text-[11px] text-text-3"></span>
<span key={`ellipsis-${i}`} className="px-2 text-[11px] text-fg-disabled"></span>
) : (
<button
key={item}
@ -934,8 +934,8 @@ function UsersPanel() {
className="px-2.5 py-1 text-[11px] border rounded transition-all font-mono"
style={
currentPage === item
? { borderColor: 'var(--cyan)', color: 'var(--cyan)', background: 'rgba(6,182,212,0.1)' }
: { borderColor: 'var(--border)', color: 'var(--t3)' }
? { borderColor: 'var(--color-accent)', color: 'var(--color-accent)', background: 'rgba(6,182,212,0.1)' }
: { borderColor: 'var(--border)', color: 'var(--fg-disabled)' }
}
>
{item}
@ -945,7 +945,7 @@ function UsersPanel() {
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-2.5 py-1 text-[11px] border border-border text-text-3 rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
className="px-2.5 py-1 text-[11px] border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
>
</button>

파일 보기

@ -69,16 +69,16 @@ function VesselMaterialsPanel() {
return (
<div className="flex flex-col h-full">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div>
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> {filtered.length} ( )</p>
<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>
</div>
<div className="flex items-center gap-3">
<select
value={regionFilter}
onChange={handleFilterChange(setRegionFilter)}
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value="전체"> </option>
<option value="남해"></option>
@ -90,7 +90,7 @@ function VesselMaterialsPanel() {
<select
value={typeFilter}
onChange={handleFilterChange(setTypeFilter)}
className="px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
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"
>
<option value="전체"> </option>
{typeOptions.map(t => (
@ -102,11 +102,11 @@ function VesselMaterialsPanel() {
placeholder="기관명, 주소 검색..."
value={searchTerm}
onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }}
className="w-56 px-3 py-2 text-xs bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none font-korean"
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"
/>
<button
onClick={load}
className="px-4 py-2 text-xs font-semibold rounded-md bg-bg-2 border border-border text-text-2 hover:border-primary-cyan hover:text-primary-cyan transition-all font-korean"
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"
>
</button>
@ -116,36 +116,36 @@ function VesselMaterialsPanel() {
{/* 테이블 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex items-center justify-center h-32 text-text-3 text-sm font-korean">
<div className="flex items-center justify-center h-32 text-fg-disabled text-sm font-korean">
...
</div>
) : (
<table className="w-full">
<thead>
<tr className="border-b border-border bg-bg-1">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-text-3 font-korean"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-primary-cyan bg-primary-cyan/5"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-text-3"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-text-3"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-text-3"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-text-3"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-text-3 font-korean"></th>
<tr className="border-b border-stroke bg-bg-surface">
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean w-10 whitespace-nowrap"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-left text-[11px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-color-accent bg-color-accent/5"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold font-korean text-fg-disabled"></th>
<th className="px-4 py-3 text-center text-[11px] font-semibold text-fg-disabled font-korean"></th>
</tr>
</thead>
<tbody>
{paged.length === 0 ? (
<tr>
<td colSpan={11} className="px-6 py-10 text-center text-xs text-text-3 font-korean">
<td colSpan={11} className="px-6 py-10 text-center text-xs text-fg-disabled font-korean">
.
</td>
</tr>
) : paged.map((org, idx) => (
<tr key={org.id} className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<td className="px-4 py-3 text-[11px] text-text-3 font-mono text-center">
<tr key={org.id} className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-mono text-center">
{(safePage - 1) * PAGE_SIZE + idx + 1}
</td>
<td className="px-4 py-3">
@ -153,31 +153,31 @@ function VesselMaterialsPanel() {
{org.type}
</span>
</td>
<td className="px-4 py-3 text-[11px] text-text-2 font-korean">
<td className="px-4 py-3 text-[11px] text-fg-sub font-korean">
{regionShort(org.jurisdiction)}
</td>
<td className="px-4 py-3 text-[11px] text-text-1 font-korean font-semibold">
<td className="px-4 py-3 text-[11px] text-fg font-korean font-semibold">
{org.name}
</td>
<td className="px-4 py-3 text-[11px] text-text-3 font-korean max-w-[200px] truncate">
<td className="px-4 py-3 text-[11px] text-fg-disabled font-korean max-w-[200px] truncate">
{org.address}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-primary-cyan font-semibold bg-primary-cyan/5">
{org.vessel > 0 ? org.vessel : <span className="text-text-3"></span>}
<td className="px-4 py-3 text-[11px] font-mono text-center text-color-accent font-semibold bg-color-accent/5">
{org.vessel > 0 ? org.vessel : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
{org.skimmer > 0 ? org.skimmer : <span className="text-text-3"></span>}
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
{org.skimmer > 0 ? org.skimmer : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
{org.pump > 0 ? org.pump : <span className="text-text-3"></span>}
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
{org.pump > 0 ? org.pump : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
{org.vehicle > 0 ? org.vehicle : <span className="text-text-3"></span>}
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
{org.vehicle > 0 ? org.vehicle : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center text-text-2">
{org.sprayer > 0 ? org.sprayer : <span className="text-text-3"></span>}
<td className="px-4 py-3 text-[11px] font-mono text-center text-fg-sub">
{org.sprayer > 0 ? org.sprayer : <span className="text-fg-disabled"></span>}
</td>
<td className="px-4 py-3 text-[11px] font-mono text-center font-bold text-primary-cyan">
<td className="px-4 py-3 text-[11px] font-mono text-center font-bold text-color-accent">
{org.totalAssets.toLocaleString()}
</td>
</tr>
@ -189,8 +189,8 @@ function VesselMaterialsPanel() {
{/* 합계 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center gap-4 px-6 py-2 border-t border-border bg-bg-0/80">
<span className="text-[10px] text-text-3 font-korean font-semibold mr-auto">
<div className="flex items-center gap-4 px-6 py-2 border-t border-stroke bg-bg-base/80">
<span className="text-[10px] text-fg-disabled font-korean font-semibold mr-auto">
({filtered.length} )
</span>
{[
@ -201,9 +201,9 @@ function VesselMaterialsPanel() {
{ label: '살포장치', value: filtered.reduce((s, o) => s + o.sprayer, 0), unit: '대', active: false },
{ label: '총자산', value: filtered.reduce((s, o) => s + o.totalAssets, 0), unit: '', active: true },
].map((t) => (
<div key={t.label} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${t.active ? 'bg-primary-cyan/10' : ''}`}>
<span className={`text-[9px] font-korean ${t.active ? 'text-primary-cyan' : 'text-text-3'}`}>{t.label}</span>
<span className={`text-[10px] font-mono font-bold ${t.active ? 'text-primary-cyan' : 'text-text-1'}`}>
<div key={t.label} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${t.active ? 'bg-color-accent/10' : ''}`}>
<span className={`text-[9px] font-korean ${t.active ? 'text-color-accent' : 'text-fg-disabled'}`}>{t.label}</span>
<span className={`text-[10px] font-mono font-bold ${t.active ? 'text-color-accent' : 'text-fg'}`}>
{t.value.toLocaleString()}{t.unit}
</span>
</div>
@ -213,15 +213,15 @@ function VesselMaterialsPanel() {
{/* 페이지네이션 */}
{!loading && filtered.length > 0 && (
<div className="flex items-center justify-between px-6 py-3 border-t border-border">
<span className="text-[11px] text-text-3 font-korean">
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke">
<span className="text-[11px] text-fg-disabled font-korean">
{(safePage - 1) * PAGE_SIZE + 1}{Math.min(safePage * PAGE_SIZE, filtered.length)} / {filtered.length}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={safePage === 1}
className="px-2.5 py-1 text-[11px] border border-border rounded text-text-2 hover:border-primary-cyan hover:text-primary-cyan disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&lt;
</button>
@ -231,7 +231,7 @@ function VesselMaterialsPanel() {
onClick={() => setCurrentPage(p)}
className="px-2.5 py-1 text-[11px] border rounded transition-colors"
style={p === safePage
? { borderColor: 'var(--cyan)', color: 'var(--cyan)', background: 'rgba(6,182,212,0.1)' }
? { borderColor: 'var(--color-accent)', color: 'var(--color-accent)', background: 'rgba(6,182,212,0.1)' }
: { borderColor: 'var(--border)', color: 'var(--text-2)' }
}
>
@ -241,7 +241,7 @@ function VesselMaterialsPanel() {
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={safePage === totalPages}
className="px-2.5 py-1 text-[11px] border border-border rounded text-text-2 hover:border-primary-cyan hover:text-primary-cyan disabled:opacity-40 transition-colors"
className="px-2.5 py-1 text-[11px] border border-stroke rounded text-fg-sub hover:border-color-accent hover:text-color-accent disabled:opacity-40 transition-colors"
>
&gt;
</button>

파일 보기

@ -125,18 +125,18 @@ export default function VesselSignalPanel() {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-3 border-b border-border-1">
<h2 className="text-sm font-semibold text-text-1"> </h2>
<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>
<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-2 border border-border-1 text-text-1"
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg"
/>
<button
onClick={load}
className="px-3 py-1 text-xs rounded bg-bg-2 border border-border-1 text-text-2 hover:bg-bg-3"
className="px-3 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-fg-sub hover:bg-bg-card"
>
</button>
@ -147,7 +147,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-text-3"> ...</span>
<span className="text-xs text-fg-disabled"> ...</span>
</div>
) : (
<div className="flex gap-2">
@ -174,14 +174,14 @@ export default function VesselSignalPanel() {
{HOURS.map(h => (
<span
key={h}
className="absolute text-[10px] text-text-3 font-mono"
className="absolute text-[10px] text-fg-disabled font-mono"
style={{ left: `${(h / 24) * 100}%`, transform: 'translateX(-50%)' }}
>
{String(h).padStart(2, '0')}
</span>
))}
<span
className="absolute text-[10px] text-text-3 font-mono"
className="absolute text-[10px] text-fg-disabled font-mono"
style={{ right: 0 }}
>
24

파일 보기

@ -1,9 +1,9 @@
export const DEFAULT_ROLE_COLORS: Record<string, string> = {
ADMIN: 'var(--red)',
ADMIN: 'var(--color-danger)',
HQ_CLEANUP: '#34d399',
MANAGER: 'var(--orange)',
USER: 'var(--cyan)',
VIEWER: 'var(--t3)',
MANAGER: 'var(--color-warning)',
USER: 'var(--color-accent)',
VIEWER: 'var(--fg-disabled)',
}
export const CUSTOM_ROLE_COLORS = [
@ -18,7 +18,7 @@ export const statusLabels: Record<string, { label: string; color: string; dot: s
PENDING: { label: '승인대기', color: 'text-yellow-400', dot: 'bg-yellow-400' },
ACTIVE: { label: '활성', color: 'text-green-400', dot: 'bg-green-400' },
LOCKED: { label: '잠김', color: 'text-red-400', dot: 'bg-red-400' },
INACTIVE: { label: '비활성', color: 'text-text-3', dot: 'bg-text-3' },
INACTIVE: { label: '비활성', color: 'text-fg-disabled', dot: 'bg-fg-disabled' },
REJECTED: { label: '거절됨', color: 'text-red-300', dot: 'bg-red-300' },
}

파일 보기

@ -28,7 +28,7 @@ export function AerialTheoryView() {
const [activePanel, setActivePanel] = useState(0)
return (
<div className="flex flex-col h-full w-full flex-1 overflow-hidden bg-bg-0">
<div className="flex flex-col h-full w-full flex-1 overflow-hidden bg-bg-base">
<div className="flex-1 overflow-y-auto px-6 py-5">
{/* 헤더 */}
<div className="flex items-center justify-between mb-5">
@ -36,21 +36,21 @@ export function AerialTheoryView() {
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center text-xl border" style={{ background: 'linear-gradient(135deg,rgba(249,115,22,.2),rgba(234,179,8,.15))', borderColor: 'rgba(249,115,22,.3)' }}>📐</div>
<div>
<div className="text-base font-bold"> · </div>
<div className="text-[10px] text-text-3 mt-0.5"> · · ESI · 10-1567431 </div>
<div className="text-[10px] text-fg-disabled mt-0.5"> · · ESI · 10-1567431 </div>
</div>
</div>
</div>
{/* 내부 네비게이션 */}
<div className="flex gap-[3px] bg-bg-3 rounded-lg p-1 mb-5 border border-border">
<div className="flex gap-[3px] bg-bg-card rounded-lg p-1 mb-5 border border-stroke">
{panels.map(p => (
<button
key={p.id}
onClick={() => setActivePanel(p.id)}
className={`flex-1 py-2 text-[10px] rounded-md border-none cursor-pointer transition-colors ${
activePanel === p.id
? 'bg-[rgba(6,182,212,.15)] text-primary-cyan font-bold'
: 'bg-transparent text-text-3 font-normal'
? 'bg-[rgba(6,182,212,.15)] text-color-accent font-bold'
: 'bg-transparent text-fg-disabled font-normal'
}`}
>
{p.icon} {p.label}
@ -68,93 +68,93 @@ export function AerialTheoryView() {
// ═══ PANEL 0: 개요 ═══
const panel0Html = `
<div style="background:linear-gradient(135deg,rgba(249,115,22,.06),rgba(234,179,8,.04));border:1px solid rgba(249,115,22,.2);border-radius:12px;padding:20px;margin-bottom:16px;position:relative;overflow:hidden">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--orange),var(--yellow),var(--cyan))"></div>
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--color-warning),var(--color-caution),var(--color-accent))"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<div style="width:30px;height:30px;border-radius:8px;background:rgba(249,115,22,.15);display:flex;align-items:center;justify-content:center;font-size:16px">📋</div>
<span style="font-size:14px;font-weight:700;color:var(--t1);font-family:var(--fK)">?</span>
<span style="font-size:14px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean)">?</span>
</div>
<div style="font-size:11px;color:var(--t2);font-family:var(--fK);line-height:1.8">
<b style="color:var(--orange)">··</b> ·· , (ESI) <b style="color:var(--cyan)"> · </b> .
<div style="font-size:11px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8">
<b style="color:var(--color-warning)">··</b> ·· , (ESI) <b style="color:var(--color-accent)"> · </b> .
</div>
</div>
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<div style="width:30px;height:30px;border-radius:8px;background:rgba(6,182,212,.15);display:flex;align-items:center;justify-content:center;font-size:16px">🎯</div>
<span style="font-size:14px;font-weight:700;color:var(--t1);font-family:var(--fK)">WING </span>
<span style="font-size:14px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean)">WING </span>
</div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:11px;color:var(--t2);font-family:var(--fK)">
<div style="padding:6px 10px;background:rgba(249,115,22,.04);border:1px solid rgba(249,115,22,.12);border-radius:6px"><span style="color:var(--orange);font-weight:700"></span> <b> ·</b> </div>
<div style="padding:6px 10px;background:rgba(6,182,212,.04);border:1px solid rgba(6,182,212,.12);border-radius:6px"><span style="color:var(--cyan);font-weight:700"></span> <b>(SST) </b> </div>
<div style="padding:6px 10px;background:rgba(34,197,94,.04);border:1px solid rgba(34,197,94,.12);border-radius:6px"><span style="color:var(--green);font-weight:700"></span> <b>ESI </b> </div>
<div style="padding:6px 10px;background:rgba(168,85,247,.04);border:1px solid rgba(168,85,247,.12);border-radius:6px"><span style="color:var(--purple);font-weight:700"></span> <b>3D </b> · </div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:11px;color:var(--fg-sub);font-family:var(--font-korean)">
<div style="padding:6px 10px;background:rgba(249,115,22,.04);border:1px solid rgba(249,115,22,.12);border-radius:6px"><span style="color:var(--color-warning);font-weight:700"></span> <b> ·</b> </div>
<div style="padding:6px 10px;background:rgba(6,182,212,.04);border:1px solid rgba(6,182,212,.12);border-radius:6px"><span style="color:var(--color-accent);font-weight:700"></span> <b>(SST) </b> </div>
<div style="padding:6px 10px;background:rgba(34,197,94,.04);border:1px solid rgba(34,197,94,.12);border-radius:6px"><span style="color:var(--color-success);font-weight:700"></span> <b>ESI </b> </div>
<div style="padding:6px 10px;background:rgba(168,85,247,.04);border:1px solid rgba(168,85,247,.12);border-radius:6px"><span style="color:var(--color-tertiary);font-weight:700"></span> <b>3D </b> · </div>
</div>
</div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:16px;margin-bottom:16px">
<div style="font-size:12px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:14px"> </div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:16px;margin-bottom:16px">
<div style="font-size:12px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:14px"> </div>
<div style="display:flex;align-items:center;justify-content:center;gap:0;flex-wrap:wrap;padding:8px 0">
<div style="padding:10px 14px;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--fK)">
<div style="padding:10px 14px;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--font-korean)">
<div style="font-size:16px;margin-bottom:4px">🛸🛰</div>
<div style="font-weight:700;color:var(--orange)"> </div>
<div style="color:var(--t3)">··</div>
<div style="font-weight:700;color:var(--color-warning)"> </div>
<div style="color:var(--fg-disabled)">··</div>
</div>
<div style="width:24px;height:1px;background:var(--bdL);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--t3);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(6,182,212,.08);border:1px solid rgba(6,182,212,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--fK)">
<div style="width:24px;height:1px;background:var(--stroke-light);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--fg-disabled);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(6,182,212,.08);border:1px solid rgba(6,182,212,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--font-korean)">
<div style="font-size:16px;margin-bottom:4px">📷🌡📡</div>
<div style="font-weight:700;color:var(--cyan)"> </div>
<div style="color:var(--t3)">·IR·SAR·SST</div>
<div style="font-weight:700;color:var(--color-accent)"> </div>
<div style="color:var(--fg-disabled)">·IR·SAR·SST</div>
</div>
<div style="width:24px;height:1px;background:var(--bdL);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--t3);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(234,179,8,.08);border:1px solid rgba(234,179,8,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--fK)">
<div style="width:24px;height:1px;background:var(--stroke-light);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--fg-disabled);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(234,179,8,.08);border:1px solid rgba(234,179,8,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--font-korean)">
<div style="font-size:16px;margin-bottom:4px">🗺📐</div>
<div style="font-weight:700;color:var(--yellow)"> </div>
<div style="color:var(--t3)">·</div>
<div style="font-weight:700;color:var(--color-caution)"> </div>
<div style="color:var(--fg-disabled)">·</div>
</div>
<div style="width:24px;height:1px;background:var(--bdL);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--t3);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(59,130,246,.08);border:2px solid rgba(59,130,246,.3);border-radius:8px;text-align:center;font-size:9px;font-family:var(--fK)">
<div style="width:24px;height:1px;background:var(--stroke-light);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--fg-disabled);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(59,130,246,.08);border:2px solid rgba(59,130,246,.3);border-radius:8px;text-align:center;font-size:9px;font-family:var(--font-korean)">
<div style="font-size:16px;margin-bottom:4px">🌊</div>
<div style="font-weight:700;color:var(--blue)"> </div>
<div style="color:var(--t3)">··SST</div>
<div style="font-weight:700;color:var(--color-info)"> </div>
<div style="color:var(--fg-disabled)">··SST</div>
</div>
<div style="width:24px;height:1px;background:var(--bdL);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--t3);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--fK)">
<div style="width:24px;height:1px;background:var(--stroke-light);position:relative"><div style="position:absolute;right:-4px;top:-4px;color:var(--fg-disabled);font-size:10px"></div></div>
<div style="padding:10px 14px;background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);border-radius:8px;text-align:center;font-size:9px;font-family:var(--font-korean)">
<div style="font-size:16px;margin-bottom:4px">🗂🚢</div>
<div style="font-weight:700;color:var(--green)"> </div>
<div style="color:var(--t3)">ESI · </div>
<div style="font-weight:700;color:var(--color-success)"> </div>
<div style="color:var(--fg-disabled)">ESI · </div>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-top:3px solid var(--orange)">
<div style="font-size:12px;font-weight:700;color:var(--orange);font-family:var(--fK);margin-bottom:8px">🛸 (UAV)</div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px"> · . · , , 3D .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--t2)">고도: 30~500m · 속도: 15~25m/s</div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--t2)">GSD: 1~5cm/px (100m )</div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--t2)">운용반경: 5~30km · 체공: 30~90</div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-top:3px solid var(--color-warning)">
<div style="font-size:12px;font-weight:700;color:var(--color-warning);font-family:var(--font-korean);margin-bottom:8px">🛸 (UAV)</div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px"> · . · , , 3D .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--fg-sub)">고도: 30~500m · 속도: 15~25m/s</div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--fg-sub)">GSD: 1~5cm/px (100m )</div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--fg-sub)">운용반경: 5~30km · 체공: 30~90</div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-top:3px solid var(--blue)">
<div style="font-size:12px;font-weight:700;color:var(--blue);font-family:var(--fK);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px"> · . ·IR·SAR·SLAR·UV . .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="padding:3px 7px;background:rgba(59,130,246,.06);border-radius:3px;color:var(--t2)">고도: 300~3,000m · 속도: 60~150m/s</div>
<div style="padding:3px 7px;background:rgba(59,130,246,.06);border-radius:3px;color:var(--t2)">탐색폭: 5~50km · 체공: 4~8</div>
<div style="padding:3px 7px;background:rgba(59,130,246,.06);border-radius:3px;color:var(--t2)">· SAR </div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-top:3px solid var(--color-info)">
<div style="font-size:12px;font-weight:700;color:var(--color-info);font-family:var(--font-korean);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px"> · . ·IR·SAR·SLAR·UV . .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:3px 7px;background:rgba(59,130,246,.06);border-radius:3px;color:var(--fg-sub)">고도: 300~3,000m · 속도: 60~150m/s</div>
<div style="padding:3px 7px;background:rgba(59,130,246,.06);border-radius:3px;color:var(--fg-sub)">탐색폭: 5~50km · 체공: 4~8</div>
<div style="padding:3px 7px;background:rgba(59,130,246,.06);border-radius:3px;color:var(--fg-sub)">· SAR </div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-top:3px solid var(--purple)">
<div style="font-size:12px;font-weight:700;color:var(--purple);font-family:var(--fK);margin-bottom:8px">🛰 </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px">· . SST(NOAA AVHRR·NGSST)·SAR(Sentinel-1)·(KOMPSAT) . .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="padding:3px 7px;background:rgba(168,85,247,.06);border-radius:3px;color:var(--t2)">NGSST: 5km · 1 </div>
<div style="padding:3px 7px;background:rgba(168,85,247,.06);border-radius:3px;color:var(--t2)">SAR: 5~25m · </div>
<div style="padding:3px 7px;background:rgba(168,85,247,.06);border-radius:3px;color:var(--t2)">KOMPSAT-5: X-band SAR 1m급</div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-top:3px solid var(--color-tertiary)">
<div style="font-size:12px;font-weight:700;color:var(--color-tertiary);font-family:var(--font-korean);margin-bottom:8px">🛰 </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px">· . SST(NOAA AVHRR·NGSST)·SAR(Sentinel-1)·(KOMPSAT) . .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:3px 7px;background:rgba(168,85,247,.06);border-radius:3px;color:var(--fg-sub)">NGSST: 5km · 1 </div>
<div style="padding:3px 7px;background:rgba(168,85,247,.06);border-radius:3px;color:var(--fg-sub)">SAR: 5~25m · </div>
<div style="padding:3px 7px;background:rgba(168,85,247,.06);border-radius:3px;color:var(--fg-sub)">KOMPSAT-5: X-band SAR 1m급</div>
</div>
</div>
</div>
@ -162,90 +162,90 @@ const panel0Html = `
// ═══ PANEL 1: 탐지 장비 ═══
const panel1Html = `
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:16px;margin-bottom:14px">
<div style="font-size:12px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:12px">🔬 </div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:16px;margin-bottom:14px">
<div style="font-size:12px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:12px">🔬 </div>
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-family:var(--fK);font-size:10px">
<table style="width:100%;border-collapse:collapse;font-family:var(--font-korean);font-size:10px">
<thead>
<tr style="background:rgba(255,255,255,.03);border-bottom:1px solid var(--bdL)">
<th style="padding:7px 10px;text-align:left;color:var(--t3);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--t3);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--t3);font-weight:600"> </th>
<th style="padding:7px 10px;text-align:center;color:var(--t3);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--t3);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--t3);font-weight:600"> </th>
<tr style="background:rgba(255,255,255,.03);border-bottom:1px solid var(--stroke-light)">
<th style="padding:7px 10px;text-align:left;color:var(--fg-disabled);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--fg-disabled);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--fg-disabled);font-weight:600"> </th>
<th style="padding:7px 10px;text-align:center;color:var(--fg-disabled);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--fg-disabled);font-weight:600"></th>
<th style="padding:7px 10px;text-align:center;color:var(--fg-disabled);font-weight:600"> </th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid rgba(255,255,255,.04)">
<td style="padding:7px 10px;font-weight:700;color:var(--orange)">(EO)</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">0.4~0.7μm</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> <br> ·</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"><br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--red);font-size:9px">· </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">··</td>
<td style="padding:7px 10px;font-weight:700;color:var(--color-warning)">(EO)</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">0.4~0.7μm</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> <br> ·</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"><br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--color-danger);font-size:9px">· </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">··</td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,.04);background:rgba(255,255,255,.01)">
<td style="padding:7px 10px;font-weight:700;color:var(--red)">(IR)</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">8~14μm</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--red);font-size:9px"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">·<br>NOAA AVHRR</td>
<td style="padding:7px 10px;font-weight:700;color:var(--color-danger)">(IR)</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">8~14μm</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--color-danger);font-size:9px"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">·<br>NOAA AVHRR</td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,.04)">
<td style="padding:7px 10px;font-weight:700;color:var(--blue)">SAR</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">1~10cm<br>()</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">· <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--red);font-size:9px"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">(Sentinel-1<br>KOMPSAT-5)</td>
<td style="padding:7px 10px;font-weight:700;color:var(--color-info)">SAR</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">1~10cm<br>()</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">· <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--color-danger);font-size:9px"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">(Sentinel-1<br>KOMPSAT-5)</td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,.04);background:rgba(255,255,255,.01)">
<td style="padding:7px 10px;font-weight:700;color:var(--purple)">SLAR/RAR</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"></td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">· <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--red);font-size:9px"> <br></td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> </td>
<td style="padding:7px 10px;font-weight:700;color:var(--color-tertiary)">SLAR/RAR</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"></td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">· <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--color-danger);font-size:9px"> <br></td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> </td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,.04)">
<td style="padding:7px 10px;font-weight:700;color:var(--yellow)">UV </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">0.3~0.4μm</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> (μm급)<br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--red);font-size:9px"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"></td>
<td style="padding:7px 10px;font-weight:700;color:var(--color-caution)">UV </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">0.3~0.4μm</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> (μm급)<br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--color-danger);font-size:9px"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"></td>
</tr>
<tr style="background:rgba(255,255,255,.01)">
<td style="padding:7px 10px;font-weight:700;color:var(--cyan)"> </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">cm<br>()</td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--cyan)"><b> </b><br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--red);font-size:9px">(50km)<br>NGSST </td>
<td style="padding:7px 10px;text-align:center;color:var(--t2)">AMSR-E <br>(NGSST )</td>
<td style="padding:7px 10px;font-weight:700;color:var(--color-accent)"> </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">cm<br>()</td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)"> <br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--color-accent)"><b> </b><br> </td>
<td style="padding:7px 10px;text-align:center;color:var(--color-danger);font-size:9px">(50km)<br>NGSST </td>
<td style="padding:7px 10px;text-align:center;color:var(--fg-sub)">AMSR-E <br>(NGSST )</td>
</tr>
</tbody>
</table>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px">
<div style="font-size:12px;font-weight:700;color:var(--cyan);font-family:var(--fK);margin-bottom:10px">🌡 NGSST (New Generation SST) 10-1567431 </div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px">
<div style="font-size:12px;font-weight:700;color:var(--color-accent);font-family:var(--font-korean);margin-bottom:10px">🌡 NGSST (New Generation SST) 10-1567431 </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.8">
Kawamura . <b style="color:var(--cyan)">(AVHRR·MODIS)</b> <b style="color:var(--orange)">(AMSR-E)</b> .
<div style="margin-top:8px;background:var(--bg0);border-radius:6px;padding:10px;font-family:var(--fM);font-size:10px;line-height:2;color:var(--t1)">
SST() = <span style="color:var(--orange)">0.15</span> × DN <span style="color:var(--red)">3.0</span><br>
<span style="color:var(--t3);font-size:9px">DN: 바이너리 1byte </span>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8">
Kawamura . <b style="color:var(--color-accent)">(AVHRR·MODIS)</b> <b style="color:var(--color-warning)">(AMSR-E)</b> .
<div style="margin-top:8px;background:var(--bg-base);border-radius:6px;padding:10px;font-family:var(--font-mono);font-size:10px;line-height:2;color:var(--fg-default)">
SST() = <span style="color:var(--color-warning)">0.15</span> × DN <span style="color:var(--color-danger)">3.0</span><br>
<span style="color:var(--fg-disabled);font-size:9px">DN: 바이너리 1byte </span>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:9px;font-family:var(--fK)">
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--cyan);font-weight:700"></span> : 116~166°E, 13~63°N ( 50×50°)</div>
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--cyan);font-weight:700"></span> : 3( 5km) · 1000×1000</div>
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--cyan);font-weight:700"></span> : 12 FTP · 5 </div>
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--cyan);font-weight:700"></span> : Akima(1978) 2 5 500m </div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><span style="color:var(--orange);font-weight:700">WING </span> : ·· </div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--color-accent);font-weight:700"></span> : 116~166°E, 13~63°N ( 50×50°)</div>
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--color-accent);font-weight:700"></span> : 3( 5km) · 1000×1000</div>
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--color-accent);font-weight:700"></span> : 12 FTP · 5 </div>
<div style="padding:6px 9px;background:rgba(6,182,212,.05);border:1px solid rgba(6,182,212,.12);border-radius:5px"><span style="color:var(--color-accent);font-weight:700"></span> : Akima(1978) 2 5 500m </div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><span style="color:var(--color-warning);font-weight:700">WING </span> : ·· </div>
</div>
</div>
</div>
@ -254,55 +254,55 @@ const panel1Html = `
// ═══ PANEL 2: 원격탐사 ═══
const panel2Html = `
<div style="background:linear-gradient(135deg,rgba(168,85,247,.06),rgba(59,130,246,.04));border:1px solid rgba(168,85,247,.2);border-radius:12px;padding:16px;margin-bottom:14px;position:relative;overflow:hidden">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--purple),var(--blue))"></div>
<div style="font-size:13px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:10px">🛰 </div>
<div style="font-size:11px;color:var(--t2);font-family:var(--fK);line-height:1.8">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--color-tertiary),var(--color-info))"></div>
<div style="font-size:13px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:10px">🛰 </div>
<div style="font-size:11px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8">
·· . · ·· .
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-left:3px solid var(--orange)">
<div style="font-size:11px;font-weight:700;color:var(--orange);font-family:var(--fK);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px">
<b style="color:var(--t1)"> </b> . 10~12μm <b style="color:var(--orange)">(Brightness Temperature) </b> .
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-left:3px solid var(--color-warning)">
<div style="font-size:11px;font-weight:700;color:var(--color-warning);font-family:var(--font-korean);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px">
<b style="color:var(--fg-default)"> </b> . 10~12μm <b style="color:var(--color-warning)">(Brightness Temperature) </b> .
</div>
<div style="background:var(--bg0);border-radius:5px;padding:8px;font-size:9px;color:var(--t3);font-family:var(--fK)"> (NGSST )</div>
<div style="background:var(--bg-base);border-radius:5px;padding:8px;font-size:9px;color:var(--fg-disabled);font-family:var(--font-korean)"> (NGSST )</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-left:3px solid var(--blue)">
<div style="font-size:11px;font-weight:700;color:var(--blue);font-family:var(--fK);margin-bottom:8px"> SAR </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px">
<b style="color:var(--t1)"> </b> . SAR에서 <b style="color:var(--blue)">(Bragg) </b> .
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-left:3px solid var(--color-info)">
<div style="font-size:11px;font-weight:700;color:var(--color-info);font-family:var(--font-korean);margin-bottom:8px"> SAR </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px">
<b style="color:var(--fg-default)"> </b> . SAR에서 <b style="color:var(--color-info)">(Bragg) </b> .
</div>
<div style="background:var(--bg0);border-radius:5px;padding:8px;font-size:9px;color:var(--t3);font-family:var(--fK)">활용: Sentinel-1(C-band) · KOMPSAT-5(X-band) · ALOS PALSAR(L-band)</div>
<div style="background:var(--bg-base);border-radius:5px;padding:8px;font-size:9px;color:var(--fg-disabled);font-family:var(--font-korean)">활용: Sentinel-1(C-band) · KOMPSAT-5(X-band) · ALOS PALSAR(L-band)</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-left:3px solid var(--yellow)">
<div style="font-size:11px;font-weight:700;color:var(--yellow);font-family:var(--fK);margin-bottom:8px"> UV </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px">
(310~400nm) <b style="color:var(--yellow)"> </b>. . μm .
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-left:3px solid var(--color-caution)">
<div style="font-size:11px;font-weight:700;color:var(--color-caution);font-family:var(--font-korean);margin-bottom:8px"> UV </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px">
(310~400nm) <b style="color:var(--color-caution)"> </b>. . μm .
</div>
<div style="background:var(--bg0);border-radius:5px;padding:8px;font-size:9px;color:var(--t3);font-family:var(--fK)">적용: 해경 · </div>
<div style="background:var(--bg-base);border-radius:5px;padding:8px;font-size:9px;color:var(--fg-disabled);font-family:var(--font-korean)">적용: 해경 · </div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-left:3px solid var(--cyan)">
<div style="font-size:11px;font-weight:700;color:var(--cyan);font-family:var(--fK);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px">
. <b style="color:var(--cyan)"> </b> . · .
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-left:3px solid var(--color-accent)">
<div style="font-size:11px;font-weight:700;color:var(--color-accent);font-family:var(--font-korean);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px">
. <b style="color:var(--color-accent)"> </b> . · .
</div>
<div style="background:var(--bg0);border-radius:5px;padding:8px;font-size:9px;color:var(--t3);font-family:var(--fK)"> (50km) (NGSST )</div>
<div style="background:var(--bg-base);border-radius:5px;padding:8px;font-size:9px;color:var(--fg-disabled);font-family:var(--font-korean)"> (50km) (NGSST )</div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px">
<div style="font-size:12px;font-weight:700;color:var(--green);font-family:var(--fK);margin-bottom:10px">🗺 (ENC) 10-1567431 </div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px">
<div style="font-size:12px;font-weight:700;color:var(--color-success);font-family:var(--font-korean);margin-bottom:10px">🗺 (ENC) 10-1567431 </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.8">
<b style="color:var(--green)">(ENC) </b> , , . 300 <b style="color:var(--green)">Akima </b> 15 .
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8">
<b style="color:var(--color-success)">(ENC) </b> , , . 300 <b style="color:var(--color-success)">Akima </b> 15 .
</div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:9px;font-family:var(--fK)">
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--green);font-weight:700"></span> : ENC </div>
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--green);font-weight:700"></span> : TIN(Triangulated Irregular Network) </div>
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--green);font-weight:700"></span> : Akima 2 5 (21 )</div>
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--green);font-weight:700"></span> : 15(463m) · 3,225,600 </div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--color-success);font-weight:700"></span> : ENC </div>
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--color-success);font-weight:700"></span> : TIN(Triangulated Irregular Network) </div>
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--color-success);font-weight:700"></span> : Akima 2 5 (21 )</div>
<div style="padding:6px 9px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.12);border-radius:5px"><span style="color:var(--color-success);font-weight:700"></span> : 15(463m) · 3,225,600 </div>
</div>
</div>
</div>
@ -311,104 +311,104 @@ const panel2Html = `
// ═══ PANEL 3: ESI 방제지도 ═══
const panel3Html = `
<div style="background:linear-gradient(135deg,rgba(34,197,94,.06),rgba(234,179,8,.04));border:1px solid rgba(34,197,94,.2);border-radius:12px;padding:16px;margin-bottom:14px;position:relative;overflow:hidden">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--green),var(--yellow))"></div>
<div style="font-size:13px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:8px">🗺 ESI (Environmental Sensitivity Index Map)</div>
<div style="font-size:11px;color:var(--t2);font-family:var(--fK);line-height:1.8">
ESI ( 10-1567431) . ·· <b style="color:var(--green)"> </b> . 1999~2002 , ······ .
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--color-success),var(--color-caution))"></div>
<div style="font-size:13px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:8px">🗺 ESI (Environmental Sensitivity Index Map)</div>
<div style="font-size:11px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8">
ESI ( 10-1567431) . ·· <b style="color:var(--color-success)"> </b> . 1999~2002 , ······ .
</div>
<div style="margin-top:8px;padding:7px 10px;background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.15);border-radius:5px;font-size:9px;color:var(--t3);font-family:var(--fK)">📚 원전: NOAA ESI Mapping Program · 적용: 해양수산부·</div>
<div style="margin-top:8px;padding:7px 10px;background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.15);border-radius:5px;font-size:9px;color:var(--fg-disabled);font-family:var(--font-korean)">📚 원전: NOAA ESI Mapping Program · 적용: 해양수산부·</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:14px">
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-top:3px solid var(--blue)">
<div style="font-size:11px;font-weight:700;color:var(--blue);font-family:var(--fK);margin-bottom:8px"> (Shoreline)</div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px"> 1~10 . · .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--t2)">ESI 1~2: 노출 · ( )</div>
<div style="padding:3px 7px;background:rgba(234,179,8,.06);border-radius:3px;color:var(--t2)">ESI 5~7: 자갈· ()</div>
<div style="padding:3px 7px;background:rgba(239,68,68,.06);border-radius:3px;color:var(--t2)">ESI 8~10: 조간대·· ()</div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-top:3px solid var(--color-info)">
<div style="font-size:11px;font-weight:700;color:var(--color-info);font-family:var(--font-korean);margin-bottom:8px"> (Shoreline)</div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px"> 1~10 . · .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--fg-sub)">ESI 1~2: 노출 · ( )</div>
<div style="padding:3px 7px;background:rgba(234,179,8,.06);border-radius:3px;color:var(--fg-sub)">ESI 5~7: 자갈· ()</div>
<div style="padding:3px 7px;background:rgba(239,68,68,.06);border-radius:3px;color:var(--fg-sub)">ESI 8~10: 조간대·· ()</div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-top:3px solid var(--green)">
<div style="font-size:11px;font-weight:700;color:var(--green);font-family:var(--fK);margin-bottom:8px"> (Biological Resources)</div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px"> ·· .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--t2)">· ·</div>
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--t2)"> · </div>
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--t2)"> </div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-top:3px solid var(--color-success)">
<div style="font-size:11px;font-weight:700;color:var(--color-success);font-family:var(--font-korean);margin-bottom:8px"> (Biological Resources)</div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px"> ·· .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--fg-sub)">· ·</div>
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--fg-sub)"> · </div>
<div style="padding:3px 7px;background:rgba(34,197,94,.06);border-radius:3px;color:var(--fg-sub)"> </div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-top:3px solid var(--orange)">
<div style="font-size:11px;font-weight:700;color:var(--orange);font-family:var(--fK);margin-bottom:8px"> · (Human-Use)</div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px">· .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--t2)">·· </div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--t2)">·· </div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--t2)">··</div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-top:3px solid var(--color-warning)">
<div style="font-size:11px;font-weight:700;color:var(--color-warning);font-family:var(--font-korean);margin-bottom:8px"> · (Human-Use)</div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px">· .</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--fg-sub)">·· </div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--fg-sub)">·· </div>
<div style="padding:3px 7px;background:rgba(249,115,22,.06);border-radius:3px;color:var(--fg-sub)">··</div>
</div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:8px">📏 ESI ( 10-1567431)</div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.8">
ESI 1999~2002( 3) <b style="color:var(--t1)">25,000:1 </b> . · (ENC) . ESI DB와 .
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:8px">📏 ESI ( 10-1567431)</div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8">
ESI 1999~2002( 3) <b style="color:var(--fg-default)">25,000:1 </b> . · (ENC) . ESI DB와 .
</div>
</div>
`
// ═══ PANEL 4: 면적 산정 ═══
const panel4Html = `
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:16px;margin-bottom:14px">
<div style="font-size:12px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:12px">📏 </div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:16px;margin-bottom:14px">
<div style="font-size:12px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:12px">📏 </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
<div style="background:var(--bg0);border:1px solid rgba(249,115,22,.2);border-radius:8px;padding:12px;border-left:3px solid var(--orange)">
<div style="font-size:11px;font-weight:700;color:var(--orange);font-family:var(--fK);margin-bottom:8px"> (Pixel Classification)</div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px"> <b style="color:var(--t1)">··</b> / . × GSD² = .</div>
<div style="background:rgba(255,255,255,.03);border-radius:5px;padding:8px;font-family:var(--fM);font-size:10px;color:var(--t1);line-height:1.8">A = N<sub>oil</sub> × (GSD)<sup>2</sup><br><span style="color:var(--t3);font-size:9px">N: 기름 , GSD: 지상 </span></div>
<div style="background:var(--bg-base);border:1px solid rgba(249,115,22,.2);border-radius:8px;padding:12px;border-left:3px solid var(--color-warning)">
<div style="font-size:11px;font-weight:700;color:var(--color-warning);font-family:var(--font-korean);margin-bottom:8px"> (Pixel Classification)</div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px"> <b style="color:var(--fg-default)">··</b> / . × GSD² = .</div>
<div style="background:rgba(255,255,255,.03);border-radius:5px;padding:8px;font-family:var(--font-mono);font-size:10px;color:var(--fg-default);line-height:1.8">A = N<sub>oil</sub> × (GSD)<sup>2</sup><br><span style="color:var(--fg-disabled);font-size:9px">N: 기름 , GSD: 지상 </span></div>
</div>
<div style="background:var(--bg0);border:1px solid rgba(6,182,212,.2);border-radius:8px;padding:12px;border-left:3px solid var(--cyan)">
<div style="font-size:11px;font-weight:700;color:var(--cyan);font-family:var(--fK);margin-bottom:8px"> (Spectral Index)</div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7;margin-bottom:8px"> <b style="color:var(--t1)"> </b> .</div>
<div style="background:rgba(255,255,255,.03);border-radius:5px;padding:8px;font-family:var(--fM);font-size:10px;color:var(--t1);line-height:1.8">OSDI = (B<sub>NIR</sub>B<sub>Red</sub>) / (B<sub>NIR</sub>+B<sub>Red</sub>)<br><span style="color:var(--t3);font-size:9px">Oil Spill Detection Index</span></div>
<div style="background:var(--bg-base);border:1px solid rgba(6,182,212,.2);border-radius:8px;padding:12px;border-left:3px solid var(--color-accent)">
<div style="font-size:11px;font-weight:700;color:var(--color-accent);font-family:var(--font-korean);margin-bottom:8px"> (Spectral Index)</div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7;margin-bottom:8px"> <b style="color:var(--fg-default)"> </b> .</div>
<div style="background:rgba(255,255,255,.03);border-radius:5px;padding:8px;font-family:var(--font-mono);font-size:10px;color:var(--fg-default);line-height:1.8">OSDI = (B<sub>NIR</sub>B<sub>Red</sub>) / (B<sub>NIR</sub>+B<sub>Red</sub>)<br><span style="color:var(--fg-disabled);font-size:9px">Oil Spill Detection Index</span></div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div style="background:var(--bg0);border:1px solid rgba(59,130,246,.2);border-radius:8px;padding:12px;border-left:3px solid var(--blue)">
<div style="font-size:11px;font-weight:700;color:var(--blue);font-family:var(--fK);margin-bottom:8px"> SAR </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7">SAR (σ°) . , 3m/s , <b style="color:var(--red)">False Alarm</b> .</div>
<div style="margin-top:6px;background:rgba(255,255,255,.03);border-radius:5px;padding:7px;font-family:var(--fM);font-size:10px;color:var(--t1)">Oil = {(x,y) | σ°(x,y) &lt; T<sub>th</sub>}<br><span style="color:var(--t3);font-size:9px">T<sub>th</sub>: ( )</span></div>
<div style="background:var(--bg-base);border:1px solid rgba(59,130,246,.2);border-radius:8px;padding:12px;border-left:3px solid var(--color-info)">
<div style="font-size:11px;font-weight:700;color:var(--color-info);font-family:var(--font-korean);margin-bottom:8px"> SAR </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7">SAR (σ°) . , 3m/s , <b style="color:var(--color-danger)">False Alarm</b> .</div>
<div style="margin-top:6px;background:rgba(255,255,255,.03);border-radius:5px;padding:7px;font-family:var(--font-mono);font-size:10px;color:var(--fg-default)">Oil = {(x,y) | σ°(x,y) &lt; T<sub>th</sub>}<br><span style="color:var(--fg-disabled);font-size:9px">T<sub>th</sub>: ( )</span></div>
</div>
<div style="background:var(--bg0);border:1px solid rgba(34,197,94,.2);border-radius:8px;padding:12px;border-left:3px solid var(--green)">
<div style="font-size:11px;font-weight:700;color:var(--green);font-family:var(--fK);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--t2);font-family:var(--fK);line-height:1.7">(A) (d) (t) (V) .</div>
<div style="margin-top:6px;background:rgba(255,255,255,.03);border-radius:5px;padding:7px;font-family:var(--fM);font-size:10px;color:var(--t1);line-height:1.8">V = A × d / (1 f<sub>e</sub>(t))<br><span style="color:var(--t3);font-size:9px">f<sub>e</sub>: (Stiver &amp; Mackay 1984)</span></div>
<div style="background:var(--bg-base);border:1px solid rgba(34,197,94,.2);border-radius:8px;padding:12px;border-left:3px solid var(--color-success)">
<div style="font-size:11px;font-weight:700;color:var(--color-success);font-family:var(--font-korean);margin-bottom:8px"> </div>
<div style="font-size:10px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7">(A) (d) (t) (V) .</div>
<div style="margin-top:6px;background:rgba(255,255,255,.03);border-radius:5px;padding:7px;font-family:var(--font-mono);font-size:10px;color:var(--fg-default);line-height:1.8">V = A × d / (1 f<sub>e</sub>(t))<br><span style="color:var(--fg-disabled);font-size:9px">f<sub>e</sub>: (Stiver &amp; Mackay 1984)</span></div>
</div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--yellow);font-family:var(--fK);margin-bottom:10px">🎨 (Bonn Agreement Color Code)</div>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:6px;font-size:9px;font-family:var(--fK)">
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--color-caution);font-family:var(--font-korean);margin-bottom:10px">🎨 (Bonn Agreement Color Code)</div>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:6px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;text-align:center">
<div style="width:20px;height:20px;background:rgba(200,200,255,.3);border-radius:3px;margin:0 auto 4px"></div>
<div style="font-weight:700;color:var(--t1)"></div><div style="color:var(--t3)">&lt; 0.1μm</div><div style="color:var(--t3)"></div>
<div style="font-weight:700;color:var(--fg-default)"></div><div style="color:var(--fg-disabled)">&lt; 0.1μm</div><div style="color:var(--fg-disabled)"></div>
</div>
<div style="padding:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;text-align:center">
<div style="width:20px;height:20px;background:rgba(180,220,180,.4);border-radius:3px;margin:0 auto 4px"></div>
<div style="font-weight:700;color:var(--t1)"></div><div style="color:var(--t3)">0.1~0.3μm</div><div style="color:var(--t3)"></div>
<div style="font-weight:700;color:var(--fg-default)"></div><div style="color:var(--fg-disabled)">0.1~0.3μm</div><div style="color:var(--fg-disabled)"></div>
</div>
<div style="padding:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;text-align:center">
<div style="width:20px;height:20px;background:rgba(200,160,80,.5);border-radius:3px;margin:0 auto 4px"></div>
<div style="font-weight:700;color:var(--t1)"></div><div style="color:var(--t3)">0.3~5μm</div><div style="color:var(--t3)"></div>
<div style="font-weight:700;color:var(--fg-default)"></div><div style="color:var(--fg-disabled)">0.3~5μm</div><div style="color:var(--fg-disabled)"></div>
</div>
<div style="padding:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;text-align:center">
<div style="width:20px;height:20px;background:rgba(120,90,40,.7);border-radius:3px;margin:0 auto 4px"></div>
<div style="font-weight:700;color:var(--orange)"></div><div style="color:var(--t3)">5~200μm</div><div style="color:var(--t3)"></div>
<div style="font-weight:700;color:var(--color-warning)"></div><div style="color:var(--fg-disabled)">5~200μm</div><div style="color:var(--fg-disabled)"></div>
</div>
<div style="padding:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;text-align:center">
<div style="width:20px;height:20px;background:rgba(40,40,40,.9);border-radius:3px;margin:0 auto 4px"></div>
<div style="font-weight:700;color:var(--t1)"></div><div style="color:var(--t3)">&gt;200μm</div><div style="color:var(--t3)"></div>
<div style="font-weight:700;color:var(--fg-default)"></div><div style="color:var(--fg-disabled)">&gt;200μm</div><div style="color:var(--fg-disabled)"></div>
</div>
</div>
</div>
@ -417,57 +417,57 @@ const panel4Html = `
// ═══ PANEL 5: 확산예측 연계 ═══
const panel5Html = `
<div style="background:linear-gradient(135deg,rgba(59,130,246,.06),rgba(6,182,212,.04));border:1px solid rgba(59,130,246,.2);border-radius:12px;padding:16px;margin-bottom:14px;position:relative;overflow:hidden">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--blue),var(--cyan))"></div>
<div style="font-size:13px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:8px">🔗 </div>
<div style="font-size:11px;color:var(--t2);font-family:var(--fK);line-height:1.8">
( 10-1567431) <b style="color:var(--cyan)">(SST)</b> · , .
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--color-info),var(--color-accent))"></div>
<div style="font-size:13px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:8px">🔗 </div>
<div style="font-size:11px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8">
( 10-1567431) <b style="color:var(--color-accent)">(SST)</b> · , .
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-left:3px solid var(--orange)">
<div style="font-size:11px;font-weight:700;color:var(--orange);font-family:var(--fK);margin-bottom:8px">📡 </div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:10px;font-family:var(--fK)">
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">📍 </div><div style="color:var(--t3)">· GPS </div></div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">🛢 </div><div style="color:var(--t3)">× </div></div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">🌡 SST </div><div style="color:var(--t3)">NGSST FTP Akima </div></div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">🎨 </div><div style="color:var(--t3)"> </div></div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-left:3px solid var(--color-warning)">
<div style="font-size:11px;font-weight:700;color:var(--color-warning);font-family:var(--font-korean);margin-bottom:8px">📡 </div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:10px;font-family:var(--font-korean)">
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">📍 </div><div style="color:var(--fg-disabled)">· GPS </div></div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">🛢 </div><div style="color:var(--fg-disabled)">× </div></div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">🌡 SST </div><div style="color:var(--fg-disabled)">NGSST FTP Akima </div></div>
<div style="padding:6px 9px;background:rgba(249,115,22,.05);border:1px solid rgba(249,115,22,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">🎨 </div><div style="color:var(--fg-disabled)"> </div></div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px;border-left:3px solid var(--blue)">
<div style="font-size:11px;font-weight:700;color:var(--blue);font-family:var(--fK);margin-bottom:8px">🔄 </div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:10px;font-family:var(--fK)">
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">🗺 </div><div style="color:var(--t3)"> </div></div>
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">📊 </div><div style="color:var(--t3)"> </div></div>
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">🏖 ESI </div><div style="color:var(--t3)">×ESI ESI </div></div>
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--t1);margin-bottom:1px">🚢 </div><div style="color:var(--t3)"> · </div></div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px;border-left:3px solid var(--color-info)">
<div style="font-size:11px;font-weight:700;color:var(--color-info);font-family:var(--font-korean);margin-bottom:8px">🔄 </div>
<div style="display:flex;flex-direction:column;gap:5px;font-size:10px;font-family:var(--font-korean)">
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">🗺 </div><div style="color:var(--fg-disabled)"> </div></div>
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">📊 </div><div style="color:var(--fg-disabled)"> </div></div>
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">🏖 ESI </div><div style="color:var(--fg-disabled)">×ESI ESI </div></div>
<div style="padding:6px 9px;background:rgba(59,130,246,.05);border:1px solid rgba(59,130,246,.12);border-radius:5px"><div style="font-weight:700;color:var(--fg-default);margin-bottom:1px">🚢 </div><div style="color:var(--fg-disabled)"> · </div></div>
</div>
</div>
</div>
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:12px">📡 10-1567431 ( 9)</div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:12px">📡 10-1567431 ( 9)</div>
<div style="display:flex;align-items:flex-start;justify-content:center;gap:0;padding:8px 0">
<div style="display:flex;flex-direction:column;gap:6px;align-items:center">
<div style="padding:8px 12px;background:rgba(6,182,212,.08);border:1px solid rgba(6,182,212,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--fK)"><div style="font-weight:700;color:var(--cyan)"> </div><div style="color:var(--t3)">··<br></div></div>
<div style="padding:8px 12px;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--fK)"><div style="font-weight:700;color:var(--orange)"> </div><div style="color:var(--t3)">SST(NGSST)<br> FTP</div></div>
<div style="padding:8px 12px;background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--fK)"><div style="font-weight:700;color:var(--green)"></div><div style="color:var(--t3)"> <br></div></div>
<div style="padding:8px 12px;background:rgba(6,182,212,.08);border:1px solid rgba(6,182,212,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--font-korean)"><div style="font-weight:700;color:var(--color-accent)"> </div><div style="color:var(--fg-disabled)">··<br></div></div>
<div style="padding:8px 12px;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--font-korean)"><div style="font-weight:700;color:var(--color-warning)"> </div><div style="color:var(--fg-disabled)">SST(NGSST)<br> FTP</div></div>
<div style="padding:8px 12px;background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--font-korean)"><div style="font-weight:700;color:var(--color-success)"></div><div style="color:var(--fg-disabled)"> <br></div></div>
</div>
<div style="display:flex;flex-direction:column;justify-content:center;height:160px;padding:0 12px;gap:2px">
<div style="width:30px;height:1px;background:var(--bdL)"></div>
<div style="width:30px;height:1px;background:var(--bdL)"></div>
<div style="width:30px;height:1px;background:var(--bdL)"></div>
<div style="width:30px;height:1px;background:var(--stroke-light)"></div>
<div style="width:30px;height:1px;background:var(--stroke-light)"></div>
<div style="width:30px;height:1px;background:var(--stroke-light)"></div>
</div>
<div style="padding:16px 20px;background:rgba(168,85,247,.08);border:2px solid rgba(168,85,247,.3);border-radius:10px;text-align:center;font-size:10px;font-family:var(--fK);align-self:center">
<div style="padding:16px 20px;background:rgba(168,85,247,.08);border:2px solid rgba(168,85,247,.3);border-radius:10px;text-align:center;font-size:10px;font-family:var(--font-korean);align-self:center">
<div style="font-size:16px;margin-bottom:4px">🖥</div>
<div style="font-weight:700;color:var(--purple)">(WING)</div><div style="color:var(--t3);font-size:9px"> ·<br> </div>
<div style="font-weight:700;color:var(--color-tertiary)">(WING)</div><div style="color:var(--fg-disabled);font-size:9px"> ·<br> </div>
</div>
<div style="width:30px;height:1px;background:var(--bdL);align-self:center"></div>
<div style="width:30px;height:1px;background:var(--stroke-light);align-self:center"></div>
<div style="display:flex;flex-direction:column;gap:6px;align-items:center;align-self:center">
<div style="padding:8px 12px;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--fK)"><div style="font-weight:700;color:var(--orange)"></div><div style="color:var(--t3)">·<br> </div></div>
<div style="padding:8px 12px;background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.2);border-radius:7px;text-align:center;font-size:9px;font-family:var(--font-korean)"><div style="font-weight:700;color:var(--color-warning)"></div><div style="color:var(--fg-disabled)">·<br> </div></div>
</div>
</div>
<div style="font-size:9px;color:var(--t3);font-family:var(--fK);text-align:center;margin-top:8px">·· CHARRY + (S10S20S30)</div>
<div style="font-size:9px;color:var(--fg-disabled);font-family:var(--font-korean);text-align:center;margin-top:8px">·· CHARRY + (S10S20S30)</div>
</div>
`
@ -476,108 +476,108 @@ const panel6Html = `
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:36px;height:36px;border-radius:9px;background:linear-gradient(135deg,rgba(234,179,8,.2),rgba(249,115,22,.15));border:1px solid rgba(234,179,8,.3);display:flex;align-items:center;justify-content:center;font-size:18px">📜</div>
<div>
<div style="font-size:14px;font-weight:700;color:var(--t1);font-family:var(--fK)"> </div>
<div style="font-size:9px;color:var(--t3);font-family:var(--fK);margin-top:1px">WING 2 </div>
<div style="font-size:14px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean)"> </div>
<div style="font-size:9px;color:var(--fg-disabled);font-family:var(--font-korean);margin-top:1px">WING 2 </div>
</div>
</div>
<!-- 1: 10-1567431 -->
<div style="background:linear-gradient(135deg,rgba(6,182,212,.05),rgba(34,197,94,.03));border:1px solid rgba(6,182,212,.25);border-radius:12px;padding:18px;margin-bottom:16px;position:relative;overflow:hidden">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--cyan),var(--green))"></div>
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--color-accent),var(--color-success))"></div>
<div style="display:flex;align-items:flex-start;gap:14px;margin-bottom:14px">
<div style="padding:8px 12px;background:rgba(6,182,212,.12);border:1px solid rgba(6,182,212,.3);border-radius:8px;text-align:center;white-space:nowrap;flex-shrink:0">
<div style="font-size:8px;color:var(--t3);font-family:var(--fK)"> </div>
<div style="font-size:13px;font-weight:800;color:var(--cyan);font-family:var(--fM)">10-1567431</div>
<div style="font-size:8px;color:var(--t3);font-family:var(--fK);margin-top:2px">등록: 2015.11.03</div>
<div style="font-size:8px;color:var(--fg-disabled);font-family:var(--font-korean)"> </div>
<div style="font-size:13px;font-weight:800;color:var(--color-accent);font-family:var(--font-mono)">10-1567431</div>
<div style="font-size:8px;color:var(--fg-disabled);font-family:var(--font-korean);margin-top:2px">등록: 2015.11.03</div>
</div>
<div style="flex:1">
<div style="font-size:13px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:5px;line-height:1.5"> </div>
<div style="font-size:9px;color:var(--t2);font-family:var(--fK);line-height:1.8"><span style="color:var(--cyan)"></span> : | <span style="color:var(--cyan)"></span> : · · · </div>
<div style="font-size:13px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:5px;line-height:1.5"> </div>
<div style="font-size:9px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8"><span style="color:var(--color-accent)"></span> : | <span style="color:var(--color-accent)"></span> : · · · </div>
</div>
</div>
<div style="background:var(--bg0);border:1px solid rgba(6,182,212,.15);border-radius:8px;padding:12px;margin-bottom:12px">
<div style="font-size:10px;font-weight:700;color:var(--cyan);font-family:var(--fK);margin-bottom:9px">📋 1 ESI 3</div>
<div style="display:flex;flex-direction:column;gap:6px;font-size:10px;font-family:var(--fK)">
<div style="display:flex;gap:10px;align-items:flex-start"><div style="min-width:32px;height:22px;background:rgba(249,115,22,.15);border:1px solid rgba(249,115,22,.3);border-radius:5px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--orange);font-size:10px;flex-shrink:0">S10</div><div style="color:var(--t2);line-height:1.7"><b style="color:var(--t1)"> </b> ·· . FTP로 +(NGSST)+ </div></div>
<div style="display:flex;gap:10px;align-items:flex-start"><div style="min-width:32px;height:22px;background:rgba(6,182,212,.15);border:1px solid rgba(6,182,212,.3);border-radius:5px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--cyan);font-size:10px;flex-shrink:0">S20</div><div style="color:var(--t2);line-height:1.7"><b style="color:var(--t1)">· </b> CHARRY모델 + (0.029×Vw, θw+18.6°)</div></div>
<div style="display:flex;gap:10px;align-items:flex-start"><div style="min-width:32px;height:22px;background:rgba(34,197,94,.15);border:1px solid rgba(34,197,94,.3);border-radius:5px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--green);font-size:10px;flex-shrink:0">S30</div><div style="color:var(--t2);line-height:1.7"><b style="color:var(--t1)"> </b> Monte Carlo + fBm + 5 ESI </div></div>
<div style="background:var(--bg-base);border:1px solid rgba(6,182,212,.15);border-radius:8px;padding:12px;margin-bottom:12px">
<div style="font-size:10px;font-weight:700;color:var(--color-accent);font-family:var(--font-korean);margin-bottom:9px">📋 1 ESI 3</div>
<div style="display:flex;flex-direction:column;gap:6px;font-size:10px;font-family:var(--font-korean)">
<div style="display:flex;gap:10px;align-items:flex-start"><div style="min-width:32px;height:22px;background:rgba(249,115,22,.15);border:1px solid rgba(249,115,22,.3);border-radius:5px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--color-warning);font-size:10px;flex-shrink:0">S10</div><div style="color:var(--fg-sub);line-height:1.7"><b style="color:var(--fg-default)"> </b> ·· . FTP로 +(NGSST)+ </div></div>
<div style="display:flex;gap:10px;align-items:flex-start"><div style="min-width:32px;height:22px;background:rgba(6,182,212,.15);border:1px solid rgba(6,182,212,.3);border-radius:5px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--color-accent);font-size:10px;flex-shrink:0">S20</div><div style="color:var(--fg-sub);line-height:1.7"><b style="color:var(--fg-default)">· </b> CHARRY모델 + (0.029×Vw, θw+18.6°)</div></div>
<div style="display:flex;gap:10px;align-items:flex-start"><div style="min-width:32px;height:22px;background:rgba(34,197,94,.15);border:1px solid rgba(34,197,94,.3);border-radius:5px;display:flex;align-items:center;justify-content:center;font-weight:800;color:var(--color-success);font-size:10px;flex-shrink:0">S30</div><div style="color:var(--fg-sub);line-height:1.7"><b style="color:var(--fg-default)"> </b> Monte Carlo + fBm + 5 ESI </div></div>
</div>
</div>
<div style="background:var(--bg0);border:1px solid rgba(34,197,94,.15);border-radius:8px;padding:11px;margin-bottom:10px">
<div style="font-size:10px;font-weight:700;color:var(--green);font-family:var(--fK);margin-bottom:7px">🌀 fBm | <span style="color:var(--t2);font-weight:400">σ²(t) = A·t^m, m=0.45~2.46</span></div>
<div style="font-size:9px;color:var(--t2);font-family:var(--fK);line-height:1.7"> (fBm) . (isotropic) .</div>
<div style="background:var(--bg-base);border:1px solid rgba(34,197,94,.15);border-radius:8px;padding:11px;margin-bottom:10px">
<div style="font-size:10px;font-weight:700;color:var(--color-success);font-family:var(--font-korean);margin-bottom:7px">🌀 fBm | <span style="color:var(--fg-sub);font-weight:400">σ²(t) = A·t^m, m=0.45~2.46</span></div>
<div style="font-size:9px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.7"> (fBm) . (isotropic) .</div>
</div>
<div style="background:var(--bg0);border:1px solid rgba(249,115,22,.15);border-radius:8px;padding:12px">
<div style="font-size:10px;font-weight:700;color:var(--orange);font-family:var(--fK);margin-bottom:9px">🧪 (Weathering) 5</div>
<div style="display:flex;flex-direction:column;gap:4px;font-size:9px;font-family:var(--fK)">
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--orange)"> </div><div style="color:var(--t2);line-height:1.6">Fay(1969): -. Mackay et al.(1980) -</div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--yellow)"> </div><div style="color:var(--t2);line-height:1.6">Stiver &amp; Mackay(1984) . ~10 25%</div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--cyan)"> </div><div style="color:var(--t2);line-height:1.6"> . · . 15%</div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--blue)"> </div><div style="color:var(--t2);line-height:1.6">Water-in-oil. Mackay et al.(1980) · </div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--t2)"> </div><div style="color:var(--t2);line-height:1.6">· . = </div></div>
<div style="background:var(--bg-base);border:1px solid rgba(249,115,22,.15);border-radius:8px;padding:12px">
<div style="font-size:10px;font-weight:700;color:var(--color-warning);font-family:var(--font-korean);margin-bottom:9px">🧪 (Weathering) 5</div>
<div style="display:flex;flex-direction:column;gap:4px;font-size:9px;font-family:var(--font-korean)">
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--color-warning)"> </div><div style="color:var(--fg-sub);line-height:1.6">Fay(1969): -. Mackay et al.(1980) -</div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--color-caution)"> </div><div style="color:var(--fg-sub);line-height:1.6">Stiver &amp; Mackay(1984) . ~10 25%</div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--color-accent)"> </div><div style="color:var(--fg-sub);line-height:1.6"> . · . 15%</div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--color-info)"> </div><div style="color:var(--fg-sub);line-height:1.6">Water-in-oil. Mackay et al.(1980) · </div></div>
<div style="display:grid;grid-template-columns:60px 1fr;gap:8px;padding:5px 8px;background:rgba(249,115,22,.04);border-radius:4px"><div style="font-weight:700;color:var(--fg-sub)"> </div><div style="color:var(--fg-sub);line-height:1.6">· . = </div></div>
</div>
</div>
</div>
<!-- 2: 10-1868791 -->
<div style="background:linear-gradient(135deg,rgba(59,130,246,.05),rgba(168,85,247,.03));border:1px solid rgba(59,130,246,.25);border-radius:12px;padding:18px;margin-bottom:16px;position:relative;overflow:hidden">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--blue),var(--purple))"></div>
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--color-info),var(--color-tertiary))"></div>
<div style="display:flex;align-items:flex-start;gap:14px;margin-bottom:14px">
<div style="padding:8px 12px;background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.3);border-radius:8px;text-align:center;white-space:nowrap;flex-shrink:0">
<div style="font-size:8px;color:var(--t3);font-family:var(--fK)"> </div>
<div style="font-size:13px;font-weight:800;color:var(--blue);font-family:var(--fM)">10-1868791</div>
<div style="font-size:8px;color:var(--t3);font-family:var(--fK);margin-top:2px">등록: 2018.06.12</div>
<div style="font-size:8px;color:var(--fg-disabled);font-family:var(--font-korean)"> </div>
<div style="font-size:13px;font-weight:800;color:var(--color-info);font-family:var(--font-mono)">10-1868791</div>
<div style="font-size:8px;color:var(--fg-disabled);font-family:var(--font-korean);margin-top:2px">등록: 2018.06.12</div>
</div>
<div style="flex:1">
<div style="font-size:13px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:5px;line-height:1.5">(Oil spill) </div>
<div style="font-size:9px;color:var(--t2);font-family:var(--fK);line-height:1.8"><span style="color:var(--blue)"></span> : | <span style="color:var(--blue)"></span> : ·····</div>
<div style="font-size:13px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:5px;line-height:1.5">(Oil spill) </div>
<div style="font-size:9px;color:var(--fg-sub);font-family:var(--font-korean);line-height:1.8"><span style="color:var(--color-info)"></span> : | <span style="color:var(--color-info)"></span> : ·····</div>
</div>
</div>
<div style="background:var(--bg0);border:1px solid rgba(59,130,246,.15);border-radius:8px;padding:11px;margin-bottom:10px">
<div style="font-size:10px;font-weight:700;color:var(--blue);font-family:var(--fK);margin-bottom:9px"> 5</div>
<div style="display:flex;flex-direction:column;gap:4px;font-size:9px;font-family:var(--fK)">
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--blue)">(a)</span><div style="color:var(--t2);line-height:1.6"><b> + </b> : GPS + · </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--blue)">(b)</span><div style="color:var(--t2);line-height:1.6"><b>1 </b> : + 1 (ΔModel) </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--blue)">(c)</span><div style="color:var(--t2);line-height:1.6"><b> </b> : Δobs 1 ΔModel </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--blue)">(d)</span><div style="color:var(--t2);line-height:1.6"><b>2 </b> : ΔModel 2 ΔRevised </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(168,85,247,.05);border:1px solid rgba(168,85,247,.12);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--purple)">(e)</span><div style="color:var(--t2);line-height:1.6"><b> </b> : ΔRevised Δobs <b style="color:var(--purple)">GA·DE·HS·PSO</b> </div></div>
<div style="background:var(--bg-base);border:1px solid rgba(59,130,246,.15);border-radius:8px;padding:11px;margin-bottom:10px">
<div style="font-size:10px;font-weight:700;color:var(--color-info);font-family:var(--font-korean);margin-bottom:9px"> 5</div>
<div style="display:flex;flex-direction:column;gap:4px;font-size:9px;font-family:var(--font-korean)">
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--color-info)">(a)</span><div style="color:var(--fg-sub);line-height:1.6"><b> + </b> : GPS + · </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--color-info)">(b)</span><div style="color:var(--fg-sub);line-height:1.6"><b>1 </b> : + 1 (ΔModel) </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--color-info)">(c)</span><div style="color:var(--fg-sub);line-height:1.6"><b> </b> : Δobs 1 ΔModel </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(59,130,246,.04);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--color-info)">(d)</span><div style="color:var(--fg-sub);line-height:1.6"><b>2 </b> : ΔModel 2 ΔRevised </div></div>
<div style="display:flex;gap:7px;padding:5px 8px;background:rgba(168,85,247,.05);border:1px solid rgba(168,85,247,.12);border-radius:4px"><span style="min-width:20px;font-weight:800;color:var(--color-tertiary)">(e)</span><div style="color:var(--fg-sub);line-height:1.6"><b> </b> : ΔRevised Δobs <b style="color:var(--color-tertiary)">GA·DE·HS·PSO</b> </div></div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:var(--bg0);border:1px solid var(--bd);border-radius:8px;padding:11px">
<div style="font-size:10px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:7px"> </div>
<div style="background:rgba(59,130,246,.04);border:1px solid rgba(59,130,246,.12);border-radius:5px;padding:8px;font-family:var(--fM);font-size:9px;color:var(--t1);line-height:2">
<span style="color:var(--t3)">1:</span> Model<sub>x</sub> = cur<sub>u</sub>·Δt + c·w<sub>u</sub>·Δt<br>
<span style="color:var(--t3)">2:</span> Rev<sub>x</sub> = a1·cur<sub>u</sub>+a2·cur<sub>v</sub>+...+a9
<div style="background:var(--bg-base);border:1px solid var(--stroke-default);border-radius:8px;padding:11px">
<div style="font-size:10px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:7px"> </div>
<div style="background:rgba(59,130,246,.04);border:1px solid rgba(59,130,246,.12);border-radius:5px;padding:8px;font-family:var(--font-mono);font-size:9px;color:var(--fg-default);line-height:2">
<span style="color:var(--fg-disabled)">1:</span> Model<sub>x</sub> = cur<sub>u</sub>·Δt + c·w<sub>u</sub>·Δt<br>
<span style="color:var(--fg-disabled)">2:</span> Rev<sub>x</sub> = a1·cur<sub>u</sub>+a2·cur<sub>v</sub>+...+a9
</div>
</div>
<div style="background:var(--bg0);border:1px solid rgba(168,85,247,.15);border-radius:8px;padding:11px">
<div style="font-size:10px;font-weight:700;color:var(--purple);font-family:var(--fK);margin-bottom:7px">4 </div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="padding:3px 7px;background:rgba(168,85,247,.04);border-radius:4px;color:var(--t2)"><b style="color:var(--purple)">GA</b> : · </div>
<div style="padding:3px 7px;background:rgba(59,130,246,.04);border-radius:4px;color:var(--t2)"><b style="color:var(--blue)">DE</b> : </div>
<div style="padding:3px 7px;background:rgba(34,197,94,.04);border-radius:4px;color:var(--t2)"><b style="color:var(--green)">HS</b> : </div>
<div style="padding:3px 7px;background:rgba(249,115,22,.04);border-radius:4px;color:var(--t2)"><b style="color:var(--orange)">PSO</b> : </div>
<div style="background:var(--bg-base);border:1px solid rgba(168,85,247,.15);border-radius:8px;padding:11px">
<div style="font-size:10px;font-weight:700;color:var(--color-tertiary);font-family:var(--font-korean);margin-bottom:7px">4 </div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="padding:3px 7px;background:rgba(168,85,247,.04);border-radius:4px;color:var(--fg-sub)"><b style="color:var(--color-tertiary)">GA</b> : · </div>
<div style="padding:3px 7px;background:rgba(59,130,246,.04);border-radius:4px;color:var(--fg-sub)"><b style="color:var(--color-info)">DE</b> : </div>
<div style="padding:3px 7px;background:rgba(34,197,94,.04);border-radius:4px;color:var(--fg-sub)"><b style="color:var(--color-success)">HS</b> : </div>
<div style="padding:3px 7px;background:rgba(249,115,22,.04);border-radius:4px;color:var(--fg-sub)"><b style="color:var(--color-warning)">PSO</b> : </div>
</div>
</div>
</div>
</div>
<!-- -->
<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--t1);font-family:var(--fK);margin-bottom:10px">📚 ( )</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--fK)">
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(6,182,212,.1);border-radius:3px;color:var(--cyan);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6"> 17 4 (· ) KOSPS | </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(6,182,212,.1);border-radius:3px;color:var(--cyan);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6"> 2008 CHARRY | </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(6,182,212,.1);border-radius:3px;color:var(--cyan);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">KR1020120121163 A </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(59,130,246,.1);border-radius:3px;color:var(--blue);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">KR101538668 B1 / KR101378463 B1 2</div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(59,130,246,.1);border-radius:3px;color:var(--blue);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6"> 10-1567431 §[0007] </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--orange);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">Fay(1969) · Mackay et al.(1980) · Stiver &amp; Mackay(1984) · Mooney(1951) 5 </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--orange);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">Akima(1978a, 1978b) 2 5 (· )</div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--orange);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">·(2000) 0.029×Vw, θw+18.6° </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--orange);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">Bowden(1983) fBm σ²=At^m (m=0.45~2.46)</div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--orange);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">Wahr(1981) Love number (k=0.3, h=0.61) · </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg0);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--orange);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--t2);line-height:1.6">Flather &amp; Heaps(1975) (tidal flat) </div></div>
<div style="background:var(--bg-card);border:1px solid var(--stroke-default);border-radius:10px;padding:14px">
<div style="font-size:11px;font-weight:700;color:var(--fg-default);font-family:var(--font-korean);margin-bottom:10px">📚 ( )</div>
<div style="display:flex;flex-direction:column;gap:3px;font-size:9px;font-family:var(--font-korean)">
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(6,182,212,.1);border-radius:3px;color:var(--color-accent);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6"> 17 4 (· ) KOSPS | </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(6,182,212,.1);border-radius:3px;color:var(--color-accent);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6"> 2008 CHARRY | </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(6,182,212,.1);border-radius:3px;color:var(--color-accent);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">KR1020120121163 A </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(59,130,246,.1);border-radius:3px;color:var(--color-info);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">KR101538668 B1 / KR101378463 B1 2</div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(59,130,246,.1);border-radius:3px;color:var(--color-info);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6"> 10-1567431 §[0007] </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--color-warning);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">Fay(1969) · Mackay et al.(1980) · Stiver &amp; Mackay(1984) · Mooney(1951) 5 </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--color-warning);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">Akima(1978a, 1978b) 2 5 (· )</div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--color-warning);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">·(2000) 0.029×Vw, θw+18.6° </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--color-warning);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">Bowden(1983) fBm σ²=At^m (m=0.45~2.46)</div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--color-warning);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">Wahr(1981) Love number (k=0.3, h=0.61) · </div></div>
<div style="display:grid;grid-template-columns:56px 1fr;gap:7px;padding:5px 8px;background:var(--bg-base);border-radius:4px"><div style="padding:2px 5px;background:rgba(249,115,22,.1);border-radius:3px;color:var(--color-warning);font-weight:700;font-size:8px;text-align:center;height:fit-content"> </div><div style="color:var(--fg-sub);line-height:1.6">Flather &amp; Heaps(1975) (tidal flat) </div></div>
</div>
</div>
`

파일 보기

@ -34,7 +34,7 @@ export function AerialView() {
}
return (
<div className="flex flex-col h-full w-full bg-bg-0">
<div className="flex flex-col h-full w-full bg-bg-base">
<div className="flex-1 overflow-auto px-6 py-5">
{renderContent()}
</div>

파일 보기

@ -228,10 +228,10 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
return (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18]">
<div className="text-2xl opacity-30 mb-2">📹</div>
<div className="text-[11px] font-korean text-text-3 opacity-70">
<div className="text-[11px] font-korean text-fg-disabled opacity-70">
{sttsCd === 'MAINT' ? '점검중' : '오프라인'}
</div>
<div className="text-[9px] font-korean text-text-3 opacity-40 mt-1">{cameraNm}</div>
<div className="text-[9px] font-korean text-fg-disabled opacity-40 mt-1">{cameraNm}</div>
</div>
);
}
@ -241,8 +241,8 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
return (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18]">
<div className="text-2xl opacity-20 mb-2">📹</div>
<div className="text-[10px] font-korean text-text-3 opacity-50"> URL </div>
<div className="text-[9px] font-korean text-text-3 opacity-30 mt-1">{cameraNm}</div>
<div className="text-[10px] font-korean text-fg-disabled opacity-50"> URL </div>
<div className="text-[9px] font-korean text-fg-disabled opacity-30 mt-1">{cameraNm}</div>
</div>
);
}
@ -252,11 +252,11 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
return (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18]">
<div className="text-2xl opacity-30 mb-2"></div>
<div className="text-[10px] font-korean text-status-red opacity-70"> </div>
<div className="text-[9px] font-korean text-text-3 opacity-40 mt-1">{cameraNm}</div>
<div className="text-[10px] font-korean text-color-danger opacity-70"> </div>
<div className="text-[9px] font-korean text-fg-disabled opacity-40 mt-1">{cameraNm}</div>
<button
onClick={() => setRetryKey(k => k + 1)}
className="mt-2 px-2.5 py-1 rounded text-[9px] font-korean bg-bg-3 border border-border text-text-2 cursor-pointer hover:bg-bg-hover transition-colors"
className="mt-2 px-2.5 py-1 rounded text-[9px] font-korean bg-bg-card border border-stroke text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors"
>
</button>
@ -270,7 +270,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
{playerState === 'loading' && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18] z-10">
<div className="text-lg opacity-40 animate-pulse mb-2">📹</div>
<div className="text-[10px] font-korean text-text-3 opacity-50"> ...</div>
<div className="text-[10px] font-korean text-fg-disabled opacity-50"> ...</div>
</div>
)}
@ -345,7 +345,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
</span>
)}
</div>
<div className="absolute bottom-2 left-2 text-[9px] font-mono px-1.5 py-0.5 rounded text-text-3 bg-black/70 z-20">
<div className="absolute bottom-2 left-2 text-[9px] font-mono px-1.5 py-0.5 rounded text-fg-disabled bg-black/70 z-20">
{coordDc ?? ''}{sourceNm ? ` · ${sourceNm}` : ''}
</div>
</div>

파일 보기

@ -155,23 +155,23 @@ export function CctvView() {
return (
<div className="flex h-full overflow-hidden" style={{ margin: '-20px -24px', height: 'calc(100% + 40px)' }}>
{/* 왼쪽: 목록 패널 */}
<div className="flex flex-col overflow-hidden bg-bg-1 border-r border-border w-[290px] min-w-[290px]">
<div className="flex flex-col overflow-hidden bg-bg-surface border-r border-stroke w-[290px] min-w-[290px]">
{/* 헤더 */}
<div className="p-3 pb-2.5 border-b border-border shrink-0 bg-bg-2">
<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-text-1 font-korean flex items-center gap-1.5">
<span className="w-[7px] h-[7px] rounded-full inline-block animate-pulse" style={{ background: 'var(--red)' }} />
<div className="text-xs 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)' }} />
CCTV
</div>
<div className="flex items-center gap-1">
{/* 지도/리스트 뷰 토글 */}
<div className="flex border border-border rounded-[5px] overflow-hidden mr-1.5">
<div className="flex border border-stroke rounded-[5px] overflow-hidden mr-1.5">
<button
onClick={() => setViewMode('map')}
className="px-1.5 py-0.5 text-[9px] font-semibold cursor-pointer border-none font-korean transition-colors"
style={viewMode === 'map'
? { background: 'rgba(6,182,212,.15)', color: 'var(--cyan)' }
: { background: 'var(--bg3)', color: 'var(--t3)' }
? { background: 'rgba(6,182,212,.15)', color: 'var(--color-accent)' }
: { background: 'var(--bg-card)', color: 'var(--fg-disabled)' }
}
title="지도 보기"
>🗺 </button>
@ -179,25 +179,25 @@ export function CctvView() {
onClick={() => setViewMode('list')}
className="px-1.5 py-0.5 text-[9px] font-semibold cursor-pointer border-none font-korean transition-colors"
style={viewMode === 'list'
? { background: 'rgba(6,182,212,.15)', color: 'var(--cyan)' }
: { background: 'var(--bg3)', color: 'var(--t3)' }
? { background: 'rgba(6,182,212,.15)', color: 'var(--color-accent)' }
: { background: 'var(--bg-card)', color: 'var(--fg-disabled)' }
}
title="리스트 보기"
> </button>
</div>
<span className="text-[9px] text-text-3 font-korean">API</span>
<span className="w-[7px] h-[7px] rounded-full inline-block" style={{ background: 'var(--green)' }} />
<span className="text-[9px] text-fg-disabled font-korean">API</span>
<span className="w-[7px] h-[7px] rounded-full inline-block" style={{ background: 'var(--color-success)' }} />
</div>
</div>
{/* 검색 */}
<div className="flex items-center gap-2 bg-bg-0 border border-border rounded-md px-2.5 py-1.5 mb-2 focus-within:border-primary-cyan/50 transition-colors">
<span className="text-text-3 text-[11px]">🔍</span>
<div className="flex items-center gap-2 bg-bg-base border border-stroke rounded-md px-2.5 py-1.5 mb-2 focus-within:border-color-accent/50 transition-colors">
<span className="text-fg-disabled text-[11px]">🔍</span>
<input
type="text"
placeholder="지점명 또는 지역 검색..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="flex-1 bg-transparent border-none text-text-1 text-[11px] font-korean outline-none"
className="flex-1 bg-transparent border-none text-fg text-[11px] font-korean outline-none"
/>
</div>
{/* 지역 필터 */}
@ -208,8 +208,8 @@ export function CctvView() {
onClick={() => setRegionFilter(r)}
className="px-2 py-0.5 rounded text-[9px] font-semibold cursor-pointer font-korean border transition-colors"
style={regionFilter === r
? { background: 'rgba(6,182,212,.15)', color: 'var(--cyan)', borderColor: 'rgba(6,182,212,.3)' }
: { background: 'var(--bg3)', color: 'var(--t2)', borderColor: 'var(--bd)' }
? { background: 'rgba(6,182,212,.15)', color: 'var(--color-accent)', borderColor: 'rgba(6,182,212,.3)' }
: { background: 'var(--bg-card)', color: 'var(--fg-sub)', borderColor: 'var(--stroke-default)' }
}
>{regionIcons[r] ? `${regionIcons[r]} ` : ''}{r}</button>
))}
@ -217,15 +217,15 @@ export function CctvView() {
</div>
{/* 상태 바 */}
<div className="flex items-center justify-between px-3.5 py-1 border-b border-border shrink-0 bg-bg-1">
<div className="text-[9px] text-text-3 font-korean">출처: 국립해양조사원 · KBS </div>
<div className="text-[10px] text-text-2 font-korean"><b className="text-text-1">{filtered.length}</b></div>
<div className="flex items-center justify-between px-3.5 py-1 border-b border-stroke shrink-0 bg-bg-surface">
<div className="text-[9px] text-fg-disabled font-korean">출처: 국립해양조사원 · KBS </div>
<div className="text-[10px] text-fg-sub font-korean"><b className="text-fg">{filtered.length}</b></div>
</div>
{/* 카메라 목록 */}
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
{loading ? (
<div className="px-3.5 py-4 text-[11px] text-text-3 font-korean"> ...</div>
<div className="px-3.5 py-4 text-[11px] text-fg-disabled font-korean"> ...</div>
) : filtered.map(cam => (
<div
key={cam.cctvSn}
@ -237,27 +237,27 @@ export function CctvView() {
}}
>
<div className="relative shrink-0">
<div className="w-8 h-8 rounded-md bg-bg-3 flex items-center justify-center">
<div className="w-8 h-8 rounded-md bg-bg-card flex items-center justify-center">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<rect x="3" y="6" width="13" height="10" rx="2" fill={cam.sttsCd === 'LIVE' ? 'var(--green)' : 'var(--t3)'} />
<circle cx="9.5" cy="11" r="2.8" fill="var(--bg3)" />
<circle cx="9.5" cy="11" r="1.3" fill={cam.sttsCd === 'LIVE' ? 'var(--green)' : 'var(--t3)'} />
<path d="M17 9l4-2v10l-4-2V9z" fill={cam.sttsCd === 'LIVE' ? 'var(--green)' : 'var(--t3)'} opacity="0.7" />
<rect x="3" y="6" width="13" height="10" rx="2" fill={cam.sttsCd === 'LIVE' ? 'var(--color-success)' : 'var(--fg-disabled)'} />
<circle cx="9.5" cy="11" r="2.8" fill="var(--bg-card)" />
<circle cx="9.5" cy="11" r="1.3" fill={cam.sttsCd === 'LIVE' ? 'var(--color-success)' : 'var(--fg-disabled)'} />
<path d="M17 9l4-2v10l-4-2V9z" fill={cam.sttsCd === 'LIVE' ? 'var(--color-success)' : 'var(--fg-disabled)'} opacity="0.7" />
</svg>
</div>
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-bg-1" style={{ background: cam.sttsCd === 'LIVE' ? 'var(--green)' : 'var(--t3)' }} />
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-bg-1" style={{ background: cam.sttsCd === 'LIVE' ? 'var(--color-success)' : 'var(--fg-disabled)' }} />
</div>
<div className="flex-1 min-w-0">
<div className="text-[11px] font-semibold text-text-1 font-korean truncate">{cam.cameraNm}</div>
<div className="text-[9px] text-text-3 font-korean truncate">{cam.locDc ?? ''}</div>
<div className="text-[11px] font-semibold text-fg font-korean truncate">{cam.cameraNm}</div>
<div className="text-[9px] text-fg-disabled font-korean truncate">{cam.locDc ?? ''}</div>
</div>
<div className="flex flex-col items-end gap-0.5 shrink-0">
{cam.sttsCd === 'LIVE' ? (
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" style={{ background: 'rgba(34,197,94,.12)', color: 'var(--green)' }}>LIVE</span>
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" style={{ background: 'rgba(34,197,94,.12)', color: 'var(--color-success)' }}>LIVE</span>
) : (
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" style={{ background: 'rgba(255,255,255,.06)', color: 'var(--t3)' }}>OFF</span>
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" style={{ background: 'rgba(255,255,255,.06)', color: 'var(--fg-disabled)' }}>OFF</span>
)}
{cam.ptzYn === 'Y' && <span className="text-[8px] text-text-3 font-mono">PTZ</span>}
{cam.ptzYn === 'Y' && <span className="text-[8px] text-fg-disabled font-mono">PTZ</span>}
</div>
</div>
))}
@ -267,33 +267,33 @@ export function CctvView() {
{/* 가운데: 영상 뷰어 */}
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-[#04070f]">
{/* 뷰어 툴바 */}
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-bg-2 shrink-0 gap-2.5">
<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-text-1 font-korean whitespace-nowrap overflow-hidden text-ellipsis">
<div className="text-xs font-bold text-fg font-korean whitespace-nowrap overflow-hidden text-ellipsis">
{selectedCamera ? `📹 ${selectedCamera.cameraNm}` : '📹 카메라를 선택하세요'}
</div>
{selectedCamera?.sttsCd === 'LIVE' && (
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold shrink-0" style={{ background: 'rgba(239,68,68,.14)', border: '1px solid rgba(239,68,68,.35)', color: 'var(--red)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--red)' }} />LIVE
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold shrink-0" style={{ background: 'rgba(239,68,68,.14)', border: '1px solid rgba(239,68,68,.35)', color: 'var(--color-danger)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--color-danger)' }} />LIVE
</div>
)}
</div>
<div className="flex items-center gap-1.5 shrink-0">
{/* PTZ 컨트롤 */}
{selectedCamera?.ptzYn === 'Y' && (
<div className="flex items-center gap-1 px-2 py-1 bg-bg-3 border border-border rounded-[5px]">
<span className="text-[9px] text-text-3 font-korean mr-1">PTZ</span>
<div className="flex items-center gap-1 px-2 py-1 bg-bg-card border border-stroke rounded-[5px]">
<span className="text-[9px] text-fg-disabled font-korean mr-1">PTZ</span>
{['◀', '▲', '▼', '▶'].map((d, i) => (
<button key={i} className="w-5 h-5 flex items-center justify-center bg-bg-0 border border-border rounded text-[9px] text-text-2 cursor-pointer hover:bg-bg-hover transition-colors">{d}</button>
<button key={i} className="w-5 h-5 flex items-center justify-center bg-bg-base border border-stroke rounded text-[9px] text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors">{d}</button>
))}
<div className="w-px h-4 bg-border mx-0.5" />
{['+', ''].map((z, i) => (
<button key={i} className="w-5 h-5 flex items-center justify-center bg-bg-0 border border-border rounded text-[9px] text-text-2 cursor-pointer hover:bg-bg-hover transition-colors">{z}</button>
<button key={i} className="w-5 h-5 flex items-center justify-center bg-bg-base border border-stroke rounded text-[9px] text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors">{z}</button>
))}
</div>
)}
{/* 분할 모드 */}
<div className="flex border border-border rounded-[5px] overflow-hidden">
<div className="flex border border-stroke rounded-[5px] overflow-hidden">
{[
{ mode: 1, icon: '▣', label: '1화면' },
{ mode: 4, icon: '⊞', label: '4분할' },
@ -305,8 +305,8 @@ export function CctvView() {
title={g.label}
className="px-2 py-1 text-[11px] cursor-pointer border-none transition-colors"
style={gridMode === g.mode
? { background: 'rgba(6,182,212,.15)', color: 'var(--cyan)' }
: { background: 'var(--bg3)', color: 'var(--t2)' }
? { background: 'rgba(6,182,212,.15)', color: 'var(--color-accent)' }
: { background: 'var(--bg-card)', color: 'var(--fg-sub)' }
}
>{g.icon}</button>
))}
@ -315,8 +315,8 @@ export function CctvView() {
onClick={() => setOilDetectionEnabled(v => !v)}
className="px-2.5 py-1 border rounded-[5px] text-[10px] font-semibold cursor-pointer font-korean transition-colors"
style={oilDetectionEnabled
? { background: 'rgba(239,68,68,.15)', borderColor: 'rgba(239,68,68,.4)', color: 'var(--red)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t2)' }
? { background: 'rgba(239,68,68,.15)', borderColor: 'rgba(239,68,68,.4)', color: 'var(--color-danger)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-sub)' }
}
title={gridMode === 9 ? '9분할 모드에서는 비활성화됩니다' : '오일 유출 감지'}
>
@ -327,7 +327,7 @@ export function CctvView() {
className="px-2.5 py-1 border rounded-[5px] text-[10px] font-semibold cursor-pointer font-korean transition-colors"
style={vesselDetectionEnabled
? { background: 'rgba(59,130,246,.15)', borderColor: 'rgba(59,130,246,.4)', color: 'var(--blue, #3b82f6)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t2)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-sub)' }
}
title={gridMode === 9 ? '9분할 모드에서는 비활성화됩니다' : '선박 출입 감지'}
>
@ -338,7 +338,7 @@ export function CctvView() {
className="px-2.5 py-1 border rounded-[5px] text-[10px] font-semibold cursor-pointer font-korean transition-colors"
style={intrusionDetectionEnabled
? { background: 'rgba(249,115,22,.15)', borderColor: 'rgba(249,115,22,.4)', color: 'var(--orange, #f97316)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t2)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-sub)' }
}
title={gridMode === 9 ? '9분할 모드에서는 비활성화됩니다' : '침입 감지'}
>
@ -348,7 +348,7 @@ export function CctvView() {
onClick={() => {
playerRefs.current.forEach(r => r?.capture())
}}
className="px-2.5 py-1 bg-bg-3 border border-border rounded-[5px] text-text-2 text-[10px] font-semibold cursor-pointer font-korean hover:bg-bg-hover transition-colors"
className="px-2.5 py-1 bg-bg-card border border-stroke rounded-[5px] text-fg-sub text-[10px] font-semibold cursor-pointer font-korean hover:bg-bg-surface-hover transition-colors"
>📷 </button>
</div>
</div>
@ -356,7 +356,7 @@ export function CctvView() {
{/* 영상 그리드 / CCTV 위치 지도 / 리스트 뷰 */}
{viewMode === 'list' && activeCells.length === 0 ? (
/* ── 리스트 뷰: 출처별 · 지역별 그리드 ── */
<div className="flex-1 overflow-y-auto p-4" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto p-4" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
{(() => {
// 출처별 그룹핑
const sourceGroups: Record<string, { label: string; icon: string; cameras: CctvCameraItem[] }> = {}
@ -385,21 +385,21 @@ export function CctvView() {
return (
<div key={srcKey} className="mb-5">
{/* 출처 헤더 */}
<div className="flex items-center gap-2 mb-2 pb-1.5 border-b border-border">
<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-[12px] font-bold text-text-1 font-korean">{group.label}</span>
<span className="text-[10px] text-text-3 font-korean ml-auto">{group.cameras.length}</span>
<span className="text-[12px] font-bold text-fg font-korean">{group.label}</span>
<span className="text-[10px] text-fg-disabled font-korean ml-auto">{group.cameras.length}</span>
</div>
{Object.entries(regionGroups).map(([rgn, cams]) => (
<div key={rgn} className="mb-3">
{/* 지역 소제목 */}
<div className="flex items-center gap-1.5 mb-1.5 px-1">
<span className="text-[10px] font-bold text-primary-cyan font-korean">{rgn}</span>
<span className="text-[9px] text-text-3">({cams.length})</span>
<span className="text-[10px] font-bold text-color-accent font-korean">{rgn}</span>
<span className="text-[9px] text-fg-disabled">({cams.length})</span>
</div>
{/* 테이블 헤더 */}
<div className="grid px-2 py-1 bg-bg-3 rounded-t text-[9px] font-bold text-text-3 font-korean border border-border"
<div className="grid px-2 py-1 bg-bg-card rounded-t text-[9px] font-bold text-fg-disabled font-korean border border-stroke"
style={{ gridTemplateColumns: '1fr 1.2fr 70px 130px' }}>
<span></span>
<span></span>
@ -411,22 +411,22 @@ export function CctvView() {
<div
key={cam.cctvSn}
onClick={() => { handleSelectCamera(cam); setViewMode('map') }}
className="grid px-2 py-1.5 border-b border-x border-border cursor-pointer transition-colors hover:bg-bg-hover"
className="grid px-2 py-1.5 border-b border-x border-stroke cursor-pointer transition-colors hover:bg-bg-surface-hover"
style={{
gridTemplateColumns: '1fr 1.2fr 70px 130px',
background: selectedCamera?.cctvSn === cam.cctvSn ? 'rgba(6,182,212,.08)' : 'transparent',
}}
>
<span className="text-[10px] text-text-1 font-korean font-semibold truncate">{cam.cameraNm}</span>
<span className="text-[9px] text-text-3 font-korean truncate">{cam.locDc ?? '—'}</span>
<span className="text-[10px] text-fg font-korean font-semibold truncate">{cam.cameraNm}</span>
<span className="text-[9px] text-fg-disabled font-korean truncate">{cam.locDc ?? '—'}</span>
<span className="text-center">
{cam.sttsCd === 'LIVE' ? (
<span className="text-[8px] font-bold px-1.5 py-px rounded-full inline-block" style={{ background: 'rgba(34,197,94,.12)', color: 'var(--green)' }}> LIVE</span>
<span className="text-[8px] font-bold px-1.5 py-px rounded-full inline-block" style={{ background: 'rgba(34,197,94,.12)', color: 'var(--color-success)' }}> LIVE</span>
) : (
<span className="text-[8px] font-bold px-1.5 py-px rounded-full inline-block" style={{ background: 'rgba(255,255,255,.06)', color: 'var(--t3)' }}> OFF</span>
<span className="text-[8px] font-bold px-1.5 py-px rounded-full inline-block" style={{ background: 'rgba(255,255,255,.06)', color: 'var(--fg-disabled)' }}> OFF</span>
)}
</span>
<span className="text-[9px] text-text-3 font-mono text-center">{now}</span>
<span className="text-[9px] text-fg-disabled font-mono text-center">{now}</span>
</div>
))}
</div>
@ -535,7 +535,7 @@ export function CctvView() {
intrusionDetectionEnabled={intrusionDetectionEnabled && gridMode !== 9}
/>
) : (
<div className="text-[10px] text-text-3 font-korean opacity-40"> </div>
<div className="text-[10px] text-fg-disabled font-korean opacity-40"> </div>
)}
</div>
)
@ -544,20 +544,20 @@ export function CctvView() {
)}
{/* 하단 정보 바 */}
<div className="flex items-center gap-3.5 px-4 py-1.5 border-t border-border bg-bg-2 shrink-0">
<div className="text-[10px] text-text-3 font-korean">: <b className="text-text-1">{selectedCamera?.cameraNm ?? ''}</b></div>
<div className="text-[10px] text-text-3 font-korean">: <span className="text-text-2">{selectedCamera?.locDc ?? ''}</span></div>
<div className="text-[10px] text-text-3 font-korean">: <span className="text-primary-cyan font-mono text-[9px]">{selectedCamera?.coordDc ?? ''}</span></div>
<div className="ml-auto text-[9px] text-text-3 font-korean">API: 국립해양조사원 TAGO CCTV</div>
<div className="flex items-center gap-3.5 px-4 py-1.5 border-t border-stroke bg-bg-elevated shrink-0">
<div className="text-[10px] text-fg-disabled font-korean">: <b className="text-fg">{selectedCamera?.cameraNm ?? ''}</b></div>
<div className="text-[10px] text-fg-disabled font-korean">: <span className="text-fg-sub">{selectedCamera?.locDc ?? ''}</span></div>
<div className="text-[10px] text-fg-disabled font-korean">: <span className="text-color-accent font-mono text-[9px]">{selectedCamera?.coordDc ?? ''}</span></div>
<div className="ml-auto text-[9px] text-fg-disabled font-korean">API: 국립해양조사원 TAGO CCTV</div>
</div>
</div>
{/* 오른쪽: 미니맵 + 정보 */}
<div className="flex flex-col overflow-hidden bg-bg-1 border-l border-border w-[232px] min-w-[232px]">
<div className="flex flex-col overflow-hidden bg-bg-surface border-l border-stroke w-[232px] min-w-[232px]">
{/* 지도 헤더 */}
<div className="px-3 py-2 border-b border-border text-[11px] font-bold text-text-1 font-korean bg-bg-2 shrink-0 flex items-center justify-between">
<div className="px-3 py-2 border-b border-stroke text-[11px] font-bold text-fg font-korean bg-bg-elevated shrink-0 flex items-center justify-between">
<span>🗺 </span>
<span className="text-[9px] text-text-3 font-normal"> </span>
<span className="text-[9px] text-fg-disabled font-normal"> </span>
</div>
{/* 미니맵 */}
<div className="w-full shrink-0 relative h-[210px] overflow-hidden">
@ -582,8 +582,8 @@ export function CctvView() {
width: selectedCamera?.cctvSn === cam.cctvSn ? 10 : 7,
height: selectedCamera?.cctvSn === cam.cctvSn ? 10 : 7,
borderRadius: '50%',
background: selectedCamera?.cctvSn === cam.cctvSn ? 'var(--cyan)' : cam.sttsCd === 'LIVE' ? 'var(--green)' : 'var(--t3)',
boxShadow: selectedCamera?.cctvSn === cam.cctvSn ? '0 0 8px var(--cyan)' : 'none',
background: selectedCamera?.cctvSn === cam.cctvSn ? 'var(--color-accent)' : cam.sttsCd === 'LIVE' ? 'var(--color-success)' : 'var(--fg-disabled)',
boxShadow: selectedCamera?.cctvSn === cam.cctvSn ? '0 0 8px var(--color-accent)' : 'none',
border: '1.5px solid rgba(255,255,255,.7)',
}}
/>
@ -593,8 +593,8 @@ export function CctvView() {
</div>
{/* 카메라 정보 */}
<div className="flex-1 overflow-y-auto px-3 py-2.5 border-t border-border" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="text-[10px] font-bold text-text-2 font-korean mb-2">📋 </div>
<div className="flex-1 overflow-y-auto px-3 py-2.5 border-t border-stroke" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
<div className="text-[10px] font-bold text-fg-sub font-korean mb-2">📋 </div>
{selectedCamera ? (
<div className="flex flex-col gap-1.5">
{[
@ -606,24 +606,24 @@ export function CctvView() {
['PTZ', selectedCamera.ptzYn === 'Y' ? '지원' : '미지원'],
['출처', selectedCamera.sourceNm ?? '—'],
].map(([k, v], i) => (
<div key={i} className="flex justify-between px-2 py-1 bg-bg-0 rounded text-[9px]">
<span className="text-text-3 font-korean">{k}</span>
<span className="font-mono text-text-1">{v}</span>
<div key={i} className="flex justify-between px-2 py-1 bg-bg-base rounded text-[9px]">
<span className="text-fg-disabled font-korean">{k}</span>
<span className="font-mono text-fg">{v}</span>
</div>
))}
</div>
) : (
<div className="text-[10px] text-text-3 font-korean"> </div>
<div className="text-[10px] text-fg-disabled font-korean"> </div>
)}
{/* 방제 즐겨찾기 */}
<div className="mt-3 pt-2.5 border-t border-border">
<div className="text-[10px] font-bold text-text-2 font-korean mb-2"> </div>
<div className="mt-3 pt-2.5 border-t border-stroke">
<div className="text-[10px] font-bold text-fg-sub font-korean mb-2"> </div>
<div className="flex flex-col gap-1">
{cctvFavorites.map((fav, i) => (
<div
key={i}
className="flex items-center gap-2 px-2 py-1.5 bg-bg-3 rounded-[5px] cursor-pointer hover:bg-bg-hover transition-colors"
className="flex items-center gap-2 px-2 py-1.5 bg-bg-card rounded-[5px] cursor-pointer hover:bg-bg-surface-hover transition-colors"
onClick={() => {
const found = cameras.find(c => c.cameraNm === fav.name)
if (found) handleSelectCamera(found)
@ -631,8 +631,8 @@ export function CctvView() {
>
<span className="text-[9px]"></span>
<div className="flex-1 min-w-0">
<div className="text-[9px] font-semibold text-text-1 font-korean truncate">{fav.name}</div>
<div className="text-[8px] text-text-3 font-korean">{fav.reason}</div>
<div className="text-[9px] font-semibold text-fg font-korean truncate">{fav.name}</div>
<div className="text-[8px] text-fg-disabled font-korean">{fav.reason}</div>
</div>
</div>
))}
@ -640,23 +640,23 @@ export function CctvView() {
</div>
{/* API 연동 현황 */}
<div className="mt-3 pt-2.5 border-t border-border">
<div className="text-[10px] font-bold text-text-2 font-korean mb-2">🔌 API </div>
<div className="mt-3 pt-2.5 border-t border-stroke">
<div className="text-[10px] font-bold text-fg-sub font-korean mb-2">🔌 API </div>
<div className="flex flex-col gap-1.5">
{[
{ name: '해양조사원 TAGO', status: '● 연결', color: 'var(--green)' },
{ name: 'KBS 재난안전포털', status: '● 연결', color: 'var(--green)' },
{ name: '해양조사원 TAGO', status: '● 연결', color: 'var(--color-success)' },
{ name: 'KBS 재난안전포털', status: '● 연결', color: 'var(--color-success)' },
].map((api, i) => (
<div key={i} className="flex items-center justify-between px-2 py-1 bg-bg-3 rounded-[5px]" style={{ border: '1px solid rgba(34,197,94,.2)' }}>
<span className="text-[9px] text-text-2 font-korean">{api.name}</span>
<div key={i} className="flex items-center justify-between px-2 py-1 bg-bg-card rounded-[5px]" style={{ border: '1px solid rgba(34,197,94,.2)' }}>
<span className="text-[9px] text-fg-sub font-korean">{api.name}</span>
<span className="text-[9px] font-bold" style={{ color: api.color }}>{api.status}</span>
</div>
))}
<div className="flex items-center justify-between px-2 py-1 bg-bg-3 rounded-[5px]" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
<span className="text-[9px] text-text-2 font-korean"> </span>
<span className="text-[9px] font-bold font-mono text-primary-blue">1 fps</span>
<div className="flex items-center justify-between px-2 py-1 bg-bg-card rounded-[5px]" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
<span className="text-[9px] text-fg-sub font-korean"> </span>
<span className="text-[9px] font-bold font-mono text-color-info">1 fps</span>
</div>
<div className="text-[9px] text-text-3 font-mono text-right mt-0.5">: {new Date().toLocaleTimeString('ko-KR')}</div>
<div className="text-[9px] text-fg-disabled font-mono text-right mt-0.5">: {new Date().toLocaleTimeString('ko-KR')}</div>
</div>
</div>
</div>

파일 보기

@ -15,23 +15,23 @@ const equipIcon = (t: string) => t === 'drone' ? '🛸' : t === 'plane' ? '✈'
const equipTagCls = (t: string) =>
t === 'drone'
? 'bg-[rgba(59,130,246,0.12)] text-primary-blue'
? 'bg-[rgba(59,130,246,0.12)] text-color-info'
: t === 'plane'
? 'bg-[rgba(34,197,94,0.12)] text-status-green'
: 'bg-[rgba(168,85,247,0.12)] text-primary-purple'
? 'bg-[rgba(34,197,94,0.12)] text-color-success'
: 'bg-[rgba(168,85,247,0.12)] text-color-tertiary'
const mediaTagCls = (t: string) =>
t === '영상'
? 'bg-[rgba(239,68,68,0.12)] text-status-red'
: 'bg-[rgba(234,179,8,0.12)] text-status-yellow'
? 'bg-[rgba(239,68,68,0.12)] text-color-danger'
: 'bg-[rgba(234,179,8,0.12)] text-color-caution'
const FilterBtn = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) => (
<button
onClick={onClick}
className={`px-2.5 py-1 text-[10px] font-semibold rounded font-korean transition-colors ${
active
? 'bg-[rgba(6,182,212,0.15)] text-primary-cyan border border-primary-cyan/30'
: 'bg-bg-3 border border-border text-text-2 hover:bg-bg-hover'
? 'bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30'
: 'bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover'
}`}
>
{label}
@ -163,13 +163,13 @@ export function MediaManagement() {
{/* Filters */}
<div className="flex items-center justify-between mb-4">
<div className="flex gap-1.5 items-center">
<span className="text-[11px] text-text-3 font-korean"> :</span>
<span className="text-[11px] text-fg-disabled font-korean"> :</span>
<FilterBtn label="전체" active={equipFilter === 'all'} onClick={() => setEquipFilter('all')} />
<FilterBtn label="🛸 드론" active={equipFilter === 'drone'} onClick={() => setEquipFilter('drone')} />
<FilterBtn label="✈ 유인항공기" active={equipFilter === 'plane'} onClick={() => setEquipFilter('plane')} />
<FilterBtn label="🛰 위성" active={equipFilter === 'satellite'} onClick={() => setEquipFilter('satellite')} />
<span className="w-px h-4 bg-border mx-1" />
<span className="text-[11px] text-text-3 font-korean">:</span>
<span className="text-[11px] text-fg-disabled font-korean">:</span>
<FilterBtn label="📷 사진" active={typeFilter.has('photo')} onClick={() => toggleTypeFilter('photo')} />
<FilterBtn label="🎬 영상" active={typeFilter.has('video')} onClick={() => toggleTypeFilter('video')} />
</div>
@ -179,7 +179,7 @@ export function MediaManagement() {
placeholder="파일명 검색..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="px-3 py-1.5 bg-bg-0 border border-border rounded-sm text-text-1 font-korean text-[11px] outline-none w-40 focus:border-primary-cyan"
className="px-3 py-1.5 bg-bg-base border border-stroke rounded-sm text-fg font-korean text-[11px] outline-none w-40 focus:border-color-accent"
/>
<select
value={sortBy}
@ -196,24 +196,24 @@ export function MediaManagement() {
{/* Summary Stats */}
<div className="flex gap-2.5 mb-4">
{[
{ icon: '📸', value: loading ? '…' : String(mediaItems.length), label: '총 파일', color: 'text-primary-cyan' },
{ icon: '🛸', value: loading ? '…' : String(droneCount), label: '드론', color: 'text-text-1' },
{ icon: '✈', value: loading ? '…' : String(planeCount), label: '유인항공기', color: 'text-text-1' },
{ icon: '🛰', value: loading ? '…' : String(satCount), label: '위성', color: 'text-text-1' },
{ icon: '💾', value: '—', label: '총 용량', color: 'text-text-1' },
{ icon: '📸', value: loading ? '…' : String(mediaItems.length), label: '총 파일', color: 'text-color-accent' },
{ icon: '🛸', value: loading ? '…' : String(droneCount), label: '드론', color: 'text-fg' },
{ icon: '✈', value: loading ? '…' : String(planeCount), label: '유인항공기', color: 'text-fg' },
{ icon: '🛰', value: loading ? '…' : String(satCount), label: '위성', color: 'text-fg' },
{ icon: '💾', value: '—', label: '총 용량', color: 'text-fg' },
].map((s, i) => (
<div key={i} className="flex-1 flex items-center gap-2.5 px-4 py-3 bg-bg-3 border border-border rounded-sm">
<div key={i} className="flex-1 flex items-center gap-2.5 px-4 py-3 bg-bg-card border border-stroke rounded-sm">
<span className="text-xl">{s.icon}</span>
<div>
<div className={`text-base font-bold font-mono ${s.color}`}>{s.value}</div>
<div className="text-[10px] text-text-3 font-korean">{s.label}</div>
<div className="text-[10px] text-fg-disabled font-korean">{s.label}</div>
</div>
</div>
))}
</div>
{/* File Table */}
<div className="flex-1 bg-bg-3 border border-border rounded-md overflow-hidden flex flex-col">
<div className="flex-1 bg-bg-card border border-stroke rounded-md overflow-hidden flex flex-col">
<div className="overflow-auto flex-1">
<table className="w-full text-left" style={{ tableLayout: 'fixed' }}>
<colgroup>
@ -230,7 +230,7 @@ export function MediaManagement() {
<col style={{ width: 50 }} />
</colgroup>
<thead>
<tr className="border-b border-border bg-bg-2">
<tr className="border-b border-stroke bg-bg-elevated">
<th className="px-2 py-2.5 text-center">
<input
type="checkbox"
@ -240,27 +240,27 @@ export function MediaManagement() {
/>
</th>
<th className="px-1 py-2.5" />
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-text-3 text-center">📥</th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled font-korean whitespace-nowrap"></th>
<th className="px-2 py-2.5 text-[10px] font-semibold text-fg-disabled text-center">📥</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={11} className="px-4 py-8 text-center text-[11px] text-text-3 font-korean"> ...</td>
<td colSpan={11} className="px-4 py-8 text-center text-[11px] text-fg-disabled font-korean"> ...</td>
</tr>
) : sorted.map(f => (
<tr
key={f.aerialMediaSn}
onClick={() => toggleId(f.aerialMediaSn)}
className={`border-b border-border/50 cursor-pointer transition-colors hover:bg-[rgba(255,255,255,0.02)] ${
className={`border-b border-stroke/50 cursor-pointer transition-colors hover:bg-[rgba(255,255,255,0.02)] ${
selectedIds.has(f.aerialMediaSn) ? 'bg-[rgba(6,182,212,0.06)]' : ''
}`}
>
@ -273,9 +273,9 @@ export function MediaManagement() {
/>
</td>
<td className="px-1 py-2 text-base">{equipIcon(f.equipTpCd)}</td>
<td className="px-2 py-2 text-[10px] font-semibold text-text-1 font-korean truncate">{f.acdntSn != null ? String(f.acdntSn) : '—'}</td>
<td className="px-2 py-2 text-[10px] text-primary-cyan font-mono truncate">{f.locDc ?? '—'}</td>
<td className="px-2 py-2 text-[11px] font-semibold text-text-1 font-korean truncate">{f.fileNm}</td>
<td className="px-2 py-2 text-[10px] font-semibold text-fg font-korean truncate">{f.acdntSn != null ? String(f.acdntSn) : '—'}</td>
<td className="px-2 py-2 text-[10px] text-color-accent font-mono truncate">{f.locDc ?? '—'}</td>
<td className="px-2 py-2 text-[11px] font-semibold text-fg font-korean truncate">{f.fileNm}</td>
<td className="px-2 py-2">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-semibold font-korean ${equipTagCls(f.equipTpCd)}`}>
{f.equipNm}
@ -293,7 +293,7 @@ export function MediaManagement() {
<button
onClick={(e) => handleDownload(e, f)}
disabled={downloadingId === f.aerialMediaSn}
className="px-2 py-1 text-[10px] rounded bg-[rgba(6,182,212,0.1)] text-primary-cyan border border-primary-cyan/20 hover:bg-[rgba(6,182,212,0.2)] transition-colors disabled:opacity-50"
className="px-2 py-1 text-[10px] rounded bg-[rgba(6,182,212,0.1)] text-color-accent border border-color-accent/20 hover:bg-[rgba(6,182,212,0.2)] transition-colors disabled:opacity-50"
>
{downloadingId === f.aerialMediaSn ? '⏳' : '📥'}
</button>
@ -306,24 +306,24 @@ export function MediaManagement() {
</div>
{/* Bottom Actions */}
<div className="flex justify-between items-center mt-4 pt-3.5 border-t border-border">
<div className="text-[11px] text-text-3 font-korean">
: <span className="text-primary-cyan font-semibold">{selectedIds.size}</span>
<div className="flex justify-between items-center mt-4 pt-3.5 border-t border-stroke">
<div className="text-[11px] text-fg-disabled font-korean">
: <span className="text-color-accent font-semibold">{selectedIds.size}</span>
</div>
<div className="flex gap-2">
<button onClick={toggleAll} className="px-3 py-1.5 text-[11px] font-semibold rounded bg-bg-3 border border-border text-text-2 hover:bg-bg-hover transition-colors font-korean">
<button onClick={toggleAll} className="px-3 py-1.5 text-[11px] font-semibold rounded bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover transition-colors font-korean">
</button>
<button
onClick={handleBulkDownload}
disabled={bulkDownloading || selectedIds.size === 0}
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-[rgba(6,182,212,0.1)] text-primary-cyan border border-primary-cyan/30 hover:bg-[rgba(6,182,212,0.2)] transition-colors font-korean disabled:opacity-50"
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-[rgba(6,182,212,0.1)] text-color-accent border border-color-accent/30 hover:bg-[rgba(6,182,212,0.2)] transition-colors font-korean disabled:opacity-50"
>
{bulkDownloading ? '⏳ 다운로드 중...' : '📥 선택 다운로드'}
</button>
<button
onClick={() => navigateToTab('prediction', 'analysis')}
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-[rgba(168,85,247,0.1)] text-primary-purple border border-primary-purple/30 hover:bg-[rgba(168,85,247,0.2)] transition-colors font-korean"
className="px-3 py-1.5 text-[11px] font-semibold rounded bg-[rgba(168,85,247,0.1)] text-color-tertiary border border-color-tertiary/30 hover:bg-[rgba(168,85,247,0.2)] transition-colors font-korean"
>
🔬
</button>
@ -333,21 +333,21 @@ export function MediaManagement() {
{/* 선택 다운로드 결과 팝업 */}
{downloadResult && (
<div className="fixed inset-0 z-[300] bg-black/60 backdrop-blur-sm flex items-center justify-center">
<div className="bg-bg-1 border border-border rounded-md p-6 w-72 text-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-[13px] font-korean text-text-2 mb-1">
<span className="text-primary-cyan font-bold">{downloadResult.total}</span>
<div className="text-[13px] font-korean text-fg-sub mb-1">
<span className="text-color-accent font-bold">{downloadResult.total}</span>
</div>
<div className="text-[13px] font-korean text-text-2 mb-4">
<span className="text-status-green font-bold">{downloadResult.success}</span>
<div className="text-[13px] font-korean text-fg-sub mb-4">
<span className="text-color-success font-bold">{downloadResult.success}</span>
{downloadResult.total - downloadResult.success > 0 && (
<> / <span className="text-status-red font-bold">{downloadResult.total - downloadResult.success}</span> </>
<> / <span className="text-color-danger font-bold">{downloadResult.total - downloadResult.success}</span> </>
)}
</div>
<button
onClick={() => setDownloadResult(null)}
className="px-6 py-2 text-sm font-semibold rounded bg-[rgba(6,182,212,0.15)] text-primary-cyan border border-primary-cyan/30 hover:bg-[rgba(6,182,212,0.25)] transition-colors font-korean"
className="px-6 py-2 text-sm font-semibold rounded bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30 hover:bg-[rgba(6,182,212,0.25)] transition-colors font-korean"
>
</button>
@ -358,18 +358,18 @@ export function MediaManagement() {
{/* Upload Modal */}
{showUpload && (
<div className="fixed inset-0 z-[200] bg-black/60 backdrop-blur-sm flex items-center justify-center">
<div ref={modalRef} className="bg-bg-1 border border-border rounded-md w-[480px] max-h-[80vh] overflow-y-auto p-6">
<div ref={modalRef} className="bg-bg-surface border border-stroke rounded-md w-[480px] max-h-[80vh] overflow-y-auto p-6">
<div className="flex justify-between items-center mb-4">
<span className="text-base font-bold font-korean">📤 · </span>
<button onClick={() => setShowUpload(false)} className="text-text-3 text-lg hover:text-text-1"></button>
<button onClick={() => setShowUpload(false)} className="text-fg-disabled text-lg hover:text-fg"></button>
</div>
<div className="border-2 border-dashed border-border-light rounded-md py-8 px-4 text-center mb-4 cursor-pointer hover:border-primary-cyan/40 transition-colors">
<div className="border-2 border-dashed border-stroke-light rounded-md py-8 px-4 text-center mb-4 cursor-pointer hover:border-color-accent/40 transition-colors">
<div className="text-3xl mb-2 opacity-50">📁</div>
<div className="text-[13px] font-semibold mb-1 font-korean"> </div>
<div className="text-[11px] text-text-3 font-korean">JPG, TIFF, GeoTIFF, MP4, MOV · 2GB</div>
<div className="text-[11px] text-fg-disabled font-korean">JPG, TIFF, GeoTIFF, MP4, MOV · 2GB</div>
</div>
<div className="mb-3">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean"> </label>
<select className="prd-i w-full">
<option> (DJI M300 RTK)</option>
<option> (DJI Mavic 3E)</option>
@ -381,7 +381,7 @@ export function MediaManagement() {
</select>
</div>
<div className="mb-3">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean"> </label>
<select className="prd-i w-full">
<option> (2026-01-18)</option>
<option> (2026-01-18)</option>
@ -389,13 +389,13 @@ export function MediaManagement() {
</select>
</div>
<div className="mb-4">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"></label>
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean"></label>
<textarea
className="prd-i w-full h-[60px] resize-y"
placeholder="촬영 조건, 비고 등..."
/>
</div>
<button className="w-full py-3 rounded-sm text-sm font-bold font-korean text-white border-none cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}>
<button className="w-full py-3 rounded-sm text-sm font-bold font-korean text-white border-none cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))' }}>
📤
</button>
</div>

파일 보기

@ -49,9 +49,9 @@ interface MetaRowProps {
function MetaRow({ label, value }: MetaRowProps) {
if (value == null) return null;
return (
<div className="flex justify-between gap-2 py-0.5 border-b border-border/40 last:border-0 font-korean">
<span className="text-text-3 shrink-0">{label}</span>
<span className="text-text-1 text-right break-all">{value}</span>
<div className="flex justify-between gap-2 py-0.5 border-b border-stroke/40 last:border-0 font-korean">
<span className="text-fg-disabled shrink-0">{label}</span>
<span className="text-fg text-right break-all">{value}</span>
</div>
);
}
@ -218,7 +218,7 @@ export function OilAreaAnalysis() {
{/* ── 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-[11px] text-text-3 mb-4 font-korean">
<div className="text-[11px] text-fg-disabled mb-4 font-korean">
.
</div>
@ -234,8 +234,8 @@ 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-border rounded-sm text-xs font-korean text-text-2
hover:border-primary-cyan hover:text-primary-cyan transition-colors cursor-pointer
className="w-full py-2 mb-3 border border-dashed border-stroke rounded-sm text-xs font-korean text-fg-sub
hover:border-color-accent hover:text-color-accent transition-colors cursor-pointer
disabled:opacity-40 disabled:cursor-not-allowed"
>
+ ({selectedFiles.length}/{MAX_IMAGES})
@ -249,16 +249,16 @@ export function OilAreaAnalysis() {
{selectedFiles.map((file, i) => (
<div key={`${file.name}-${i}`}>
<div
className={`flex items-center gap-2 px-2 py-1.5 bg-bg-3 border rounded-sm text-[11px] font-korean cursor-pointer transition-colors
${selectedImageIndex === i ? 'border-primary-cyan' : 'border-border'}`}
className={`flex items-center gap-2 px-2 py-1.5 bg-bg-card border rounded-sm text-[11px] font-korean cursor-pointer transition-colors
${selectedImageIndex === i ? 'border-color-accent' : 'border-stroke'}`}
onClick={() => setSelectedImageIndex(i)}
>
<span className="text-primary-cyan">📷</span>
<span className="flex-1 truncate text-text-1">{file.name}</span>
<span className="text-color-accent">📷</span>
<span className="flex-1 truncate text-fg">{file.name}</span>
<button
onClick={e => { e.stopPropagation(); handleRemoveFile(i); }}
disabled={isStitching || isAnalyzing}
className="text-text-3 hover:text-status-red transition-colors cursor-pointer
className="text-fg-disabled hover:text-color-danger transition-colors cursor-pointer
disabled:opacity-40 disabled:cursor-not-allowed ml-1 shrink-0"
title="제거"
>
@ -266,7 +266,7 @@ export function OilAreaAnalysis() {
</button>
</div>
{selectedImageIndex === i && imageExifs[i] !== undefined && (
<div className="mt-1 mb-1 px-2 py-1.5 bg-bg-0 border border-border/60 rounded-sm text-[11px] font-korean">
<div className="mt-1 mb-1 px-2 py-1.5 bg-bg-base border border-stroke/60 rounded-sm text-[11px] font-korean">
<MetaRow label="파일 크기" value={formatFileSize(file.size)} />
<MetaRow
label="해상도"
@ -323,7 +323,7 @@ export function OilAreaAnalysis() {
{/* 에러 메시지 */}
{error && (
<div className="mb-3 px-2.5 py-2 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.3)] rounded-sm text-[11px] text-status-red font-korean">
<div className="mb-3 px-2.5 py-2 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.3)] rounded-sm text-[11px] text-color-danger font-korean">
{error}
</div>
)}
@ -333,7 +333,7 @@ export function OilAreaAnalysis() {
onClick={handleStitch}
disabled={!canStitch}
className="w-full py-2.5 mb-2 rounded-sm text-[12px] font-bold font-korean cursor-pointer transition-colors
border border-primary-cyan text-primary-cyan bg-[rgba(6,182,212,0.06)]
border border-color-accent text-color-accent bg-[rgba(6,182,212,0.06)]
hover:bg-[rgba(6,182,212,0.15)] disabled:opacity-40 disabled:cursor-not-allowed"
>
{isStitching ? '⏳ 합성 중...' : stitchedBlob ? '✅ 다시 합성' : '🔗 이미지 합성'}
@ -345,7 +345,7 @@ export function OilAreaAnalysis() {
disabled={!canAnalyze}
className="w-full py-3 rounded-sm text-[13px] font-bold font-korean cursor-pointer border-none transition-colors
disabled:opacity-40 disabled:cursor-not-allowed text-white"
style={canAnalyze ? { background: 'linear-gradient(135deg, var(--cyan), var(--blue))' } : { background: 'var(--bg-3)' }}
style={canAnalyze ? { background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))' } : { background: 'var(--bg-3)' }}
>
{isAnalyzing ? '⏳ 분석 중...' : '🧩 분석 시작'}
</button>
@ -359,9 +359,9 @@ export function OilAreaAnalysis() {
{Array.from({ length: MAX_IMAGES }).map((_, i) => (
<div
key={i}
className={`bg-bg-3 border rounded-sm overflow-hidden flex flex-col transition-colors
className={`bg-bg-card border rounded-sm overflow-hidden flex flex-col transition-colors
${previewUrls[i] ? 'cursor-pointer' : ''}
${selectedImageIndex === i ? 'border-primary-cyan' : 'border-border'}`}
${selectedImageIndex === i ? 'border-color-accent' : 'border-stroke'}`}
style={{ height: '300px' }}
onClick={() => { if (previewUrls[i]) setSelectedImageIndex(i); }}
>
@ -374,24 +374,24 @@ export function OilAreaAnalysis() {
className="w-full h-full object-cover"
/>
</div>
<div className="px-2 py-1 bg-bg-0 border-t border-border shrink-0 flex items-start justify-between gap-1">
<div className="text-[10px] text-text-2 truncate font-korean flex-1 min-w-0">
<div className="px-2 py-1 bg-bg-base border-t border-stroke shrink-0 flex items-start justify-between gap-1">
<div className="text-[10px] text-fg-sub truncate font-korean flex-1 min-w-0">
{selectedFiles[i]?.name}
</div>
{imageExifs[i] === undefined ? (
<div className="text-[10px] text-text-3 font-korean shrink-0">GPS ...</div>
<div className="text-[10px] text-fg-disabled font-korean shrink-0">GPS ...</div>
) : imageExifs[i]?.lat !== null ? (
<div className="text-[10px] text-primary-cyan font-mono leading-tight text-right shrink-0">
<div className="text-[10px] text-color-accent font-mono leading-tight text-right shrink-0">
{decimalToDMS(imageExifs[i]!.lat!, true)}<br />
{decimalToDMS(imageExifs[i]!.lon!, false)}
</div>
) : (
<div className="text-[10px] text-text-3 font-korean shrink-0">GPS </div>
<div className="text-[10px] text-fg-disabled font-korean shrink-0">GPS </div>
)}
</div>
</>
) : (
<div className="flex items-center justify-center h-full text-text-3 text-lg font-mono opacity-20">
<div className="flex items-center justify-center h-full text-fg-disabled text-lg font-mono opacity-20">
{i + 1}
</div>
)}
@ -402,7 +402,7 @@ export function OilAreaAnalysis() {
{/* 합성 결과 */}
<div className="text-[11px] font-bold mb-2 font-korean"> </div>
<div
className="relative bg-bg-0 border border-border rounded-sm overflow-hidden flex items-center justify-center"
className="relative bg-bg-base border border-stroke rounded-sm overflow-hidden flex items-center justify-center"
style={{ minHeight: '160px', flex: '1 1 0' }}
>
{stitchedPreviewUrl ? (
@ -412,7 +412,7 @@ export function OilAreaAnalysis() {
className="max-w-full max-h-full object-contain"
/>
) : (
<div className="text-[12px] text-text-3 font-korean text-center px-4">
<div className="text-[12px] text-fg-disabled font-korean text-center px-4">
{isStitching
? '⏳ 이미지를 합성하고 있습니다...'
: '이미지를 선택하고 합성 버튼을 클릭하면\n합성 결과가 여기에 표시됩니다.'}

파일 보기

@ -147,7 +147,7 @@ const OilDetectionOverlay = memo(({ result, isAnalyzing = false, error = null }:
{/* 분석 중 */}
{isAnalyzing && (
<span className='text-[9px] font-korean text-text-3 animate-pulse px-1'>
<span className='text-[9px] font-korean text-fg-disabled animate-pulse px-1'>
...
</span>
)}

파일 보기

@ -110,10 +110,10 @@ export function RealtimeDrone() {
const statusInfo = (status: string) => {
switch (status) {
case 'streaming': return { label: '송출중', color: 'var(--green)', bg: 'rgba(34,197,94,.12)' }
case 'starting': return { label: '연결중', color: 'var(--cyan)', bg: 'rgba(6,182,212,.12)' }
case 'error': return { label: '오류', color: 'var(--red)', bg: 'rgba(239,68,68,.12)' }
default: return { label: '대기', color: 'var(--t3)', bg: 'rgba(255,255,255,.06)' }
case 'streaming': return { label: '송출중', color: 'var(--color-success)', bg: 'rgba(34,197,94,.12)' }
case 'starting': return { label: '연결중', color: 'var(--color-accent)', bg: 'rgba(6,182,212,.12)' }
case 'error': return { label: '오류', color: 'var(--color-danger)', bg: 'rgba(239,68,68,.12)' }
default: return { label: '대기', color: 'var(--fg-disabled)', bg: 'rgba(255,255,255,.06)' }
}
}
@ -123,26 +123,26 @@ export function RealtimeDrone() {
return (
<div className="flex h-full overflow-hidden" style={{ margin: '-20px -24px', height: 'calc(100% + 40px)' }}>
{/* 좌측: 드론 스트림 목록 */}
<div className="flex flex-col overflow-hidden bg-bg-1 border-r border-border w-[260px] min-w-[260px]">
<div className="flex flex-col overflow-hidden bg-bg-surface border-r border-stroke w-[260px] min-w-[260px]">
{/* 헤더 */}
<div className="p-3 pb-2.5 border-b border-border shrink-0 bg-bg-2">
<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-text-1 font-korean flex items-center gap-1.5">
<span className="w-[7px] h-[7px] rounded-full inline-block" style={{ background: streams.some(s => s.status === 'streaming') ? 'var(--green)' : 'var(--t3)' }} />
<div className="text-xs font-bold text-fg font-korean flex items-center gap-1.5">
<span className="w-[7px] h-[7px] rounded-full inline-block" style={{ background: streams.some(s => s.status === 'streaming') ? 'var(--color-success)' : 'var(--fg-disabled)' }} />
</div>
<button
onClick={loadStreams}
className="px-2 py-0.5 text-[9px] font-korean bg-bg-3 border border-border rounded text-text-2 cursor-pointer hover:bg-bg-hover transition-colors"
className="px-2 py-0.5 text-[9px] font-korean bg-bg-card border border-stroke rounded text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors"
></button>
</div>
<div className="text-[9px] text-text-3 font-korean">ViewLink RTSP · </div>
<div className="text-[9px] text-fg-disabled font-korean">ViewLink RTSP · </div>
</div>
{/* 드론 스트림 카드 */}
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
{loading ? (
<div className="px-3.5 py-4 text-[11px] text-text-3 font-korean"> ...</div>
<div className="px-3.5 py-4 text-[11px] text-fg-disabled font-korean"> ...</div>
) : streams.map(stream => {
const si = statusInfo(stream.status)
const isSelected = selectedStream?.id === stream.id
@ -160,8 +160,8 @@ export function RealtimeDrone() {
<div className="flex items-center gap-2">
<div className="text-sm">🚁</div>
<div>
<div className="text-[11px] font-semibold text-text-1 font-korean">{stream.shipName} <span className="text-[9px] text-primary-cyan font-semibold">({stream.droneModel})</span></div>
<div className="text-[9px] text-text-3 font-mono">{stream.ip}</div>
<div className="text-[11px] font-semibold text-fg font-korean">{stream.shipName} <span className="text-[9px] text-color-accent font-semibold">({stream.droneModel})</span></div>
<div className="text-[9px] text-fg-disabled font-mono">{stream.ip}</div>
</div>
</div>
<span
@ -171,12 +171,12 @@ export function RealtimeDrone() {
</div>
<div className="flex items-center gap-1.5">
<span className="text-[8px] text-text-3 font-korean px-1.5 py-0.5 rounded bg-bg-3">{stream.region}</span>
<span className="text-[8px] text-text-3 font-mono px-1.5 py-0.5 rounded bg-bg-3">RTSP :554</span>
<span className="text-[8px] text-fg-disabled font-korean px-1.5 py-0.5 rounded bg-bg-card">{stream.region}</span>
<span className="text-[8px] text-fg-disabled font-mono px-1.5 py-0.5 rounded bg-bg-card">RTSP :554</span>
</div>
{stream.error && (
<div className="mt-1.5 text-[8px] text-status-red font-korean px-1.5 py-1 rounded bg-[rgba(239,68,68,.06)]">
<div className="mt-1.5 text-[8px] text-color-danger font-korean px-1.5 py-1 rounded bg-[rgba(239,68,68,.06)]">
{stream.error}
</div>
)}
@ -187,13 +187,13 @@ export function RealtimeDrone() {
<button
onClick={(e) => { e.stopPropagation(); handleStartStream(stream.id) }}
className="flex-1 px-2 py-1 text-[9px] font-bold font-korean rounded border cursor-pointer transition-colors"
style={{ background: 'rgba(34,197,94,.1)', borderColor: 'rgba(34,197,94,.3)', color: 'var(--green)' }}
style={{ background: 'rgba(34,197,94,.1)', borderColor: 'rgba(34,197,94,.3)', color: 'var(--color-success)' }}
> </button>
) : (
<button
onClick={(e) => { e.stopPropagation(); handleStopStream(stream.id) }}
className="flex-1 px-2 py-1 text-[9px] font-bold font-korean rounded border cursor-pointer transition-colors"
style={{ background: 'rgba(239,68,68,.1)', borderColor: 'rgba(239,68,68,.3)', color: 'var(--red)' }}
style={{ background: 'rgba(239,68,68,.1)', borderColor: 'rgba(239,68,68,.3)', color: 'var(--color-danger)' }}
> </button>
)}
</div>
@ -203,8 +203,8 @@ export function RealtimeDrone() {
</div>
{/* 하단 안내 */}
<div className="px-3 py-2 border-t border-border bg-bg-2 shrink-0">
<div className="text-[8px] text-text-3 font-korean leading-relaxed">
<div className="px-3 py-2 border-t border-stroke bg-bg-elevated shrink-0">
<div className="text-[8px] text-fg-disabled font-korean leading-relaxed">
RTSP .
ViewLink .
</div>
@ -214,25 +214,25 @@ export function RealtimeDrone() {
{/* 중앙: 영상 뷰어 */}
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-[#04070f]">
{/* 툴바 */}
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-bg-2 shrink-0 gap-2.5">
<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-text-1 font-korean whitespace-nowrap overflow-hidden text-ellipsis">
<div className="text-xs font-bold text-fg font-korean whitespace-nowrap overflow-hidden text-ellipsis">
{selectedStream ? `🚁 ${selectedStream.shipName}` : '🚁 드론 스트림을 선택하세요'}
</div>
{selectedStream?.status === 'streaming' && (
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold shrink-0" style={{ background: 'rgba(34,197,94,.14)', border: '1px solid rgba(34,197,94,.35)', color: 'var(--green)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--green)' }} />LIVE
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold shrink-0" style={{ background: 'rgba(34,197,94,.14)', border: '1px solid rgba(34,197,94,.35)', color: 'var(--color-success)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--color-success)' }} />LIVE
</div>
)}
{selectedStream?.status === 'starting' && (
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold shrink-0" style={{ background: 'rgba(6,182,212,.14)', border: '1px solid rgba(6,182,212,.35)', color: 'var(--cyan)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--cyan)' }} />
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold shrink-0" style={{ background: 'rgba(6,182,212,.14)', border: '1px solid rgba(6,182,212,.35)', color: 'var(--color-accent)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--color-accent)' }} />
</div>
)}
</div>
<div className="flex items-center gap-1.5 shrink-0">
{/* 분할 모드 */}
<div className="flex border border-border rounded-[5px] overflow-hidden">
<div className="flex border border-stroke rounded-[5px] overflow-hidden">
{[
{ mode: 1, icon: '▣', label: '1화면' },
{ mode: 4, icon: '⊞', label: '4분할' },
@ -243,15 +243,15 @@ export function RealtimeDrone() {
title={g.label}
className="px-2 py-1 text-[11px] cursor-pointer border-none transition-colors"
style={gridMode === g.mode
? { background: 'rgba(6,182,212,.15)', color: 'var(--cyan)' }
: { background: 'var(--bg3)', color: 'var(--t2)' }
? { background: 'rgba(6,182,212,.15)', color: 'var(--color-accent)' }
: { background: 'var(--bg-card)', color: 'var(--fg-sub)' }
}
>{g.icon}</button>
))}
</div>
<button
onClick={() => playerRefs.current.forEach(r => r?.capture())}
className="px-2.5 py-1 bg-bg-3 border border-border rounded-[5px] text-text-2 text-[10px] font-semibold cursor-pointer font-korean hover:bg-bg-hover transition-colors"
className="px-2.5 py-1 bg-bg-card border border-stroke rounded-[5px] text-fg-sub text-[10px] font-semibold cursor-pointer font-korean hover:bg-bg-surface-hover transition-colors"
>📷 </button>
</div>
</div>
@ -363,7 +363,7 @@ export function RealtimeDrone() {
style={{ background: 'rgba(6,182,212,.15)', borderColor: 'rgba(6,182,212,.3)', color: '#67e8f9' }}
> </button>
) : (
<div className="text-[9px] text-primary-cyan font-korean text-center animate-pulse"> ...</div>
<div className="text-[9px] text-color-accent font-korean text-center animate-pulse"> ...</div>
)}
</div>
</Popup>
@ -398,21 +398,21 @@ export function RealtimeDrone() {
) : stream && stream.status === 'starting' ? (
<div className="flex flex-col items-center justify-center gap-2">
<div className="text-lg opacity-40 animate-pulse">🚁</div>
<div className="text-[10px] text-primary-cyan font-korean animate-pulse">RTSP ...</div>
<div className="text-[8px] text-text-3 font-mono">{stream.ip}:554</div>
<div className="text-[10px] text-color-accent font-korean animate-pulse">RTSP ...</div>
<div className="text-[8px] text-fg-disabled font-mono">{stream.ip}:554</div>
</div>
) : stream && stream.status === 'error' ? (
<div className="flex flex-col items-center justify-center gap-2">
<div className="text-lg opacity-30"></div>
<div className="text-[10px] text-status-red font-korean"> </div>
<div className="text-[8px] text-text-3 font-korean max-w-[200px] text-center">{stream.error}</div>
<div className="text-[10px] text-color-danger font-korean"> </div>
<div className="text-[8px] text-fg-disabled font-korean max-w-[200px] text-center">{stream.error}</div>
<button
onClick={() => handleStartStream(stream.id)}
className="mt-1 px-2.5 py-1 rounded text-[9px] font-korean bg-bg-3 border border-border text-text-2 cursor-pointer hover:bg-bg-hover transition-colors"
className="mt-1 px-2.5 py-1 rounded text-[9px] font-korean bg-bg-card border border-stroke text-fg-sub cursor-pointer hover:bg-bg-surface-hover transition-colors"
></button>
</div>
) : (
<div className="text-[10px] text-text-3 font-korean opacity-40">
<div className="text-[10px] text-fg-disabled font-korean opacity-40">
{streams.length > 0 ? '스트림을 시작하고 선택하세요' : '드론 스트림을 선택하세요'}
</div>
)}
@ -423,22 +423,22 @@ export function RealtimeDrone() {
)}
{/* 하단 정보 바 */}
<div className="flex items-center gap-3.5 px-4 py-1.5 border-t border-border bg-bg-2 shrink-0">
<div className="text-[10px] text-text-3 font-korean">: <b className="text-text-1">{selectedStream?.shipName ?? ''}</b></div>
<div className="text-[10px] text-text-3 font-korean">IP: <span className="text-primary-cyan font-mono text-[9px]">{selectedStream?.ip ?? ''}</span></div>
<div className="text-[10px] text-text-3 font-korean">: <span className="text-text-2">{selectedStream?.region ?? ''}</span></div>
<div className="ml-auto text-[9px] text-text-3 font-korean">RTSP HLS · ViewLink </div>
<div className="flex items-center gap-3.5 px-4 py-1.5 border-t border-stroke bg-bg-elevated shrink-0">
<div className="text-[10px] text-fg-disabled font-korean">: <b className="text-fg">{selectedStream?.shipName ?? ''}</b></div>
<div className="text-[10px] text-fg-disabled font-korean">IP: <span className="text-color-accent font-mono text-[9px]">{selectedStream?.ip ?? ''}</span></div>
<div className="text-[10px] text-fg-disabled font-korean">: <span className="text-fg-sub">{selectedStream?.region ?? ''}</span></div>
<div className="ml-auto text-[9px] text-fg-disabled font-korean">RTSP HLS · ViewLink </div>
</div>
</div>
{/* 우측: 정보 패널 */}
<div className="flex flex-col overflow-hidden bg-bg-1 border-l border-border w-[220px] min-w-[220px]">
<div className="flex flex-col overflow-hidden bg-bg-surface border-l border-stroke w-[220px] min-w-[220px]">
{/* 헤더 */}
<div className="px-3 py-2 border-b border-border text-[11px] font-bold text-text-1 font-korean bg-bg-2 shrink-0">
<div className="px-3 py-2 border-b border-stroke text-[11px] font-bold text-fg font-korean bg-bg-elevated shrink-0">
📋
</div>
<div className="flex-1 overflow-y-auto px-3 py-2.5" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto px-3 py-2.5" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
{selectedStream ? (
<div className="flex flex-col gap-1.5">
{[
@ -451,49 +451,49 @@ export function RealtimeDrone() {
['프로토콜', 'RTSP → HLS'],
['상태', statusInfo(selectedStream.status).label],
].map(([k, v], i) => (
<div key={i} className="flex justify-between px-2 py-1 bg-bg-0 rounded text-[9px]">
<span className="text-text-3 font-korean">{k}</span>
<span className="font-mono text-text-1">{v}</span>
<div key={i} className="flex justify-between px-2 py-1 bg-bg-base rounded text-[9px]">
<span className="text-fg-disabled font-korean">{k}</span>
<span className="font-mono text-fg">{v}</span>
</div>
))}
{selectedStream.hlsUrl && (
<div className="px-2 py-1 bg-bg-0 rounded text-[8px]">
<div className="text-text-3 font-korean mb-0.5">HLS URL</div>
<div className="font-mono text-primary-cyan break-all">{selectedStream.hlsUrl}</div>
<div className="px-2 py-1 bg-bg-base rounded text-[8px]">
<div className="text-fg-disabled font-korean mb-0.5">HLS URL</div>
<div className="font-mono text-color-accent break-all">{selectedStream.hlsUrl}</div>
</div>
)}
</div>
) : (
<div className="text-[10px] text-text-3 font-korean"> </div>
<div className="text-[10px] text-fg-disabled font-korean"> </div>
)}
{/* 연동 시스템 */}
<div className="mt-3 pt-2.5 border-t border-border">
<div className="text-[10px] font-bold text-text-2 font-korean mb-2">🔗 </div>
<div className="mt-3 pt-2.5 border-t border-stroke">
<div className="text-[10px] font-bold text-fg-sub font-korean mb-2">🔗 </div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between px-2 py-1.5 bg-bg-3 rounded-[5px]" style={{ border: '1px solid rgba(6,182,212,.2)' }}>
<span className="text-[9px] text-text-2 font-korean">ViewLink 3.5</span>
<span className="text-[9px] font-bold" style={{ color: 'var(--cyan)' }}> RTSP</span>
<div className="flex items-center justify-between px-2 py-1.5 bg-bg-card rounded-[5px]" style={{ border: '1px solid rgba(6,182,212,.2)' }}>
<span className="text-[9px] text-fg-sub font-korean">ViewLink 3.5</span>
<span className="text-[9px] font-bold" style={{ color: 'var(--color-accent)' }}> RTSP</span>
</div>
<div className="flex items-center justify-between px-2 py-1.5 bg-bg-3 rounded-[5px]" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
<span className="text-[9px] text-text-2 font-korean">FFmpeg </span>
<div className="flex items-center justify-between px-2 py-1.5 bg-bg-card rounded-[5px]" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
<span className="text-[9px] text-fg-sub font-korean">FFmpeg </span>
<span className="text-[9px] font-bold" style={{ color: 'var(--blue, #3b82f6)' }}>RTSPHLS</span>
</div>
</div>
</div>
{/* 전체 상태 요약 */}
<div className="mt-3 pt-2.5 border-t border-border">
<div className="text-[10px] font-bold text-text-2 font-korean mb-2">📊 </div>
<div className="mt-3 pt-2.5 border-t border-stroke">
<div className="text-[10px] font-bold text-fg-sub font-korean mb-2">📊 </div>
<div className="grid grid-cols-2 gap-1.5">
{[
{ label: '전체', value: streams.length, color: 'text-text-1' },
{ label: '송출중', value: streams.filter(s => s.status === 'streaming').length, color: 'text-status-green' },
{ label: '연결중', value: streams.filter(s => s.status === 'starting').length, color: 'text-primary-cyan' },
{ label: '오류', value: streams.filter(s => s.status === 'error').length, color: 'text-status-red' },
{ label: '전체', value: streams.length, color: 'text-fg' },
{ label: '송출중', value: streams.filter(s => s.status === 'streaming').length, color: 'text-color-success' },
{ label: '연결중', value: streams.filter(s => s.status === 'starting').length, color: 'text-color-accent' },
{ label: '오류', value: streams.filter(s => s.status === 'error').length, color: 'text-color-danger' },
].map((item, i) => (
<div key={i} className="px-2 py-1.5 bg-bg-0 rounded text-center">
<div className="text-[8px] text-text-3 font-korean">{item.label}</div>
<div key={i} className="px-2 py-1.5 bg-bg-base rounded text-center">
<div className="text-[8px] text-fg-disabled font-korean">{item.label}</div>
<div className={`text-sm font-bold font-mono ${item.color}`}>{item.value}</div>
</div>
))}

파일 보기

@ -36,10 +36,10 @@ const satRequests: SatRequest[] = [
]
const satellites = [
{ name: 'KOMPSAT-3A', desc: '해상도 0.5m · 광학 / IR · 촬영 가능', status: '가용', statusColor: 'var(--green)', borderColor: 'rgba(34,197,94,.2)', pulse: true },
{ name: 'KOMPSAT-3', desc: '해상도 1.0m · 광학 · 임무 중', status: '임무중', statusColor: 'var(--yellow)', borderColor: 'rgba(234,179,8,.2)', pulse: true },
{ name: 'Sentinel-1 (ESA)', desc: '해상도 20m · SAR · 야간/우천 가능', status: '가용', statusColor: 'var(--green)', borderColor: 'var(--bd)', pulse: false },
{ name: 'Sentinel-2 (ESA)', desc: '해상도 10m · 다분광 · 수질 분석 적합', status: '가용', statusColor: 'var(--green)', borderColor: 'var(--bd)', pulse: false },
{ name: 'KOMPSAT-3A', desc: '해상도 0.5m · 광학 / IR · 촬영 가능', status: '가용', statusColor: 'var(--color-success)', borderColor: 'rgba(34,197,94,.2)', pulse: true },
{ name: 'KOMPSAT-3', desc: '해상도 1.0m · 광학 · 임무 중', status: '임무중', statusColor: 'var(--color-caution)', borderColor: 'rgba(234,179,8,.2)', pulse: true },
{ name: 'Sentinel-1 (ESA)', desc: '해상도 20m · SAR · 야간/우천 가능', status: '가용', statusColor: 'var(--color-success)', borderColor: 'var(--stroke-default)', pulse: false },
{ name: 'Sentinel-2 (ESA)', desc: '해상도 10m · 다분광 · 수질 분석 적합', status: '가용', statusColor: 'var(--color-success)', borderColor: 'var(--stroke-default)', pulse: false },
]
const passSchedules = [
@ -161,26 +161,26 @@ export function SatelliteRequest() {
const statusBadge = (s: SatRequest['status']) => {
if (s === '촬영중') return (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(234,179,8,.15)', border: '1px solid rgba(234,179,8,.3)', color: 'var(--yellow)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--yellow)' }} />
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(234,179,8,.15)', border: '1px solid rgba(234,179,8,.3)', color: 'var(--color-caution)' }}>
<span className="w-[5px] h-[5px] rounded-full inline-block animate-pulse" style={{ background: 'var(--color-caution)' }} />
</span>
)
if (s === '대기') return (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(59,130,246,.15)', border: '1px solid rgba(59,130,246,.3)', color: 'var(--blue)' }}> </span>
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(59,130,246,.15)', border: '1px solid rgba(59,130,246,.3)', color: 'var(--color-info)' }}> </span>
)
if (s === '취소') return (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)', color: 'var(--red)' }}> </span>
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)', color: 'var(--color-danger)' }}> </span>
)
return (
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)', color: 'var(--green)' }}> </span>
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)', color: 'var(--color-success)' }}> </span>
)
}
const stats = [
{ value: '3', label: '요청 대기', color: 'var(--blue)' },
{ value: '1', label: '촬영 진행 중', color: 'var(--yellow)' },
{ value: '7', label: '수신 완료', color: 'var(--green)' },
{ value: '0.5m', label: '최고 해상도', color: 'var(--cyan)' },
{ value: '3', label: '요청 대기', color: 'var(--color-info)' },
{ value: '1', label: '촬영 진행 중', color: 'var(--color-caution)' },
{ value: '7', label: '수신 완료', color: 'var(--color-success)' },
{ value: '0.5m', label: '최고 해상도', color: 'var(--color-accent)' },
]
const filters = ['전체', '대기', '진행', '완료', '취소']
@ -204,44 +204,44 @@ 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" style={{ background: 'linear-gradient(135deg,rgba(59,130,246,.2),rgba(168,85,247,.2))', border: '1px solid rgba(59,130,246,.3)' }}>🛰</div>
<div className="text-[12px] font-bold font-korean text-text-1"> </div>
<div className="text-[12px] font-bold font-korean text-fg"> </div>
</div>
<div className="flex gap-1 h-7">
<button
onClick={() => setMainTab('list')}
className="px-2.5 h-full rounded text-[10px] font-bold font-korean cursor-pointer border transition-colors"
style={mainTab === 'list'
? { background: 'rgba(59,130,246,.12)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--blue)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
? { background: 'rgba(59,130,246,.12)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--color-info)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>📋 </button>
<button
onClick={() => setMainTab('map')}
className="px-2.5 h-full rounded text-[10px] font-bold font-korean cursor-pointer border transition-colors"
style={mainTab === 'map'
? { background: 'rgba(59,130,246,.12)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--blue)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
? { background: 'rgba(59,130,246,.12)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--color-info)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>🗺 </button>
</div>
<button onClick={() => setModalPhase('provider')} className="ml-auto px-3 h-7 text-white border-none rounded-sm text-[10px] font-semibold cursor-pointer font-korean flex items-center gap-1 shrink-0" style={{ background: 'linear-gradient(135deg,var(--blue),var(--purple))' }}>🛰 </button>
<button onClick={() => setModalPhase('provider')} className="ml-auto px-3 h-7 text-white border-none rounded-sm text-[10px] font-semibold cursor-pointer font-korean flex items-center gap-1 shrink-0" style={{ background: 'linear-gradient(135deg,var(--color-info),var(--color-tertiary))' }}>🛰 </button>
</div>
{mainTab === 'list' && (<>
{/* 요약 통계 */}
<div className="grid grid-cols-4 gap-3 mb-5">
{stats.map((s, i) => (
<div key={i} className="bg-bg-2 border border-border rounded-md p-3.5 text-center">
<div key={i} className="bg-bg-elevated border border-stroke rounded-md p-3.5 text-center">
<div className="text-[22px] font-bold font-mono" style={{ color: s.color }}>{s.value}</div>
<div className="text-[10px] text-text-3 mt-1 font-korean">{s.label}</div>
<div className="text-[10px] text-fg-disabled mt-1 font-korean">{s.label}</div>
</div>
))}
</div>
{/* 요청 목록 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden mb-5">
<div className="flex items-center justify-between px-4 py-3.5 border-b border-border">
<div className="text-[13px] font-bold font-korean text-text-1">📋 </div>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden mb-5">
<div className="flex items-center justify-between px-4 py-3.5 border-b border-stroke">
<div className="text-[13px] font-bold font-korean text-fg">📋 </div>
<div className="flex gap-1.5">
{filters.map(f => (
<button
@ -249,8 +249,8 @@ export function SatelliteRequest() {
onClick={() => setStatusFilter(f)}
className="px-2.5 py-1 rounded text-[10px] font-semibold cursor-pointer font-korean border"
style={statusFilter === f
? { background: 'rgba(59,130,246,.15)', color: 'var(--blue)', borderColor: 'rgba(59,130,246,.3)' }
: { background: 'var(--bg3)', color: 'var(--t2)', borderColor: 'var(--bd)' }
? { background: 'rgba(59,130,246,.15)', color: 'var(--color-info)', borderColor: 'rgba(59,130,246,.3)' }
: { background: 'var(--bg-card)', color: 'var(--fg-sub)', borderColor: 'var(--stroke-default)' }
}
>{f}</button>
))}
@ -258,9 +258,9 @@ export function SatelliteRequest() {
</div>
{/* 헤더 행 */}
<div className="grid gap-0 px-4 py-2 bg-bg-3 border-b border-border" style={{ gridTemplateColumns: '60px 1fr 100px 100px 120px 80px 90px' }}>
<div className="grid gap-0 px-4 py-2 bg-bg-card border-b border-stroke" style={{ gridTemplateColumns: '60px 1fr 100px 100px 120px 80px 90px' }}>
{['번호', '촬영 구역', '위성', '요청일시', '예상수신', '해상도', '상태'].map(h => (
<div key={h} className="text-[9px] font-bold text-text-3 font-korean uppercase tracking-wider">{h}</div>
<div key={h} className="text-[9px] font-bold text-fg-disabled font-korean uppercase tracking-wider">{h}</div>
))}
</div>
@ -269,7 +269,7 @@ export function SatelliteRequest() {
<div key={r.id}>
<div
onClick={() => setSelectedRequest(selectedRequest?.id === r.id ? null : r)}
className="grid gap-0 px-4 py-3 border-b items-center cursor-pointer hover:bg-bg-hover/30 transition-colors"
className="grid gap-0 px-4 py-3 border-b items-center cursor-pointer hover:bg-bg-surface-hover/30 transition-colors"
style={{
gridTemplateColumns: '60px 1fr 100px 100px 120px 80px 90px',
borderColor: 'rgba(255,255,255,.04)',
@ -277,15 +277,15 @@ export function SatelliteRequest() {
opacity: (r.status === '완료' || r.status === '취소') ? 0.6 : 1,
}}
>
<div className="text-[11px] font-mono text-text-2">{r.id}</div>
<div className="text-[11px] font-mono text-fg-sub">{r.id}</div>
<div>
<div className="text-xs font-semibold text-text-1 font-korean">{r.zone}</div>
<div className="text-[10px] text-text-3 font-mono mt-0.5">{r.zoneCoord} · {r.zoneArea}</div>
<div className="text-xs font-semibold text-fg font-korean">{r.zone}</div>
<div className="text-[10px] text-fg-disabled font-mono mt-0.5">{r.zoneCoord} · {r.zoneArea}</div>
</div>
<div className="text-[11px] font-semibold text-text-1 font-korean">{r.satellite}</div>
<div className="text-[10px] text-text-2 font-mono">{r.requestDate}</div>
<div className="text-[10px] font-semibold font-mono" style={{ color: r.status === '촬영중' ? 'var(--yellow)' : 'var(--t2)' }}>{r.expectedReceive}</div>
<div className="text-[11px] font-bold font-mono" style={{ color: r.status === '완료' ? 'var(--t3)' : 'var(--cyan)' }}>{r.resolution}</div>
<div className="text-[11px] font-semibold text-fg font-korean">{r.satellite}</div>
<div className="text-[10px] text-fg-sub font-mono">{r.requestDate}</div>
<div className="text-[10px] font-semibold font-mono" style={{ color: r.status === '촬영중' ? 'var(--color-caution)' : 'var(--fg-sub)' }}>{r.expectedReceive}</div>
<div className="text-[11px] font-bold font-mono" style={{ color: r.status === '완료' ? 'var(--fg-disabled)' : 'var(--color-accent)' }}>{r.resolution}</div>
<div>{statusBadge(r.status)}</div>
</div>
{/* 상세 정보 패널 */}
@ -298,16 +298,16 @@ export function SatelliteRequest() {
['요청자', r.requester || '-'],
['촬영 면적', r.zoneArea],
].map(([k, v], i) => (
<div key={i} className="px-2.5 py-2 bg-bg-0 rounded">
<div className="text-[8px] font-bold text-text-3 font-korean mb-1 uppercase">{k}</div>
<div className="text-[10px] font-semibold text-text-1 font-korean">{v}</div>
<div key={i} className="px-2.5 py-2 bg-bg-base rounded">
<div className="text-[8px] font-bold text-fg-disabled font-korean mb-1 uppercase">{k}</div>
<div className="text-[10px] font-semibold text-fg font-korean">{v}</div>
</div>
))}
</div>
<div className="flex gap-2">
<button className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-hover transition-colors" style={{ background: 'rgba(6,182,212,.08)', borderColor: 'rgba(6,182,212,.2)', color: 'var(--cyan)' }}>📍 </button>
<button className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-surface-hover transition-colors" style={{ background: 'rgba(6,182,212,.08)', borderColor: 'rgba(6,182,212,.2)', color: 'var(--color-accent)' }}>📍 </button>
{r.status === '완료' && (
<button className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-hover transition-colors" style={{ background: 'rgba(34,197,94,.08)', borderColor: 'rgba(34,197,94,.2)', color: 'var(--green)' }}>📥 </button>
<button className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-surface-hover transition-colors" style={{ background: 'rgba(34,197,94,.08)', borderColor: 'rgba(34,197,94,.2)', color: 'var(--color-success)' }}>📥 </button>
)}
{(r.status === '대기' || r.status === '촬영중') && (
<button
@ -318,8 +318,8 @@ export function SatelliteRequest() {
setSelectedRequest(null)
}
}}
className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-hover transition-colors"
style={{ background: 'rgba(239,68,68,.08)', borderColor: 'rgba(239,68,68,.2)', color: 'var(--red)' }}
className="px-3 py-1.5 text-[10px] font-semibold font-korean rounded border cursor-pointer hover:bg-bg-surface-hover transition-colors"
style={{ background: 'rgba(239,68,68,.08)', borderColor: 'rgba(239,68,68,.2)', color: 'var(--color-danger)' }}
> </button>
)}
</div>
@ -330,7 +330,7 @@ export function SatelliteRequest() {
{/* 페이징 */}
<div className="flex items-center justify-between px-4 py-2">
<div className="text-[9px] text-text-3 font-korean">
<div className="text-[9px] text-fg-disabled font-korean">
{filtered.length} {(currentPage - 1) * PAGE_SIZE + 1}{Math.min(currentPage * PAGE_SIZE, filtered.length)}
</div>
<div className="flex items-center gap-1">
@ -338,7 +338,7 @@ export function SatelliteRequest() {
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage <= 1}
className="px-2 py-1 rounded text-[10px] font-mono cursor-pointer border transition-colors"
style={{ background: 'var(--bg3)', borderColor: 'var(--bd)', color: currentPage <= 1 ? 'var(--t3)' : 'var(--t1)', opacity: currentPage <= 1 ? 0.5 : 1 }}
style={{ background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: currentPage <= 1 ? 'var(--fg-disabled)' : 'var(--fg-default)', opacity: currentPage <= 1 ? 0.5 : 1 }}
></button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
<button
@ -346,8 +346,8 @@ export function SatelliteRequest() {
onClick={() => setCurrentPage(p)}
className="w-7 h-7 rounded text-[10px] font-bold font-mono cursor-pointer border transition-colors"
style={currentPage === p
? { background: 'rgba(59,130,246,.15)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--blue)' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
? { background: 'rgba(59,130,246,.15)', borderColor: 'rgba(59,130,246,.3)', color: 'var(--color-info)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>{p}</button>
))}
@ -355,7 +355,7 @@ export function SatelliteRequest() {
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage >= totalPages}
className="px-2 py-1 rounded text-[10px] font-mono cursor-pointer border transition-colors"
style={{ background: 'var(--bg3)', borderColor: 'var(--bd)', color: currentPage >= totalPages ? 'var(--t3)' : 'var(--t1)', opacity: currentPage >= totalPages ? 0.5 : 1 }}
style={{ background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: currentPage >= totalPages ? 'var(--fg-disabled)' : 'var(--fg-default)', opacity: currentPage >= totalPages ? 0.5 : 1 }}
></button>
</div>
</div>
@ -364,15 +364,15 @@ export function SatelliteRequest() {
{/* 위성 궤도 정보 */}
<div className="grid grid-cols-2 gap-3.5">
{/* 가용 위성 현황 */}
<div className="bg-bg-2 border border-border rounded-md p-4">
<div className="text-xs font-bold text-text-1 font-korean mb-3">🛰 </div>
<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="flex flex-col gap-2">
{satellites.map((sat, i) => (
<div key={i} className="flex items-center gap-2.5 px-3 py-2 bg-bg-3 rounded-md" style={{ border: `1px solid ${sat.borderColor}` }}>
<div key={i} className="flex items-center gap-2.5 px-3 py-2 bg-bg-card rounded-md" style={{ border: `1px solid ${sat.borderColor}` }}>
<div className={`w-2 h-2 rounded-full shrink-0 ${sat.pulse ? 'animate-pulse' : ''}`} style={{ background: sat.statusColor }} />
<div className="flex-1">
<div className="text-[11px] font-semibold text-text-1 font-korean">{sat.name}</div>
<div className="text-[9px] text-text-3 font-korean">{sat.desc}</div>
<div className="text-[11px] font-semibold text-fg font-korean">{sat.name}</div>
<div className="text-[9px] text-fg-disabled font-korean">{sat.desc}</div>
</div>
<div className="text-[10px] font-bold font-korean" style={{ color: sat.statusColor }}>{sat.status}</div>
</div>
@ -381,8 +381,8 @@ export function SatelliteRequest() {
</div>
{/* 오늘 촬영 가능 시간 */}
<div className="bg-bg-2 border border-border rounded-md p-4">
<div className="text-xs font-bold text-text-1 font-korean mb-3"> (KST)</div>
<div className="bg-bg-elevated border border-stroke rounded-md p-4">
<div className="text-xs font-bold text-fg font-korean mb-3"> (KST)</div>
<div className="flex flex-col gap-1.5">
{passSchedules.map((ps, i) => (
<div
@ -393,8 +393,8 @@ export function SatelliteRequest() {
border: ps.today ? '1px solid rgba(34,197,94,.15)' : '1px solid rgba(59,130,246,.15)',
}}
>
<span className="text-[10px] font-bold font-mono min-w-[90px]" style={{ color: ps.today ? 'var(--cyan)' : 'var(--blue)' }}>{ps.time}</span>
<span className="text-[10px] text-text-1 font-korean">{ps.desc}</span>
<span className="text-[10px] font-bold font-mono min-w-[90px]" style={{ color: ps.today ? 'var(--color-accent)' : 'var(--color-info)' }}>{ps.time}</span>
<span className="text-[10px] text-fg font-korean">{ps.desc}</span>
</div>
))}
</div>
@ -407,7 +407,7 @@ export function SatelliteRequest() {
const dateFiltered = requests.filter(r => r.dateKey === mapSelectedDate)
const dateHasDots = [...new Set(requests.map(r => r.dateKey).filter(Boolean))]
return (
<div className="bg-bg-2 border border-border rounded-md overflow-hidden relative" style={{ height: 'calc(100vh - 160px)' }}>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden relative" style={{ height: 'calc(100vh - 160px)' }}>
<Map
initialViewState={{ longitude: 127.5, latitude: 34.5, zoom: 7 }}
mapStyle={SAT_MAP_STYLE}
@ -471,15 +471,15 @@ export function SatelliteRequest() {
</Map>
{/* 좌상단: 캘린더 + 날짜별 리스트 */}
<div className="absolute top-3 left-3 w-[260px] rounded-lg border border-border z-10 overflow-hidden" style={{ background: 'rgba(18,25,41,.92)', backdropFilter: 'blur(8px)' }}>
<div className="absolute top-3 left-3 w-[260px] rounded-lg border border-stroke z-10 overflow-hidden" style={{ background: 'rgba(18,25,41,.92)', backdropFilter: 'blur(8px)' }}>
{/* 캘린더 헤더 */}
<div className="px-3 py-2 border-b border-border">
<div className="text-[10px] font-bold text-text-2 font-korean mb-2">📅 </div>
<div className="px-3 py-2 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-sub font-korean mb-2">📅 </div>
<input
type="date"
value={mapSelectedDate}
onChange={e => { setMapSelectedDate(e.target.value); setMapSelectedItem(null) }}
className="w-full px-2.5 py-1.5 bg-bg-3 border border-border rounded text-[11px] font-mono text-text-1 outline-none focus:border-[var(--cyan)] transition-colors"
className="w-full px-2.5 py-1.5 bg-bg-card border border-stroke rounded text-[11px] font-mono text-fg outline-none focus:border-[var(--color-accent)] transition-colors"
/>
{/* 촬영 이력 있는 날짜 점 표시 */}
<div className="flex flex-wrap gap-1 mt-2">
@ -489,8 +489,8 @@ export function SatelliteRequest() {
onClick={() => { setMapSelectedDate(d!); setMapSelectedItem(null) }}
className="px-1.5 py-0.5 rounded text-[8px] font-mono cursor-pointer border transition-colors"
style={mapSelectedDate === d
? { background: 'rgba(6,182,212,.2)', borderColor: 'var(--cyan)', color: 'var(--cyan)' }
: { background: 'var(--bg0)', borderColor: 'var(--bd)', color: 'var(--t3)' }
? { background: 'rgba(6,182,212,.2)', borderColor: 'var(--color-accent)', color: 'var(--color-accent)' }
: { background: 'var(--bg-base)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>{d?.slice(5)}</button>
))}
@ -498,12 +498,12 @@ export function SatelliteRequest() {
</div>
{/* 날짜별 촬영 리스트 */}
<div className="max-h-[35vh] overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="px-3 py-1.5 border-b border-border text-[9px] text-text-3 font-korean sticky top-0" style={{ background: 'rgba(18,25,41,.95)' }}>
<div className="max-h-[35vh] overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
<div className="px-3 py-1.5 border-b border-stroke text-[9px] text-fg-disabled font-korean sticky top-0" style={{ background: 'rgba(18,25,41,.95)' }}>
{mapSelectedDate} · {dateFiltered.length}
</div>
{dateFiltered.length === 0 ? (
<div className="px-3 py-4 text-[10px] text-text-3 font-korean text-center"> </div>
<div className="px-3 py-4 text-[10px] text-fg-disabled font-korean text-center"> </div>
) : dateFiltered.map(r => {
const statusColor = r.status === '촬영중' ? '#eab308' : r.status === '완료' ? '#22c55e' : r.status === '취소' ? '#ef4444' : '#3b82f6'
const isSelected = mapSelectedItem?.id === r.id
@ -518,13 +518,13 @@ export function SatelliteRequest() {
}}
>
<div className="flex items-center justify-between mb-0.5">
<span className="text-[10px] font-mono text-text-2">{r.id}</span>
<span className="text-[10px] font-mono text-fg-sub">{r.id}</span>
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" style={{ background: `${statusColor}20`, color: statusColor }}>{r.status}</span>
</div>
<div className="text-[9px] text-text-1 font-korean truncate">{r.zone}</div>
<div className="text-[8px] text-text-3 font-mono mt-0.5">{r.satellite} · {r.resolution}</div>
<div className="text-[9px] text-fg font-korean truncate">{r.zone}</div>
<div className="text-[8px] text-fg-disabled font-mono mt-0.5">{r.satellite} · {r.resolution}</div>
{r.status === '완료' && (
<div className="mt-1 text-[8px] font-korean" style={{ color: 'var(--cyan)' }}>📷 </div>
<div className="mt-1 text-[8px] font-korean" style={{ color: 'var(--color-accent)' }}>📷 </div>
)}
</div>
)
@ -533,8 +533,8 @@ export function SatelliteRequest() {
</div>
{/* 우상단: 범례 */}
<div className="absolute top-3 right-3 px-3 py-2.5 rounded-lg border border-border z-10" style={{ background: 'rgba(18,25,41,.9)', backdropFilter: 'blur(8px)' }}>
<div className="text-[10px] font-bold text-text-2 font-korean mb-2"> </div>
<div className="absolute top-3 right-3 px-3 py-2.5 rounded-lg border border-stroke z-10" style={{ background: 'rgba(18,25,41,.9)', backdropFilter: 'blur(8px)' }}>
<div className="text-[10px] font-bold text-fg-sub font-korean mb-2"> </div>
{[
{ label: '촬영중', color: '#eab308' },
{ label: '대기', color: '#3b82f6' },
@ -543,30 +543,30 @@ export function SatelliteRequest() {
].map(item => (
<div key={item.label} className="flex items-center gap-2 mb-1">
<div className="w-3 h-3 rounded-sm" style={{ background: item.color, opacity: 0.4, border: `1px solid ${item.color}` }} />
<span className="text-[9px] text-text-3 font-korean">{item.label}</span>
<span className="text-[9px] text-fg-disabled font-korean">{item.label}</span>
</div>
))}
<div className="text-[8px] text-text-3 font-korean mt-1.5 pt-1.5 border-t border-border"> {requests.length}</div>
<div className="text-[8px] text-fg-disabled font-korean mt-1.5 pt-1.5 border-t border-stroke"> {requests.length}</div>
</div>
{/* 선택된 항목 상세 (하단) */}
{mapSelectedItem && (
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 px-4 py-3 rounded-lg border border-border z-10 max-w-[500px]" style={{ background: 'rgba(18,25,41,.95)', backdropFilter: 'blur(8px)' }}>
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 px-4 py-3 rounded-lg border border-stroke z-10 max-w-[500px]" style={{ background: 'rgba(18,25,41,.95)', backdropFilter: 'blur(8px)' }}>
<div className="flex items-center gap-3">
<div className="flex-1">
<div className="text-[11px] font-bold text-text-1 font-korean mb-0.5">{mapSelectedItem.zone}</div>
<div className="text-[9px] text-text-3 font-mono">{mapSelectedItem.satellite} · {mapSelectedItem.resolution} · {mapSelectedItem.zoneCoord}</div>
<div className="text-[11px] font-bold text-fg font-korean mb-0.5">{mapSelectedItem.zone}</div>
<div className="text-[9px] text-fg-disabled font-mono">{mapSelectedItem.satellite} · {mapSelectedItem.resolution} · {mapSelectedItem.zoneCoord}</div>
</div>
<div className="text-center shrink-0">
<div className="text-[8px] text-text-3 font-korean"></div>
<div className="text-[10px] font-mono text-text-1">{mapSelectedItem.requestDate}</div>
<div className="text-[8px] text-fg-disabled font-korean"></div>
<div className="text-[10px] font-mono text-fg">{mapSelectedItem.requestDate}</div>
</div>
{mapSelectedItem.status === '완료' && (
<div className="px-2 py-1 rounded text-[9px] font-bold font-korean shrink-0" style={{ background: 'rgba(34,197,94,.15)', color: '#22c55e' }}>
📷
</div>
)}
<button onClick={() => setMapSelectedItem(null)} className="text-text-3 bg-transparent border-none cursor-pointer text-sm"></button>
<button onClick={() => setMapSelectedItem(null)} className="text-fg-disabled bg-transparent border-none cursor-pointer text-sm"></button>
</div>
</div>
)}
@ -581,7 +581,7 @@ export function SatelliteRequest() {
{/* ── 제공자 선택 ── */}
{modalPhase === 'provider' && (
<div className="border rounded-2xl w-[640px] overflow-hidden" style={{ background: 'var(--bg1)', borderColor: 'rgba(99,102,241,.3)', boxShadow: '0 24px 80px rgba(0,0,0,.6)' }}>
<div className="border rounded-2xl w-[640px] overflow-hidden" style={{ background: 'var(--bg-surface)', borderColor: 'rgba(99,102,241,.3)', boxShadow: '0 24px 80px rgba(0,0,0,.6)' }}>
{/* 헤더 */}
<div className="px-7 pt-6 pb-4 relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#6366f1,#3b82f6,#06b6d4)' }} />
@ -589,11 +589,11 @@ export function SatelliteRequest() {
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg,rgba(99,102,241,.15),rgba(59,130,246,.08))' }}>🛰</div>
<div>
<div className="text-base font-bold text-text-1 font-korean"> </div>
<div className="text-[10px] text-text-3 font-korean mt-0.5"> </div>
<div className="text-base font-bold text-fg font-korean"> </div>
<div className="text-[10px] text-fg-disabled font-korean mt-0.5"> </div>
</div>
</div>
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer text-text-3 p-1 bg-transparent border-none hover:text-text-1 transition-colors"></button>
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer text-fg-disabled p-1 bg-transparent border-none hover:text-fg transition-colors"></button>
</div>
</div>
@ -602,7 +602,7 @@ export function SatelliteRequest() {
{/* BlackSky (Maxar) */}
<div
onClick={() => setModalPhase('blacksky')}
className="cursor-pointer bg-bg-2 border border-border rounded-xl p-5 relative overflow-hidden hover:border-[rgba(99,102,241,.5)] hover:bg-[rgba(99,102,241,.04)] transition-all"
className="cursor-pointer bg-bg-elevated border border-stroke rounded-xl p-5 relative overflow-hidden hover:border-[rgba(99,102,241,.5)] hover:bg-[rgba(99,102,241,.04)] transition-all"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2.5">
@ -610,36 +610,36 @@ export function SatelliteRequest() {
<span className="text-[11px] font-extrabold font-mono text-[#818cf8] tracking-[-0.5px]">B<span className="text-[#a78bfa]">Sky</span></span>
</div>
<div>
<div className="text-sm font-bold text-text-1 font-korean">BlackSky</div>
<div className="text-[9px] text-text-3 font-korean mt-px">Maxar Electro-Optical API</div>
<div className="text-sm font-bold text-fg font-korean">BlackSky</div>
<div className="text-[9px] text-fg-disabled font-korean mt-px">Maxar Electro-Optical API</div>
</div>
</div>
<div className="flex items-center gap-1.5">
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API </span>
<span className="text-base text-text-3"></span>
<span className="text-base text-fg-disabled"></span>
</div>
</div>
<div className="grid grid-cols-4 gap-2 mb-2.5">
{[
['유형', 'EO (광학)', '#818cf8'],
['해상도', '~1m', 'var(--t1)'],
['재방문', '≤1시간', 'var(--t1)'],
['해상도', '~1m', 'var(--fg-default)'],
['재방문', '≤1시간', 'var(--fg-default)'],
['납기', '90분 이내', '#22c55e'],
].map(([k, v, c], i) => (
<div key={i} className="text-center p-1.5 bg-bg-0 rounded-md">
<div className="text-[7px] text-text-3 font-korean mb-0.5">{k}</div>
<div key={i} className="text-center p-1.5 bg-bg-base rounded-md">
<div className="text-[7px] text-fg-disabled font-korean mb-0.5">{k}</div>
<div className="text-[10px] font-bold font-mono" style={{ color: c }}>{v}</div>
</div>
))}
</div>
<div className="text-[9px] text-text-3 font-korean leading-relaxed"> . . Dawn-to-Dusk .</div>
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#818cf8]">eapi.maxar.com/e1so/rapidoc</span></div>
<div className="text-[9px] text-fg-disabled font-korean leading-relaxed"> . . Dawn-to-Dusk .</div>
<div className="mt-2 text-[8px] text-fg-disabled font-mono">API: <span className="text-[#818cf8]">eapi.maxar.com/e1so/rapidoc</span></div>
</div>
{/* UP42 (EO + SAR) */}
<div
onClick={() => setModalPhase('up42')}
className="cursor-pointer bg-bg-2 border border-border rounded-xl p-5 relative overflow-hidden hover:border-[rgba(59,130,246,.5)] hover:bg-[rgba(59,130,246,.04)] transition-all"
className="cursor-pointer bg-bg-elevated border border-stroke rounded-xl p-5 relative overflow-hidden hover:border-[rgba(59,130,246,.5)] hover:bg-[rgba(59,130,246,.04)] transition-all"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2.5">
@ -647,24 +647,24 @@ export function SatelliteRequest() {
<span className="text-[13px] font-extrabold font-mono text-[#60a5fa] tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
</div>
<div>
<div className="text-sm font-bold text-text-1 font-korean">UP42 EO + SAR</div>
<div className="text-[9px] text-text-3 font-korean mt-px">Optical · SAR · Elevation </div>
<div className="text-sm font-bold text-fg font-korean">UP42 EO + SAR</div>
<div className="text-[9px] text-fg-disabled font-korean mt-px">Optical · SAR · Elevation </div>
</div>
</div>
<div className="flex items-center gap-1.5">
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API </span>
<span className="text-base text-text-3"></span>
<span className="text-base text-fg-disabled"></span>
</div>
</div>
<div className="grid grid-cols-4 gap-2 mb-2.5">
{[
['유형', 'EO + SAR', '#60a5fa'],
['해상도', '0.3~5m', 'var(--t1)'],
['위성 수', '16+ 컬렉션', 'var(--t1)'],
['해상도', '0.3~5m', 'var(--fg-default)'],
['위성 수', '16+ 컬렉션', 'var(--fg-default)'],
['야간/악천후', 'SAR 가능', '#22c55e'],
].map(([k, v, c], i) => (
<div key={i} className="text-center p-1.5 bg-bg-0 rounded-md">
<div className="text-[7px] text-text-3 font-korean mb-0.5">{k}</div>
<div key={i} className="text-center p-1.5 bg-bg-base rounded-md">
<div className="text-[7px] text-fg-disabled font-korean mb-0.5">{k}</div>
<div className="text-[10px] font-bold font-mono" style={{ color: c }}>{v}</div>
</div>
))}
@ -674,19 +674,19 @@ export function SatelliteRequest() {
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.08)', border: '1px solid rgba(59,130,246,.15)' }}>{t}</span>
))}
{['TerraSAR-X', 'Capella SAR', 'ICEYE'].map((t, i) => (
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.15)', color: 'var(--cyan)' }}>{t}</span>
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.15)', color: 'var(--color-accent)' }}>{t}</span>
))}
<span className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(139,148,158,.08)', border: '1px solid rgba(139,148,158,.15)', color: 'var(--t3)' }}>+11 more</span>
<span className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(139,148,158,.08)', border: '1px solid rgba(139,148,158,.15)', color: 'var(--fg-disabled)' }}>+11 more</span>
</div>
<div className="text-[9px] text-text-3 font-korean leading-relaxed">(EO) + (SAR) . · SAR . .</div>
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#60a5fa]">up42.com</span></div>
<div className="text-[9px] text-fg-disabled font-korean leading-relaxed">(EO) + (SAR) . · SAR . .</div>
<div className="mt-2 text-[8px] text-fg-disabled font-mono">API: <span className="text-[#60a5fa]">up42.com</span></div>
</div>
</div>
{/* 하단 */}
<div className="px-7 pb-5 flex items-center justify-between">
<div className="text-[9px] text-text-3 font-korean leading-relaxed">💡 촬영: BlackSky (90 ) · /악천후: UP42 SAR </div>
<button onClick={() => setModalPhase('none')} className="px-4 py-2 rounded-lg border text-[11px] font-semibold cursor-pointer font-korean bg-bg-3 text-text-2 border-border"></button>
<div className="text-[9px] text-fg-disabled font-korean leading-relaxed">💡 촬영: BlackSky (90 ) · /악천후: UP42 SAR </div>
<button onClick={() => setModalPhase('none')} className="px-4 py-2 rounded-lg border text-[11px] font-semibold cursor-pointer font-korean bg-bg-card text-fg-sub border-stroke"></button>
</div>
</div>
)}

파일 보기

@ -305,9 +305,9 @@ 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-primary-cyan/40 text-xs font-mono animate-pulse"> ...</div>
<div className="w-24 h-0.5 bg-bg-3 rounded-full mt-2 mx-auto overflow-hidden">
<div className="h-full bg-primary-cyan/40 rounded-full" style={{ width: '64%', animation: 'pulse 2s infinite' }} />
<div className="text-color-accent/40 text-xs font-mono animate-pulse"> ...</div>
<div className="w-24 h-0.5 bg-bg-card rounded-full mt-2 mx-auto overflow-hidden">
<div className="h-full bg-color-accent/40 rounded-full" style={{ width: '64%', animation: 'pulse 2s infinite' }} />
</div>
</div>
</div>
@ -565,7 +565,7 @@ function Pollution3DModel({ viewMode, status }: { viewMode: string; status: stri
<canvas ref={canvasRef} style={{ filter: isProcessing ? 'saturate(0.3) opacity(0.5)' : undefined }} />
{viewMode === '3d' && !isProcessing && (
<div className="absolute bottom-2 right-2 flex items-center gap-1" style={{ fontSize: '8px', color: 'var(--t3)', fontFamily: 'var(--fM)' }}>
<div className="absolute bottom-2 right-2 flex items-center gap-1" style={{ fontSize: '8px', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)' }}>
<span>0mm</span>
<div style={{ width: '60px', height: '4px', borderRadius: '2px', background: 'linear-gradient(90deg, rgba(234,179,8,0.6), rgba(249,115,22,0.7), rgba(239,68,68,0.8))' }} />
<span>3.2mm</span>
@ -575,9 +575,9 @@ 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-status-red/40 text-xs font-mono animate-pulse"> ...</div>
<div className="w-24 h-0.5 bg-bg-3 rounded-full mt-2 mx-auto overflow-hidden">
<div className="h-full bg-status-red/40 rounded-full" style={{ width: '52%', animation: 'pulse 2s infinite' }} />
<div className="text-color-danger/40 text-xs font-mono animate-pulse"> ...</div>
<div className="w-24 h-0.5 bg-bg-card rounded-full mt-2 mx-auto overflow-hidden">
<div className="h-full bg-color-danger/40 rounded-full" style={{ width: '52%', animation: 'pulse 2s infinite' }} />
</div>
</div>
</div>
@ -596,17 +596,17 @@ export function SensorAnalysis() {
return (
<div className="flex h-full overflow-hidden" style={{ margin: '-20px -24px', height: 'calc(100% + 40px)' }}>
{/* Left Panel */}
<div className="w-[280px] bg-bg-1 border-r border-border flex flex-col overflow-auto">
<div className="w-[280px] bg-bg-surface border-r border-stroke flex flex-col overflow-auto">
{/* 3D Reconstruction List */}
<div className="p-2.5 px-3 border-b border-border">
<div className="text-[10px] font-bold text-text-3 mb-1.5 uppercase tracking-wider">📋 3D </div>
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-1.5 uppercase tracking-wider">📋 3D </div>
<div className="flex gap-1 mb-2">
<button
onClick={() => setSubTab('vessel')}
className={`flex-1 py-1.5 text-center text-[9px] font-semibold rounded cursor-pointer border transition-colors font-korean ${
subTab === 'vessel'
? 'text-primary-cyan bg-[rgba(6,182,212,0.08)] border-primary-cyan/20'
: 'text-text-3 bg-bg-0 border-border'
? 'text-color-accent bg-[rgba(6,182,212,0.08)] border-color-accent/20'
: 'text-fg-disabled bg-bg-base border-stroke'
}`}
>
🚢
@ -615,8 +615,8 @@ export function SensorAnalysis() {
onClick={() => setSubTab('pollution')}
className={`flex-1 py-1.5 text-center text-[9px] font-semibold rounded cursor-pointer border transition-colors font-korean ${
subTab === 'pollution'
? 'text-primary-cyan bg-[rgba(6,182,212,0.08)] border-primary-cyan/20'
: 'text-text-3 bg-bg-0 border-border'
? 'text-color-accent bg-[rgba(6,182,212,0.08)] border-color-accent/20'
: 'text-fg-disabled bg-bg-base border-stroke'
}`}
>
🛢
@ -629,15 +629,15 @@ export function SensorAnalysis() {
onClick={() => setSelectedItem(item)}
className={`flex items-center gap-2 px-2 py-2 rounded-sm cursor-pointer transition-colors border ${
selectedItem.id === item.id
? 'bg-[rgba(6,182,212,0.08)] border-primary-cyan/20'
? 'bg-[rgba(6,182,212,0.08)] border-color-accent/20'
: 'border-transparent hover:bg-white/[0.02]'
}`}
>
<div className="flex-1 min-w-0">
<div className="text-[10px] font-bold text-text-1 font-korean">{item.name}</div>
<div className="text-[8px] text-text-3 font-mono">{item.id} · {item.points} pts</div>
<div className="text-[10px] font-bold text-fg font-korean">{item.name}</div>
<div className="text-[8px] text-fg-disabled font-mono">{item.id} · {item.points} pts</div>
</div>
<span className={`text-[8px] font-semibold ${item.status === 'complete' ? 'text-status-green' : 'text-status-orange'}`}>
<span className={`text-[8px] font-semibold ${item.status === 'complete' ? 'text-color-success' : 'text-color-warning'}`}>
{item.status === 'complete' ? '✅ 완료' : '⏳ 처리중'}
</span>
</div>
@ -647,19 +647,19 @@ export function SensorAnalysis() {
{/* Source Images */}
<div className="p-2.5 px-3 flex-1 min-h-0 flex flex-col">
<div className="text-[10px] font-bold text-text-3 mb-1.5 uppercase tracking-wider">📹 </div>
<div className="text-[10px] font-bold text-fg-disabled mb-1.5 uppercase tracking-wider">📹 </div>
<div className="grid grid-cols-2 gap-1">
{[
{ label: 'D-01 정면', sensor: '광학', color: 'text-primary-blue' },
{ label: 'D-02 좌현', sensor: 'IR', color: 'text-status-red' },
{ label: 'D-03 우현', sensor: '광학', color: 'text-primary-purple' },
{ label: 'D-02 상부', sensor: 'IR', color: 'text-status-red' },
{ label: 'D-01 정면', sensor: '광학', color: 'text-color-info' },
{ label: 'D-02 좌현', sensor: 'IR', color: 'text-color-danger' },
{ label: 'D-03 우현', sensor: '광학', color: 'text-color-tertiary' },
{ label: 'D-02 상부', sensor: 'IR', color: 'text-color-danger' },
].map((src, i) => (
<div key={i} className="relative rounded-sm bg-bg-0 border border-border overflow-hidden aspect-square">
<div key={i} className="relative rounded-sm bg-bg-base border border-stroke overflow-hidden aspect-square">
<div className="absolute inset-0 flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #0c1624, #1a1a2e)' }}>
<div className="text-text-3/10 text-xs font-mono">{src.label.split(' ')[0]}</div>
<div className="text-fg-disabled/10 text-xs font-mono">{src.label.split(' ')[0]}</div>
</div>
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-1 flex justify-between text-[8px] text-text-3 font-korean">
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-1 flex justify-between text-[8px] text-fg-disabled font-korean">
<span>{src.label}</span>
<span className={src.color}>{src.sensor}</span>
</div>
@ -670,7 +670,7 @@ export function SensorAnalysis() {
</div>
{/* Center Panel - 3D Canvas */}
<div className="flex-1 relative bg-bg-0 border-x border-border flex items-center justify-center overflow-hidden">
<div className="flex-1 relative bg-bg-base border-x border-stroke flex items-center justify-center overflow-hidden">
{/* Simulated 3D viewport */}
<div className="absolute inset-0" style={{ background: 'radial-gradient(ellipse at 50% 50%, #0c1a2e, #060c18)' }}>
{/* Grid floor */}
@ -684,7 +684,7 @@ export function SensorAnalysis() {
)}
{/* Axis indicator */}
<div className="absolute bottom-16 left-4" style={{ fontSize: '9px', fontFamily: 'var(--fM)' }}>
<div className="absolute bottom-16 left-4" style={{ fontSize: '9px', fontFamily: 'var(--font-mono)' }}>
<div style={{ color: '#ef4444' }}>X </div>
<div className="text-green-500">Y </div>
<div className="text-blue-500">Z </div>
@ -693,9 +693,9 @@ export function SensorAnalysis() {
{/* Title */}
<div className="absolute top-3 left-3 z-[2]">
<div className="text-[10px] font-bold text-text-3 uppercase tracking-wider">3D Vessel Analysis</div>
<div className="text-[13px] font-bold text-primary-cyan my-1 font-korean">{selectedItem.name} </div>
<div className="text-[9px] text-text-3 font-mono">34.58°N, 129.30°E · {selectedItem.status === 'complete' ? '재구성 완료' : '처리중'}</div>
<div className="text-[10px] font-bold text-fg-disabled uppercase tracking-wider">3D Vessel Analysis</div>
<div className="text-[13px] font-bold text-color-accent my-1 font-korean">{selectedItem.name} </div>
<div className="text-[9px] text-fg-disabled font-mono">34.58°N, 129.30°E · {selectedItem.status === 'complete' ? '재구성 완료' : '처리중'}</div>
</div>
{/* View Mode Buttons */}
@ -710,8 +710,8 @@ export function SensorAnalysis() {
onClick={() => setViewMode(m.id)}
className={`px-2.5 py-1.5 text-[10px] font-semibold rounded-sm cursor-pointer border font-korean transition-colors ${
viewMode === m.id
? 'bg-[rgba(6,182,212,0.2)] border-primary-cyan/50 text-primary-cyan'
: 'bg-black/40 border-primary-cyan/20 text-text-3 hover:bg-black/60 hover:border-primary-cyan/40'
? 'bg-[rgba(6,182,212,0.2)] border-color-accent/50 text-color-accent'
: 'bg-black/40 border-color-accent/20 text-fg-disabled hover:bg-black/60 hover:border-color-accent/40'
}`}
>
{m.label}
@ -729,18 +729,18 @@ 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-primary-cyan">{s.value}</div>
<div className="text-[8px] text-text-3 mt-0.5 font-korean">{s.label}</div>
<div className="font-mono font-bold text-sm text-color-accent">{s.value}</div>
<div className="text-[8px] text-fg-disabled mt-0.5 font-korean">{s.label}</div>
</div>
))}
</div>
</div>
{/* Right Panel - Analysis Details */}
<div className="w-[270px] bg-bg-1 border-l border-border flex flex-col overflow-auto">
<div className="w-[270px] bg-bg-surface border-l border-stroke flex flex-col overflow-auto">
{/* Ship/Pollution Info */}
<div className="p-2.5 px-3 border-b border-border">
<div className="text-[10px] font-bold text-text-3 mb-2 uppercase tracking-wider">📊 </div>
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-2 uppercase tracking-wider">📊 </div>
<div className="flex flex-col gap-1.5 text-[10px]">
{(selectedItem.type === 'vessel' ? [
['대상', selectedItem.name],
@ -761,34 +761,34 @@ export function SensorAnalysis() {
['확산 속도', '0.3 km/h (ESE 방향)'],
]).map(([k, v], i) => (
<div key={i} className="flex justify-between items-start">
<span className="text-text-3 font-korean">{k}</span>
<span className="font-mono font-semibold text-text-1 text-right ml-2">{v}</span>
<span className="text-fg-disabled font-korean">{k}</span>
<span className="font-mono font-semibold text-fg text-right ml-2">{v}</span>
</div>
))}
</div>
</div>
{/* AI Detection Results */}
<div className="p-2.5 px-3 border-b border-border">
<div className="text-[10px] font-bold text-text-3 mb-2 uppercase tracking-wider">🤖 AI </div>
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-2 uppercase tracking-wider">🤖 AI </div>
<div className="flex flex-col gap-1">
{(selectedItem.type === 'vessel' ? [
{ label: '선박 식별', confidence: 94, color: 'bg-status-green' },
{ label: '선종 분류', confidence: 78, color: 'bg-status-yellow' },
{ label: '손상 감지', confidence: 45, color: 'bg-status-orange' },
{ label: '화물 분석', confidence: 62, color: 'bg-status-yellow' },
{ label: '선박 식별', confidence: 94, color: 'bg-color-success' },
{ label: '선종 분류', confidence: 78, color: 'bg-color-caution' },
{ label: '손상 감지', confidence: 45, color: 'bg-color-warning' },
{ label: '화물 분석', confidence: 62, color: 'bg-color-caution' },
] : [
{ label: '유막 탐지', confidence: 97, color: 'bg-status-green' },
{ label: '유종 분류', confidence: 85, color: 'bg-status-green' },
{ label: '두께 추정', confidence: 72, color: 'bg-status-yellow' },
{ label: '확산 예측', confidence: 68, color: 'bg-status-orange' },
{ label: '유막 탐지', confidence: 97, color: 'bg-color-success' },
{ label: '유종 분류', confidence: 85, color: 'bg-color-success' },
{ label: '두께 추정', confidence: 72, color: 'bg-color-caution' },
{ label: '확산 예측', confidence: 68, color: 'bg-color-warning' },
]).map((r, i) => (
<div key={i}>
<div className="flex justify-between text-[9px] mb-0.5">
<span className="text-text-3 font-korean">{r.label}</span>
<span className="font-mono font-semibold text-text-1">{r.confidence}%</span>
<span className="text-fg-disabled font-korean">{r.label}</span>
<span className="font-mono font-semibold text-fg">{r.confidence}%</span>
</div>
<div className="w-full h-1 bg-bg-0 rounded-full overflow-hidden">
<div className="w-full h-1 bg-bg-base rounded-full overflow-hidden">
<div className={`h-full rounded-full ${r.color}`} style={{ width: `${r.confidence}%` }} />
</div>
</div>
@ -797,8 +797,8 @@ export function SensorAnalysis() {
</div>
{/* Comparison / Measurements */}
<div className="p-2.5 px-3 border-b border-border">
<div className="text-[10px] font-bold text-text-3 mb-2 uppercase tracking-wider">📐 3D </div>
<div className="p-2.5 px-3 border-b border-stroke">
<div className="text-[10px] font-bold text-fg-disabled mb-2 uppercase tracking-wider">📐 3D </div>
<div className="flex flex-col gap-1 text-[10px]">
{(selectedItem.type === 'vessel' ? [
['전장 (LOA)', '84.7 m'],
@ -813,9 +813,9 @@ export function SensorAnalysis() {
['평균 두께', '0.8 mm'],
['최대 두께', '3.2 mm'],
]).map(([k, v], i) => (
<div key={i} className="flex justify-between px-2 py-1 bg-bg-0 rounded">
<span className="text-text-3 font-korean">{k}</span>
<span className="font-mono font-semibold text-primary-cyan">{v}</span>
<div key={i} className="flex justify-between px-2 py-1 bg-bg-base rounded">
<span className="text-fg-disabled font-korean">{k}</span>
<span className="font-mono font-semibold text-color-accent">{v}</span>
</div>
))}
</div>
@ -823,10 +823,10 @@ export function SensorAnalysis() {
{/* Action Buttons */}
<div className="p-2.5 px-3">
<button className="w-full py-2.5 rounded-sm text-xs font-bold font-korean text-white border-none cursor-pointer mb-2" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}>
<button className="w-full py-2.5 rounded-sm text-xs font-bold font-korean text-white border-none cursor-pointer mb-2" style={{ background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))' }}>
📊
</button>
<button className="w-full py-2 border border-border bg-bg-3 text-text-2 rounded-sm text-[11px] font-semibold font-korean cursor-pointer hover:bg-bg-hover transition-colors">
<button className="w-full py-2 border border-stroke bg-bg-card text-fg-sub rounded-sm text-[11px] font-semibold font-korean cursor-pointer hover:bg-bg-surface-hover transition-colors">
📥 3D
</button>
</div>

파일 보기

@ -50,7 +50,7 @@ export function WingAI() {
>
🤖
</div>
<div className="text-[12px] font-bold font-korean text-text-1">AI /</div>
<div className="text-[12px] font-bold font-korean text-fg">AI /</div>
<span className="text-[9px] px-1.5 py-0.5 rounded font-bold" style={{ background: 'rgba(168,85,247,.12)', color: '#a855f7', border: '1px solid rgba(168,85,247,.25)' }}>WingAI</span>
</div>
<div className="flex gap-1 h-7">
@ -62,7 +62,7 @@ export function WingAI() {
style={
activeTab === t.id
? { background: 'rgba(168,85,247,.12)', borderColor: 'rgba(168,85,247,.3)', color: '#a855f7' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>
{t.icon} {t.label}
@ -141,19 +141,19 @@ function DetectPanel() {
{/* 통계 카드 */}
<div className="grid grid-cols-4 gap-3 mb-5">
{stats.map((s, i) => (
<div key={i} className="bg-bg-2 border border-border rounded-md p-3.5 text-center">
<div key={i} className="bg-bg-elevated border border-stroke rounded-md p-3.5 text-center">
<div className="text-[22px] font-bold font-mono" style={{ color: s.color }}>{s.value}</div>
<div className="text-[10px] text-text-3 mt-1 font-korean">{s.label}</div>
<div className="text-[10px] text-fg-disabled mt-1 font-korean">{s.label}</div>
</div>
))}
</div>
<div className="grid grid-cols-[1fr_380px] gap-4">
{/* 탐지 결과 지도 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden" style={{ minHeight: 480 }}>
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="text-[11px] font-bold font-korean text-text-1">🎯 </div>
<div className="text-[9px] text-text-3 font-mono">{filtered.length} </div>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden" style={{ minHeight: 480 }}>
<div className="flex items-center justify-between px-4 py-3 border-b border-stroke">
<div className="text-[11px] font-bold font-korean text-fg">🎯 </div>
<div className="text-[9px] text-fg-disabled font-mono">{filtered.length} </div>
</div>
<div className="relative" style={{ height: 440 }}>
<Map
@ -187,7 +187,7 @@ function DetectPanel() {
]} />
</Map>
{/* 범례 */}
<div className="absolute bottom-3 left-3 bg-bg-2/90 border border-border rounded px-3 py-2 backdrop-blur-sm">
<div className="absolute bottom-3 left-3 bg-bg-elevated/90 border border-stroke rounded px-3 py-2 backdrop-blur-sm">
<div className="flex items-center gap-3">
{[
{ color: '#ef4444', label: '불일치' },
@ -197,7 +197,7 @@ function DetectPanel() {
].map((l) => (
<div key={l.label} className="flex items-center gap-1">
<div className="w-2.5 h-2.5 rounded-full" style={{ background: l.color }} />
<span className="text-[8px] font-korean text-text-3">{l.label}</span>
<span className="text-[8px] font-korean text-fg-disabled">{l.label}</span>
</div>
))}
</div>
@ -206,11 +206,11 @@ function DetectPanel() {
</div>
{/* 탐지 목록 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden flex flex-col" style={{ maxHeight: 520 }}>
<div className="px-4 py-3 border-b border-border shrink-0">
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden flex flex-col" style={{ maxHeight: 520 }}>
<div className="px-4 py-3 border-b border-stroke shrink-0">
<div className="flex items-center justify-between mb-2">
<div className="text-[11px] font-bold font-korean text-text-1">📋 MMSI </div>
<div className="text-[9px] text-text-3 font-mono">{filtered.length}</div>
<div className="text-[11px] font-bold font-korean text-fg">📋 MMSI </div>
<div className="text-[9px] text-fg-disabled font-mono">{filtered.length}</div>
</div>
<div className="flex gap-1">
{filters.map((f) => (
@ -220,7 +220,7 @@ function DetectPanel() {
className="px-2 py-0.5 rounded text-[9px] font-bold font-korean cursor-pointer border transition-colors"
style={filterStatus === f
? { background: 'rgba(168,85,247,.12)', borderColor: 'rgba(168,85,247,.3)', color: '#a855f7' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>
{f}
@ -233,13 +233,13 @@ function DetectPanel() {
<div
key={d.id}
onClick={() => setSelectedId(selectedId === d.id ? null : d.id)}
className="px-4 py-3 hover:bg-bg-hover/30 transition-colors cursor-pointer"
className="px-4 py-3 hover:bg-bg-surface-hover/30 transition-colors cursor-pointer"
style={{ background: selectedId === d.id ? 'rgba(168,85,247,.04)' : undefined }}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-text-3">{d.id}</span>
<span className="text-[10px] font-bold font-korean text-text-1">{d.vesselName}</span>
<span className="text-[10px] font-mono text-fg-disabled">{d.id}</span>
<span className="text-[10px] font-bold font-korean text-fg">{d.vesselName}</span>
</div>
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold font-korean" style={statusStyle(d.status)}>
{d.status}
@ -392,7 +392,7 @@ function ChangeDetectPanel() {
const sourceStyle = (src: SourceConfig, active = true) => active
? { background: `${src.color}18`, color: src.color, border: `1px solid ${src.color}40` }
: { background: 'var(--bg3)', color: 'var(--t4)', border: '1px solid var(--bd)', opacity: 0.5 };
: { background: 'var(--bg-card)', color: 'var(--t4)', border: '1px solid var(--stroke-default)', opacity: 0.5 };
const filteredChanges = sourceFilter === 'all'
? changes
@ -402,7 +402,7 @@ function ChangeDetectPanel() {
<div>
{/* 레이어 토글 바 */}
<div className="flex items-center gap-3 mb-4">
<div className="text-[10px] font-bold font-korean text-text-3 shrink-0"> </div>
<div className="text-[10px] font-bold font-korean text-fg-disabled shrink-0"> </div>
<div className="flex gap-1.5">
{SOURCES.map((s) => (
<button
@ -422,15 +422,15 @@ function ChangeDetectPanel() {
{/* AS-IS / 현재 시점 비교 뷰 */}
<div className="grid grid-cols-2 gap-4 mb-4" style={{ minHeight: 400 }}>
{/* AS-IS 시점 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border" style={{ background: 'rgba(234,179,8,.05)' }}>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-stroke" style={{ background: 'rgba(234,179,8,.05)' }}>
<div className="flex items-center gap-2">
<span className="text-[11px] font-bold font-korean" style={{ color: '#eab308' }}>AS-IS</span>
<span className="text-[9px] px-1.5 py-0.5 rounded font-bold" style={{ background: 'rgba(234,179,8,.12)', color: '#eab308', border: '1px solid rgba(234,179,8,.25)' }}> </span>
</div>
<div className="flex items-center gap-2">
<input type="date" defaultValue="2026-03-14" className="text-[9px] font-mono px-2 py-1 rounded border outline-none" style={{ background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t2)' }} />
<input type="time" defaultValue="14:00" className="text-[9px] font-mono px-2 py-1 rounded border outline-none" style={{ background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t2)' }} />
<input type="date" defaultValue="2026-03-14" className="text-[9px] font-mono px-2 py-1 rounded border outline-none" style={{ background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-sub)' }} />
<input type="time" defaultValue="14:00" className="text-[9px] font-mono px-2 py-1 rounded border outline-none" style={{ background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-sub)' }} />
</div>
</div>
{/* 활성 레이어 표시 */}
@ -443,12 +443,12 @@ function ChangeDetectPanel() {
{activeCount === 0 && <span className="text-[9px] text-text-4 font-korean"> </span>}
</div>
{/* 지도 플레이스홀더 */}
<div className="flex items-center justify-center text-text-3" style={{ height: 320 }}>
<div className="flex items-center justify-center text-fg-disabled" style={{ height: 320 }}>
<div className="text-center">
<div className="flex justify-center gap-2 text-2xl mb-3 opacity-30">
{SOURCES.filter((s) => layers[s.id]).map((s) => <span key={s.id}>{s.icon}</span>)}
</div>
<div className="text-[11px] font-korean text-text-3"> </div>
<div className="text-[11px] font-korean text-fg-disabled"> </div>
<div className="text-[9px] text-text-4 mt-1">
{SOURCES.filter((s) => layers[s.id]).map((s) => s.label).join(' + ')}
</div>
@ -457,13 +457,13 @@ function ChangeDetectPanel() {
</div>
{/* 현재 시점 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border" style={{ background: 'rgba(59,130,246,.05)' }}>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-stroke" style={{ background: 'rgba(59,130,246,.05)' }}>
<div className="flex items-center gap-2">
<span className="text-[11px] font-bold font-korean" style={{ color: '#3b82f6' }}></span>
<span className="text-[9px] px-1.5 py-0.5 rounded font-bold" style={{ background: 'rgba(59,130,246,.12)', color: '#3b82f6', border: '1px solid rgba(59,130,246,.25)' }}>NOW</span>
</div>
<span className="text-[9px] text-text-3 font-mono">2026-03-16 14:23</span>
<span className="text-[9px] text-fg-disabled font-mono">2026-03-16 14:23</span>
</div>
{/* 활성 레이어 표시 */}
<div className="flex gap-1 px-3 py-2 border-b" style={{ borderColor: 'rgba(255,255,255,.04)' }}>
@ -475,12 +475,12 @@ function ChangeDetectPanel() {
{activeCount === 0 && <span className="text-[9px] text-text-4 font-korean"> </span>}
</div>
{/* 지도 플레이스홀더 */}
<div className="flex items-center justify-center text-text-3" style={{ height: 320 }}>
<div className="flex items-center justify-center text-fg-disabled" style={{ height: 320 }}>
<div className="text-center">
<div className="flex justify-center gap-2 text-2xl mb-3 opacity-30">
{SOURCES.filter((s) => layers[s.id]).map((s) => <span key={s.id}>{s.icon}</span>)}
</div>
<div className="text-[11px] font-korean text-text-3"> </div>
<div className="text-[11px] font-korean text-fg-disabled"> </div>
<div className="text-[9px] text-text-4 mt-1">
{SOURCES.filter((s) => layers[s.id]).map((s) => s.label).join(' + ')}
</div>
@ -490,15 +490,15 @@ function ChangeDetectPanel() {
</div>
{/* 복합 변화 감지 목록 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="text-[11px] font-bold font-korean text-text-1">🔄 </div>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-stroke">
<div className="text-[11px] font-bold font-korean text-fg">🔄 </div>
<div className="flex items-center gap-2">
<div className="flex gap-1">
<button
onClick={() => setSourceFilter('all')}
className="px-2 py-0.5 rounded text-[9px] font-bold font-korean cursor-pointer border transition-colors"
style={sourceFilter === 'all' ? { background: 'rgba(168,85,247,.12)', borderColor: 'rgba(168,85,247,.3)', color: '#a855f7' } : { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }}
style={sourceFilter === 'all' ? { background: 'rgba(168,85,247,.12)', borderColor: 'rgba(168,85,247,.3)', color: '#a855f7' } : { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }}
>
</button>
@ -507,13 +507,13 @@ function ChangeDetectPanel() {
key={s.id}
onClick={() => setSourceFilter(s.id)}
className="px-2 py-0.5 rounded text-[9px] font-bold font-korean cursor-pointer border transition-colors"
style={sourceFilter === s.id ? sourceStyle(s) : { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }}
style={sourceFilter === s.id ? sourceStyle(s) : { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }}
>
{s.icon}
</button>
))}
</div>
<div className="text-[9px] text-text-3 font-mono">{filteredChanges.length}</div>
<div className="text-[9px] text-fg-disabled font-mono">{filteredChanges.length}</div>
</div>
</div>
@ -525,14 +525,14 @@ function ChangeDetectPanel() {
{/* 요약 행 */}
<div
onClick={() => setSelectedChange(isOpen ? null : c.id)}
className="grid gap-0 px-4 py-3 items-center hover:bg-bg-hover/30 transition-colors cursor-pointer"
className="grid gap-0 px-4 py-3 items-center hover:bg-bg-surface-hover/30 transition-colors cursor-pointer"
style={{ gridTemplateColumns: '52px 1fr 100px 130px 60px', background: isOpen ? 'rgba(168,85,247,.04)' : undefined }}
>
<div className="text-[10px] font-mono text-text-3">{c.id}</div>
<div className="text-[10px] font-mono text-fg-disabled">{c.id}</div>
<div>
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold text-text-1 font-korean">{c.area}</span>
<span className="text-[10px] font-semibold font-korean" style={{ color: 'var(--t3)' }}>{c.type}</span>
<span className="text-[11px] font-semibold text-fg font-korean">{c.area}</span>
<span className="text-[10px] font-semibold font-korean" style={{ color: 'var(--fg-disabled)' }}>{c.type}</span>
</div>
<div className="text-[9px] text-text-4 font-korean mt-0.5">{c.detail}</div>
</div>
@ -542,7 +542,7 @@ function ChangeDetectPanel() {
return <span key={sid} className="px-1.5 py-0.5 rounded text-[8px] font-bold" style={sourceStyle(cfg)}>{cfg.icon}</span>;
})}
</div>
<div className="text-[9px] font-mono text-text-2">
<div className="text-[9px] font-mono text-fg-sub">
<span style={{ color: '#eab308' }}>{c.date1} {c.time1}</span>
<span className="text-text-4 mx-1"></span>
<span style={{ color: '#3b82f6' }}>{c.date2} {c.time2}</span>
@ -575,8 +575,8 @@ function ChangeDetectPanel() {
<div className="flex items-center gap-1.5">
<span className="text-[8px] font-bold px-1.5 py-0.5 rounded" style={sourceStyle(cfg)}>{cfg.icon} {cfg.label}</span>
</div>
<div className="text-[9px] font-korean text-text-3">{c.asIsDetail[sid] || '-'}</div>
<div className="text-[9px] font-korean text-text-1 font-semibold">{c.nowDetail[sid] || '-'}</div>
<div className="text-[9px] font-korean text-fg-disabled">{c.asIsDetail[sid] || '-'}</div>
<div className="text-[9px] font-korean text-fg font-semibold">{c.nowDetail[sid] || '-'}</div>
</div>
);
})}
@ -872,20 +872,20 @@ function AoiPanel() {
{ value: String(warningCount), label: '주의', color: '#eab308' },
{ value: String(totalAlerts), label: '미확인 알림', color: '#a855f7' },
].map((s, i) => (
<div key={i} className="bg-bg-2 border border-border rounded-md p-3 text-center">
<div key={i} className="bg-bg-elevated border border-stroke rounded-md p-3 text-center">
<div className="text-[20px] font-bold font-mono" style={{ color: s.color }}>{s.value}</div>
<div className="text-[10px] text-text-3 mt-0.5 font-korean">{s.label}</div>
<div className="text-[10px] text-fg-disabled mt-0.5 font-korean">{s.label}</div>
</div>
))}
</div>
<div className="grid grid-cols-[1fr_360px] gap-4">
{/* 지도 영역 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden" style={{ minHeight: 520 }}>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden" style={{ minHeight: 520 }}>
{/* 지도 헤더 */}
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-stroke">
<div className="flex items-center gap-2">
<div className="text-[11px] font-bold font-korean text-text-1">📍 </div>
<div className="text-[11px] font-bold font-korean text-fg">📍 </div>
{isDrawing && (
<span className="text-[9px] px-1.5 py-0.5 rounded font-bold animate-pulse" style={{ background: 'rgba(168,85,247,.15)', color: '#a855f7', border: '1px solid rgba(168,85,247,.3)' }}>
· ({drawingPoints.length})
@ -907,14 +907,14 @@ function AoiPanel() {
onClick={() => setDrawingPoints((p) => p.slice(0, -1))}
disabled={drawingPoints.length === 0}
className="px-2 h-6 rounded-sm text-[9px] font-semibold cursor-pointer font-korean border disabled:opacity-40"
style={{ background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t2)' }}
style={{ background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-sub)' }}
>
</button>
<button
onClick={cancelDrawing}
className="px-2 h-6 rounded-sm text-[9px] font-semibold cursor-pointer font-korean border"
style={{ background: 'var(--bg3)', borderColor: 'var(--bd)', color: '#ef4444' }}
style={{ background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: '#ef4444' }}
>
</button>
@ -949,13 +949,13 @@ function AoiPanel() {
<div className="flex flex-col gap-3" style={{ maxHeight: 520, overflowY: 'auto' }}>
{/* 등록 폼: 이름만 입력 → 바로 등록 */}
{showForm && drawingPoints.length >= 3 && (
<div className="bg-bg-2 border rounded-md overflow-hidden" style={{ borderColor: 'rgba(168,85,247,.3)' }}>
<div className="bg-bg-elevated border rounded-md overflow-hidden" style={{ borderColor: 'rgba(168,85,247,.3)' }}>
<div className="px-4 py-2.5 border-b font-korean" style={{ borderColor: 'rgba(168,85,247,.15)', background: 'rgba(168,85,247,.05)' }}>
<div className="text-[11px] font-bold" style={{ color: '#a855f7' }}> </div>
<div className="text-[9px] text-text-4 mt-0.5"> {drawingPoints.length} </div>
</div>
<div className="px-4 py-3">
<label className="text-[9px] font-bold text-text-3 font-korean block mb-1.5"> </label>
<label className="text-[9px] font-bold text-fg-disabled font-korean block mb-1.5"> </label>
<input
value={formName}
onChange={(e) => setFormName(e.target.value)}
@ -977,7 +977,7 @@ function AoiPanel() {
<button
onClick={cancelDrawing}
className="px-4 py-2 rounded text-[10px] font-bold cursor-pointer font-korean border"
style={{ background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t2)' }}
style={{ background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-sub)' }}
>
</button>
@ -991,11 +991,11 @@ function AoiPanel() {
const z = zones.find((zone) => zone.id === selectedZone);
if (!z) return null;
return (
<div className="bg-bg-2 border rounded-md overflow-hidden" style={{ borderColor: `${z.color}40` }}>
<div className="bg-bg-elevated border rounded-md overflow-hidden" style={{ borderColor: `${z.color}40` }}>
<div className="flex items-center justify-between px-4 py-2.5 border-b" style={{ borderColor: `${z.color}20`, background: `${z.color}08` }}>
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-text-3">{z.id}</span>
<span className="text-[11px] font-bold font-korean text-text-1">{z.name}</span>
<span className="text-[10px] font-mono text-fg-disabled">{z.id}</span>
<span className="text-[11px] font-bold font-korean text-fg">{z.name}</span>
</div>
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold font-korean" style={statusStyle(z.status)}>
{z.status}
@ -1004,7 +1004,7 @@ function AoiPanel() {
<div className="px-4 py-3 space-y-3">
{/* 감시 주기 */}
<div>
<label className="text-[9px] font-bold text-text-3 font-korean block mb-1.5"> </label>
<label className="text-[9px] font-bold text-fg-disabled font-korean block mb-1.5"> </label>
<div className="flex gap-1">
{INTERVAL_OPTIONS.map((iv) => (
<button
@ -1013,7 +1013,7 @@ function AoiPanel() {
className="px-2.5 py-1 rounded text-[9px] font-bold font-mono cursor-pointer border transition-colors"
style={z.interval === iv
? { background: 'rgba(59,130,246,.12)', borderColor: 'rgba(59,130,246,.3)', color: '#3b82f6' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>
{iv}
@ -1024,7 +1024,7 @@ function AoiPanel() {
{/* 모니터링 방법 (정보원 소스) */}
<div>
<label className="text-[9px] font-bold text-text-3 font-korean block mb-1.5"> </label>
<label className="text-[9px] font-bold text-fg-disabled font-korean block mb-1.5"> </label>
<div className="space-y-1.5">
{MONITOR_SOURCES.map((src) => {
const active = z.sources.includes(src.id);
@ -1035,7 +1035,7 @@ function AoiPanel() {
className="w-full flex items-center gap-2.5 px-3 py-2 rounded border cursor-pointer transition-all text-left"
style={active
? { background: `${src.color}10`, borderColor: `${src.color}40`, color: src.color }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t4)', opacity: 0.6 }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--t4)', opacity: 0.6 }
}
>
<span className="text-sm shrink-0">{active ? '◉' : '○'}</span>
@ -1060,7 +1060,7 @@ function AoiPanel() {
className="px-3 py-1.5 rounded text-[9px] font-bold font-korean cursor-pointer border transition-colors"
style={z.monitoring
? { background: 'rgba(34,197,94,.12)', borderColor: 'rgba(34,197,94,.25)', color: '#22c55e' }
: { background: 'var(--bg3)', borderColor: 'var(--bd)', color: 'var(--t3)' }
: { background: 'var(--bg-card)', borderColor: 'var(--stroke-default)', color: 'var(--fg-disabled)' }
}
>
{z.monitoring ? '◉ 감시 중' : '○ 일시정지'}
@ -1080,23 +1080,23 @@ function AoiPanel() {
})()}
{/* 감시 구역 목록 */}
<div className="bg-bg-2 border border-border rounded-md overflow-hidden flex-1">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border">
<div className="text-[11px] font-bold font-korean text-text-1">📋 </div>
<div className="text-[9px] text-text-3 font-mono">{zones.length}</div>
<div className="bg-bg-elevated border border-stroke rounded-md overflow-hidden flex-1">
<div className="flex items-center justify-between px-4 py-2.5 border-b border-stroke">
<div className="text-[11px] font-bold font-korean text-fg">📋 </div>
<div className="text-[9px] text-fg-disabled font-mono">{zones.length}</div>
</div>
<div className="divide-y" style={{ borderColor: 'rgba(255,255,255,.04)' }}>
{zones.map((z) => (
<div
key={z.id}
onClick={() => setSelectedZone(selectedZone === z.id ? null : z.id)}
className="px-4 py-2.5 hover:bg-bg-hover/30 transition-colors cursor-pointer"
className="px-4 py-2.5 hover:bg-bg-surface-hover/30 transition-colors cursor-pointer"
style={{ background: selectedZone === z.id ? `${z.color}08` : undefined, borderLeft: `3px solid ${z.monitoring ? z.color : 'transparent'}` }}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span className="text-[10px] font-mono text-text-3">{z.id}</span>
<span className="text-[11px] font-semibold text-text-1 font-korean">{z.name}</span>
<span className="text-[10px] font-mono text-fg-disabled">{z.id}</span>
<span className="text-[11px] font-semibold text-fg font-korean">{z.name}</span>
</div>
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold font-korean" style={statusStyle(z.status)}>
{z.status}

파일 보기

@ -79,7 +79,7 @@ function AssetManagement() {
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-text-3 text-sm font-korean"> ...</div>
<div className="text-fg-disabled text-sm font-korean"> ...</div>
</div>
)
}
@ -87,14 +87,14 @@ function AssetManagement() {
return (
<div className="flex flex-col h-full">
{/* View Switcher & Filters */}
<div className="flex items-center justify-between mb-3 pb-3 border-b border-border">
<div className="flex items-center justify-between mb-3 pb-3 border-b border-stroke">
<div className="flex gap-1">
<button
onClick={() => setViewMode('list')}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-sm font-korean transition-colors ${
viewMode === 'list'
? 'bg-[rgba(6,182,212,0.15)] text-primary-cyan border border-primary-cyan/30'
: 'bg-bg-3 border border-border text-text-2 hover:bg-bg-hover'
? 'bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30'
: 'bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover'
}`}
>
📋
@ -103,8 +103,8 @@ function AssetManagement() {
onClick={() => setViewMode('map')}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-sm font-korean transition-colors ${
viewMode === 'map'
? 'bg-[rgba(6,182,212,0.15)] text-primary-cyan border border-primary-cyan/30'
: 'bg-bg-3 border border-border text-text-2 hover:bg-bg-hover'
? 'bg-[rgba(6,182,212,0.15)] text-color-accent border border-color-accent/30'
: 'bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover'
}`}
>
🗺
@ -153,7 +153,7 @@ function AssetManagement() {
{viewMode === 'list' ? (
/* ── LIST VIEW ── */
<div className="flex-1 bg-bg-3 border border-border rounded-md overflow-hidden flex flex-col">
<div className="flex-1 bg-bg-card border border-stroke rounded-md overflow-hidden flex flex-col">
<div className="flex-1">
<table className="w-full text-left" style={{ tableLayout: 'fixed' }}>
<colgroup>
@ -170,12 +170,12 @@ function AssetManagement() {
<col style={{ width: '5%' }} />
</colgroup>
<thead>
<tr className="border-b border-border bg-bg-0">
<tr className="border-b border-stroke bg-bg-base">
{['번호', '유형', '관할청', '기관명', '주소', '방제선', '유회수기', '이송펌프', '방제차량', '살포장치', '총자산'].map((h, i) => {
const equipColMap: Record<string, number> = { vessel: 5, skimmer: 6, pump: 7, vehicle: 8, sprayer: 9 }
const isHighlight = equipFilter !== 'all' && equipColMap[equipFilter] === i
return (
<th key={i} className={`px-2.5 py-2.5 text-[10px] font-bold font-korean border-b border-border ${[0,5,6,7,8,9,10].includes(i) ? 'text-center' : ''} ${isHighlight ? 'text-primary-cyan bg-primary-cyan/5' : 'text-text-2'}`}>
<th key={i} className={`px-2.5 py-2.5 text-[10px] font-bold font-korean border-b border-stroke ${[0,5,6,7,8,9,10].includes(i) ? 'text-center' : ''} ${isHighlight ? 'text-color-accent bg-color-accent/5' : 'text-fg-sub'}`}>
{h}
</th>
)
@ -186,7 +186,7 @@ function AssetManagement() {
{paged.map((org, idx) => (
<tr
key={org.id}
className={`border-b border-border/50 hover:bg-[rgba(255,255,255,0.02)] cursor-pointer transition-colors ${
className={`border-b border-stroke/50 hover:bg-[rgba(255,255,255,0.02)] cursor-pointer transition-colors ${
selectedOrg?.id === org.id ? 'bg-[rgba(6,182,212,0.03)]' : ''
}`}
onClick={() => { handleSelectOrg(org); setViewMode('map') }}
@ -196,14 +196,14 @@ function AssetManagement() {
<span className={`text-[9px] px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}>{org.type}</span>
</td>
<td className="px-2.5 py-2 text-[10px] font-semibold font-korean">{regionShort(org.jurisdiction)}</td>
<td className="px-2.5 py-2 text-[10px] font-semibold text-primary-cyan font-korean cursor-pointer truncate">{org.name}</td>
<td className="px-2.5 py-2 text-[10px] text-text-3 font-korean truncate">{org.address}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] font-semibold ${equipFilter === 'vessel' ? 'text-primary-cyan bg-primary-cyan/5' : ''}`}>{org.vessel}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'skimmer' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.skimmer}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'pump' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.pump}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'vehicle' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.vehicle}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'sprayer' ? 'text-primary-cyan font-semibold bg-primary-cyan/5' : ''}`}>{org.sprayer}</td>
<td className="px-2.5 py-2 text-center font-bold text-primary-cyan font-mono text-[10px]">{org.totalAssets}</td>
<td className="px-2.5 py-2 text-[10px] font-semibold text-color-accent font-korean cursor-pointer truncate">{org.name}</td>
<td className="px-2.5 py-2 text-[10px] text-fg-disabled font-korean truncate">{org.address}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] font-semibold ${equipFilter === 'vessel' ? 'text-color-accent bg-color-accent/5' : ''}`}>{org.vessel}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'skimmer' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}>{org.skimmer}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'pump' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}>{org.pump}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'vehicle' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}>{org.vehicle}</td>
<td className={`px-2.5 py-2 text-center font-mono text-[10px] ${equipFilter === 'sprayer' ? 'text-color-accent font-semibold bg-color-accent/5' : ''}`}>{org.sprayer}</td>
<td className="px-2.5 py-2 text-center font-bold text-color-accent font-mono text-[10px]">{org.totalAssets}</td>
</tr>
))}
</tbody>
@ -211,8 +211,8 @@ function AssetManagement() {
</div>
{/* Totals Summary */}
<div className="flex items-center justify-end gap-4 px-4 py-2 border-t border-border bg-bg-0/80">
<span className="text-[10px] text-text-3 font-korean font-semibold mr-auto">
<div className="flex items-center justify-end gap-4 px-4 py-2 border-t border-stroke bg-bg-base/80">
<span className="text-[10px] text-fg-disabled font-korean font-semibold mr-auto">
({filtered.length} )
</span>
{[
@ -225,30 +225,30 @@ function AssetManagement() {
].map((t) => {
const isActive = equipFilter === t.key || t.key === 'total'
return (
<div key={t.key} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${equipFilter === t.key ? 'bg-primary-cyan/10' : ''}`}>
<span className={`text-[9px] font-korean ${isActive ? 'text-primary-cyan' : 'text-text-3'}`}>{t.label}</span>
<span className={`text-[10px] font-mono font-bold ${isActive ? 'text-primary-cyan' : 'text-text-1'}`}>{t.value.toLocaleString()}{t.unit}</span>
<div key={t.key} className={`flex items-center gap-1 px-1.5 py-0.5 rounded ${equipFilter === t.key ? 'bg-color-accent/10' : ''}`}>
<span className={`text-[9px] font-korean ${isActive ? 'text-color-accent' : 'text-fg-disabled'}`}>{t.label}</span>
<span className={`text-[10px] font-mono font-bold ${isActive ? 'text-color-accent' : 'text-fg'}`}>{t.value.toLocaleString()}{t.unit}</span>
</div>
)
})}
</div>
{/* Pagination */}
<div className="flex items-center justify-center gap-4 px-4 py-2.5 border-t border-border bg-bg-0">
<span className="text-[10px] text-text-3 font-korean">
<span className="font-semibold text-text-2">{filtered.length}</span> {' '}
<span className="font-semibold text-text-2">{(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filtered.length)}</span>
<div className="flex items-center justify-center gap-4 px-4 py-2.5 border-t border-stroke bg-bg-base">
<span className="text-[10px] text-fg-disabled font-korean">
<span className="font-semibold text-fg-sub">{filtered.length}</span> {' '}
<span className="font-semibold text-fg-sub">{(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filtered.length)}</span>
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(1)}
disabled={safePage <= 1}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
>«</button>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={safePage <= 1}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
></button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
<button
@ -256,27 +256,27 @@ function AssetManagement() {
onClick={() => setCurrentPage(p)}
className={`w-6 h-6 text-[10px] font-bold rounded transition-colors cursor-pointer ${
p === safePage
? 'bg-primary-cyan/20 text-primary-cyan border border-primary-cyan/40'
: 'border border-border bg-bg-3 text-text-3 hover:bg-bg-hover'
? 'bg-color-accent/20 text-color-accent border border-color-accent/40'
: 'border border-stroke bg-bg-card text-fg-disabled hover:bg-bg-surface-hover'
}`}
>{p}</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={safePage >= totalPages}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
></button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={safePage >= totalPages}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
className="px-1.5 py-1 text-[10px] rounded border border-stroke bg-bg-card text-fg-sub disabled:opacity-30 hover:bg-bg-surface-hover transition-colors cursor-pointer disabled:cursor-default"
>»</button>
</div>
</div>
</div>
) : (
/* ── MAP VIEW ── */
<div className="flex-1 flex overflow-hidden rounded-md border border-border">
<div className="flex-1 flex overflow-hidden rounded-md border border-stroke">
{/* Map */}
<div className="flex-1 relative overflow-hidden">
<AssetMap
@ -290,24 +290,24 @@ function AssetManagement() {
{/* Right Detail Panel */}
{selectedOrg && (
<aside className="w-[340px] min-w-[340px] bg-bg-1 border-l border-border flex flex-col">
<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-border">
<div className="p-4 border-b border-stroke">
<div className="text-sm font-bold mb-1 font-korean">{selectedOrg.name}</div>
<div className="text-[11px] text-text-2 font-semibold font-korean mb-1">{selectedOrg.type} · {regionShort(selectedOrg.jurisdiction)} · {selectedOrg.area}</div>
<div className="text-[11px] text-text-3 font-korean">{selectedOrg.address}</div>
<div className="text-[11px] text-fg-sub font-semibold font-korean mb-1">{selectedOrg.type} · {regionShort(selectedOrg.jurisdiction)} · {selectedOrg.area}</div>
<div className="text-[11px] text-fg-disabled font-korean">{selectedOrg.address}</div>
</div>
{/* Sub-tabs */}
<div className="flex border-b border-border">
<div className="flex border-b border-stroke">
{(['equip', 'material', 'contact'] as const).map(t => (
<button
key={t}
onClick={() => setDetailTab(t)}
className={`flex-1 py-2.5 text-center text-[11px] font-semibold font-korean border-b-2 transition-colors cursor-pointer ${
detailTab === t
? 'text-primary-cyan border-primary-cyan'
: 'text-text-3 border-transparent hover:text-text-2'
? 'text-color-accent border-color-accent'
: 'text-fg-disabled border-transparent hover:text-fg-sub'
}`}
>
{t === 'equip' ? '장비' : t === 'material' ? '자재' : '연락처'}
@ -324,9 +324,9 @@ function AssetManagement() {
{ value: `${selectedOrg.skimmer}`, label: '유회수기' },
{ value: String(selectedOrg.totalAssets), label: '총 자산' },
].map((s, i) => (
<div key={i} className="bg-bg-3 border border-border rounded-sm p-2.5 text-center">
<div className="text-lg font-bold text-primary-cyan font-mono">{s.value}</div>
<div className="text-[9px] text-text-3 mt-0.5 font-korean">{s.label}</div>
<div key={i} className="bg-bg-card border border-stroke rounded-sm p-2.5 text-center">
<div className="text-lg font-bold text-color-accent font-mono">{s.value}</div>
<div className="text-[9px] text-fg-disabled mt-0.5 font-korean">{s.label}</div>
</div>
))}
</div>
@ -342,15 +342,15 @@ function AssetManagement() {
}
const unit = unitMap[cat.category] || '개'
return (
<div key={ci} className="flex items-center justify-between px-2.5 py-2 bg-bg-3 border border-border rounded-sm hover:bg-bg-hover transition-colors">
<div key={ci} className="flex items-center justify-between px-2.5 py-2 bg-bg-card border border-stroke rounded-sm hover:bg-bg-surface-hover transition-colors">
<span className="text-[11px] font-semibold flex items-center gap-1.5 font-korean">
{cat.icon} {cat.category}
</span>
<span className="text-[11px] font-bold font-mono"><span className="text-primary-cyan">{cat.count}</span><span className="text-text-3 font-normal ml-0.5">{unit}</span></span>
<span className="text-[11px] font-bold font-mono"><span className="text-color-accent">{cat.count}</span><span className="text-fg-disabled font-normal ml-0.5">{unit}</span></span>
</div>
)
}) : (
<div className="text-center text-text-3 text-xs py-8 font-korean"> .</div>
<div className="text-center text-fg-disabled text-xs py-8 font-korean"> .</div>
)}
</div>
)}
@ -365,9 +365,9 @@ function AssetManagement() {
['살포장치', `${selectedOrg.sprayer}`],
['총 자산', `${selectedOrg.totalAssets}`],
].map(([k, v], i) => (
<div key={i} className="flex justify-between px-2.5 py-2 bg-bg-0 rounded text-[11px]">
<span className="text-text-3 font-korean">{k}</span>
<span className="font-mono font-semibold text-text-1">{v}</span>
<div key={i} className="flex justify-between px-2.5 py-2 bg-bg-base rounded text-[11px]">
<span className="text-fg-disabled font-korean">{k}</span>
<span className="font-mono font-semibold text-fg">{v}</span>
</div>
))}
</div>
@ -376,8 +376,8 @@ function AssetManagement() {
{detailTab === 'contact' && (
<div className="flex flex-col gap-2">
{/* 기관 기본 정보 */}
<div className="bg-bg-3 border border-border rounded-sm p-3">
<div className="text-[10px] font-bold text-text-3 mb-2 font-korean"> </div>
<div className="bg-bg-card border border-stroke rounded-sm p-3">
<div className="text-[10px] font-bold text-fg-disabled mb-2 font-korean"> </div>
{[
['기관명', selectedOrg.name],
['유형', selectedOrg.type],
@ -385,17 +385,17 @@ function AssetManagement() {
['주소', selectedOrg.address],
...(selectedOrg.phone ? [['대표 연락처', selectedOrg.phone]] : []),
].map(([k, v], j) => (
<div key={j} className="flex justify-between py-1.5 text-[11px] border-b border-border/30 last:border-b-0">
<span className="text-text-3 font-korean shrink-0 mr-2">{k}</span>
<span className={`text-text-1 text-right ${k === '대표 연락처' ? 'font-mono font-semibold text-primary-cyan' : 'font-korean'}`}>{v}</span>
<div key={j} className="flex justify-between py-1.5 text-[11px] border-b border-stroke/30 last:border-b-0">
<span className="text-fg-disabled font-korean shrink-0 mr-2">{k}</span>
<span className={`text-fg text-right ${k === '대표 연락처' ? 'font-mono font-semibold text-color-accent' : 'font-korean'}`}>{v}</span>
</div>
))}
</div>
{/* 담당자 목록 */}
{selectedOrg.contacts.length > 0 && (
<div className="bg-bg-3 border border-border rounded-sm p-3">
<div className="text-[10px] font-bold text-text-3 mb-2 font-korean"></div>
<div className="bg-bg-card border border-stroke rounded-sm p-3">
<div className="text-[10px] font-bold text-fg-disabled mb-2 font-korean"></div>
{selectedOrg.contacts.map((c, i) => (
<div key={i} className="mb-2.5 last:mb-0">
{[
@ -403,12 +403,12 @@ function AssetManagement() {
['담당자', c.name],
['연락처', c.phone],
].filter(([, v]) => v).map(([k, v], j) => (
<div key={j} className="flex justify-between py-1.5 text-[11px] border-b border-border/30 last:border-b-0">
<span className="text-text-3 font-korean">{k}</span>
<span className={`text-text-1 ${k === '연락처' ? 'font-mono font-semibold text-primary-cyan' : 'font-korean'}`}>{v}</span>
<div key={j} className="flex justify-between py-1.5 text-[11px] border-b border-stroke/30 last:border-b-0">
<span className="text-fg-disabled font-korean">{k}</span>
<span className={`text-fg ${k === '연락처' ? 'font-mono font-semibold text-color-accent' : 'font-korean'}`}>{v}</span>
</div>
))}
{i < selectedOrg.contacts.length - 1 && <div className="border-t border-border mt-1" />}
{i < selectedOrg.contacts.length - 1 && <div className="border-t border-stroke mt-1" />}
</div>
))}
</div>
@ -418,11 +418,11 @@ function AssetManagement() {
</div>
{/* Bottom Actions */}
<div className="p-3.5 border-t border-border flex gap-2">
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean text-white border-none cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }} >
<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 border-none cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))' }} >
📥
</button>
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean bg-bg-3 border border-border text-text-2 cursor-pointer hover:bg-bg-hover transition-colors">
<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>
</div>

파일 보기

@ -136,8 +136,8 @@ function AssetMap({
onClick={() => onRegionFilterChange(r.value)}
className={`px-2.5 py-1.5 text-[10px] font-bold rounded font-korean transition-colors ${
regionFilter === r.value
? 'bg-primary-cyan/20 text-primary-cyan border border-primary-cyan/40'
: 'bg-bg-0/80 text-text-2 border border-border hover:bg-bg-hover/80'
? 'bg-color-accent/20 text-color-accent border border-color-accent/40'
: 'bg-bg-base/80 text-fg-sub border border-stroke hover:bg-bg-surface-hover/80'
}`}
>
{r.label}
@ -146,8 +146,8 @@ function AssetMap({
</div>
{/* Legend overlay */}
<div className="absolute bottom-3 left-3 z-[1000] bg-bg-0/90 border border-border rounded-sm p-2.5 backdrop-blur-sm">
<div className="text-[9px] text-text-3 font-bold mb-1.5 font-korean"></div>
<div className="absolute bottom-3 left-3 z-[1000] bg-bg-base/90 border border-stroke rounded-sm p-2.5 backdrop-blur-sm">
<div className="text-[9px] text-fg-disabled font-bold mb-1.5 font-korean"></div>
{[
{ color: '#06b6d4', label: '해경관할' },
{ color: '#3b82f6', label: '해경경찰서' },
@ -163,7 +163,7 @@ function AssetMap({
].map((item, i) => (
<div key={i} className="flex items-center gap-1.5 mb-0.5 last:mb-0">
<span className="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0" style={{ background: item.color }} />
<span className="text-[10px] text-text-2 font-korean">{item.label}</span>
<span className="text-[10px] text-fg-sub font-korean">{item.label}</span>
</div>
))}
</div>

파일 보기

@ -18,7 +18,7 @@ interface TheorySection {
const THEORY_SECTIONS: TheorySection[] = [
{
icon: '🚢', title: '방제선 성능 기준', color: 'var(--blue)', bgTint: 'rgba(59,130,246,.08)',
icon: '🚢', title: '방제선 성능 기준', color: 'var(--color-info)', bgTint: 'rgba(59,130,246,.08)',
items: [
{
title: '해양경찰청 방제선 성능기준 고시',
@ -53,7 +53,7 @@ const THEORY_SECTIONS: TheorySection[] = [
],
},
{
icon: '⚙️', title: '방제자원 배치·동원 이론', color: 'var(--purple)', bgTint: 'rgba(168,85,247,.08)',
icon: '⚙️', title: '방제자원 배치·동원 이론', color: 'var(--color-tertiary)', bgTint: 'rgba(168,85,247,.08)',
dividerAfter: 2, dividerLabel: '📐 최적화 수리모델 참고문헌',
items: [
{
@ -78,9 +78,9 @@ const THEORY_SECTIONS: TheorySection[] = [
desc: '혼합정수계획법으로 응급 방제자원 거점 위치 선택 + 자원 할당 동시 최적화. 비용·응답시간 트레이드오프 파레토 분석.',
highlight: true,
tags: [
{ label: 'MIP 수리모델', color: 'var(--purple)' },
{ label: '자원 위치 선택', color: 'var(--blue)' },
{ label: '북극해 적용', color: 'var(--cyan)' },
{ label: 'MIP 수리모델', color: 'var(--color-tertiary)' },
{ label: '자원 위치 선택', color: 'var(--color-info)' },
{ label: '북극해 적용', color: 'var(--color-accent)' },
],
},
{
@ -89,7 +89,7 @@ const THEORY_SECTIONS: TheorySection[] = [
desc: 'GA(유전알고리즘)로 방제자원 배치 최적화 및 시뮬레이션 분포도 분석. 국내 해역 실정에 맞는 자원 배치 패턴 도출.',
highlight: true,
tags: [
{ label: 'GA 메타휴리스틱', color: 'var(--purple)' },
{ label: 'GA 메타휴리스틱', color: 'var(--color-tertiary)' },
{ label: '국내 연구', color: 'var(--green, #22c55e)' },
{ label: '배치 분포도 분석', color: 'var(--boom, #f59e0b)' },
],
@ -100,8 +100,8 @@ const THEORY_SECTIONS: TheorySection[] = [
desc: '확률적 MILP 2단계 프레임워크로 불확실성 포함 최적 자원 배치. 환경민감구역 가중치 반영.',
highlight: true,
tags: [
{ label: '확률적 MILP', color: 'var(--purple)' },
{ label: '2단계 최적화', color: 'var(--blue)' },
{ label: '확률적 MILP', color: 'var(--color-tertiary)' },
{ label: '2단계 최적화', color: 'var(--color-info)' },
{ label: '환경민감구역', color: 'var(--green, #22c55e)' },
],
},
@ -111,7 +111,7 @@ const THEORY_SECTIONS: TheorySection[] = [
desc: '동적 최적화(MINLP/MILP) 프레임워크로 오일스필 대응 스케줄링 + 오일 풍화·거동 물리모델 통합.',
highlight: true,
tags: [
{ label: 'MINLP 동적 최적화', color: 'var(--purple)' },
{ label: 'MINLP 동적 최적화', color: 'var(--color-tertiary)' },
{ label: '오일 풍화 모델 통합', color: 'var(--boom, #f59e0b)' },
],
},
@ -135,9 +135,9 @@ const THEORY_SECTIONS: TheorySection[] = [
]
const TAG_COLORS: Record<string, { bg: string; bd: string; fg: string }> = {
'var(--purple)': { bg: 'rgba(168,85,247,0.08)', bd: 'rgba(168,85,247,0.2)', fg: '#a855f7' },
'var(--blue)': { bg: 'rgba(59,130,246,0.08)', bd: 'rgba(59,130,246,0.2)', fg: '#3b82f6' },
'var(--cyan)': { bg: 'rgba(6,182,212,0.08)', bd: 'rgba(6,182,212,0.2)', fg: '#06b6d4' },
'var(--color-tertiary)': { bg: 'rgba(168,85,247,0.08)', bd: 'rgba(168,85,247,0.2)', fg: '#a855f7' },
'var(--color-info)': { bg: 'rgba(59,130,246,0.08)', bd: 'rgba(59,130,246,0.2)', fg: '#3b82f6' },
'var(--color-accent)': { bg: 'rgba(6,182,212,0.08)', bd: 'rgba(6,182,212,0.2)', fg: '#06b6d4' },
'var(--green, #22c55e)': { bg: 'rgba(34,197,94,0.08)', bd: 'rgba(34,197,94,0.2)', fg: '#22c55e' },
'var(--boom, #f59e0b)': { bg: 'rgba(245,158,11,0.08)', bd: 'rgba(245,158,11,0.2)', fg: '#f59e0b' },
}
@ -145,9 +145,9 @@ const TAG_COLORS: Record<string, { bg: string; bd: string; fg: string }> = {
function TheoryCard({ section }: { section: TheorySection }) {
const badgeBg = section.bgTint.replace(/[\d.]+\)$/, '0.15)')
return (
<div className="bg-bg-3 border border-border rounded-md overflow-hidden">
<div className="bg-bg-card border border-stroke rounded-md overflow-hidden">
{/* Section Header */}
<div className="px-4 py-3 border-b border-border flex items-center gap-2" style={{ background: section.bgTint }}>
<div className="px-4 py-3 border-b border-stroke flex items-center gap-2" style={{ background: section.bgTint }}>
<span className="text-sm">{section.icon}</span>
<span className="text-xs font-bold" style={{ color: section.color }}>
{section.title}
@ -160,13 +160,13 @@ function TheoryCard({ section }: { section: TheorySection }) {
<div key={i}>
{/* Divider */}
{section.dividerAfter !== undefined && i === section.dividerAfter + 1 && (
<div className="mt-1 mb-3 pt-2" style={{ borderTop: '1px dashed var(--bd)' }}>
<div className="mt-1 mb-3 pt-2" style={{ borderTop: '1px dashed var(--stroke-default)' }}>
<div className="text-[8px] font-bold mb-1.5 opacity-70" style={{ color: section.color }}>
{section.dividerLabel}
</div>
</div>
)}
<div className="grid gap-2 px-2.5 py-2 bg-bg-0 rounded-md" style={{
<div className="grid gap-2 px-2.5 py-2 bg-bg-base rounded-md" style={{
gridTemplateColumns: '24px 1fr',
borderLeft: item.highlight ? `2px solid ${section.color}` : undefined,
}}>
@ -183,7 +183,7 @@ function TheoryCard({ section }: { section: TheorySection }) {
<div className="font-bold mb-0.5">
{item.title}
</div>
<div className="text-text-3 leading-[1.6]">
<div className="text-fg-disabled leading-[1.6]">
{item.source}
</div>
{/* Tags */}
@ -201,7 +201,7 @@ function TheoryCard({ section }: { section: TheorySection }) {
})}
</div>
)}
<div className="mt-0.5 text-text-2">
<div className="mt-0.5 text-fg-sub">
{item.desc}
</div>
</div>
@ -219,7 +219,7 @@ function AssetTheory() {
<div className="text-[18px] font-bold mb-1">
📚
</div>
<div className="text-xs text-text-3 mb-6">
<div className="text-xs text-fg-disabled mb-6">
· ·
</div>

파일 보기

@ -25,18 +25,18 @@ function AssetUpload() {
<div className="text-[13px] font-bold mb-3.5 font-korean">📤 </div>
{/* Drop Zone */}
<div className="border-2 border-dashed border-border-light rounded-md py-10 px-5 text-center mb-5 cursor-pointer hover:border-primary-cyan/40 transition-colors">
<div className="border-2 border-dashed border-stroke-light rounded-md py-10 px-5 text-center mb-5 cursor-pointer hover:border-color-accent/40 transition-colors">
<div className="text-4xl mb-2.5 opacity-50">📁</div>
<div className="text-sm font-semibold mb-1.5 font-korean"> </div>
<div className="text-[11px] text-text-3 mb-4 font-korean">(.xlsx), CSV · 10MB</div>
<button className="px-7 py-2.5 text-[13px] font-semibold rounded-sm text-white border-none cursor-pointer font-korean" style={{ background: 'linear-gradient(135deg, var(--blue), #2563eb)' }}>
<div className="text-[11px] text-fg-disabled mb-4 font-korean">(.xlsx), CSV · 10MB</div>
<button className="px-7 py-2.5 text-[13px] font-semibold rounded-sm text-white border-none cursor-pointer font-korean" style={{ background: 'linear-gradient(135deg, var(--color-info), #2563eb)' }}>
</button>
</div>
{/* Asset Classification */}
<div className="mb-4">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean"> </label>
<select className="prd-i w-full">
<option></option>
<option></option>
@ -49,7 +49,7 @@ function AssetUpload() {
{/* Jurisdiction */}
<div className="mb-4">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean"> </label>
<select className="prd-i w-full">
<option> - </option>
<option> - </option>
@ -63,8 +63,8 @@ function AssetUpload() {
{/* Upload Mode */}
<div className="mb-5">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<div className="flex gap-4 text-xs text-text-2 font-korean">
<label className="block text-xs font-semibold mb-1.5 text-fg-sub font-korean"> </label>
<div className="flex gap-4 text-xs text-fg-sub font-korean">
<label className="flex items-center gap-1.5 cursor-pointer">
<input type="radio" checked={uploadMode === 'add'} onChange={() => setUploadMode('add')} className="accent-primary-blue" />
( + )
@ -81,10 +81,10 @@ function AssetUpload() {
onClick={handleUpload}
className={`w-full py-3.5 rounded-sm text-sm font-bold font-korean border-none cursor-pointer transition-all ${
uploaded
? 'bg-[rgba(34,197,94,0.2)] text-status-green border border-status-green'
? 'bg-[rgba(34,197,94,0.2)] text-color-success border border-status-green'
: 'text-white'
}`}
style={!uploaded ? { background: 'linear-gradient(135deg, var(--blue), #2563eb)' } : undefined}
style={!uploaded ? { background: 'linear-gradient(135deg, var(--color-info), #2563eb)' } : undefined}
>
{uploaded ? '✅ 업로드 완료!' : '📤 업로드 실행'}
</button>
@ -96,16 +96,16 @@ function AssetUpload() {
<div className="text-[13px] font-bold mb-3.5 font-korean">🔐 </div>
<div className="flex flex-col gap-2 mb-7">
{[
{ icon: '👑', role: '본청 관리자', desc: '전체 자산 조회·수정·삭제·업로드', color: 'text-status-red', bg: 'rgba(239,68,68,0.15)' },
{ icon: '🏛', role: '지방청 담당자', desc: '소속 지방청 및 하위 해경서 자산 수정·업로드', color: 'text-status-orange', bg: 'rgba(249,115,22,0.15)' },
{ icon: '⚓', role: '해경서 담당자', desc: '소속 해경서 자산 수정·업로드', color: 'text-primary-blue', bg: 'rgba(59,130,246,0.15)' },
{ icon: '👤', role: '일반 사용자', desc: '조회·다운로드만 가능', color: 'text-text-2', bg: 'rgba(100,116,139,0.15)' },
{ icon: '👑', role: '본청 관리자', desc: '전체 자산 조회·수정·삭제·업로드', color: 'text-color-danger', bg: 'rgba(239,68,68,0.15)' },
{ icon: '🏛', role: '지방청 담당자', desc: '소속 지방청 및 하위 해경서 자산 수정·업로드', color: 'text-color-warning', bg: 'rgba(249,115,22,0.15)' },
{ icon: '⚓', role: '해경서 담당자', desc: '소속 해경서 자산 수정·업로드', color: 'text-color-info', bg: 'rgba(59,130,246,0.15)' },
{ icon: '👤', role: '일반 사용자', desc: '조회·다운로드만 가능', color: 'text-fg-sub', bg: 'rgba(100,116,139,0.15)' },
].map((p, i) => (
<div key={i} className="flex items-center gap-3 p-3.5 px-4 bg-bg-3 border border-border rounded-sm">
<div key={i} className="flex items-center gap-3 p-3.5 px-4 bg-bg-card border border-stroke rounded-sm">
<div className="w-9 h-9 rounded-full flex items-center justify-center text-base" style={{ background: p.bg }}>{p.icon}</div>
<div>
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
<div className="text-[10px] text-text-3 font-korean">{p.desc}</div>
<div className="text-[10px] text-fg-disabled font-korean">{p.desc}</div>
</div>
</div>
))}
@ -115,12 +115,12 @@ function AssetUpload() {
<div className="text-[13px] font-bold mb-3.5 font-korean">📋 </div>
<div className="flex flex-col gap-2">
{uploadHistory.map((h) => (
<div key={h.logSn} className="flex justify-between items-center p-3.5 px-4 bg-bg-3 border border-border rounded-sm">
<div key={h.logSn} 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-[10px] text-text-3 mt-0.5 font-korean">{new Date(h.regDtm).toLocaleString('ko-KR')} · {h.uploaderNm} · {h.uploadCnt}</div>
<div className="text-[10px] text-fg-disabled mt-0.5 font-korean">{new Date(h.regDtm).toLocaleString('ko-KR')} · {h.uploaderNm} · {h.uploadCnt}</div>
</div>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-[rgba(34,197,94,0.15)] text-status-green"></span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-[rgba(34,197,94,0.15)] text-color-success"></span>
</div>
))}
</div>

파일 보기

@ -15,9 +15,9 @@ export function AssetsView() {
useFeatureTracking(`assets:${activeTab}`)
return (
<div className="flex flex-col h-full w-full bg-bg-0">
<div className="flex flex-col h-full w-full bg-bg-base">
{/* Tab Navigation */}
<div className="flex items-center justify-between border-b border-border bg-bg-1 shrink-0">
<div className="flex items-center justify-between border-b border-stroke bg-bg-surface shrink-0">
<div className="flex">
{([
{ id: 'management' as const, icon: '🗂', label: '자산 관리' },
@ -30,15 +30,15 @@ export function AssetsView() {
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-primary-cyan border-primary-cyan'
: 'text-text-3 border-transparent hover:text-text-2'
? 'text-color-accent border-color-accent'
: 'text-fg-disabled border-transparent hover:text-fg-sub'
}`}
>
{tab.icon} {tab.label}
</button>
))}
</div>
<div className="flex items-center gap-1.5 px-3.5 py-1.5 border rounded-full text-[11px] text-primary-blue font-korean mr-4" style={{ borderColor: 'rgba(59,130,246,0.3)' }}>
<div className="flex items-center gap-1.5 px-3.5 py-1.5 border rounded-full text-[11px] text-color-info font-korean mr-4" style={{ borderColor: 'rgba(59,130,246,0.3)' }}>
👤 _방제과 ( )
</div>
</div>

파일 보기

@ -56,7 +56,7 @@ function ShipInsurance() {
return (
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold" style={{
background: isY ? 'rgba(34,197,94,.15)' : 'rgba(100,116,139,.1)',
color: isY ? 'var(--green)' : 'var(--text-3)',
color: isY ? 'var(--color-success)' : 'var(--text-3)',
}}>
{isY ? 'Y' : 'N'}
</span>
@ -132,27 +132,27 @@ function ShipInsurance() {
<div className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[10px] font-bold"
style={{
background: total > 0 ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
color: total > 0 ? 'var(--green)' : 'var(--red)',
color: total > 0 ? 'var(--color-success)' : 'var(--color-danger)',
border: `1px solid ${total > 0 ? 'rgba(34,197,94,.25)' : 'rgba(239,68,68,.25)'}`,
}}>
<span className="w-1.5 h-1.5 rounded-full inline-block" style={{ background: total > 0 ? 'var(--green)' : 'var(--red)' }} />
<span className="w-1.5 h-1.5 rounded-full inline-block" style={{ background: total > 0 ? 'var(--color-success)' : 'var(--color-danger)' }} />
{total > 0 ? `${total.toLocaleString()}` : '데이터 없음'}
</div>
</div>
<div className="text-xs text-text-3"> </div>
<div className="text-xs text-fg-disabled"> </div>
</div>
<div className="flex gap-2">
<button
onClick={() => window.open('https://www.haewoon.or.kr', '_blank', 'noopener')}
className="px-4 py-2 text-[11px] font-bold cursor-pointer rounded-sm"
style={{ background: 'rgba(59,130,246,.12)', color: 'var(--blue)', border: '1px solid rgba(59,130,246,.3)' }}
style={{ background: 'rgba(59,130,246,.12)', color: 'var(--color-info)', border: '1px solid rgba(59,130,246,.3)' }}
>
API
</button>
<button
onClick={() => window.open('https://new.portmis.go.kr', '_blank', 'noopener')}
className="px-4 py-2 text-[11px] font-bold cursor-pointer rounded-sm"
style={{ background: 'rgba(168,85,247,.12)', color: 'var(--purple)', border: '1px solid rgba(168,85,247,.3)' }}
style={{ background: 'rgba(168,85,247,.12)', color: 'var(--color-tertiary)', border: '1px solid rgba(168,85,247,.3)' }}
>
PortMIS
</button>
@ -160,28 +160,28 @@ function ShipInsurance() {
</div>
{/* 필터 */}
<div className="bg-bg-3 border border-border rounded-md px-5 py-4 mb-4">
<div className="bg-bg-card border border-stroke rounded-md px-5 py-4 mb-4">
<div className="flex gap-2.5 items-end flex-wrap">
<div className="flex-1 min-w-[200px]">
<label className="block text-[10px] font-semibold text-text-3 mb-1"> (//IMO/)</label>
<label className="block text-[10px] font-semibold text-fg-disabled mb-1"> (//IMO/)</label>
<input
type="text" value={search} onChange={e => setSearch(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSearch()}
placeholder="선박명, 호출부호, IMO, 선주명"
className="w-full px-3.5 py-2 bg-bg-0 border border-border 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-xs outline-none box-border"
/>
</div>
<div>
<label className="block text-[10px] font-semibold text-text-3 mb-1"></label>
<select value={shipTpFilter} onChange={e => setShipTpFilter(e.target.value)} className="prd-i min-w-[120px] border-border">
<label className="block text-[10px] font-semibold text-fg-disabled mb-1"></label>
<select value={shipTpFilter} onChange={e => setShipTpFilter(e.target.value)} className="prd-i min-w-[120px] border-stroke">
<option value=""></option>
<option value="일반선박"></option>
<option value="유조선"></option>
</select>
</div>
<div>
<label className="block text-[10px] font-semibold text-text-3 mb-1"></label>
<select value={issueOrgFilter} onChange={e => setIssueOrgFilter(e.target.value)} className="prd-i min-w-[160px] border-border">
<label className="block text-[10px] font-semibold text-fg-disabled mb-1"></label>
<select value={issueOrgFilter} onChange={e => setIssueOrgFilter(e.target.value)} className="prd-i min-w-[160px] border-stroke">
<option value=""></option>
<option></option>
<option></option>
@ -197,44 +197,44 @@ function ShipInsurance() {
<option></option>
</select>
</div>
<button onClick={handleSearch} className="px-5 py-2 text-white border-none rounded-sm text-xs font-bold cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}></button>
<button onClick={handleReset} className="px-4 py-2 bg-bg-0 text-text-2 border border-border rounded-sm text-xs cursor-pointer"></button>
<button onClick={handleDownload} disabled={total === 0} className="px-4 py-2 text-xs font-bold cursor-pointer rounded-sm disabled:opacity-30 disabled:cursor-default" style={{ background: 'rgba(34,197,94,.12)', color: 'var(--green)', border: '1px solid rgba(34,197,94,.3)' }}> </button>
<button onClick={handleSearch} className="px-5 py-2 text-white border-none rounded-sm text-xs font-bold cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--color-accent), var(--color-info))' }}></button>
<button onClick={handleReset} className="px-4 py-2 bg-bg-base text-fg-sub border border-stroke rounded-sm text-xs cursor-pointer"></button>
<button onClick={handleDownload} disabled={total === 0} className="px-4 py-2 text-xs font-bold cursor-pointer rounded-sm disabled:opacity-30 disabled:cursor-default" style={{ background: 'rgba(34,197,94,.12)', color: 'var(--color-success)', border: '1px solid rgba(34,197,94,.3)' }}> </button>
</div>
</div>
{/* 로딩 */}
{isLoading && (
<div className="flex flex-col items-center justify-center p-16 bg-bg-3 border border-border rounded-md">
<div className="w-9 h-9 rounded-full mb-3.5" style={{ border: '3px solid var(--bd)', borderTopColor: 'var(--cyan)', animation: 'spin 0.8s linear infinite' }} />
<div className="text-[13px] text-text-2"> ...</div>
<div className="flex flex-col items-center justify-center p-16 bg-bg-card border border-stroke rounded-md">
<div className="w-9 h-9 rounded-full mb-3.5" style={{ border: '3px solid var(--stroke-default)', borderTopColor: 'var(--color-accent)', animation: 'spin 0.8s linear infinite' }} />
<div className="text-[13px] text-fg-sub"> ...</div>
</div>
)}
{/* 에러 */}
{error && !isLoading && (
<div className="flex flex-col items-center justify-center py-16 px-5 bg-bg-3 border border-border rounded-md">
<div className="text-sm font-bold text-status-red mb-2"> </div>
<div className="text-xs text-text-3">{error}</div>
<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>
)}
{/* 테이블 */}
{!isLoading && !error && (
<>
<div className="bg-bg-3 border border-border rounded-md overflow-hidden mb-3">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="bg-bg-card 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">
<span className="text-primary-cyan">{total.toLocaleString()}</span>
{totalPages > 1 && <span className="text-text-3 font-normal ml-2">({page}/{totalPages} )</span>}
<span className="text-color-accent">{total.toLocaleString()}</span>
{totalPages > 1 && <span className="text-fg-disabled font-normal ml-2">({page}/{totalPages} )</span>}
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-[11px] border-collapse whitespace-nowrap">
<thead>
<tr className="bg-bg-0">
<tr className="bg-bg-base">
{['No', '선박명', '호출부호', 'IMO', '선박종류', '선주', '총톤수', '보험사', '책임', '유류', '연료유', '난파물', '유효기간', '발급기관', '상태'].map((h, i) => (
<th key={i} className="px-3 py-2.5 font-bold text-text-2 border-b border-border text-center">{h}</th>
<th key={i} className="px-3 py-2.5 font-bold text-fg-sub border-b border-stroke text-center">{h}</th>
))}
</tr>
</thead>
@ -245,14 +245,14 @@ function ShipInsurance() {
const isSoon = st === 'soon'
const rowNum = (page - 1) * PAGE_SIZE + i + 1
return (
<tr key={r.insSn} className="border-b border-border" style={{ background: isExp ? 'rgba(239,68,68,.03)' : undefined }}>
<td className="px-3 py-2 text-center text-text-3 font-mono">{rowNum}</td>
<tr key={r.insSn} className="border-b border-stroke" style={{ background: isExp ? 'rgba(239,68,68,.03)' : undefined }}>
<td className="px-3 py-2 text-center text-fg-disabled font-mono">{rowNum}</td>
<td className="px-3 py-2 font-semibold">{r.shipNm}</td>
<td className="px-3 py-2 text-center font-mono">{r.callSign || '—'}</td>
<td className="px-3 py-2 text-center font-mono">{r.imoNo || '—'}</td>
<td className="px-3 py-2 text-center">
<span className="text-[10px]">{r.shipTp}</span>
{r.shipTpDetail && <span className="text-text-3 text-[9px] ml-1">({r.shipTpDetail})</span>}
{r.shipTpDetail && <span className="text-fg-disabled text-[9px] ml-1">({r.shipTpDetail})</span>}
</td>
<td className="px-3 py-2 max-w-[150px] truncate">{r.ownerNm}</td>
<td className="px-3 py-2 text-right font-mono">{r.grossTon ? Number(r.grossTon).toLocaleString() : '—'}</td>
@ -261,14 +261,14 @@ function ShipInsurance() {
<td className="px-3 py-2 text-center">{ynBadge(r.oilPollutionYn)}</td>
<td className="px-3 py-2 text-center">{ynBadge(r.fuelOilYn)}</td>
<td className="px-3 py-2 text-center">{ynBadge(r.wreckRemovalYn)}</td>
<td className="px-3 py-2 text-center font-mono text-[10px]" style={{ color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : undefined, fontWeight: isExp || isSoon ? 700 : undefined }}>
<td className="px-3 py-2 text-center font-mono text-[10px]" style={{ color: isExp ? 'var(--color-danger)' : isSoon ? 'var(--color-caution)' : undefined, fontWeight: isExp || isSoon ? 700 : undefined }}>
{r.validStart} ~ {r.validEnd}
</td>
<td className="px-3 py-2 text-center text-[10px]">{r.issueOrg}</td>
<td className="px-3 py-2 text-center">
<span className="px-2 py-0.5 rounded-full text-[9px] font-semibold" style={{
background: isExp ? 'rgba(239,68,68,.15)' : isSoon ? 'rgba(234,179,8,.15)' : 'rgba(34,197,94,.15)',
color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : 'var(--green)',
color: isExp ? 'var(--color-danger)' : isSoon ? 'var(--color-caution)' : 'var(--color-success)',
}}>
{isExp ? '만료' : isSoon ? '만료임박' : '유효'}
</span>
@ -286,7 +286,7 @@ function ShipInsurance() {
<div className="flex items-center justify-center gap-1.5 mb-4">
<button
onClick={() => loadData(page - 1)} disabled={page <= 1}
className="px-3 py-1.5 text-[11px] rounded-sm border border-border bg-bg-0 cursor-pointer disabled:opacity-30 disabled:cursor-default"
className="px-3 py-1.5 text-[11px] rounded-sm border border-stroke bg-bg-base cursor-pointer disabled:opacity-30 disabled:cursor-default"
>
</button>
@ -299,9 +299,9 @@ function ShipInsurance() {
key={p} onClick={() => loadData(p)}
className="w-8 h-8 text-[11px] rounded-sm border cursor-pointer font-mono"
style={{
background: p === page ? 'var(--cyan)' : 'var(--bg-0)',
background: p === page ? 'var(--color-accent)' : 'var(--bg-0)',
color: p === page ? '#fff' : 'var(--text-2)',
borderColor: p === page ? 'var(--cyan)' : 'var(--bd)',
borderColor: p === page ? 'var(--color-accent)' : 'var(--stroke-default)',
fontWeight: p === page ? 700 : 400,
}}
>
@ -311,7 +311,7 @@ function ShipInsurance() {
})}
<button
onClick={() => loadData(page + 1)} disabled={page >= totalPages}
className="px-3 py-1.5 text-[11px] rounded-sm border border-border bg-bg-0 cursor-pointer disabled:opacity-30 disabled:cursor-default"
className="px-3 py-1.5 text-[11px] rounded-sm border border-stroke bg-bg-base cursor-pointer disabled:opacity-30 disabled:cursor-default"
>
</button>
@ -321,10 +321,10 @@ function ShipInsurance() {
)}
{/* 푸터 */}
<div className="mt-auto px-4 py-3 bg-bg-3 border border-border rounded-sm">
<div className="text-[10px] text-text-3 leading-[1.7]">
<span className="text-text-2 font-bold"> :</span> · <br />
<span className="text-text-2 font-bold">:</span> , , , , , , ,
<div className="mt-auto px-4 py-3 bg-bg-card border border-stroke rounded-sm">
<div className="text-[10px] text-fg-disabled leading-[1.7]">
<span className="text-fg-sub font-bold"> :</span> · <br />
<span className="text-fg-sub font-bold">:</span> , , , , , , ,
</div>
</div>
</div>

파일 보기

@ -34,12 +34,12 @@ export interface InsuranceRow {
}
export const typeTagCls = (type: string) => {
if (type === '해경관할') return 'bg-[rgba(239,68,68,0.1)] text-status-red'
if (type === '해경경찰서') return 'bg-[rgba(59,130,246,0.1)] text-primary-blue'
if (type === '파출소') return 'bg-[rgba(34,197,94,0.1)] text-status-green'
if (type === '관련기관') return 'bg-[rgba(168,85,247,0.1)] text-primary-purple'
if (type === '해양환경공단') return 'bg-[rgba(6,182,212,0.1)] text-primary-cyan'
if (type === '업체') return 'bg-[rgba(245,158,11,0.1)] text-status-orange'
if (type === '해경관할') return 'bg-[rgba(239,68,68,0.1)] text-color-danger'
if (type === '해경경찰서') return 'bg-[rgba(59,130,246,0.1)] text-color-info'
if (type === '파출소') return 'bg-[rgba(34,197,94,0.1)] text-color-success'
if (type === '관련기관') return 'bg-[rgba(168,85,247,0.1)] text-color-tertiary'
if (type === '해양환경공단') return 'bg-[rgba(6,182,212,0.1)] text-color-accent'
if (type === '업체') return 'bg-[rgba(245,158,11,0.1)] text-color-warning'
if (type === '지자체') return 'bg-[rgba(236,72,153,0.1)] text-[#ec4899]'
if (type === '기름저장시설') return 'bg-[rgba(139,92,246,0.1)] text-[#8b5cf6]'
if (type === '정유사') return 'bg-[rgba(20,184,166,0.1)] text-[#14b8a6]'

파일 보기

@ -52,8 +52,8 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
if (isLoading || !post) {
return (
<div className="flex flex-col h-full bg-bg-0 items-center justify-center">
<p className="text-text-3 text-sm"> ...</p>
<div className="flex flex-col h-full bg-bg-base items-center justify-center">
<p className="text-fg-disabled text-sm"> ...</p>
</div>
)
}
@ -62,12 +62,12 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
const isAuthor = user?.id === post.authorId
return (
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b border-border bg-bg-1">
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<button
onClick={onBack}
className="flex items-center gap-2 text-sm font-semibold text-text-2 hover:text-text-1 transition-colors"
className="flex items-center gap-2 text-sm font-semibold text-fg-sub hover:text-fg transition-colors"
>
<span></span>
<span></span>
@ -76,7 +76,7 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
<div className="flex items-center gap-2">
<button
onClick={onEdit}
className="px-4 py-2 text-sm font-semibold rounded bg-bg-2 text-text-1 border border-border hover:bg-bg-3 transition-colors"
className="px-4 py-2 text-sm font-semibold rounded bg-bg-elevated text-fg border border-stroke hover:bg-bg-card transition-colors"
>
</button>
@ -94,7 +94,7 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
<div className="flex-1 overflow-auto">
<div className="max-w-4xl mx-auto px-8 py-8">
{/* 게시글 헤더 */}
<div className="pb-6 border-b border-border">
<div className="pb-6 border-b border-stroke">
<div className="flex items-center gap-3 mb-3">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded text-xs font-semibold ${CATEGORY_COLORS[post.categoryCd] || 'bg-blue-500/20 text-blue-400'}`}>
{CATEGORY_LABELS[post.categoryCd] || post.categoryCd}
@ -105,9 +105,9 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
</span>
)}
</div>
<h1 className="text-2xl font-bold text-text-1 mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-sm text-text-3">
<span>: <span className="text-text-2 font-semibold">{post.authorName}</span></span>
<h1 className="text-2xl font-bold text-fg mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-sm text-fg-disabled">
<span>: <span className="text-fg-sub font-semibold">{post.authorName}</span></span>
<span>|</span>
<span>: {new Date(post.regDtm).toLocaleDateString('ko-KR')}</span>
<span>|</span>
@ -124,16 +124,16 @@ export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetai
{/* 본문 */}
<div className="py-8">
<div className="prose prose-invert max-w-none">
<div className="text-text-1 text-[15px] leading-relaxed whitespace-pre-wrap">
<div className="text-fg text-[15px] leading-relaxed whitespace-pre-wrap">
{post.content || '(내용 없음)'}
</div>
</div>
</div>
{/* 댓글 섹션 (향후 구현 예정) */}
<div className="py-6 border-t border-border">
<div className="py-6 border-t border-stroke">
<div className="text-center py-8">
<p className="text-text-3 text-sm"> .</p>
<p className="text-fg-disabled text-sm"> .</p>
</div>
</div>
</div>

파일 보기

@ -95,9 +95,9 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
};
return (
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* Header with Search and Write Button */}
<div className="flex items-center justify-between px-8 py-4 border-b border-border bg-bg-1">
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<div className="flex items-center gap-4">
<div className="flex gap-2">
{CATEGORY_FILTER.map((cat) => (
@ -106,8 +106,8 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
onClick={() => handleCategoryChange(cat.code)}
className={`px-4 py-2 text-sm font-semibold rounded transition-all ${
selectedCategory === cat.code
? 'bg-primary-cyan text-bg-0'
: 'bg-bg-2 text-text-3 hover:bg-bg-3 hover:text-text-1'
? 'bg-color-accent text-bg-0'
: 'bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg'
}`}
>
{cat.label}
@ -123,13 +123,13 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="px-4 py-2 text-sm bg-bg-2 border border-border rounded text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none w-64"
className="px-4 py-2 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none w-64"
/>
{canWrite && (
<button
onClick={onWriteClick}
className="px-6 py-2 text-sm font-semibold rounded bg-primary-cyan text-bg-0 hover:opacity-90 transition-opacity flex items-center gap-2"
className="px-6 py-2 text-sm font-semibold rounded bg-color-accent text-bg-0 hover:opacity-90 transition-opacity flex items-center gap-2"
>
<span>+</span>
<span></span>
@ -142,19 +142,19 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<div className="flex-1 overflow-auto px-8 py-6">
{loading ? (
<div className="text-center py-20">
<p className="text-text-3 text-sm"> ...</p>
<p className="text-fg-disabled text-sm"> ...</p>
</div>
) : (
<>
<table className="w-full border-collapse">
<thead>
<tr className="border-b-2 border-border">
<th className="px-4 py-3 text-left text-sm font-semibold text-text-2 w-20"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-text-2 w-32"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-text-2"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-text-2 w-32"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-text-2 w-32"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-text-2 w-24"></th>
<tr className="border-b-2 border-stroke">
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-20"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-32"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-32"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-32"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub w-24"></th>
</tr>
</thead>
<tbody>
@ -162,9 +162,9 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<tr
key={post.sn}
onClick={() => onPostClick(post.sn)}
className="border-b border-border hover:bg-bg-2 cursor-pointer transition-colors"
className="border-b border-stroke hover:bg-bg-elevated cursor-pointer transition-colors"
>
<td className="px-4 py-4 text-sm text-text-1">
<td className="px-4 py-4 text-sm text-fg">
{post.pinnedYn === 'Y' ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-red-500/20 text-red-400">
@ -185,15 +185,15 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<td className="px-4 py-4">
<span
className={`text-sm ${
post.pinnedYn === 'Y' ? 'font-semibold text-text-1' : 'text-text-1'
} hover:text-primary-cyan transition-colors`}
post.pinnedYn === 'Y' ? 'font-semibold text-fg' : 'text-fg'
} hover:text-color-accent transition-colors`}
>
{post.title}
</span>
</td>
<td className="px-4 py-4 text-sm text-text-2">{post.authorName}</td>
<td className="px-4 py-4 text-sm text-text-3">{formatDate(post.regDtm)}</td>
<td className="px-4 py-4 text-sm text-text-3">{post.viewCnt}</td>
<td className="px-4 py-4 text-sm text-fg-sub">{post.authorName}</td>
<td className="px-4 py-4 text-sm text-fg-disabled">{formatDate(post.regDtm)}</td>
<td className="px-4 py-4 text-sm text-fg-disabled">{post.viewCnt}</td>
</tr>
))}
</tbody>
@ -201,7 +201,7 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
{posts.length === 0 && (
<div className="text-center py-20">
<p className="text-text-3 text-sm"> .</p>
<p className="text-fg-disabled text-sm"> .</p>
</div>
)}
</>
@ -210,11 +210,11 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 px-8 py-4 border-t border-border bg-bg-1">
<div className="flex items-center justify-center gap-2 px-8 py-4 border-t border-stroke bg-bg-surface">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page <= 1}
className="px-3 py-1.5 text-sm rounded bg-bg-2 text-text-3 hover:bg-bg-3 hover:text-text-1 transition-colors disabled:opacity-40"
className="px-3 py-1.5 text-sm rounded bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors disabled:opacity-40"
>
</button>
@ -224,8 +224,8 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
onClick={() => setPage(p)}
className={`px-3 py-1.5 text-sm rounded ${
page === p
? 'bg-primary-cyan text-bg-0 font-semibold'
: 'bg-bg-2 text-text-3 hover:bg-bg-3 hover:text-text-1 transition-colors'
? 'bg-color-accent text-bg-0 font-semibold'
: 'bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors'
}`}
>
{p}
@ -234,7 +234,7 @@ export function BoardListTable({ onPostClick, onWriteClick }: BoardListTableProp
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page >= totalPages}
className="px-3 py-1.5 text-sm rounded bg-bg-2 text-text-3 hover:bg-bg-3 hover:text-text-1 transition-colors disabled:opacity-40"
className="px-3 py-1.5 text-sm rounded bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg transition-colors disabled:opacity-40"
>
</button>

파일 보기

@ -204,23 +204,23 @@ export function BoardView() {
return (
<div className="flex flex-1 overflow-hidden">
<div className="flex-1 relative overflow-hidden">
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b" style={{ borderColor: 'var(--bd)', background: 'var(--bg1)' }}>
<div className="flex items-center justify-between px-8 py-4 border-b" style={{ borderColor: 'var(--stroke-default)', background: 'var(--bg-surface)' }}>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span className="text-lg">📘</span>
<span className="text-[15px] font-bold"></span>
<span className="text-[10px] ml-1 text-text-3"> {filteredManuals.length}</span>
<span className="text-[10px] ml-1 text-fg-disabled"> {filteredManuals.length}</span>
</div>
<div className="flex gap-1 ml-4">
{manualCategories.map(cat => (
<button key={cat} onClick={() => setManualCategory(cat)}
className="px-3 py-1.5 text-[11px] font-semibold rounded-md transition-all"
style={{
background: manualCategory === cat ? 'rgba(6,182,212,.15)' : 'var(--bg3)',
border: manualCategory === cat ? '1px solid rgba(6,182,212,.3)' : '1px solid var(--bd)',
color: manualCategory === cat ? 'var(--cyan)' : 'var(--t3)',
background: manualCategory === cat ? 'rgba(6,182,212,.15)' : 'var(--bg-card)',
border: manualCategory === cat ? '1px solid rgba(6,182,212,.3)' : '1px solid var(--stroke-default)',
color: manualCategory === cat ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}>
{cat}
</button>
@ -229,7 +229,7 @@ export function BoardView() {
</div>
<div className="flex items-center gap-3">
<input type="text" placeholder="매뉴얼 검색..." value={manualSearch} onChange={e => setManualSearch(e.target.value)}
className="px-4 py-2 text-sm rounded w-64" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', outline: 'none' }} />
className="px-4 py-2 text-sm rounded w-64" style={{ background: 'var(--bg-elevated)', border: '1px solid var(--stroke-default)', outline: 'none' }} />
<button onClick={() => setShowUploadModal(true)}
className="px-5 py-2 text-[12px] font-semibold rounded-md transition-all flex items-center gap-1.5"
style={{ background: 'rgba(6,182,212,.15)', border: '1px solid rgba(6,182,212,.3)', color: '#22d3ee', cursor: 'pointer' }}>
@ -242,7 +242,7 @@ export function BoardView() {
<div className="flex-1 overflow-auto px-8 py-6">
{manualLoading ? (
<div className="text-center py-20">
<p className="text-sm text-text-3"> ...</p>
<p className="text-sm text-fg-disabled"> ...</p>
</div>
) : (
<div className="grid gap-3" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))' }}>
@ -250,11 +250,11 @@ export function BoardView() {
const cc = catColor(file.catgNm)
return (
<div key={file.manualSn} className="rounded-xl p-4 transition-all" style={{
background: 'var(--bg3)', border: '1px solid var(--bd)',
background: 'var(--bg-card)', border: '1px solid var(--stroke-default)',
cursor: 'pointer',
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(6,182,212,.4)' }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--bd)' }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--stroke-default)' }}
>
<div className="flex items-center justify-between mb-3">
<span className="px-2 py-0.5 rounded text-[10px] font-semibold" style={{ background: cc.bg, color: cc.text }}>
@ -272,7 +272,7 @@ export function BoardView() {
<span style={{ fontSize: 12 }}>📄</span>
<span className="text-[10px] font-semibold" style={{ color: '#ef4444' }}>{file.fileTp || 'PDF'}</span>
</div>
<span className="text-[10px]" style={{ color: 'var(--t3)', fontFamily: 'var(--fM)' }}>{file.fileSz}</span>
<span className="text-[10px]" style={{ color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)' }}>{file.fileSz}</span>
</div>
<div className="flex items-center justify-end gap-1 mb-2">
<button onClick={(e) => {
@ -309,13 +309,13 @@ export function BoardView() {
🗑
</button>
</div>
<div className="flex items-center justify-between pt-3" style={{ borderTop: '1px solid var(--bd)' }}>
<div className="flex items-center gap-3 text-[10px] text-text-3">
<div className="flex items-center justify-between pt-3" style={{ borderTop: '1px solid var(--stroke-default)' }}>
<div className="flex items-center gap-3 text-[10px] text-fg-disabled">
<span>{file.authorNm}</span>
<span>{new Date(file.regDtm).toLocaleDateString('ko-KR')}</span>
</div>
<div className="flex items-center gap-3">
<span className="text-[10px]" style={{ color: 'var(--t3)', fontFamily: 'var(--fM)' }}>
<span className="text-[10px]" style={{ color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)' }}>
{file.dwnldCnt}
</span>
<button onClick={async (e) => {
@ -367,7 +367,7 @@ export function BoardView() {
{!manualLoading && filteredManuals.length === 0 && (
<div className="text-center py-20">
<div style={{ fontSize: 32, opacity: 0.3, marginBottom: 8 }}>📘</div>
<p className="text-sm text-text-3"> .</p>
<p className="text-sm text-fg-disabled"> .</p>
</div>
)}
</div>
@ -377,19 +377,19 @@ export function BoardView() {
{showUploadModal && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,.55)' }}
onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}>
<div className="w-[480px] bg-bg-1 border border-border rounded-xl overflow-hidden"
<div className="w-[480px] bg-bg-surface border border-stroke rounded-xl overflow-hidden"
onClick={e => e.stopPropagation()}>
<div className="px-5 py-4 border-b border-border flex items-center justify-between">
<div className="px-5 py-4 border-b border-stroke flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-base">{editingManualId ? '✏️' : '📤'}</span>
<span className="text-sm font-bold">{editingManualId ? '매뉴얼 수정' : '매뉴얼 업로드'}</span>
</div>
<span onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}
className="cursor-pointer text-text-3 text-base leading-none"></span>
className="cursor-pointer text-fg-disabled text-base leading-none"></span>
</div>
<div className="p-5 flex flex-col gap-4">
<div>
<label className="block text-[11px] font-semibold text-text-2 mb-1.5"></label>
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5"></label>
<div className="flex gap-1.5">
{['방제매뉴얼', '대응매뉴얼', '교육자료', '법령·규정'].map(cat => {
const cc = catColor(cat)
@ -398,9 +398,9 @@ export function BoardView() {
<button key={cat} onClick={() => setUploadForm(prev => ({ ...prev, category: cat }))}
className="flex-1 py-2 px-1 rounded-md text-[11px] font-semibold cursor-pointer"
style={{
background: isActive ? cc.bg : 'var(--bg3)',
border: isActive ? `1px solid ${cc.text}40` : '1px solid var(--bd)',
color: isActive ? cc.text : 'var(--t3)',
background: isActive ? cc.bg : 'var(--bg-card)',
border: isActive ? `1px solid ${cc.text}40` : '1px solid var(--stroke-default)',
color: isActive ? cc.text : 'var(--fg-disabled)',
}}>
{cat}
</button>
@ -409,20 +409,20 @@ export function BoardView() {
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-text-2 mb-1.5"> </label>
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5"> </label>
<input type="text" placeholder="매뉴얼 제목을 입력하세요" value={uploadForm.title}
onChange={e => setUploadForm(prev => ({ ...prev, title: e.target.value }))}
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-2 border border-border outline-none" style={{ boxSizing: 'border-box' }} />
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-elevated border border-stroke outline-none" style={{ boxSizing: 'border-box' }} />
</div>
<div>
<label className="block text-[11px] font-semibold text-text-2 mb-1.5"></label>
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5"></label>
<input type="text" placeholder="예: v1.0" value={uploadForm.version}
onChange={e => setUploadForm(prev => ({ ...prev, version: e.target.value }))}
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-2 border border-border outline-none" style={{ boxSizing: 'border-box' }} />
className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-elevated border border-stroke outline-none" style={{ boxSizing: 'border-box' }} />
</div>
<div>
<label className="block text-[11px] font-semibold text-text-2 mb-1.5"> </label>
<div className="border-2 border-dashed border-border rounded-md py-6 px-4 text-center bg-bg-2 cursor-pointer relative"
<label className="block text-[11px] font-semibold text-fg-sub mb-1.5"> </label>
<div className="border-2 border-dashed border-stroke rounded-md py-6 px-4 text-center bg-bg-elevated cursor-pointer relative"
onClick={() => {
const input = document.createElement('input')
input.type = 'file'
@ -441,24 +441,24 @@ export function BoardView() {
<span className="text-xl">📄</span>
<div className="text-left">
<div className="text-xs font-semibold">{uploadForm.fileName}</div>
<div className="text-[10px] text-text-3 font-mono">{uploadForm.fileSize}</div>
<div className="text-[10px] text-fg-disabled font-mono">{uploadForm.fileSize}</div>
</div>
<span onClick={(e) => { e.stopPropagation(); setUploadForm(prev => ({ ...prev, fileName: '', fileSize: '' })) }}
className="text-xs text-text-3 cursor-pointer ml-2"></span>
className="text-xs text-fg-disabled cursor-pointer ml-2"></span>
</div>
) : (
<>
<div className="text-[28px] opacity-30 mb-1.5">📁</div>
<div className="text-[11px] text-text-3"> </div>
<div className="text-[9px] text-text-3 font-mono mt-1">PDF, DOC, HWP, XLSX ( 100MB)</div>
<div className="text-[11px] text-fg-disabled"> </div>
<div className="text-[9px] text-fg-disabled font-mono mt-1">PDF, DOC, HWP, XLSX ( 100MB)</div>
</>
)}
</div>
</div>
</div>
<div className="px-5 py-3 border-t border-border flex justify-end gap-2">
<div className="px-5 py-3 border-t border-stroke flex justify-end gap-2">
<button onClick={() => { setShowUploadModal(false); setEditingManualId(null) }}
className="px-5 py-2 rounded-md text-xs font-semibold bg-bg-3 border border-border text-text-3 cursor-pointer">
className="px-5 py-2 rounded-md text-xs font-semibold bg-bg-card border border-stroke text-fg-disabled cursor-pointer">
</button>
<button onClick={async () => {
@ -545,11 +545,11 @@ export function BoardView() {
return (
<div className="flex flex-1 overflow-hidden">
<div className="flex-1 relative overflow-hidden">
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b border-border bg-bg-1">
<div className="text-sm text-text-3">
<span className="text-text-1 font-semibold">{totalCount}</span>
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<div className="text-sm text-fg-disabled">
<span className="text-fg font-semibold">{totalCount}</span>
</div>
<div className="flex items-center gap-3">
<input
@ -558,12 +558,12 @@ export function BoardView() {
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="px-4 py-2 text-sm bg-bg-2 border border-border rounded text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none w-64"
className="px-4 py-2 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none w-64"
/>
{hasPermission(getWriteResource(), 'CREATE') && (
<button
onClick={handleWriteClick}
className="px-6 py-2 text-sm font-semibold rounded bg-primary-cyan text-bg-0 hover:opacity-90 transition-opacity"
className="px-6 py-2 text-sm font-semibold rounded bg-color-accent text-bg-0 hover:opacity-90 transition-opacity"
>
</button>
@ -575,28 +575,28 @@ export function BoardView() {
<div className="flex-1 overflow-auto px-8 py-6">
{isLoading ? (
<div className="text-center py-20">
<p className="text-text-3 text-sm"> ...</p>
<p className="text-fg-disabled text-sm"> ...</p>
</div>
) : (
<>
<table className="w-full border-collapse">
<thead>
<tr className="border-b-2 border-border">
<th className="px-4 py-3 text-center text-sm font-semibold text-text-2 w-16"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-text-2 w-24"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-text-2"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-text-2 w-24"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-text-2 w-28"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-text-2 w-16"></th>
<tr className="border-b-2 border-stroke">
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-16"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-24"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-fg-sub"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-24"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-28"></th>
<th className="px-4 py-3 text-center text-sm font-semibold text-fg-sub w-16"></th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr
key={post.sn}
className="border-b border-border hover:bg-bg-2 transition-colors"
className="border-b border-stroke hover:bg-bg-elevated transition-colors"
>
<td className="px-4 py-4 text-sm text-text-1 text-center">{post.sn}</td>
<td className="px-4 py-4 text-sm text-fg text-center">{post.sn}</td>
<td className="px-4 py-4 text-center">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold ${CATEGORY_COLORS[post.categoryCd] || 'bg-blue-500/20 text-blue-400'}`}>
{CATEGORY_LABELS[post.categoryCd] || post.categoryCd}
@ -606,15 +606,15 @@ export function BoardView() {
className="px-4 py-4 cursor-pointer"
onClick={() => handlePostClick(post.sn)}
>
<span className={`text-sm ${post.pinnedYn === 'Y' ? 'font-semibold text-text-1' : 'text-text-1'} hover:text-primary-cyan transition-colors`}>
<span className={`text-sm ${post.pinnedYn === 'Y' ? 'font-semibold text-fg' : 'text-fg'} hover:text-color-accent transition-colors`}>
{post.pinnedYn === 'Y' && '📌 '}{post.title}
</span>
</td>
<td className="px-4 py-4 text-sm text-text-2 text-center">{post.authorName}</td>
<td className="px-4 py-4 text-sm text-text-3 text-center">
<td className="px-4 py-4 text-sm text-fg-sub text-center">{post.authorName}</td>
<td className="px-4 py-4 text-sm text-fg-disabled text-center">
{new Date(post.regDtm).toLocaleDateString('ko-KR')}
</td>
<td className="px-4 py-4 text-sm text-text-3 text-center">{post.viewCnt}</td>
<td className="px-4 py-4 text-sm text-fg-disabled text-center">{post.viewCnt}</td>
</tr>
))}
</tbody>
@ -622,7 +622,7 @@ export function BoardView() {
{posts.length === 0 && (
<div className="text-center py-20">
<p className="text-text-3 text-sm"> .</p>
<p className="text-fg-disabled text-sm"> .</p>
</div>
)}
</>
@ -631,15 +631,15 @@ export function BoardView() {
{/* 페이지네이션 */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 px-8 py-4 border-t border-border bg-bg-1">
<div className="flex items-center justify-center gap-2 px-8 py-4 border-t border-stroke bg-bg-surface">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
<button
key={p}
onClick={() => setPage(p)}
className={`px-3 py-1.5 text-sm rounded transition-colors ${
p === page
? 'bg-primary-cyan/20 text-primary-cyan font-semibold'
: 'bg-bg-2 text-text-3 hover:bg-bg-3 hover:text-text-1'
? 'bg-color-accent/20 text-color-accent font-semibold'
: 'bg-bg-elevated text-fg-disabled hover:bg-bg-card hover:text-fg'
}`}
>
{p}

파일 보기

@ -112,17 +112,17 @@ export function BoardWriteForm({ postSn, defaultCategoryCd, onSaveComplete, onCa
if (isFetching) {
return (
<div className="flex flex-col h-full bg-bg-0 items-center justify-center">
<p className="text-text-3 text-sm"> ...</p>
<div className="flex flex-col h-full bg-bg-base items-center justify-center">
<p className="text-fg-disabled text-sm"> ...</p>
</div>
)
}
return (
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b border-border bg-bg-1">
<h2 className="text-lg font-semibold text-text-1">
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<h2 className="text-lg font-semibold text-fg">
{isEditMode ? '게시글 수정' : '게시글 작성'}
</h2>
</div>
@ -133,14 +133,14 @@ export function BoardWriteForm({ postSn, defaultCategoryCd, onSaveComplete, onCa
<div className="max-w-4xl mx-auto space-y-6">
{/* 분류 선택 */}
<div>
<label className="block text-sm font-semibold text-text-2 mb-2">
<label className="block text-sm font-semibold text-fg-sub mb-2">
<span className="text-red-500">*</span>
</label>
<select
value={categoryCd}
onChange={(e) => setCategoryCd(e.target.value)}
disabled={isEditMode}
className="w-full px-4 py-2.5 text-sm bg-bg-2 border border-border rounded text-text-1 focus:border-primary-cyan focus:outline-none disabled:opacity-50"
className="w-full px-4 py-2.5 text-sm bg-bg-elevated border border-stroke rounded text-fg focus:border-color-accent focus:outline-none disabled:opacity-50"
>
{CATEGORY_OPTIONS.map(opt => (
<option key={opt.code} value={opt.code}>{opt.label}</option>
@ -150,7 +150,7 @@ export function BoardWriteForm({ postSn, defaultCategoryCd, onSaveComplete, onCa
{/* 제목 */}
<div>
<label className="block text-sm font-semibold text-text-2 mb-2">
<label className="block text-sm font-semibold text-fg-sub mb-2">
<span className="text-red-500">*</span>
</label>
<input
@ -159,13 +159,13 @@ export function BoardWriteForm({ postSn, defaultCategoryCd, onSaveComplete, onCa
onChange={(e) => setTitle(e.target.value)}
maxLength={200}
placeholder="제목을 입력하세요"
className="w-full px-4 py-2.5 text-sm bg-bg-2 border border-border rounded text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none"
className="w-full px-4 py-2.5 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
/>
</div>
{/* 내용 */}
<div>
<label className="block text-sm font-semibold text-text-2 mb-2">
<label className="block text-sm font-semibold text-fg-sub mb-2">
<span className="text-red-500">*</span>
</label>
<textarea
@ -174,13 +174,13 @@ export function BoardWriteForm({ postSn, defaultCategoryCd, onSaveComplete, onCa
maxLength={10000}
placeholder="내용을 입력하세요"
rows={15}
className="w-full px-4 py-3 text-sm bg-bg-2 border border-border rounded text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none resize-none"
className="w-full px-4 py-3 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none resize-none"
/>
</div>
{/* 파일 첨부 (향후 API 연동 예정) */}
<div>
<label className="block text-sm font-semibold text-text-2 mb-2"></label>
<label className="block text-sm font-semibold text-fg-sub mb-2"></label>
<div className="flex items-center gap-3">
<input
type="file"
@ -192,29 +192,29 @@ export function BoardWriteForm({ postSn, defaultCategoryCd, onSaveComplete, onCa
/>
<label
htmlFor="file-upload"
className="px-4 py-2 text-sm font-semibold rounded bg-bg-2 text-text-1 border border-border hover:bg-bg-3 cursor-pointer transition-colors"
className="px-4 py-2 text-sm font-semibold rounded bg-bg-elevated text-fg border border-stroke hover:bg-bg-card cursor-pointer transition-colors"
>
</label>
<span className="text-sm text-text-3"> </span>
<span className="text-sm text-fg-disabled"> </span>
</div>
</div>
</div>
</div>
{/* 하단 버튼 */}
<div className="flex items-center justify-end gap-3 px-8 py-4 border-t border-border bg-bg-1">
<div className="flex items-center justify-end gap-3 px-8 py-4 border-t border-stroke bg-bg-surface">
<button
type="button"
onClick={onCancel}
className="px-6 py-2.5 text-sm font-semibold rounded bg-bg-2 text-text-1 border border-border hover:bg-bg-3 transition-colors"
className="px-6 py-2.5 text-sm font-semibold rounded bg-bg-elevated text-fg border border-stroke hover:bg-bg-card transition-colors"
>
</button>
<button
type="submit"
disabled={isLoading}
className="px-6 py-2.5 text-sm font-semibold rounded bg-primary-cyan text-bg-0 hover:opacity-90 transition-opacity disabled:opacity-50"
className="px-6 py-2.5 text-sm font-semibold rounded bg-color-accent text-bg-0 hover:opacity-90 transition-opacity disabled:opacity-50"
>
{isLoading ? '저장 중...' : isEditMode ? '수정하기' : '등록하기'}
</button>

파일 보기

@ -15,10 +15,10 @@ const RISK_LABEL: Record<string, string> = {
}
const RISK_STYLE: Record<string, { bg: string; color: string }> = {
CRITICAL: { bg: 'rgba(239,68,68,0.2)', color: 'var(--red)' },
HIGH: { bg: 'rgba(239,68,68,0.15)', color: 'var(--red)' },
MEDIUM: { bg: 'rgba(249,115,22,0.15)', color: 'var(--orange)' },
LOW: { bg: 'rgba(34,197,94,0.15)', color: 'var(--green)' },
CRITICAL: { bg: 'rgba(239,68,68,0.2)', color: 'var(--color-danger)' },
HIGH: { bg: 'rgba(239,68,68,0.15)', color: 'var(--color-danger)' },
MEDIUM: { bg: 'rgba(249,115,22,0.15)', color: 'var(--color-warning)' },
LOW: { bg: 'rgba(34,197,94,0.15)', color: 'var(--color-success)' },
}
function formatDate(dtm: string | null, mode: 'full' | 'date') {
@ -98,15 +98,15 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
}, [loadData])
return (
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* Header */}
<div className="px-5 py-4 border-b border-border flex justify-between items-center bg-bg-1">
<div className="px-5 py-4 border-b border-stroke flex justify-between items-center bg-bg-surface">
<div className="flex items-center gap-3">
<div className="text-base font-bold flex items-center gap-2">
<span className="text-[18px]">📋</span>
HNS
</div>
<span className="text-[10px] text-text-3 bg-bg-3 font-mono px-[10px] py-1 rounded-xl">
<span className="text-[10px] text-fg-disabled bg-bg-card font-mono px-[10px] py-1 rounded-xl">
{analyses.length}
</span>
</div>
@ -114,11 +114,11 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
<input
type="text"
placeholder="검색..."
className="bg-bg-3 border border-border rounded-sm text-[11px] px-3 py-2 w-[200px]"
className="bg-bg-card border border-stroke rounded-sm text-[11px] px-3 py-2 w-[200px]"
/>
<button
onClick={() => onTabChange('analysis')}
className="text-status-orange text-[11px] font-semibold cursor-pointer flex items-center gap-1 px-4 py-2 rounded-sm"
className="text-color-warning text-[11px] font-semibold cursor-pointer flex items-center gap-1 px-4 py-2 rounded-sm"
style={{
border: '1px solid rgba(249,115,22,0.3)',
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(239,68,68,0.1))',
@ -132,41 +132,41 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
{/* Table */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="text-center text-text-3 text-[12px] py-20"> ...</div>
<div className="text-center text-fg-disabled text-[12px] py-20"> ...</div>
) : (
<table className="w-full text-[11px] border-collapse">
<thead className="sticky top-0 z-10">
<tr className="bg-bg-2 border-b border-border">
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[50px]"></th>
<th className="text-left text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[180px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[130px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
<tr className="bg-bg-elevated border-b border-stroke">
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[50px]"></th>
<th className="text-left text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[180px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[100px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[130px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[100px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[120px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[90px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[80px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]">
<div className="flex flex-col items-center gap-0.5">
<span>AEGL-3</span>
<span className="text-[8px] text-text-3"></span>
<span className="text-[8px] text-fg-disabled"></span>
</div>
</th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]">
<div className="flex flex-col items-center gap-0.5">
<span>AEGL-2</span>
<span className="text-[8px] text-text-3"></span>
<span className="text-[8px] text-fg-disabled"></span>
</div>
</th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]">
<div className="flex flex-col items-center gap-0.5">
<span>AEGL-1</span>
<span className="text-[8px] text-text-3"></span>
<span className="text-[8px] text-fg-disabled"></span>
</div>
</th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]"></th>
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[80px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[90px]"></th>
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[120px]"></th>
</tr>
</thead>
<tbody>
@ -174,7 +174,7 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
const rslt = item.rsltData as Record<string, unknown> | null
const isLocal = !!(item as HnsAnalysisItem & { _isLocal?: boolean })._isLocal
const riskLabel = RISK_LABEL[item.riskCd || ''] || item.riskCd || '—'
const riskStyle = RISK_STYLE[item.riskCd || ''] || { bg: 'rgba(100,100,100,0.1)', color: 'var(--t3)' }
const riskStyle = RISK_STYLE[item.riskCd || ''] || { bg: 'rgba(100,100,100,0.1)', color: 'var(--fg-disabled)' }
const aegl3 = rslt?.aegl3 as boolean | undefined
const aegl2 = rslt?.aegl2 as boolean | undefined
const aegl1 = rslt?.aegl1 as boolean | undefined
@ -184,44 +184,44 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
return (
<tr
key={item.hnsAnlysSn}
className="border-b border-border cursor-pointer"
className="border-b border-stroke cursor-pointer"
onClick={() => onSelectAnalysis?.(item.hnsAnlysSn, isLocal && rslt ? rslt : undefined)}
style={{
transition: 'background 0.15s',
background: index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg2)'}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-elevated)'}
onMouseLeave={(e) => e.currentTarget.style.background = index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'}
>
<td className="text-center text-text-3 font-mono px-4 py-3">{item.hnsAnlysSn}</td>
<td className="text-center text-fg-disabled font-mono px-4 py-3">{item.hnsAnlysSn}</td>
<td className="font-medium px-4 py-3">{item.anlysNm}</td>
<td className="text-center px-4 py-3">
<span
className="text-[9px] font-semibold text-status-orange px-2 py-1 rounded"
className="text-[9px] font-semibold text-color-warning px-2 py-1 rounded"
style={{ background: 'rgba(249,115,22,0.12)' }}
>
{substanceTag(item.sbstNm)}
</span>
</td>
<td className="text-center text-text-2 font-mono text-[10px] px-4 py-3">{formatDate(item.acdntDtm, 'full')}</td>
<td className="text-center text-text-3 font-mono text-[10px] px-4 py-3">{formatDate(item.regDtm, 'date')}</td>
<td className="text-center text-text-2 px-4 py-3">{item.locNm || '—'}</td>
<td className="text-center text-text-2 font-mono px-4 py-3">{amount}</td>
<td className="text-center text-fg-sub font-mono text-[10px] px-4 py-3">{formatDate(item.acdntDtm, 'full')}</td>
<td className="text-center text-fg-disabled font-mono text-[10px] px-4 py-3">{formatDate(item.regDtm, 'date')}</td>
<td className="text-center text-fg-sub px-4 py-3">{item.locNm || '—'}</td>
<td className="text-center text-fg-sub font-mono px-4 py-3">{amount}</td>
<td className="text-center px-4 py-3">
<span
className="text-[9px] font-semibold text-primary-cyan px-2 py-1 rounded"
className="text-[9px] font-semibold text-color-accent px-2 py-1 rounded"
style={{ background: 'rgba(6,182,212,0.12)' }}
>
{item.algoCd || '—'}
</span>
</td>
<td className="text-center text-text-2 font-mono px-4 py-3">{item.fcstHr ? `${item.fcstHr}H` : '—'}</td>
<td className="text-center text-fg-sub font-mono px-4 py-3">{item.fcstHr ? `${item.fcstHr}H` : '—'}</td>
<td className="text-center px-4 py-3">
<div
className="w-6 h-6 rounded-full mx-auto"
style={{
background: aegl3 ? 'rgba(239,68,68,0.8)' : 'rgba(255,255,255,0.1)',
border: aegl3 ? 'none' : '1px solid var(--bd)'
border: aegl3 ? 'none' : '1px solid var(--stroke-default)'
}}
/>
</td>
@ -230,7 +230,7 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
className="w-6 h-6 rounded-full mx-auto"
style={{
background: aegl2 ? 'rgba(249,115,22,0.8)' : 'rgba(255,255,255,0.1)',
border: aegl2 ? 'none' : '1px solid var(--bd)'
border: aegl2 ? 'none' : '1px solid var(--stroke-default)'
}}
/>
</td>
@ -239,7 +239,7 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
className="w-6 h-6 rounded-full mx-auto"
style={{
background: aegl1 ? 'rgba(234,179,8,0.8)' : 'rgba(255,255,255,0.1)',
border: aegl1 ? 'none' : '1px solid var(--bd)'
border: aegl1 ? 'none' : '1px solid var(--stroke-default)'
}}
/>
</td>
@ -251,8 +251,8 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
{riskLabel}
</span>
</td>
<td className="text-center text-text-2 font-mono px-4 py-3">{damageRadius}</td>
<td className="text-center text-text-3 text-[10px] px-4 py-3">{item.analystNm || '—'}</td>
<td className="text-center text-fg-sub font-mono px-4 py-3">{damageRadius}</td>
<td className="text-center text-fg-disabled text-[10px] px-4 py-3">{item.analystNm || '—'}</td>
</tr>
)
})}
@ -261,7 +261,7 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
)}
{!loading && analyses.length === 0 && (
<div className="text-center text-text-3 text-[12px] py-20"> .</div>
<div className="text-center text-fg-disabled text-[12px] py-20"> .</div>
)}
</div>
</div>

파일 보기

@ -191,9 +191,9 @@ export function HNSLeftPanel({
};
return (
<div className="w-80 min-w-[320px] flex flex-col h-full bg-bg-1 border-r border-border overflow-hidden">
<div className="w-80 min-w-[320px] flex flex-col h-full bg-bg-surface border-r border-stroke overflow-hidden">
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent bg-bg-0">
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent bg-bg-base">
{activeSubTab === 'analysis' && (
<div className="p-4">
{/* Header */}
@ -207,10 +207,10 @@ export function HNSLeftPanel({
}}
>🧪</div>
<div>
<div className="text-[13px] font-bold text-text-2 font-korean">
<div className="text-[13px] font-bold text-fg-sub font-korean">
HNS
</div>
<div className="text-[10px] text-text-3">
<div className="text-[10px] text-fg-disabled">
ALOHA/CAMEO
</div>
</div>
@ -222,7 +222,7 @@ export function HNSLeftPanel({
{/* 사고 기본정보 */}
<div>
<div className="text-[13px] font-bold text-text-2 font-korean mb-3 flex items-center gap-1.5">
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
📋
</div>
<div className="flex flex-col gap-[6px]">
@ -249,7 +249,7 @@ export function HNSLeftPanel({
{/* 사고 발생 일시 */}
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> </label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> </label>
<div className="grid grid-cols-2 gap-1">
<input
className="prd-i"
@ -296,8 +296,8 @@ export function HNSLeftPanel({
</div>
{/* DMS 표시 */}
<div className="text-[9px] text-text-3 font-mono border border-border bg-bg-0"
style={{ padding: '4px 8px', borderRadius: 'var(--rS)' }}>
<div className="text-[9px] text-fg-disabled font-mono border border-stroke bg-bg-base"
style={{ padding: '4px 8px', borderRadius: 'var(--radius-sm)' }}>
{incidentCoord ? `${toDMS(incidentCoord.lat, 'lat')} / ${toDMS(incidentCoord.lon, 'lon')}` : '지도에서 위치를 선택하세요'}
</div>
@ -369,7 +369,7 @@ export function HNSLeftPanel({
{/* 알고리즘 선택 */}
<div className="grid grid-cols-2 gap-1">
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> </label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> </label>
<ComboBox
className="prd-i"
value={algorithm}
@ -383,7 +383,7 @@ export function HNSLeftPanel({
/>
</div>
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> </label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> </label>
<ComboBox
className="prd-i"
value={criteriaModel}
@ -402,7 +402,7 @@ export function HNSLeftPanel({
{/* 모델 파라미터 & 물질 정보 */}
<div>
<div className="text-[13px] font-bold text-text-2 font-korean mb-3 flex items-center gap-1.5">
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
🧪 {releaseType === '연속 유출' ? 'Plume' : releaseType === '순간 유출' ? 'Puff' : 'Dense Gas'}
</div>
<div className="flex flex-col gap-[6px]">
@ -417,7 +417,7 @@ export function HNSLeftPanel({
<div className="flex flex-col gap-[6px]">
<div className="grid grid-cols-2 gap-1">
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (g/s)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (g/s)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -428,7 +428,7 @@ export function HNSLeftPanel({
/>
</div>
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (s)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (s)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -440,7 +440,7 @@ export function HNSLeftPanel({
</div>
</div>
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (m)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (m)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -457,7 +457,7 @@ export function HNSLeftPanel({
{releaseType === '순간 유출' && (
<div className="flex flex-col gap-[6px]">
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (g)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (g)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -468,7 +468,7 @@ export function HNSLeftPanel({
/>
</div>
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (m)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (m)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -486,7 +486,7 @@ export function HNSLeftPanel({
<div className="flex flex-col gap-[6px]">
<div className="grid grid-cols-2 gap-1">
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (g/s)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (g/s)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -497,7 +497,7 @@ export function HNSLeftPanel({
/>
</div>
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (m)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (m)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -509,7 +509,7 @@ export function HNSLeftPanel({
</div>
</div>
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> (m)</label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> (m)</label>
<input
className="prd-i w-full font-mono"
type="number"
@ -523,7 +523,7 @@ export function HNSLeftPanel({
)}
{/* 모델 설명 */}
<div className="text-[9px] text-text-3 mt-1 leading-[1.4]">
<div className="text-[9px] text-fg-disabled mt-1 leading-[1.4]">
{releaseType === '연속 유출' && '정상상태 연속 배출. 바람 방향으로 플룸이 형성됩니다.'}
{releaseType === '순간 유출' && '한 번에 전량 방출. 시간에 따라 구름이 이동하며 확산됩니다.'}
{releaseType === '풀(Pool) 증발' && '고밀도 가스가 지표면을 따라 확산됩니다 (Britter-McQuaid 모델).'}
@ -535,25 +535,25 @@ export function HNSLeftPanel({
className="p-2 rounded-sm mt-0.5"
style={{ background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)' }}
>
<div className="text-[10px] font-bold text-status-orange mb-1">
<div className="text-[10px] font-bold text-color-warning mb-1">
</div>
<div className="grid grid-cols-2 gap-[3px] text-[9px]">
<div className="flex justify-between">
<span className="text-text-3"></span>
<span className="text-fg-disabled"></span>
<span className="font-mono">{tox.mw} g/mol</span>
</div>
<div className="flex justify-between">
<span className="text-text-3"></span>
<span className="text-fg-disabled"></span>
<span className="font-mono">{tox.densityGas} kg/m³</span>
</div>
<div className="flex justify-between">
<span className="text-text-3"></span>
<span className="text-fg-disabled"></span>
<span className="font-mono">{tox.vaporPressure} mmHg</span>
</div>
<div className="flex justify-between">
<span className="text-text-3">IDLH</span>
<span className="text-status-red font-semibold font-mono">{tox.idlh} ppm</span>
<span className="text-fg-disabled">IDLH</span>
<span className="text-color-danger font-semibold font-mono">{tox.idlh} ppm</span>
</div>
</div>
</div>
@ -563,21 +563,21 @@ export function HNSLeftPanel({
className="p-2 rounded-sm"
style={{ background: 'rgba(168,85,247,0.05)', border: '1px solid rgba(168,85,247,0.12)' }}
>
<div className="text-[10px] font-bold text-primary-purple mb-1">
<div className="text-[10px] font-bold text-color-tertiary mb-1">
📊 (AEGL)
</div>
<div className="flex flex-col gap-0.5 text-[9px]">
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(239,68,68,0.7)' }}></div>
<span className="text-text-3">AEGL-3 () {tox.aegl3} ppm</span>
<span className="text-fg-disabled">AEGL-3 () {tox.aegl3} ppm</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(249,115,22,0.7)' }}></div>
<span className="text-text-3">AEGL-2 () {tox.aegl2} ppm</span>
<span className="text-fg-disabled">AEGL-2 () {tox.aegl2} ppm</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(234,179,8,0.7)' }}></div>
<span className="text-text-3">AEGL-1 () {tox.aegl1} ppm</span>
<span className="text-fg-disabled">AEGL-1 () {tox.aegl1} ppm</span>
</div>
</div>
</div>
@ -618,24 +618,24 @@ export function HNSLeftPanel({
}}
>📋</div>
<div>
<div className="text-[13px] font-bold text-text-2 font-korean">
<div className="text-[13px] font-bold text-fg-sub font-korean">
</div>
<div className="text-[10px] text-text-3">
<div className="text-[10px] text-fg-disabled">
</div>
</div>
</div>
{/* 필터 섹션 */}
<div className="bg-bg-3 border border-border rounded-md p-[14px] mb-3">
<div className="text-[13px] font-bold text-text-2 font-korean mb-3 flex items-center gap-1.5">
<div className="bg-bg-card border border-stroke rounded-md p-[14px] mb-3">
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
🔍
</div>
<div className="flex flex-col gap-2">
{/* 기간 선택 */}
<div>
<label className="text-[10px] text-text-3 block mb-0.5"></label>
<label className="text-[10px] text-fg-disabled block mb-0.5"></label>
<ComboBox
value="최근 7일"
onChange={() => {}}
@ -650,7 +650,7 @@ export function HNSLeftPanel({
{/* 물질 분류 */}
<div>
<label className="text-[10px] text-text-3 block mb-0.5"> </label>
<label className="text-[10px] text-fg-disabled block mb-0.5"> </label>
<ComboBox
value="전체"
onChange={() => {}}
@ -666,7 +666,7 @@ export function HNSLeftPanel({
{/* 위험도 */}
<div>
<label className="text-[10px] text-text-3 block mb-0.5"></label>
<label className="text-[10px] text-fg-disabled block mb-0.5"></label>
<ComboBox
value="전체"
onChange={() => {}}
@ -682,22 +682,22 @@ export function HNSLeftPanel({
</div>
{/* 통계 요약 */}
<div className="bg-bg-3 border border-border rounded-md p-[14px]">
<div className="text-[13px] font-bold text-text-2 font-korean mb-3 flex items-center gap-1.5">
<div className="bg-bg-card border border-stroke rounded-md p-[14px]">
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
📊
</div>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
<span className="text-[10px] text-text-3"> </span>
<span className="text-sm font-bold text-primary-cyan font-mono">8</span>
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
<span className="text-[10px] text-fg-disabled"> </span>
<span className="text-sm font-bold text-color-accent font-mono">8</span>
</div>
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
<span className="text-[10px] text-text-3"> (AEGL-3)</span>
<span className="text-sm font-bold text-status-red font-mono">3</span>
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
<span className="text-[10px] text-fg-disabled"> (AEGL-3)</span>
<span className="text-sm font-bold text-color-danger font-mono">3</span>
</div>
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
<span className="text-[10px] text-text-3"> (AEGL-2)</span>
<span className="text-sm font-bold text-status-orange font-mono">5</span>
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
<span className="text-[10px] text-fg-disabled"> (AEGL-2)</span>
<span className="text-sm font-bold text-color-warning font-mono">5</span>
</div>
</div>
</div>

파일 보기

@ -76,11 +76,11 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)' }}
>
<div
className="w-[380px] bg-bg-1 border border-border rounded-[14px] overflow-hidden flex flex-col"
className="w-[380px] bg-bg-surface border border-stroke rounded-[14px] overflow-hidden flex flex-col"
style={{ boxShadow: '0 20px 60px rgba(0,0,0,0.5)' }}
>
{/* Header */}
<div className="px-5 py-4 border-b border-border flex items-center gap-3">
<div className="px-5 py-4 border-b border-stroke flex items-center gap-3">
<div
className="w-9 h-9 rounded-[10px] border border-[rgba(249,115,22,0.3)] flex items-center justify-center text-base shrink-0"
style={{ background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))' }}
@ -89,13 +89,13 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
</div>
<div className="flex-1">
<h2 className="text-[15px] font-bold m-0"> </h2>
<div className="text-[10px] text-text-3 mt-0.5">
<div className="text-[10px] text-fg-disabled mt-0.5">
</div>
</div>
<button
onClick={onClose}
className="w-7 h-7 rounded-md border border-border bg-bg-3 text-text-3 text-xs cursor-pointer flex items-center justify-center shrink-0"
className="w-7 h-7 rounded-md border border-stroke bg-bg-card text-fg-disabled text-xs cursor-pointer flex items-center justify-center shrink-0"
>
</button>
@ -104,7 +104,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
{/* Content */}
<div
className="px-5 py-4 flex flex-col gap-3"
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}
>
{/* HNS 물질 */}
<FG label="HNS 물질">
@ -180,10 +180,10 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
</div>
{/* Footer */}
<div className="px-5 py-[14px] border-t border-border flex gap-2">
<div className="px-5 py-[14px] border-t border-stroke flex gap-2">
<button
onClick={onClose}
className="flex-1 py-2.5 text-xs font-semibold rounded-md cursor-pointer bg-bg-3 border border-border text-text-2"
className="flex-1 py-2.5 text-xs font-semibold rounded-md cursor-pointer bg-bg-card border border-stroke text-fg-sub"
>
</button>
@ -192,7 +192,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
className="flex-[2] py-2.5 text-xs font-bold rounded-md text-white"
style={{
cursor: 'pointer',
background: 'linear-gradient(135deg, var(--orange), #ef4444)',
background: 'linear-gradient(135deg, var(--color-warning), #ef4444)',
border: 'none',
}}
>
@ -207,7 +207,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
function FG({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--t2)', marginBottom: '6px' }}>
<div style={{ fontSize: '10px', fontWeight: 700, color: 'var(--fg-sub)', marginBottom: '6px' }}>
{label}
</div>
{children}

파일 보기

@ -35,8 +35,8 @@ export function HNSRightPanel({
}: HNSRightPanelProps) {
if (!dispersionResult) {
return (
<div className="w-[300px] bg-bg-1 border-l border-border p-4 overflow-auto">
<div className="flex flex-col gap-3 items-center justify-center h-full text-text-3 text-xs">
<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-xs">
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
<div> </div>
</div>
@ -54,7 +54,7 @@ export function HNSRightPanel({
: 'ALOHA';
return (
<div className="w-[300px] bg-bg-1 border-l border-border p-4 overflow-auto flex flex-col gap-4">
<div className="w-[300px] 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">
@ -62,14 +62,14 @@ export function HNSRightPanel({
width: '6px',
height: '6px',
borderRadius: '50%',
background: 'var(--orange)',
background: 'var(--color-warning)',
animation: 'pulse 1.5s infinite'
}}></div>
<h3 className="text-[13px] font-bold m-0">
</h3>
</div>
<div className="text-[10px] text-text-3 font-mono">
<div className="text-[10px] text-fg-disabled font-mono">
{dispersionResult.substance} · {modelLabel}
</div>
</div>
@ -77,28 +77,28 @@ export function HNSRightPanel({
{/* KPI Cards */}
<div className="flex flex-col gap-2">
{/* 최대 농도 */}
<div className="p-3 bg-bg-3 border border-[rgba(239,68,68,0.2)] rounded-[var(--rS)]">
<div className="text-[10px] text-text-3 mb-1.5">
<div className="p-3 bg-bg-card border border-[rgba(239,68,68,0.2)] rounded-[var(--radius-sm)]">
<div className="text-[10px] text-fg-disabled mb-1.5">
</div>
<div className="text-[20px] font-bold font-mono text-status-red">
<div className="text-[20px] font-bold font-mono text-color-danger">
{maxConc > 0 ? maxConc.toFixed(1) : '—'} <span className="text-[10px] font-medium">ppm</span>
</div>
</div>
{/* 확산 면적 */}
<div className="p-3 bg-bg-3 border border-[rgba(6,182,212,0.2)] rounded-[var(--rS)]">
<div className="text-[10px] text-text-3 mb-1.5">
<div className="p-3 bg-bg-card border border-[rgba(6,182,212,0.2)] rounded-[var(--radius-sm)]">
<div className="text-[10px] text-fg-disabled mb-1.5">
AEGL-1
</div>
<div className="text-[20px] font-bold font-mono text-primary-cyan">
<div className="text-[20px] font-bold font-mono text-color-accent">
{area > 0 ? area.toFixed(2) : '—'} <span className="text-[10px] font-medium">km²</span>
</div>
</div>
{/* 풍속 */}
<div className="p-3 bg-bg-3 border border-border rounded-[var(--rS)]">
<div className="text-[10px] text-text-3 mb-1.5">
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
<div className="text-[10px] text-fg-disabled mb-1.5">
</div>
<div className="text-[20px] font-bold font-mono">
@ -107,8 +107,8 @@ export function HNSRightPanel({
</div>
{/* 풍향 */}
<div className="p-3 bg-bg-3 border border-border rounded-[var(--rS)]">
<div className="text-[10px] text-text-3 mb-1.5">
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
<div className="text-[10px] text-fg-disabled mb-1.5">
</div>
<div className="text-[20px] font-bold font-mono">
@ -119,22 +119,22 @@ export function HNSRightPanel({
{/* AEGL Zone Details */}
<div>
<h4 className="text-[11px] font-semibold text-text-2 mt-0 mb-2.5">
<h4 className="text-[11px] font-semibold text-fg-sub mt-0 mb-2.5">
AEGL
</h4>
<div className="flex flex-col gap-2">
{/* AEGL-3 */}
<div
className="py-2.5 px-3 bg-bg-2 rounded-[var(--rS)]"
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
style={{ borderLeft: '3px solid rgba(239,68,68,1)' }}
>
<div className="flex justify-between items-center mb-1">
<span className="text-[11px] font-semibold">AEGL-3 ()</span>
<span className="text-[10px] font-mono text-text-3">
<span className="text-[10px] font-mono text-fg-disabled">
{computedResult?.aeglDistances.aegl3 || 0}m
</span>
</div>
<div className="flex justify-between text-[10px] text-text-3">
<div className="flex justify-between text-[10px] text-fg-disabled">
<span>{dispersionResult.concentration['AEGL-3']}</span>
<span className="font-mono">{computedResult?.aeglAreas.aegl3 ?? 0} km²</span>
</div>
@ -142,16 +142,16 @@ export function HNSRightPanel({
{/* AEGL-2 */}
<div
className="py-2.5 px-3 bg-bg-2 rounded-[var(--rS)]"
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
style={{ borderLeft: '3px solid rgba(249,115,22,1)' }}
>
<div className="flex justify-between items-center mb-1">
<span className="text-[11px] font-semibold">AEGL-2 ()</span>
<span className="text-[10px] font-mono text-text-3">
<span className="text-[10px] font-mono text-fg-disabled">
{computedResult?.aeglDistances.aegl2 || 0}m
</span>
</div>
<div className="flex justify-between text-[10px] text-text-3">
<div className="flex justify-between text-[10px] text-fg-disabled">
<span>{dispersionResult.concentration['AEGL-2']}</span>
<span className="font-mono">{computedResult?.aeglAreas.aegl2 ?? 0} km²</span>
</div>
@ -159,16 +159,16 @@ export function HNSRightPanel({
{/* AEGL-1 */}
<div
className="py-2.5 px-3 bg-bg-2 rounded-[var(--rS)]"
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
style={{ borderLeft: '3px solid rgba(234,179,8,1)' }}
>
<div className="flex justify-between items-center mb-1">
<span className="text-[11px] font-semibold">AEGL-1 ()</span>
<span className="text-[10px] font-mono text-text-3">
<span className="text-[10px] font-mono text-fg-disabled">
{computedResult?.aeglDistances.aegl1 || 0}m
</span>
</div>
<div className="flex justify-between text-[10px] text-text-3">
<div className="flex justify-between text-[10px] text-fg-disabled">
<span>{dispersionResult.concentration['AEGL-1']}</span>
<span className="font-mono">{computedResult?.aeglAreas.aegl1 ?? 0} km²</span>
</div>
@ -178,11 +178,11 @@ export function HNSRightPanel({
{/* 시간 정보 (puff/dense_gas) */}
{computedResult && computedResult.modelType !== 'plume' && (
<div className="p-2.5 bg-bg-3 border border-border rounded-[var(--rS)]">
<div className="text-[10px] text-text-3 mb-1"> </div>
<div className="text-[14px] font-bold font-mono text-primary-cyan">
<div className="p-2.5 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
<div className="text-[10px] text-fg-disabled mb-1"> </div>
<div className="text-[14px] font-bold font-mono text-color-accent">
t = {computedResult.timeStep}s
<span className="text-[10px] font-normal text-text-3 ml-1.5">
<span className="text-[10px] font-normal text-fg-disabled ml-1.5">
({(computedResult.timeStep / 60).toFixed(1)})
</span>
</div>
@ -190,16 +190,16 @@ export function HNSRightPanel({
)}
{/* Timestamp */}
<div className="mt-auto pt-3 border-t border-border text-[10px] text-text-3 font-mono">
<div className="mt-auto pt-3 border-t border-stroke text-[10px] text-fg-disabled font-mono">
: {new Date(dispersionResult.timestamp).toLocaleString('ko-KR')}
</div>
{/* Bottom Action Buttons */}
<div className="flex gap-1.5 pt-3 border-t border-border">
<div className="flex gap-1.5 pt-3 border-t border-stroke">
<button onClick={onSave} className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean">
💾
</button>
<button onClick={onOpenRecalc} className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(249,115,22,0.1)] border border-[rgba(249,115,22,0.3)] text-status-orange font-korean">
<button onClick={onOpenRecalc} className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(249,115,22,0.1)] border border-[rgba(249,115,22,0.3)] text-color-warning font-korean">
🔄
</button>
<button onClick={onOpenReport} className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-primary-cyan to-primary-blue text-white font-korean">

파일 보기

@ -130,16 +130,16 @@ export function HNSScenarioView() {
}
return (
<div className="flex flex-col flex-1 w-full h-full overflow-hidden bg-bg-0">
<div className="flex flex-col flex-1 w-full h-full overflow-hidden bg-bg-base">
{/* Header */}
<div className="flex items-center justify-between shrink-0 border-b border-border px-5 py-[14px] bg-bg-1">
<div className="flex items-center justify-between shrink-0 border-b border-stroke px-5 py-[14px] bg-bg-surface">
<div className="flex items-center gap-2.5">
<span className="text-base">📊</span>
<div>
<div className="text-sm font-bold">
HNS
</div>
<div className="text-[10px] text-text-3">
<div className="text-[10px] text-fg-disabled">
· ·
</div>
</div>
@ -161,7 +161,7 @@ export function HNSScenarioView() {
</select>
<button
onClick={() => setModalOpen(true)}
className="cursor-pointer whitespace-nowrap font-bold text-status-orange text-[11px] px-[14px] py-1.5 rounded-sm"
className="cursor-pointer whitespace-nowrap font-bold text-color-warning text-[11px] px-[14px] py-1.5 rounded-sm"
style={{
background: 'rgba(249,115,22,0.12)',
border: '1px solid rgba(249,115,22,0.3)',
@ -175,16 +175,16 @@ export function HNSScenarioView() {
{/* Body: Left list + Right detail */}
<div className="flex flex-1 overflow-hidden">
{/* ── Left: Scenario List ── */}
<div className="flex flex-col overflow-hidden shrink-0 border-r border-border bg-bg-1" style={{ width: '370px', minWidth: '370px' }}>
<div className="flex items-center justify-between border-b border-border px-[14px] py-2.5">
<span className="text-[11px] font-bold text-text-3">
<div className="flex flex-col overflow-hidden shrink-0 border-r border-stroke bg-bg-surface" style={{ width: '370px', minWidth: '370px' }}>
<div className="flex items-center justify-between border-b border-stroke px-[14px] py-2.5">
<span className="text-[11px] font-bold text-fg-disabled">
</span>
<div className="flex gap-1">
{['시간순', '위험도순'].map((label, i) => (
<button key={i} className="cursor-pointer px-2 py-[3px] text-[9px] font-semibold rounded-sm border border-border" style={{
background: i === 0 ? 'rgba(249,115,22,0.08)' : 'var(--bg3)',
color: i === 0 ? 'var(--orange)' : 'var(--t3)',
<button key={i} className="cursor-pointer px-2 py-[3px] text-[9px] font-semibold rounded-sm border border-stroke" style={{
background: i === 0 ? 'rgba(249,115,22,0.08)' : 'var(--bg-card)',
color: i === 0 ? 'var(--color-warning)' : 'var(--fg-disabled)',
}}>
{label}
</button>
@ -193,7 +193,7 @@ export function HNSScenarioView() {
</div>
{/* Scrollable list */}
<div className="flex-1 overflow-y-auto flex flex-col gap-1.5 p-2" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto flex flex-col gap-1.5 p-2" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
{scenarios.map((scn, idx) => {
const sev = SEVERITY_STYLE[scn.severity]
const isSel = selectedIdx === idx
@ -211,7 +211,7 @@ export function HNSScenarioView() {
checked={checked.has(idx)}
onChange={() => toggleCheck(idx)}
onClick={(e) => e.stopPropagation()}
style={{ accentColor: 'var(--orange)' }}
style={{ accentColor: 'var(--color-warning)' }}
/>
<span className="text-[12px] font-bold">
{scn.id} {scn.name}
@ -224,11 +224,11 @@ export function HNSScenarioView() {
{/* Time row */}
<div className="flex items-center gap-1.5 mb-1.5">
<span className="font-bold font-mono text-status-orange text-[9px] px-1.5 py-[2px] rounded-[3px]" style={{ background: 'rgba(249,115,22,0.1)' }}>
<span className="font-bold font-mono text-color-warning text-[9px] px-1.5 py-[2px] rounded-[3px]" style={{ background: 'rgba(249,115,22,0.1)' }}>
{scn.timeStep}
</span>
<span className="text-[9px] text-text-3 font-mono">{scn.datetime}</span>
<span className="ml-auto text-text-3 text-[8px]">{scn.wind}</span>
<span className="text-[9px] text-fg-disabled font-mono">{scn.datetime}</span>
<span className="ml-auto text-fg-disabled text-[8px]">{scn.wind}</span>
</div>
{/* Metrics grid */}
@ -239,15 +239,15 @@ export function HNSScenarioView() {
{ label: 'ERPG-2', value: scn.erpg2, color: '#f97316' },
{ label: '영향인구', value: scn.population, color: '#f87171' },
].map((m, i) => (
<div key={i} className="text-center p-[3px] bg-bg-0 rounded-[3px]">
<div className="text-text-3 text-[7px]">{m.label}</div>
<div key={i} className="text-center p-[3px] bg-bg-base rounded-[3px]">
<div className="text-fg-disabled text-[7px]">{m.label}</div>
<div className="font-bold" style={{ color: m.color }}>{m.value}</div>
</div>
))}
</div>
{/* Description */}
<div className="text-text-2 mt-1.5 text-[8px] leading-[1.4]">
<div className="text-fg-sub mt-1.5 text-[8px] leading-[1.4]">
{scn.description}
</div>
</div>
@ -256,10 +256,10 @@ export function HNSScenarioView() {
</div>
{/* Bottom buttons */}
<div className="flex gap-2 border-t border-border px-[14px] py-2.5">
<div className="flex gap-2 border-t border-stroke px-[14px] py-2.5">
<button
onClick={() => setActiveView(1)}
className="flex-1 cursor-pointer font-bold text-status-orange text-[11px] p-2 rounded-sm"
className="flex-1 cursor-pointer font-bold text-color-warning text-[11px] p-2 rounded-sm"
style={{
background: 'rgba(249,115,22,0.1)',
border: '1px solid rgba(249,115,22,0.3)',
@ -267,7 +267,7 @@ export function HNSScenarioView() {
>
📊
</button>
<button className="cursor-pointer font-semibold text-text-2 text-[11px] px-[14px] py-2 rounded-sm bg-bg-3 border border-border">
<button className="cursor-pointer font-semibold text-fg-sub text-[11px] px-[14px] py-2 rounded-sm bg-bg-card border border-stroke">
📄
</button>
</div>
@ -276,7 +276,7 @@ export function HNSScenarioView() {
{/* ── Right: Detail Views ── */}
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
{/* View Tabs */}
<div className="flex border-b border-border shrink-0 px-4 bg-bg-1">
<div className="flex border-b border-stroke shrink-0 px-4 bg-bg-surface">
{['📋 시나리오 상세', '📊 비교 차트', '🗺 확산범위 오버레이'].map((label, i) => (
<button
key={i}
@ -338,7 +338,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
}}>
{scenario.severity}
</span>
<span className="ml-auto text-[10px] text-text-3 font-mono">
<span className="ml-auto text-[10px] text-fg-disabled font-mono">
{scenario.datetime}
</span>
</div>
@ -347,12 +347,12 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
{ label: '최대농도', value: d.maxConc, color: '#f87171' },
{ label: 'IDLH 반경', value: d.idlhRadius, color: '#f87171' },
{ label: 'ERPG-2', value: d.erpg2, color: '#f97316' },
{ label: '풍향/풍속', value: `${d.windDir}\n${d.windSpeed}`, color: 'var(--cyan)' },
{ label: '풍향/풍속', value: `${d.windDir}\n${d.windSpeed}`, color: 'var(--color-accent)' },
{ label: '영향인구', value: d.population, color: '#f87171' },
{ label: '유출량', value: d.spillAmount, color: 'var(--orange)' },
{ label: '유출량', value: d.spillAmount, color: 'var(--color-warning)' },
].map((m, i) => (
<div key={i} className="text-center rounded-sm p-2" style={{ background: 'rgba(0,0,0,0.15)' }}>
<div className="text-text-3 text-[8px]">{m.label}</div>
<div className="text-fg-disabled text-[8px]">{m.label}</div>
<div className="text-base font-bold font-mono whitespace-pre-line mt-[2px]" style={{ color: m.color, lineHeight: 1.2 }}>{m.value}</div>
</div>
))}
@ -362,7 +362,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
{/* Two-column section */}
<div className="grid grid-cols-2 gap-3">
{/* Threat Zones */}
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
<h4 className="text-[12px] font-bold mb-2.5">
</h4>
@ -373,8 +373,8 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
{ label: 'ERPG-1 (주의권고)', value: scenario.zones.erpg1, color: '#fbbf24' },
{ label: 'TWA (작업허용)', value: scenario.zones.twa, color: '#22c55e' },
].map((z, i) => (
<div key={i} className="flex justify-between items-center bg-bg-0 rounded-sm" style={{ padding: '6px 8px', borderLeft: `3px solid ${z.color}` }}>
<span className="text-[10px] text-text-2">{z.label}</span>
<div key={i} className="flex justify-between items-center bg-bg-base rounded-sm" style={{ padding: '6px 8px', borderLeft: `3px solid ${z.color}` }}>
<span className="text-[10px] text-fg-sub">{z.label}</span>
<span className="text-[11px] font-bold font-mono" style={{ color: z.color }}>{z.value}</span>
</div>
))}
@ -382,14 +382,14 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
</div>
{/* Actions */}
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
<h4 className="text-[12px] font-bold mb-2.5">
🛡
</h4>
<div className="flex flex-col gap-1.5">
{scenario.actions.map((action, i) => (
<div key={i} className="flex items-start gap-1.5 text-[10px] text-text-2 bg-bg-0 rounded-sm leading-[1.4] py-[5px] px-2">
<span className="text-status-orange font-bold shrink-0"></span>
<div key={i} className="flex items-start gap-1.5 text-[10px] text-fg-sub bg-bg-base rounded-sm leading-[1.4] py-[5px] px-2">
<span className="text-color-warning font-bold shrink-0"></span>
{action}
</div>
))}
@ -398,7 +398,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
</div>
{/* Weather */}
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
<h4 className="text-[12px] font-bold mb-2.5">
🌊
</h4>
@ -411,10 +411,10 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
{ label: '습도', value: scenario.weather.humidity, icon: '💧' },
{ label: '혼합층', value: scenario.weather.mixHeight, icon: '📏' },
].map((w, i) => (
<div key={i} className="text-center p-2 rounded-sm bg-bg-0">
<div key={i} className="text-center p-2 rounded-sm bg-bg-base">
<div className="text-sm mb-0.5">{w.icon}</div>
<div className="text-[12px] font-bold font-mono">{w.value}</div>
<div className="text-text-3 mt-0.5 text-[8px]">{w.label}</div>
<div className="text-fg-disabled mt-0.5 text-[8px]">{w.label}</div>
</div>
))}
</div>
@ -456,14 +456,14 @@ function ScenarioComparison() {
const barX = [30, 70, 110, 150, 190]
return (
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 px-5 py-4" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 px-5 py-4" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
{/* Title */}
<div className="text-[13px] font-bold mb-0.5">
📊
</div>
{/* ── Chart 1: 최대 지표면 농도 추이 (Line + Area) ── */}
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
<div className="text-[11px] font-bold mb-2.5">
(ppm)
</div>
@ -479,9 +479,9 @@ function ScenarioComparison() {
<line x1="50" y1="120" x2="480" y2="120" stroke="#21262d" strokeWidth={0.5} />
{/* Threshold lines */}
<line x1="50" y1={10 + (1 - 500 / concMax) * 110} x2="480" y2={10 + (1 - 500 / concMax) * 110} stroke="rgba(239,68,68,.2)" strokeWidth={0.5} strokeDasharray="4,3" />
<text x="5" y={10 + (1 - 500 / concMax) * 110 + 4} fill="#f87171" fontSize="7" fontFamily="var(--fM)">500 IDLH</text>
<text x="5" y={10 + (1 - 500 / concMax) * 110 + 4} fill="#f87171" fontSize="7" fontFamily="var(--font-mono)">500 IDLH</text>
<line x1="50" y1={10 + (1 - 300 / concMax) * 110} x2="480" y2={10 + (1 - 300 / concMax) * 110} stroke="rgba(249,115,22,.2)" strokeWidth={0.5} strokeDasharray="4,3" />
<text x="5" y={10 + (1 - 300 / concMax) * 110 + 4} fill="#fb923c" fontSize="7" fontFamily="var(--fM)">300 ERPG2</text>
<text x="5" y={10 + (1 - 300 / concMax) * 110 + 4} fill="#fb923c" fontSize="7" fontFamily="var(--font-mono)">300 ERPG2</text>
{/* Area fill */}
<polygon points={concArea} fill="url(#hnsGrad)" />
{/* Line */}
@ -490,8 +490,8 @@ function ScenarioComparison() {
{D.map((d, i) => (
<g key={d.id}>
<circle cx={concX[i]} cy={concY[i]} r={4} fill={SEV_COLOR[d.severity]} stroke="#0d1117" strokeWidth={2} />
<text x={concX[i] - 6} y={concY[i] - 8} fill={SEV_COLOR[d.severity]} fontSize="7" textAnchor="end" fontFamily="var(--fM)">{d.conc}</text>
<text x={concX[i]} y={134} fill="#8b949e" fontSize="7" textAnchor="middle" fontFamily="var(--fM)">{d.label}</text>
<text x={concX[i] - 6} y={concY[i] - 8} fill={SEV_COLOR[d.severity]} fontSize="7" textAnchor="end" fontFamily="var(--font-mono)">{d.conc}</text>
<text x={concX[i]} y={134} fill="#8b949e" fontSize="7" textAnchor="middle" fontFamily="var(--font-mono)">{d.label}</text>
</g>
))}
</svg>
@ -500,7 +500,7 @@ function ScenarioComparison() {
{/* ── Charts 2 & 3: 2-column grid ── */}
<div className="grid grid-cols-2 gap-[14px]">
{/* Chart 2: 위험 반경 변화 (Multi-line) */}
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
<div className="text-[11px] font-bold mb-2.5">
(km)
</div>
@ -517,20 +517,20 @@ function ScenarioComparison() {
return (
<g key={d.id}>
<circle cx={radX[i]} cy={idlhY[i]} r={3} fill={c} stroke="#0d1117" strokeWidth={1.5} />
<text x={radX[i]} y={96} fill="#8b949e" fontSize="6" textAnchor="middle" fontFamily="var(--fM)">{d.label.replace('+', '+')}</text>
<text x={radX[i]} y={96} fill="#8b949e" fontSize="6" textAnchor="middle" fontFamily="var(--font-mono)">{d.label.replace('+', '+')}</text>
</g>
)
})}
{/* Legend */}
<line x1="170" y1="14" x2="185" y2="14" stroke="#ef4444" strokeWidth={1.5} />
<text x="188" y="17" fill="#ef4444" fontSize="6" fontFamily="var(--fK)">IDLH</text>
<text x="188" y="17" fill="#ef4444" fontSize="6" fontFamily="var(--font-korean)">IDLH</text>
<line x1="170" y1="24" x2="185" y2="24" stroke="#f97316" strokeWidth={1.5} strokeDasharray="4,2" />
<text x="188" y="27" fill="#f97316" fontSize="6" fontFamily="var(--fK)">ERPG-2</text>
<text x="188" y="27" fill="#f97316" fontSize="6" fontFamily="var(--font-korean)">ERPG-2</text>
</svg>
</div>
{/* Chart 3: 영향 인구 변화 (Bar) */}
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
<div className="text-[11px] font-bold mb-2.5">
()
</div>
@ -545,10 +545,10 @@ function ScenarioComparison() {
return (
<g key={d.id}>
<rect x={barX[i]} y={y} width={barW} height={h} rx={2} fill={barColor} />
<text x={barX[i] + barW / 2} y={y - 3} fill={color} fontSize="6" textAnchor="middle" fontFamily="var(--fM)">
<text x={barX[i] + barW / 2} y={y - 3} fill={color} fontSize="6" textAnchor="middle" fontFamily="var(--font-mono)">
{d.pop > 0 ? d.pop.toLocaleString() : '0'}
</text>
<text x={barX[i] + barW / 2} y={96} fill="#8b949e" fontSize="6" textAnchor="middle" fontFamily="var(--fM)">{d.label.replace('+', '+')}</text>
<text x={barX[i] + barW / 2} y={96} fill="#8b949e" fontSize="6" textAnchor="middle" fontFamily="var(--font-mono)">{d.label.replace('+', '+')}</text>
</g>
)
})}
@ -557,15 +557,15 @@ function ScenarioComparison() {
</div>
{/* ── Chart 4: 시나리오 비교표 ── */}
<div className="rounded-md border border-border overflow-x-auto bg-bg-3 p-[14px]">
<div className="rounded-md border border-stroke overflow-x-auto bg-bg-card p-[14px]">
<div className="text-[11px] font-bold mb-2.5">
📋
</div>
<table className="w-full text-[10px] border-collapse">
<thead>
<tr className="bg-bg-0">
<tr className="bg-bg-base">
{['지표', ...D.map(d => `${d.id} (${d.label})`)].map((h, i) => (
<th key={i} className="text-text-3 border-b border-border text-[9px] px-[10px] py-2" style={{ textAlign: i === 0 ? 'left' : 'center' }}>
<th key={i} className="text-fg-disabled border-b border-stroke text-[9px] px-[10px] py-2" style={{ textAlign: i === 0 ? 'left' : 'center' }}>
{h}
</th>
))}
@ -573,43 +573,43 @@ function ScenarioComparison() {
</thead>
<tbody>
{/* 최대농도 */}
<tr className="border-b border-border">
<td className="text-text-2 px-[10px] py-1.5"> (ppm)</td>
<tr className="border-b border-stroke">
<td className="text-fg-sub px-[10px] py-1.5"> (ppm)</td>
{D.map(d => (
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.conc}</td>
))}
</tr>
{/* IDLH 반경 */}
<tr className="border-b border-border">
<td className="text-text-2 px-[10px] py-1.5">IDLH (km)</td>
<tr className="border-b border-stroke">
<td className="text-fg-sub px-[10px] py-1.5">IDLH (km)</td>
{D.map(d => (
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: d.idlh > 0 ? '#f87171' : '#22c55e' }}>{d.idlh || 0}</td>
))}
</tr>
{/* ERPG-2 반경 */}
<tr className="border-b border-border">
<td className="text-text-2 px-[10px] py-1.5">ERPG-2 (km)</td>
<tr className="border-b border-stroke">
<td className="text-fg-sub px-[10px] py-1.5">ERPG-2 (km)</td>
{D.map(d => (
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: d.erpg2 > 0 ? '#f97316' : '#22c55e' }}>{d.erpg2 || 0}</td>
))}
</tr>
{/* 영향인구 */}
<tr className="border-b border-border">
<td className="text-text-2 px-[10px] py-1.5"> ()</td>
<tr className="border-b border-stroke">
<td className="text-fg-sub px-[10px] py-1.5"> ()</td>
{D.map(d => (
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.pop.toLocaleString()}</td>
))}
</tr>
{/* 풍향/풍속 */}
<tr className="border-b border-border">
<td className="text-text-2 px-[10px] py-1.5"> / </td>
<tr className="border-b border-stroke">
<td className="text-fg-sub px-[10px] py-1.5"> / </td>
{D.map(d => (
<td key={d.id} className="text-center font-mono text-primary-cyan p-1.5">{d.wind}</td>
<td key={d.id} className="text-center font-mono text-color-accent p-1.5">{d.wind}</td>
))}
</tr>
{/* 위험 등급 */}
<tr>
<td className="text-text-2 px-[10px] py-1.5"> </td>
<td className="text-fg-sub px-[10px] py-1.5"> </td>
{D.map(d => (
<td key={d.id} className="text-center font-bold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.severity}</td>
))}
@ -625,7 +625,7 @@ function ScenarioComparison() {
function ScenarioMapOverlay() {
return (
<div className="flex-1 flex items-center justify-center flex-col gap-4">
<div className="flex items-center justify-center rounded-md border border-border text-text-3 text-[13px] bg-bg-2" style={{
<div className="flex items-center justify-center rounded-md border border-stroke text-fg-disabled text-[13px] bg-bg-elevated" style={{
width: '80%', maxWidth: '600px', height: '300px',
}}>
[ ]
@ -639,7 +639,7 @@ function ScenarioMapOverlay() {
].map((item, i) => (
<div key={i} className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full opacity-50" style={{ background: item.color }} />
<span className="text-[10px] text-text-2">{item.label}</span>
<span className="text-[10px] text-fg-sub">{item.label}</span>
</div>
))}
</div>
@ -685,9 +685,9 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
return (
<div ref={backdropRef} className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)' }}>
<div className="flex flex-col overflow-hidden rounded-[14px] bg-bg-1 border border-border w-[520px] max-h-[calc(100vh-80px)]" style={{ boxShadow: '0 20px 60px rgba(0,0,0,0.5)' }}>
<div className="flex flex-col overflow-hidden rounded-[14px] bg-bg-surface border border-stroke w-[520px] max-h-[calc(100vh-80px)]" style={{ boxShadow: '0 20px 60px rgba(0,0,0,0.5)' }}>
{/* Header */}
<div className="flex items-center gap-3 border-b border-border px-5 py-4">
<div className="flex items-center gap-3 border-b border-stroke px-5 py-4">
<div className="flex items-center justify-center text-base w-9 h-9 rounded-[10px]" style={{
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))',
border: '1px solid rgba(249,115,22,0.3)',
@ -696,11 +696,11 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
<h2 className="text-[15px] font-bold m-0">
HNS
</h2>
<div className="text-[10px] text-text-3 mt-0.5">
<div className="text-[10px] text-fg-disabled mt-0.5">
··
</div>
</div>
<button onClick={onClose} className="flex items-center justify-center w-7 h-7 cursor-pointer text-text-3 text-[12px] rounded-sm border border-border bg-bg-3"></button>
<button onClick={onClose} className="flex items-center justify-center w-7 h-7 cursor-pointer text-fg-disabled text-[12px] rounded-sm border border-stroke bg-bg-card"></button>
</div>
{/* Scrollable content */}
@ -743,8 +743,8 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
{ label: 'ERPG-2', value: mat.erpg2 },
].map((p, i) => (
<div key={i} className="text-center">
<div className="text-text-3 text-[8px]">{p.label}</div>
<div className="text-[10px] font-bold text-status-orange font-mono">{p.value}</div>
<div className="text-fg-disabled text-[8px]">{p.label}</div>
<div className="text-[10px] font-bold text-color-warning font-mono">{p.value}</div>
</div>
))}
</div>
@ -803,11 +803,11 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
</div>
{/* Footer */}
<div className="flex gap-2 border-t border-border px-5 py-[14px]">
<button onClick={onClose} className="flex-1 text-[12px] font-semibold cursor-pointer rounded-md text-text-2 p-[10px] bg-bg-3 border border-border"></button>
<div className="flex gap-2 border-t border-stroke px-5 py-[14px]">
<button onClick={onClose} className="flex-1 text-[12px] font-semibold cursor-pointer rounded-md text-fg-sub p-[10px] bg-bg-card border border-stroke"></button>
<button onClick={handleSubmit} className="cursor-pointer rounded-md text-[12px] font-bold text-white p-[10px]" style={{
flex: 2,
background: 'linear-gradient(135deg, var(--orange), #ef4444)',
background: 'linear-gradient(135deg, var(--color-warning), #ef4444)',
border: 'none',
opacity: name.trim() ? 1 : 0.5,
}}>
@ -823,7 +823,7 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
function ModalSection({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div>
<div className="text-[11px] font-bold text-status-orange mb-2 pb-1" style={{ borderBottom: '1px solid rgba(249,115,22,0.15)' }}>{title}</div>
<div className="text-[11px] font-bold text-color-warning mb-2 pb-1" style={{ borderBottom: '1px solid rgba(249,115,22,0.15)' }}>{title}</div>
<div className="flex flex-col gap-2">{children}</div>
</div>
)
@ -832,7 +832,7 @@ function ModalSection({ title, children }: { title: string; children: React.Reac
function ModalField({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<div className="text-[9px] font-semibold text-text-3 mb-1">{label}</div>
<div className="text-[9px] font-semibold text-fg-disabled mb-1">{label}</div>
{children}
</div>
)

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -25,41 +25,41 @@ function HNSManualViewer() {
const card = 'rounded-md p-4 mb-3'
return (
<div className="flex-1 overflow-y-auto bg-bg-0" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto bg-bg-base" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
<div className="px-5 py-4 max-w-[1200px] mx-auto">
{/* 헤더 */}
<div className="flex items-center justify-between mb-4">
<div>
<div className="text-base font-bold">📖 HNS </div>
<div className="text-[10px] text-text-3 mt-0.5">Marine HNS Response Manual Bonn Agreement / HELCOM / REMPEC (WestMOPoCo 2024 )</div>
<div className="text-[10px] text-fg-disabled mt-0.5">Marine HNS Response Manual Bonn Agreement / HELCOM / REMPEC (WestMOPoCo 2024 )</div>
</div>
</div>
{/* 목차 카드 그리드 */}
<div className="grid mb-5" style={{ gridTemplateColumns: 'repeat(4,1fr)', gap: '10px' }}>
{[
{ icon: '📘', title: '1. 서론', desc: 'HNS 정의 · OPRC-HNS 의정서 · HNS 협약 범위 및 목적', color: 'var(--cyan)' },
{ icon: '⚖️', title: '2. IMO 협약·의정서·규칙', desc: 'SOLAS · MARPOL · IBC Code · IMDG Code · IGC Code', color: 'var(--cyan)' },
{ icon: '🔬', title: '3. HNS 거동 및 유해요소', desc: 'SEBC 거동분류 · MSDS · GESAMP · 물리화학적 특성', color: 'var(--purple)' },
{ icon: '🛡️', title: '4. 대비', desc: '위험 평가 · 비상 계획 · 교육훈련 · 장비 비축', color: 'var(--orange)' },
{ icon: '🚨', title: '5. 대응', desc: '최초 조치 · 안전구역 · PPE · 모니터링 · 대응 기술', color: 'var(--red)' },
{ icon: '🔄', title: '6. 유출 후 관리', desc: '비용 문서화 · 환경 회복 · 사고 검토 · 교훈', color: 'var(--cyan)' },
{ icon: '📋', title: '7. 사례연구', desc: '실제 HNS 해양사고 사례 분석 및 교훈', color: 'var(--cyan)' },
{ icon: '📊', title: '8. 자료표', desc: '물질별 데이터시트 · AEGL · 노출 한계값', color: 'var(--cyan)' },
{ icon: '📘', title: '1. 서론', desc: 'HNS 정의 · OPRC-HNS 의정서 · HNS 협약 범위 및 목적', color: 'var(--color-accent)' },
{ icon: '⚖️', title: '2. IMO 협약·의정서·규칙', desc: 'SOLAS · MARPOL · IBC Code · IMDG Code · IGC Code', color: 'var(--color-accent)' },
{ icon: '🔬', title: '3. HNS 거동 및 유해요소', desc: 'SEBC 거동분류 · MSDS · GESAMP · 물리화학적 특성', color: 'var(--color-tertiary)' },
{ icon: '🛡️', title: '4. 대비', desc: '위험 평가 · 비상 계획 · 교육훈련 · 장비 비축', color: 'var(--color-warning)' },
{ icon: '🚨', title: '5. 대응', desc: '최초 조치 · 안전구역 · PPE · 모니터링 · 대응 기술', color: 'var(--color-danger)' },
{ icon: '🔄', title: '6. 유출 후 관리', desc: '비용 문서화 · 환경 회복 · 사고 검토 · 교훈', color: 'var(--color-accent)' },
{ icon: '📋', title: '7. 사례연구', desc: '실제 HNS 해양사고 사례 분석 및 교훈', color: 'var(--color-accent)' },
{ icon: '📊', title: '8. 자료표', desc: '물질별 데이터시트 · AEGL · 노출 한계값', color: 'var(--color-accent)' },
].map(ch => (
<div key={ch.title} className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', cursor: 'pointer', transition: '.2s' }}>
<div key={ch.title} className={card} style={{ background: 'var(--bg-card)', border: '1px solid var(--stroke-default)', cursor: 'pointer', transition: '.2s' }}>
<div className="text-[20px] mb-1.5">{ch.icon}</div>
<div className="text-[11px] font-bold">{ch.title}</div>
<div className="text-[9px] text-text-3 mt-1 leading-[1.4]">{ch.desc}</div>
<div className="text-[9px] text-fg-disabled mt-1 leading-[1.4]">{ch.desc}</div>
</div>
))}
</div>
{/* SEBC 거동 분류 */}
<div className={`${card} bg-bg-3 border border-border`}>
<div className={`${card} bg-bg-card border border-stroke`}>
<div className="text-[13px] font-bold mb-2.5">SEBC (Standard European Behaviour Classification)</div>
<div className="text-[9px] text-text-3 mb-2.5 leading-normal"> · (, , , ) 5 + 7 </div>
<div className="text-[9px] text-fg-disabled mb-2.5 leading-normal"> · (, , , ) 5 + 7 </div>
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(5,1fr)' }}>
{[
{ icon: '💨', label: 'G — 가스', desc: '대기 중 확산\n증기압 > 101.3kPa\n예: 암모니아, 염소', color: 'rgba(139,92,246' },
@ -71,14 +71,14 @@ function HNSManualViewer() {
<div key={s.label} className="text-center px-[6px] py-[10px] rounded-sm" style={{ background: `${s.color},.08)`, border: `1px solid ${s.color},.2)` }}>
<div className="text-[22px] mb-1">{s.icon}</div>
<div className="text-[11px] font-bold" style={{ color: `${s.color},1)` }}>{s.label}</div>
<div className="text-text-3 whitespace-pre-line text-[8px] mt-[3px] leading-[1.3]">{s.desc}</div>
<div className="text-fg-disabled whitespace-pre-line text-[8px] mt-[3px] leading-[1.3]">{s.desc}</div>
</div>
))}
</div>
</div>
{/* 출처 */}
<div className="text-text-3 rounded-sm bg-bg-3 p-[10px] text-[8px] leading-[1.5]">
<div className="text-fg-disabled rounded-sm bg-bg-card p-[10px] text-[8px] leading-[1.5]">
<b>:</b> Marine HNS Response Manual Bonn Agreement / HELCOM / REMPEC (WestMOPoCo Project, 2024 )<br />
번역: 원해민, , , , KRISO / NOWPAP MERRAC<br />
원본: Alcaro L., Brandt J., Giraud W., Mannozzi M., Nicolas-Kopec A. (2021) ISBN: 978-2-87893-147-1
@ -131,8 +131,8 @@ function DispersionTimeSlider({
<div className="flex-1 flex flex-col gap-1">
<div className="flex items-center justify-between text-[9px]">
<span className="text-primary-cyan font-mono font-bold">t = {currentTime}s</span>
<span className="text-text-3 font-mono">{endTime}s</span>
<span className="text-color-accent font-mono font-bold">t = {currentTime}s</span>
<span className="text-fg-disabled font-mono">{endTime}s</span>
</div>
<input
type="range"
@ -140,12 +140,12 @@ function DispersionTimeSlider({
max={totalFrames - 1}
value={currentFrame}
onChange={(e) => onFrameChange(parseInt(e.target.value))}
className="w-full h-1 accent-[var(--cyan)]"
className="w-full h-1 accent-[var(--color-accent)]"
style={{ cursor: 'pointer' }}
/>
</div>
<div className="text-[9px] text-text-3 font-mono whitespace-nowrap">
<div className="text-[9px] text-fg-disabled font-mono whitespace-nowrap">
{currentFrame + 1}/{totalFrames}
</div>
</div>

파일 보기

@ -18,15 +18,15 @@ export function IncidentTable() {
});
return (
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
<div className="flex items-center justify-between px-5 py-4 border-b border-stroke">
<div>
<h1 className="text-xl font-bold text-text-1"> </h1>
<p className="text-sm text-text-3 mt-1"> {filteredIncidents.length}</p>
<h1 className="text-xl font-bold text-fg"> </h1>
<p className="text-sm 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-border rounded-md bg-bg-3 text-text-2 hover:bg-bg-hover hover:text-text-1 transition-all">
<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>
<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-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none"
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"
/>
</div>
<button className="px-4 py-2 text-sm font-semibold rounded-md bg-primary-cyan text-bg-0 hover:shadow-[0_0_16px_rgba(6,182,212,0.3)] transition-all">
<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>
</div>
@ -47,47 +47,47 @@ export function IncidentTable() {
{/* 테이블 */}
<div className="flex-1 overflow-auto">
<table className="w-full">
<thead className="sticky top-0 bg-bg-1 border-b border-border z-10">
<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-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-right text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs 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>
<th className="px-4 py-3 text-left text-xs 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>
<th className="px-4 py-3 text-left text-xs 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>
<th className="px-4 py-3 text-left text-xs 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>
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{filteredIncidents.map((incident) => (
<tr
key={incident.acdntSn}
className="hover:bg-bg-2 transition-colors cursor-pointer group"
className="hover:bg-bg-elevated transition-colors cursor-pointer group"
>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{incident.acdntSn}</td>
<td className="px-4 py-3 text-sm 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-status-red animate-pulse" />
<span className="text-sm font-semibold text-text-1 group-hover:text-primary-cyan transition-colors">
<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">
{incident.acdntNm}
</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{incident.occrnDtm}</td>
<td className="px-4 py-3 text-sm text-text-2">{incident.vesselTp ?? '—'}</td>
<td className="px-4 py-3 text-sm text-text-2">{incident.oilTpCd ?? '—'}</td>
<td className="px-4 py-3 text-sm text-text-1 font-mono text-right font-semibold">
<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">
{incident.spilQty != null ? incident.spilQty.toFixed(2) : '—'}
</td>
<td className="px-4 py-3 text-sm text-text-2">{incident.acdntTpCd}</td>
<td className="px-4 py-3 text-sm 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">
{incident.phaseCd}
</span>
</td>
<td className="px-4 py-3 text-sm text-text-2">{incident.analystNm ?? '—'}</td>
<td className="px-4 py-3 text-sm text-fg-sub">{incident.analystNm ?? '—'}</td>
</tr>
))}
</tbody>

파일 보기

@ -170,9 +170,9 @@ export function IncidentsLeftPanel({
}
return (
<div className="flex flex-col bg-bg-1 border-r border-border overflow-hidden shrink-0 w-[360px]">
<div className="flex flex-col bg-bg-surface border-r border-stroke overflow-hidden shrink-0 w-[360px]">
{/* Search */}
<div className="px-4 py-3 border-b border-border shrink-0">
<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>
<input
@ -180,43 +180,43 @@ export function IncidentsLeftPanel({
placeholder="사고명, 선박명 검색..."
value={searchTerm}
onChange={(e) => { setSearchTerm(e.target.value); resetPage() }}
className="w-full py-2 pr-3 pl-8 bg-bg-0 border border-border text-xs outline-none"
style={{ borderRadius: 'var(--rS)' }}
className="w-full py-2 pr-3 pl-8 bg-bg-base border border-stroke text-xs outline-none"
style={{ borderRadius: 'var(--radius-sm)' }}
/>
</div>
</div>
{/* Date Range */}
<div className="flex items-center gap-1.5 px-4 py-2 border-b border-border shrink-0">
<div className="flex items-center gap-1.5 px-4 py-2 border-b border-stroke shrink-0">
<input type="date" value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setSelectedPeriod(''); resetPage() }}
className="bg-bg-0 border border-border font-mono text-[11px] outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--rS)' }}
className="bg-bg-base border border-stroke font-mono text-[11px] outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--radius-sm)' }}
/>
<span className="text-text-3 text-[11px]">~</span>
<span className="text-fg-disabled text-[11px]">~</span>
<input type="date" value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setSelectedPeriod(''); resetPage() }}
className="bg-bg-0 border border-border font-mono text-[11px] outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--rS)' }}
className="bg-bg-base border border-stroke font-mono text-[11px] outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--radius-sm)' }}
/>
<button onClick={resetPage} className="text-[11px] font-semibold cursor-pointer whitespace-nowrap text-white border-none" style={{ padding: '5px 12px', background: 'linear-gradient(135deg,var(--cyan),var(--blue))', borderRadius: 'var(--rS)' }}></button>
<button onClick={resetPage} className="text-[11px] font-semibold cursor-pointer whitespace-nowrap text-white border-none" style={{ padding: '5px 12px', background: 'linear-gradient(135deg,var(--color-accent),var(--color-info))', borderRadius: 'var(--radius-sm)' }}></button>
</div>
{/* Period Presets */}
<div className="flex gap-1 px-4 py-1.5 border-b border-border shrink-0">
<div className="flex gap-1 px-4 py-1.5 border-b border-stroke shrink-0">
{PERIOD_PRESETS.map(p => (
<button key={p} onClick={() => handlePeriodClick(p)} className="text-[10px] font-semibold cursor-pointer"
style={{
padding: '3px 8px', borderRadius: '14px', border: selectedPeriod === p ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
padding: '3px 8px', borderRadius: '14px', border: selectedPeriod === p ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--stroke-default)',
background: selectedPeriod === p ? 'rgba(6,182,212,0.1)' : 'transparent',
color: selectedPeriod === p ? 'var(--cyan)' : 'var(--t3)',
color: selectedPeriod === p ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}>{p}</button>
))}
</div>
{/* Today Summary */}
<div className="px-4 py-2.5 border-b border-border shrink-0" style={{ background: 'rgba(6,182,212,0.03)' }}>
<div className="text-[10px] font-bold text-text-3 mb-2" style={{ letterSpacing: '0.8px' }}>
<div className="px-4 py-2.5 border-b border-stroke shrink-0" style={{ background: 'rgba(6,182,212,0.03)' }}>
<div className="text-[10px] font-bold text-fg-disabled mb-2" style={{ letterSpacing: '0.8px' }}>
📅 ({todayLabel})
</div>
<div className="flex gap-1.5 flex-wrap">
@ -226,13 +226,13 @@ export function IncidentsLeftPanel({
return (
<button key={r} onClick={() => { setSelectedRegion(r); resetPage() }} className="text-[11px] cursor-pointer"
style={{
padding: '4px 10px', borderRadius: 'var(--rS)',
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg3)',
border: isActive ? '1px solid rgba(6,182,212,0.25)' : '1px solid var(--bd)',
color: isActive ? 'var(--cyan)' : 'var(--t2)', fontWeight: isActive ? 700 : 400,
padding: '4px 10px', borderRadius: 'var(--radius-sm)',
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg-card)',
border: isActive ? '1px solid rgba(6,182,212,0.25)' : '1px solid var(--stroke-default)',
color: isActive ? 'var(--color-accent)' : 'var(--fg-sub)', fontWeight: isActive ? 700 : 400,
}}>
{r === '전체' ? '전체 ' : `${r} `}
<span className="font-bold font-mono" style={{ color: isActive ? 'var(--cyan)' : undefined }}>
<span className="font-bold font-mono" style={{ color: isActive ? 'var(--color-accent)' : undefined }}>
{r === '전체' ? count : `(${count})`}
</span>
</button>
@ -242,18 +242,18 @@ export function IncidentsLeftPanel({
</div>
{/* Status Filter */}
<div className="flex gap-[5px] px-4 py-2 border-b border-border shrink-0">
<div className="flex gap-[5px] px-4 py-2 border-b border-stroke shrink-0">
{[
{ id: '전체', label: '전체', dot: '' },
{ id: 'active', label: `대응중 (${statusCounts.active})`, dot: 'var(--red)' },
{ id: 'investigating', label: `조사중 (${statusCounts.investigating})`, dot: 'var(--orange)' },
{ id: 'closed', label: `종료 (${statusCounts.closed})`, dot: 'var(--t3)' },
{ id: 'active', label: `대응중 (${statusCounts.active})`, dot: 'var(--color-danger)' },
{ id: 'investigating', label: `조사중 (${statusCounts.investigating})`, dot: 'var(--color-warning)' },
{ id: 'closed', label: `종료 (${statusCounts.closed})`, dot: 'var(--fg-disabled)' },
].map(s => (
<button key={s.id} onClick={() => { setSelectedStatus(s.id); resetPage() }}
className="flex items-center gap-1 text-[10px] font-semibold cursor-pointer"
style={{
padding: '4px 10px', borderRadius: '12px', border: selectedStatus === s.id ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
background: 'transparent', color: selectedStatus === s.id ? 'var(--t2)' : 'var(--t3)',
padding: '4px 10px', borderRadius: '12px', border: selectedStatus === s.id ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--stroke-default)',
background: 'transparent', color: selectedStatus === s.id ? 'var(--fg-sub)' : 'var(--fg-disabled)',
}}>
{s.dot && <span style={{ width: '6px', height: '6px', borderRadius: '50%', background: s.dot }} />}
{s.label}
@ -262,39 +262,39 @@ export function IncidentsLeftPanel({
</div>
{/* Count */}
<div className="px-4 py-1.5 text-[11px] text-text-3 shrink-0 border-b border-border">
<div className="px-4 py-1.5 text-[11px] text-fg-disabled shrink-0 border-b border-stroke">
{filteredIncidents.length}
</div>
{/* Incident List */}
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin' as const, scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin' as const, scrollbarColor: 'var(--stroke-light) transparent' }}>
{pagedIncidents.length === 0 ? (
<div className="px-4 py-10 text-center text-text-3 text-[11px]">
<div className="px-4 py-10 text-center text-fg-disabled text-[11px]">
.
</div>
) : pagedIncidents.map(inc => {
const isSel = selectedIncidentId === inc.id
const dotStyle: Record<string, string> = {
active: 'var(--red)', investigating: 'var(--orange)', closed: 'var(--t3)',
active: 'var(--color-danger)', investigating: 'var(--color-warning)', closed: 'var(--fg-disabled)',
}
const dotShadow: Record<string, string> = {
active: '0 0 6px var(--red)', investigating: '0 0 6px var(--orange)', closed: 'none',
active: '0 0 6px var(--color-danger)', investigating: '0 0 6px var(--color-warning)', closed: 'none',
}
const stBg: Record<string, string> = {
active: 'rgba(239,68,68,0.15)', investigating: 'rgba(249,115,22,0.15)', closed: 'rgba(100,116,139,0.15)',
}
const stColor: Record<string, string> = {
active: 'var(--red)', investigating: 'var(--orange)', closed: 'var(--t3)',
active: 'var(--color-danger)', investigating: 'var(--color-warning)', closed: 'var(--fg-disabled)',
}
const stLabel: Record<string, string> = {
active: '대응중', investigating: '조사중', closed: '종료',
}
return (
<div key={inc.id} onClick={() => onIncidentSelect(isSel ? null : inc.id)}
className="px-4 py-3 border-b border-border cursor-pointer"
className="px-4 py-3 border-b border-stroke cursor-pointer"
style={{
background: isSel ? 'rgba(6,182,212,0.04)' : undefined,
borderLeft: isSel ? '3px solid var(--cyan)' : '3px solid transparent',
borderLeft: isSel ? '3px solid var(--color-accent)' : '3px solid transparent',
transition: 'background 0.15s',
}}
onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = 'rgba(255,255,255,0.02)' }}
@ -311,7 +311,7 @@ export function IncidentsLeftPanel({
</span>
</div>
{/* Row 2: meta */}
<div className="flex items-center gap-2 text-[10px] text-text-3 mb-[5px]">
<div className="flex items-center gap-2 text-[10px] text-fg-disabled mb-[5px]">
<span>📅 {inc.date} {inc.time}</span>
<span>🏛 {inc.office}</span>
</div>
@ -319,17 +319,17 @@ export function IncidentsLeftPanel({
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-1">
{inc.causeType && (
<span className="text-[10px] font-medium text-text-2" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(100,116,139,0.08)', border: '1px solid rgba(100,116,139,0.2)' }}>
<span className="text-[10px] font-medium text-fg-sub" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(100,116,139,0.08)', border: '1px solid rgba(100,116,139,0.2)' }}>
{inc.causeType}
</span>
)}
{inc.oilType && (
<span className="text-[10px] font-medium text-status-orange" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(249,115,22,0.08)', border: '1px solid rgba(249,115,22,0.2)' }}>
<span className="text-[10px] font-medium text-color-warning" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(249,115,22,0.08)', border: '1px solid rgba(249,115,22,0.2)' }}>
{inc.oilType}
</span>
)}
{inc.prediction && (
<span className="text-[10px] font-medium text-status-green" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(34,197,94,0.08)', border: '1px solid rgba(34,197,94,0.2)' }}>
<span className="text-[10px] font-medium text-color-success" style={{ padding: '2px 8px', borderRadius: '3px', background: 'rgba(34,197,94,0.08)', border: '1px solid rgba(34,197,94,0.2)' }}>
{inc.prediction}
</span>
)}
@ -372,8 +372,8 @@ export function IncidentsLeftPanel({
)}
{/* Pagination */}
<div className="flex items-center justify-between bg-bg-1 shrink-0 border-t border-border px-3 py-2">
<div className="text-[9px] text-text-3">
<div className="flex items-center justify-between bg-bg-surface shrink-0 border-t border-stroke px-3 py-2">
<div className="text-[9px] text-fg-disabled">
<b>{filteredIncidents.length}</b> {(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filteredIncidents.length)}
</div>
<div className="flex items-center gap-[3px]">
@ -386,7 +386,7 @@ export function IncidentsLeftPanel({
<PgBtn label="⏭" disabled={safePage >= totalPages} onClick={() => setCurrentPage(totalPages)} />
</div>
<select onChange={(e) => { /* page size change placeholder */ void e }}
className="bg-bg-0 border border-border text-text-2 text-[9px] outline-none rounded px-1.5 py-[3px]">
className="bg-bg-base border border-stroke text-fg-sub text-[9px] outline-none rounded px-1.5 py-[3px]">
<option>6</option><option>10</option><option>20</option>
</select>
</div>
@ -400,9 +400,9 @@ function PgBtn({ label, active, disabled, onClick }: { label: string; active?: b
className="flex items-center justify-center font-mono text-[9px]"
style={{
minWidth: '24px', height: '24px', padding: '0 5px', borderRadius: '4px', fontWeight: active ? 700 : 600,
background: active ? 'rgba(6,182,212,0.15)' : 'var(--bg3)',
border: active ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--bd)',
color: active ? 'var(--cyan)' : 'var(--t3)',
background: active ? 'rgba(6,182,212,0.15)' : 'var(--bg-card)',
border: active ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--stroke-default)',
color: active ? 'var(--color-accent)' : 'var(--fg-disabled)',
opacity: disabled ? 0.4 : 1, pointerEvents: disabled ? 'none' : undefined,
cursor: disabled ? 'default' : 'pointer',
}}>{label}</button>
@ -419,23 +419,23 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
}>(({ data, position, onClose }, ref) => {
const forecast = data?.forecast ?? []
return (
<div ref={ref} className="fixed overflow-hidden rounded-xl border border-border bg-bg-1" style={{
<div ref={ref} className="fixed overflow-hidden rounded-xl border border-stroke bg-bg-surface" style={{
zIndex: 9990, width: 280,
top: position.top, left: position.left,
borderColor: 'rgba(59,130,246,0.3)',
boxShadow: '0 12px 40px rgba(0,0,0,0.5)', backdropFilter: 'blur(12px)',
}}>
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-3.5 py-2.5"
<div className="flex items-center justify-between border-b border-stroke px-3.5 py-2.5"
style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.08), rgba(6,182,212,0.04))' }}>
<div className="flex items-center gap-1.5">
<span className="text-sm">🌤</span>
<div>
<div className="text-[11px] font-bold">{data?.locNm || '기상정보 없음'}</div>
<div className="text-text-3 font-mono text-[8px]">{data?.obsDtm || '-'}</div>
<div className="text-fg-disabled font-mono text-[8px]">{data?.obsDtm || '-'}</div>
</div>
</div>
<span onClick={onClose} className="cursor-pointer text-text-3 text-sm p-0.5"></span>
<span onClick={onClose} className="cursor-pointer text-fg-disabled text-sm p-0.5"></span>
</div>
{/* Body */}
@ -445,7 +445,7 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
<div className="text-[28px]">{data?.icon || '❓'}</div>
<div>
<div className="font-bold font-mono text-[20px]">{data?.temp || '-'}</div>
<div className="text-text-3 text-[9px]">{data?.weatherDc || '-'}</div>
<div className="text-fg-disabled text-[9px]">{data?.weatherDc || '-'}</div>
</div>
</div>
@ -465,7 +465,7 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
style={{ background: 'rgba(59,130,246,0.06)', border: '1px solid rgba(59,130,246,0.1)' }}>
<span className="text-xs"></span>
<div>
<div className="text-text-3 text-[7px]"> ()</div>
<div className="text-fg-disabled text-[7px]"> ()</div>
<div className="font-bold font-mono text-[10px] text-[#60a5fa]">{data?.highTide || '-'}</div>
</div>
</div>
@ -473,17 +473,17 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
style={{ background: 'rgba(6,182,212,0.06)', border: '1px solid rgba(6,182,212,0.1)' }}>
<span className="text-xs"></span>
<div>
<div className="text-text-3 text-[7px]"> ()</div>
<div className="text-primary-cyan font-bold font-mono text-[10px]">{data?.lowTide || '-'}</div>
<div className="text-fg-disabled text-[7px]"> ()</div>
<div className="text-color-accent font-bold font-mono text-[10px]">{data?.lowTide || '-'}</div>
</div>
</div>
</div>
{/* 24h Forecast */}
<div className="bg-bg-0 mt-2.5 px-2.5 py-2 rounded-md">
<div className="font-bold text-text-3 text-[8px] mb-1.5">24h </div>
<div className="bg-bg-base mt-2.5 px-2.5 py-2 rounded-md">
<div className="font-bold text-fg-disabled text-[8px] mb-1.5">24h </div>
{forecast.length > 0 ? (
<div className="flex justify-between font-mono text-text-2 text-[8px]">
<div className="flex justify-between font-mono text-fg-sub text-[8px]">
{forecast.map((f, i) => (
<div key={i} className="text-center">
<div>{f.hour}</div>
@ -493,7 +493,7 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
))}
</div>
) : (
<div className="text-text-3 text-center text-[8px] py-1"> </div>
<div className="text-fg-disabled text-center text-[8px] py-1"> </div>
)}
</div>
@ -502,8 +502,8 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
padding: '6px 10px',
background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)',
}}>
<div className="font-bold text-status-orange text-[8px] mb-[3px]"> </div>
<div className="text-text-2 text-[8px] leading-[1.5]">{data?.impactDc || '-'}</div>
<div className="font-bold text-color-warning text-[8px] mb-[3px]"> </div>
<div className="text-fg-sub text-[8px] leading-[1.5]">{data?.impactDc || '-'}</div>
</div>
</div>
</div>
@ -513,10 +513,10 @@ WeatherPopup.displayName = 'WeatherPopup'
function WxCell({ icon, label, value }: { icon: string; label: string; value?: string | null }) {
return (
<div className="flex items-center bg-bg-0 rounded gap-[6px] py-1.5 px-2">
<div className="flex items-center bg-bg-base rounded gap-[6px] py-1.5 px-2">
<span className="text-[12px]">{icon}</span>
<div>
<div className="text-text-3 text-[7px]">{label}</div>
<div className="text-fg-disabled text-[7px]">{label}</div>
<div className="font-semibold font-mono">{value || '-'}</div>
</div>
</div>

파일 보기

@ -218,8 +218,8 @@ export function IncidentsRightPanel({
if (!incident) {
return (
<div className="flex flex-col items-center justify-center bg-bg-1 border-l border-border w-[280px] min-w-[280px]">
<div className="text-center text-text-3 text-[11px]">
<div className="flex flex-col items-center justify-center bg-bg-surface border-l border-stroke w-[280px] min-w-[280px]">
<div className="text-center text-fg-disabled text-[11px]">
<div className="text-[32px] mb-2 opacity-30">📊</div>
<br />
</div>
@ -228,26 +228,26 @@ export function IncidentsRightPanel({
}
return (
<div className="flex flex-col bg-bg-1 border-l border-border 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-[280px] min-w-[280px]">
{/* Header */}
<div className="px-[14px] py-2.5 border-b border-border shrink-0">
<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-[9px] text-text-3">
: <b className="text-primary-cyan">{incident.name}</b>
<div className="text-[9px] text-fg-disabled">
: <b className="text-color-accent">{incident.name}</b>
</div>
</div>
{/* Scrollable Content */}
<div className="flex-1 h-0 overflow-y-auto flex flex-col gap-2 p-2" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
<div className="flex-1 h-0 overflow-y-auto flex flex-col gap-2 p-2" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}>
{/* 유출유 확산예측 섹션 */}
{(() => {
const sec = oilSection
const checkedCount = sec.items.filter(it => it.checked).length
return (
<div className="bg-bg-2 border border-border rounded-md p-2.5">
<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>
@ -265,7 +265,7 @@ export function IncidentsRightPanel({
</div>
<div className="flex flex-col gap-1">
{sec.items.length === 0 ? (
<div className="text-[9px] text-text-3 text-center py-1.5"> </div>
<div className="text-[9px] text-fg-disabled text-center py-1.5"> </div>
) : (
sec.items.map(item => (
<div key={item.id} className="flex items-center gap-1.5"
@ -286,12 +286,12 @@ export function IncidentsRightPanel({
<div className="text-[10px] font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
{item.name}
</div>
<div className="text-text-3 font-mono text-[8px]">{item.sub}</div>
<div className="text-fg-disabled font-mono text-[8px]">{item.sub}</div>
</div>
<span
onClick={() => removePredItem(item.id)}
title="제거"
className="text-[10px] cursor-pointer text-text-3 shrink-0"
className="text-[10px] cursor-pointer text-fg-disabled shrink-0"
>
</span>
@ -299,7 +299,7 @@ export function IncidentsRightPanel({
))
)}
</div>
<div className="flex items-center gap-1.5 mt-1.5 text-[9px] text-text-3">
<div className="flex items-center gap-1.5 mt-1.5 text-[9px] text-fg-disabled">
: <b style={{ color: sec.color }}>{checkedCount}</b> · {sec.totalLabel}
</div>
</div>
@ -308,7 +308,7 @@ export function IncidentsRightPanel({
{/* HNS 대기확산 / 긴급구난 섹션 (미개발 - 구조 유지) */}
{STATIC_SECTIONS.map(sec => (
<div key={sec.key} className="bg-bg-2 border border-border rounded-md p-2.5">
<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>
@ -324,22 +324,22 @@ export function IncidentsRightPanel({
📋
</button>
</div>
<div className="text-[9px] text-text-3 text-center py-1.5"> </div>
<div className="flex items-center gap-1.5 mt-1.5 text-[9px] text-text-3">
<div className="text-[9px] text-fg-disabled text-center py-1.5"> </div>
<div className="flex items-center gap-1.5 mt-1.5 text-[9px] text-fg-disabled">
: <b style={{ color: sec.color }}>0</b> · 0
</div>
</div>
))}
{/* 민감자원 */}
<div className="bg-bg-2 border border-border rounded-md p-2.5">
<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-[#22c55e]"></span>
</div>
<div className="flex flex-col gap-[3px]">
{sensCategories.length === 0 ? (
<div className="text-[9px] text-text-3 text-center py-1.5"> </div>
<div className="text-[9px] text-fg-disabled text-center py-1.5"> </div>
) : (
sensCategories.map((cat, i) => {
const icon = CATEGORY_ICON[cat.category] ?? '🌊'
@ -360,7 +360,7 @@ export function IncidentsRightPanel({
<span style={{ width: 8, height: 8, borderRadius: '50%', background: hex, 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-text-3 font-mono shrink-0">({areaLabel})</span>
<span className="text-fg-disabled font-mono shrink-0">({areaLabel})</span>
</label>
)
})
@ -369,7 +369,7 @@ export function IncidentsRightPanel({
</div>
{/* 근처 방제자원 */}
<div className="bg-bg-2 border border-border rounded-md p-2.5">
<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-[#f59e0b]">
@ -381,14 +381,14 @@ export function IncidentsRightPanel({
</div>
{!selectedVessel ? (
<div className="py-2.5 text-center text-text-3 text-[10px] leading-[1.7]">
<div className="py-2.5 text-center text-fg-disabled text-[10px] leading-[1.7]">
<div className="text-xl mb-1 opacity-40">🚢</div>
<br />
</div>
) : nearbyLoading ? (
<div className="py-2.5 text-center text-text-3 text-[10px]"> ...</div>
<div className="py-2.5 text-center text-fg-disabled text-[10px]"> ...</div>
) : nearbyOrgs.length === 0 ? (
<div className="py-2.5 text-center text-text-3 text-[10px]"> </div>
<div className="py-2.5 text-center text-fg-disabled text-[10px]"> </div>
) : (
<div className="flex flex-col gap-[3px] max-h-[200px] overflow-y-auto">
{nearbyOrgs.map(org => (
@ -400,9 +400,9 @@ export function IncidentsRightPanel({
style={{ background: 'rgba(245,158,11,0.15)', color: '#f59e0b' }}>
{org.orgTp}
</span>
<span className="text-[10px] font-bold text-text-1 truncate">{org.orgNm}</span>
<span className="text-[10px] font-bold text-fg truncate">{org.orgNm}</span>
</div>
<div className="text-[9px] text-text-3">
<div className="text-[9px] text-fg-disabled">
{org.areaNm}{org.totalAssets > 0 ? ` · 장비 ${org.totalAssets}` : ''}
</div>
</div>
@ -417,7 +417,7 @@ export function IncidentsRightPanel({
{/* Radius slider */}
<div className="mt-2 pt-2" style={{ borderTop: '1px solid rgba(245,158,11,0.1)' }}>
<div className="flex items-center justify-between mb-[5px]">
<span className="text-[9px] text-text-3"> </span>
<span className="text-[9px] text-fg-disabled"> </span>
<span className="text-[10px] font-bold font-mono text-[#f59e0b]">
{nearbyRadius} nm
</span>
@ -432,7 +432,7 @@ export function IncidentsRightPanel({
className="w-full outline-none cursor-pointer"
style={{
height: '4px',
background: 'var(--bd)', borderRadius: '2px',
background: 'var(--stroke-default)', borderRadius: '2px',
accentColor: '#f59e0b',
}}
/>
@ -441,7 +441,7 @@ export function IncidentsRightPanel({
</div>
{/* Footer */}
<div className="flex flex-col gap-1.5 p-2.5 border-t border-border shrink-0">
<div className="flex flex-col gap-1.5 p-2.5 border-t border-stroke shrink-0">
{/* View Mode */}
<div className="flex gap-1">
{([
@ -453,11 +453,11 @@ export function IncidentsRightPanel({
return (
<button key={v.mode} onClick={() => onViewModeChange(v.mode)} className="flex-1 text-[10px] cursor-pointer"
style={{
padding: '6px', borderRadius: 'var(--rS)',
padding: '6px', borderRadius: 'var(--radius-sm)',
fontWeight: isActive ? 700 : 600,
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg3)',
border: isActive ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
color: isActive ? 'var(--cyan)' : 'var(--t3)',
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg-card)',
border: isActive ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--stroke-default)',
color: isActive ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}>
{v.icon} {v.label}
</button>
@ -481,8 +481,8 @@ export function IncidentsRightPanel({
? '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))',
border: analysisActive ? '1px solid rgba(239,68,68,0.3)' : '1px solid rgba(6,182,212,0.3)',
borderRadius: 'var(--rS)',
color: analysisActive ? '#f87171' : 'var(--cyan)',
borderRadius: 'var(--radius-sm)',
color: analysisActive ? '#f87171' : 'var(--color-accent)',
}}>
{analysisActive ? '✕ 분석 닫기' : '🔬 통합 분석 비교 실행'}
</button>

파일 보기

@ -540,18 +540,18 @@ export function IncidentsView() {
{/* Analysis Bar */}
{analysisActive && (
<div
className="shrink-0 flex items-center justify-between border-b border-border"
className="shrink-0 flex items-center justify-between border-b border-stroke"
style={{
height: 36,
padding: '0 16px',
background: 'linear-gradient(90deg,rgba(6,182,212,0.06),var(--bg1))',
background: 'linear-gradient(90deg,rgba(6,182,212,0.06),var(--bg-surface))',
}}
>
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold">
🔬
</span>
<span className="text-[9px] text-text-3">
<span className="text-[9px] text-fg-disabled">
{selectedIncident?.name}
</span>
<div className="flex gap-1">
@ -585,9 +585,9 @@ export function IncidentsView() {
className="text-[9px] font-semibold cursor-pointer rounded-sm"
style={{
padding: '3px 10px',
background: viewMode === v.mode ? 'rgba(6,182,212,0.12)' : 'var(--bg3)',
border: viewMode === v.mode ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)',
color: viewMode === v.mode ? 'var(--cyan)' : 'var(--t3)',
background: viewMode === v.mode ? 'rgba(6,182,212,0.12)' : 'var(--bg-card)',
border: viewMode === v.mode ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--stroke-default)',
color: viewMode === v.mode ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}
>
{v.icon} {v.label}
@ -814,16 +814,16 @@ export function IncidentsView() {
<span className="text-[10px] font-bold">
AIS Live
</span>
<span className="text-[8px] text-text-3 font-mono">MarineTraffic</span>
<span className="text-[8px] text-fg-disabled font-mono">MarineTraffic</span>
</div>
<div className="flex gap-2.5 text-[9px] font-mono">
<div className="text-text-2">
<b className="text-primary-cyan">20</b>
<div className="text-fg-sub">
<b className="text-color-accent">20</b>
</div>
<div className="text-text-2">
<div className="text-fg-sub">
<b className="text-red-400">6</b>
</div>
<div className="text-text-2">
<div className="text-fg-sub">
<b className="text-cyan-500">2</b>
</div>
</div>
@ -839,7 +839,7 @@ export function IncidentsView() {
backdropFilter: 'blur(8px)',
}}
>
<div className="text-[9px] font-bold text-text-2">
<div className="text-[9px] font-bold text-fg-sub">
</div>
<div className="flex gap-2.5">
@ -850,11 +850,11 @@ export function IncidentsView() {
].map(s => (
<div key={s.l} className="flex items-center gap-1">
<div style={{ width: 8, height: 8, borderRadius: '50%', background: s.c }} />
<span className="text-[8px] text-text-3">{s.l}</span>
<span className="text-[8px] text-fg-disabled">{s.l}</span>
</div>
))}
</div>
<div className="text-[9px] font-bold text-text-2 mt-0.5">
<div className="text-[9px] font-bold text-fg-sub mt-0.5">
AIS
</div>
<div className="flex flex-wrap gap-[6px_12px]">
@ -869,7 +869,7 @@ export function IncidentsView() {
borderBottom: `7px solid ${vl.color}`,
}}
/>
<span className="text-[8px] text-text-3">{vl.type}</span>
<span className="text-[8px] text-fg-disabled">{vl.type}</span>
</div>
))}
</div>
@ -901,28 +901,28 @@ export function IncidentsView() {
<div className="flex h-full">
<div
className="flex-1 flex flex-col overflow-hidden"
style={{ borderRight: '2px solid var(--cyan)' }}
style={{ borderRight: '2px solid var(--color-accent)' }}
>
<div className="flex items-center shrink-0 bg-bg-1 border-b border-border" style={{ height: 28, padding: '0 10px' }}>
<span className="text-[9px] font-bold text-primary-cyan">
<div className="flex items-center shrink-0 bg-bg-surface border-b border-stroke" style={{ height: 28, padding: '0 10px' }}>
<span className="text-[9px] font-bold text-color-accent">
{analysisTags[0]
? `${analysisTags[0].icon} ${analysisTags[0].label}`
: '— 분석 결과를 선택하세요 —'}
</span>
</div>
<div className="flex-1 overflow-y-auto bg-bg-0 flex flex-col gap-2" style={{ padding: 12 }}>
<div className="flex-1 overflow-y-auto bg-bg-base flex flex-col gap-2" style={{ padding: 12 }}>
<SplitPanelContent tag={analysisTags[0]} incident={selectedIncident} />
</div>
</div>
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex items-center shrink-0 bg-bg-1 border-b border-border" style={{ height: 28, padding: '0 10px' }}>
<span className="text-[9px] font-bold text-primary-cyan">
<div className="flex items-center shrink-0 bg-bg-surface border-b border-stroke" style={{ height: 28, padding: '0 10px' }}>
<span className="text-[9px] font-bold text-color-accent">
{analysisTags[1]
? `${analysisTags[1].icon} ${analysisTags[1].label}`
: '— 분석 결과를 선택하세요 —'}
</span>
</div>
<div className="flex-1 overflow-y-auto bg-bg-0 flex flex-col gap-2" style={{ padding: 12 }}>
<div className="flex-1 overflow-y-auto bg-bg-base flex flex-col gap-2" style={{ padding: 12 }}>
<SplitPanelContent tag={analysisTags[1]} incident={selectedIncident} />
</div>
</div>
@ -933,13 +933,13 @@ export function IncidentsView() {
{analysisActive && viewMode === 'split3' && (
<div className="flex h-full">
<div
className="flex-1 flex flex-col overflow-hidden border-r border-border"
className="flex-1 flex flex-col overflow-hidden border-r border-stroke"
>
<div
className="flex items-center shrink-0 border-b border-border"
className="flex items-center shrink-0 border-b border-stroke"
style={{
height: 28,
background: 'linear-gradient(90deg,rgba(249,115,22,0.08),var(--bg1))',
background: 'linear-gradient(90deg,rgba(249,115,22,0.08),var(--bg-surface))',
padding: '0 10px',
}}
>
@ -947,7 +947,7 @@ export function IncidentsView() {
🛢
</span>
</div>
<div className="flex-1 overflow-y-auto bg-bg-0 flex flex-col gap-1.5" style={{ padding: 10 }}>
<div className="flex-1 overflow-y-auto bg-bg-base flex flex-col gap-1.5" style={{ padding: 10 }}>
<SplitPanelContent
tag={{ icon: '🛢', label: '유출유', color: '#f97316' }}
incident={selectedIncident}
@ -955,13 +955,13 @@ export function IncidentsView() {
</div>
</div>
<div
className="flex-1 flex flex-col overflow-hidden border-r border-border"
className="flex-1 flex flex-col overflow-hidden border-r border-stroke"
>
<div
className="flex items-center shrink-0 border-b border-border"
className="flex items-center shrink-0 border-b border-stroke"
style={{
height: 28,
background: 'linear-gradient(90deg,rgba(168,85,247,0.08),var(--bg1))',
background: 'linear-gradient(90deg,rgba(168,85,247,0.08),var(--bg-surface))',
padding: '0 10px',
}}
>
@ -969,7 +969,7 @@ export function IncidentsView() {
🧪 HNS
</span>
</div>
<div className="flex-1 overflow-y-auto bg-bg-0 flex flex-col gap-1.5" style={{ padding: 10 }}>
<div className="flex-1 overflow-y-auto bg-bg-base flex flex-col gap-1.5" style={{ padding: 10 }}>
<SplitPanelContent
tag={{ icon: '🧪', label: 'HNS', color: '#a855f7' }}
incident={selectedIncident}
@ -978,18 +978,18 @@ export function IncidentsView() {
</div>
<div className="flex-1 flex flex-col overflow-hidden">
<div
className="flex items-center shrink-0 border-b border-border"
className="flex items-center shrink-0 border-b border-stroke"
style={{
height: 28,
background: 'linear-gradient(90deg,rgba(6,182,212,0.08),var(--bg1))',
background: 'linear-gradient(90deg,rgba(6,182,212,0.08),var(--bg-surface))',
padding: '0 10px',
}}
>
<span className="text-[9px] font-bold text-primary-cyan">
<span className="text-[9px] font-bold text-color-accent">
🚨
</span>
</div>
<div className="flex-1 overflow-y-auto bg-bg-0 flex flex-col gap-1.5" style={{ padding: 10 }}>
<div className="flex-1 overflow-y-auto bg-bg-base flex flex-col gap-1.5" style={{ padding: 10 }}>
<SplitPanelContent
tag={{ icon: '🚨', label: '구난', color: '#06b6d4' }}
incident={selectedIncident}
@ -1002,8 +1002,8 @@ export function IncidentsView() {
{/* Decision Bar */}
{analysisActive && (
<div className="shrink-0 flex items-center justify-between bg-bg-1 border-t border-border" style={{ padding: '6px 16px' }}>
<div className="text-[9px] text-text-3">
<div className="shrink-0 flex items-center justify-between bg-bg-surface border-t border-stroke" style={{ padding: '6px 16px' }}>
<div className="text-[9px] text-fg-disabled">
📊 {selectedIncident?.name} · {analysisTags.map(t => t.label).join(' + ')}
</div>
<div className="flex gap-[6px]">
@ -1062,7 +1062,7 @@ function SplitPanelContent({
}) {
if (!tag) {
return (
<div className="flex-1 flex items-center justify-center text-text-3 text-[11px]">
<div className="flex-1 flex items-center justify-center text-fg-disabled text-[11px]">
R&D
</div>
)
@ -1136,29 +1136,29 @@ function SplitPanelContent({
>
{tag.icon} {data.title}
</div>
<div className="text-[8px] text-text-3 font-mono">{data.model}</div>
<div className="text-[8px] text-fg-disabled font-mono">{data.model}</div>
{incident && (
<div className="text-[8px] text-text-3" style={{ marginTop: 2 }}>
<div className="text-[8px] text-fg-disabled" style={{ marginTop: 2 }}>
: {incident.name} · {incident.date} {incident.time}
</div>
)}
</div>
<div className="rounded-sm border border-border overflow-hidden">
<div className="rounded-sm border border-stroke overflow-hidden">
{data.items.map((item, i) => (
<div
key={i}
className="flex justify-between items-center"
style={{
padding: '6px 10px',
borderBottom: i < data.items.length - 1 ? '1px solid var(--bd)' : 'none',
background: i % 2 === 0 ? 'var(--bg1)' : 'var(--bg2)',
borderBottom: i < data.items.length - 1 ? '1px solid var(--stroke-default)' : 'none',
background: i % 2 === 0 ? 'var(--bg-surface)' : 'var(--bg-elevated)',
}}
>
<span className="text-[9px] text-text-3">{item.label}</span>
<span className="text-[9px] text-fg-disabled">{item.label}</span>
<span
className="text-[10px] font-semibold font-mono"
style={{ color: item.color || 'var(--t1)' }}
style={{ color: item.color || 'var(--fg-default)' }}
>
{item.value}
</span>
@ -1167,7 +1167,7 @@ function SplitPanelContent({
</div>
<div
className="rounded-sm text-[9px] text-text-2"
className="rounded-sm text-[9px] text-fg-sub"
style={{
padding: '8px 10px',
background: `${tag.color}06`,
@ -1179,11 +1179,11 @@ function SplitPanelContent({
</div>
<div
className="rounded-sm bg-bg-0 border border-border flex items-center justify-center flex-col gap-1"
className="rounded-sm bg-bg-base border border-stroke flex items-center justify-center flex-col gap-1"
style={{ height: 120 }}
>
<div className="text-[32px] opacity-30">{tag.icon}</div>
<div className="text-[9px] text-text-3"> </div>
<div className="text-[9px] text-fg-disabled"> </div>
</div>
</>
)

파일 보기

@ -408,7 +408,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{Array.from({ length: num(media.cctvMeta, 'camCount') }).map((_, i) => (
<button key={i} onClick={() => setSelectedCam(i)} style={{
padding: '6px 16px', borderRadius: 4, fontSize: 10, fontWeight: 600,
fontFamily: 'var(--fM)', cursor: 'pointer',
fontFamily: 'var(--font-mono)', cursor: 'pointer',
background: selectedCam === i ? 'rgba(168,85,247,0.12)' : '#161b22',
border: selectedCam === i ? '1px solid rgba(168,85,247,0.4)' : '1px solid #30363d',
color: selectedCam === i ? '#c084fc' : '#8b949e',

파일 보기

@ -48,13 +48,13 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
)
case 'pending':
return (
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(138,150,168,0.15)] text-text-3">
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(138,150,168,0.15)] text-fg-disabled">
</span>
)
case 'error':
return (
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(239,68,68,0.15)] text-status-red">
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(239,68,68,0.15)] text-color-danger">
</span>
)
@ -97,7 +97,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
return pages.map((page, index) => {
if (page === '...') {
return (
<span key={`ellipsis-${index}`} className="px-3 py-1 text-text-3">
<span key={`ellipsis-${index}`} className="px-3 py-1 text-fg-disabled">
...
</span>
)
@ -108,8 +108,8 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
onClick={() => setCurrentPage(page as number)}
className={`px-3 py-1 text-sm font-medium rounded transition-colors ${
currentPage === page
? 'bg-primary-cyan text-bg-0'
: 'text-text-2 hover:bg-bg-2'
? 'bg-color-accent text-bg-0'
: 'text-fg-sub hover:bg-bg-elevated'
}`}
>
{page}
@ -119,12 +119,12 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
}
return (
<div className="flex flex-col h-full bg-bg-0">
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
<div className="flex items-center justify-between px-5 py-4 border-b border-stroke">
<div>
<h1 className="text-xl font-bold text-text-1"> </h1>
<p className="text-sm text-text-3 mt-1"> {analyses.length}</p>
<h1 className="text-xl font-bold text-fg"> </h1>
<p className="text-sm text-fg-disabled mt-1"> {analyses.length}</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
@ -133,10 +133,10 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
placeholder="검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-64 px-4 py-2 text-sm bg-bg-2 border border-border rounded-md text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none"
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"
/>
</div>
<button onClick={() => onTabChange('analysis')} className="px-4 py-2 text-sm font-semibold rounded-md bg-primary-cyan text-bg-0 hover:shadow-[0_0_16px_rgba(6,182,212,0.3)] transition-all">
<button onClick={() => onTabChange('analysis')} 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>
</div>
@ -145,38 +145,38 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
{/* 테이블 */}
<div className="flex-1 overflow-auto">
{loading ? (
<div className="text-center py-20 text-text-3 text-sm"> ...</div>
<div className="text-center py-20 text-fg-disabled text-sm"> ...</div>
) : (
<table className="w-full">
<thead className="sticky top-0 bg-bg-1 border-b border-border z-10">
<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-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"> </th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-right text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-center text-xs font-bold text-text-3 uppercase tracking-wider">KOSPS</th>
<th className="px-4 py-3 text-center text-xs font-bold text-text-3 uppercase tracking-wider">POSEIDON</th>
<th className="px-4 py-3 text-center text-xs font-bold text-text-3 uppercase tracking-wider">OpenDrift</th>
<th className="px-4 py-3 text-center text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs font-bold text-text-3 uppercase tracking-wider"></th>
<th className="px-4 py-3 text-left text-xs 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>
<th className="px-4 py-3 text-left text-xs 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>
<th className="px-4 py-3 text-left text-xs 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>
<th className="px-4 py-3 text-right text-xs font-bold text-fg-disabled uppercase tracking-wider"></th>
<th className="px-4 py-3 text-center text-xs font-bold text-fg-disabled uppercase tracking-wider">KOSPS</th>
<th className="px-4 py-3 text-center text-xs font-bold text-fg-disabled uppercase tracking-wider">POSEIDON</th>
<th className="px-4 py-3 text-center text-xs font-bold text-fg-disabled uppercase tracking-wider">OpenDrift</th>
<th className="px-4 py-3 text-center text-xs 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>
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{currentAnalyses.map((analysis) => (
<tr
key={analysis.predRunSn ?? analysis.acdntSn}
className="hover:bg-bg-2 transition-colors cursor-pointer group"
className="hover:bg-bg-elevated transition-colors cursor-pointer group"
>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{analysis.acdntSn}</td>
<td className="px-4 py-3 text-sm 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-status-red animate-pulse" />
<span className="w-2 h-2 rounded-full bg-color-danger animate-pulse" />
<span
className="text-sm font-semibold text-primary-cyan hover:underline transition-all cursor-pointer"
className="text-sm font-semibold text-color-accent hover:underline transition-all cursor-pointer"
onClick={(e) => {
e.stopPropagation()
if (onSelectAnalysis) {
@ -188,19 +188,19 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{analysis.occurredAt ? new Date(analysis.occurredAt).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{analysis.runDtm ? new Date(analysis.runDtm).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
<td className="px-4 py-3 text-sm text-text-2 font-mono">{analysis.duration}</td>
<td className="px-4 py-3 text-sm text-text-2">{analysis.oilType}</td>
<td className="px-4 py-3 text-sm text-text-1 font-mono text-right font-semibold">
<td className="px-4 py-3 text-sm text-fg-sub font-mono">{analysis.occurredAt ? new Date(analysis.occurredAt).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
<td className="px-4 py-3 text-sm text-fg-sub font-mono">{analysis.runDtm ? new Date(analysis.runDtm).toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—'}</td>
<td className="px-4 py-3 text-sm text-fg-sub font-mono">{analysis.duration}</td>
<td className="px-4 py-3 text-sm text-fg-sub">{analysis.oilType}</td>
<td className="px-4 py-3 text-sm text-fg font-mono text-right font-semibold">
{analysis.volume != null ? analysis.volume.toFixed(2) : '—'}
</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.kospsStatus)}</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.poseidonStatus)}</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.opendriftStatus)}</td>
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.backtrackStatus)}</td>
<td className="px-4 py-3 text-sm text-text-2">{analysis.analyst}</td>
<td className="px-4 py-3 text-sm text-text-2">{analysis.officeName}</td>
<td className="px-4 py-3 text-sm text-fg-sub">{analysis.analyst}</td>
<td className="px-4 py-3 text-sm text-fg-sub">{analysis.officeName}</td>
</tr>
))}
</tbody>
@ -208,23 +208,23 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
)}
{!loading && analyses.length === 0 && (
<div className="text-center py-20 text-text-3 text-sm"> .</div>
<div className="text-center py-20 text-fg-disabled text-sm"> .</div>
)}
</div>
{/* 페이지네이션 */}
<div className="flex items-center justify-center gap-2 px-5 py-4 border-t border-border">
<div className="flex items-center justify-center gap-2 px-5 py-4 border-t border-stroke">
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="px-3 py-1 text-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1 text-sm 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-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1 text-sm font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
@ -234,14 +234,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-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1 text-sm 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-sm font-medium text-text-2 hover:bg-bg-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="px-3 py-1 text-sm font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>

파일 보기

@ -43,14 +43,14 @@ export function BacktrackModal({
>
<div style={{
width: '580px', maxHeight: 'calc(100vh - 120px)',
background: 'var(--bg1)',
background: 'var(--bg-surface)',
borderRadius: '14px',
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
}} className="border border-border overflow-hidden flex flex-col">
}} className="border border-stroke overflow-hidden flex flex-col">
{/* Header */}
<div style={{
padding: '20px 24px',
}} className="border-b border-border flex items-center gap-[14px]">
}} className="border-b border-stroke flex items-center gap-[14px]">
<div style={{
width: '40px', height: '40px', borderRadius: '10px',
background: 'linear-gradient(135deg, rgba(168,85,247,0.2), rgba(6,182,212,0.2))',
@ -63,7 +63,7 @@ export function BacktrackModal({
<h2 className="text-base font-bold m-0">
</h2>
<div className="text-[11px] text-text-3 mt-[2px]">
<div className="text-[11px] text-fg-disabled mt-[2px]">
AIS
</div>
</div>
@ -71,10 +71,10 @@ export function BacktrackModal({
onClick={onClose}
style={{
width: '32px', height: '32px', borderRadius: '8px',
background: 'var(--bg3)',
background: 'var(--bg-card)',
fontSize: '14px',
}}
className="border border-border text-text-3 cursor-pointer flex items-center justify-center"
className="border border-stroke text-fg-disabled cursor-pointer flex items-center justify-center"
>
</button>
@ -86,7 +86,7 @@ export function BacktrackModal({
}} className="flex-1 overflow-y-auto flex flex-col gap-4">
{/* Analysis Conditions */}
<div>
<h3 className="text-[12px] font-bold text-text-2 mb-[10px]">
<h3 className="text-[12px] font-bold text-fg-sub mb-[10px]">
</h3>
<div style={{
@ -99,10 +99,10 @@ export function BacktrackModal({
{ label: '유출 위치', value: `${conditions.spillLocation.lat.toFixed(4)}°N, ${conditions.spillLocation.lon.toFixed(4)}°E` },
].map((item, i) => (
<div key={i} style={{
padding: '10px 12px', background: 'var(--bg3)',
padding: '10px 12px', background: 'var(--bg-card)',
borderRadius: '8px',
}} className="border border-border">
<div className="text-[9px] text-text-3 mb-1">
}} className="border border-stroke">
<div className="text-[9px] text-fg-disabled mb-1">
{item.label}
</div>
<div className="text-[12px] font-semibold font-mono">
@ -111,15 +111,15 @@ export function BacktrackModal({
</div>
))}
<div style={{
padding: '10px 12px', background: 'var(--bg3)',
padding: '10px 12px', background: 'var(--bg-card)',
border: '1px solid rgba(168,85,247,0.3)', borderRadius: '8px',
gridColumn: '1 / -1',
}}>
<div className="text-[9px] text-text-3 mb-1">
<div className="text-[9px] text-fg-disabled mb-1">
</div>
<div className="text-sm font-bold text-primary-purple font-mono">
{conditions.totalVessels} <span className="text-[10px] font-medium text-text-3">(AIS )</span>
<div className="text-sm font-bold text-color-tertiary font-mono">
{conditions.totalVessels} <span className="text-[10px] font-medium text-fg-disabled">(AIS )</span>
</div>
</div>
</div>
@ -129,13 +129,13 @@ export function BacktrackModal({
{phase === 'results' && vessels.length > 0 && (
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-[12px] font-bold text-text-2 m-0">
<h3 className="text-[12px] font-bold text-fg-sub m-0">
</h3>
<div style={{
padding: '4px 10px', borderRadius: '12px',
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)',
}} className="text-[10px] font-bold text-status-red">
}} className="text-[10px] font-bold text-color-danger">
{conditions.totalVessels} {vessels.length}
</div>
</div>
@ -152,14 +152,14 @@ export function BacktrackModal({
{/* Footer */}
<div style={{
padding: '16px 24px',
}} className="border-t border-border flex gap-2">
}} className="border-t border-stroke flex gap-2">
{phase === 'conditions' && (
<button
onClick={onRunAnalysis}
style={{
padding: '12px',
borderRadius: '8px',
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
background: 'linear-gradient(135deg, var(--color-tertiary), var(--color-accent))',
border: 'none', color: '#fff',
}}
className="flex-1 text-[13px] font-bold cursor-pointer"
@ -173,10 +173,10 @@ export function BacktrackModal({
style={{
padding: '12px',
borderRadius: '8px',
background: 'var(--bg3)',
color: 'var(--purple)', cursor: 'wait',
background: 'var(--bg-card)',
color: 'var(--color-tertiary)', cursor: 'wait',
}}
className="flex-1 text-[13px] font-bold border border-border"
className="flex-1 text-[13px] font-bold border border-stroke"
>
AIS ...
</button>
@ -187,7 +187,7 @@ export function BacktrackModal({
style={{
padding: '12px',
borderRadius: '8px',
background: 'linear-gradient(135deg, var(--purple), var(--cyan))',
background: 'linear-gradient(135deg, var(--color-tertiary), var(--color-accent))',
border: 'none', color: '#fff',
}}
className="flex-1 text-[13px] font-bold cursor-pointer"
@ -202,15 +202,15 @@ export function BacktrackModal({
}
function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
const probColor = vessel.probability >= 80 ? 'var(--red)' :
vessel.probability >= 20 ? 'var(--orange)' : 'var(--t3)'
const probColor = vessel.probability >= 80 ? 'var(--color-danger)' :
vessel.probability >= 20 ? 'var(--color-warning)' : 'var(--fg-disabled)'
return (
<div style={{
padding: '14px', background: 'var(--bg0)',
padding: '14px', background: 'var(--bg-base)',
borderLeft: `4px solid ${vessel.color}`,
borderRadius: '10px',
}} className="border border-border">
}} className="border border-stroke">
{/* Header row */}
<div className="flex items-center gap-[10px] mb-[10px]">
<div style={{
@ -224,7 +224,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
<div className="text-[13px] font-bold font-mono">
{vessel.name}
</div>
<div className="text-[9px] text-text-3 font-mono mt-[2px]">
<div className="text-[9px] text-fg-disabled font-mono mt-[2px]">
IMO: {vessel.imo} · {vessel.type} · {vessel.flag}
</div>
</div>
@ -232,7 +232,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
<div style={{ fontSize: '22px', color: probColor, lineHeight: 1 }} className="font-bold font-mono">
{vessel.probability}%
</div>
<div className="text-[8px] text-text-3"> </div>
<div className="text-[8px] text-fg-disabled"> </div>
</div>
</div>
@ -245,14 +245,14 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
{ label: 'AIS 상태', value: vessel.aisStatus, highlight: vessel.aisStatus === '충돌신호' },
].map((s, i) => (
<div key={i} style={{
padding: '6px', background: 'var(--bg3)', borderRadius: '6px',
border: s.highlight ? '1px solid rgba(239,68,68,0.3)' : '1px solid var(--bd)',
padding: '6px', background: 'var(--bg-card)', borderRadius: '6px',
border: s.highlight ? '1px solid rgba(239,68,68,0.3)' : '1px solid var(--stroke-default)',
}}>
<div className="text-[8px] text-text-3 mb-[2px]">
<div className="text-[8px] text-fg-disabled mb-[2px]">
{s.label}
</div>
<div style={{
color: s.highlight ? 'var(--red)' : 'var(--t1)',
color: s.highlight ? 'var(--color-danger)' : 'var(--fg-default)',
}} className="text-[10px] font-semibold font-mono">
{s.value}
</div>
@ -266,7 +266,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
padding: '8px 10px', background: 'rgba(239,68,68,0.05)',
border: '1px solid rgba(239,68,68,0.15)', borderRadius: '6px',
lineHeight: '1.5',
}} className="text-[9px] text-text-2">
}} className="text-[9px] text-fg-sub">
{vessel.description}
</div>
)}

파일 보기

@ -17,7 +17,7 @@ export function BoomDeploymentTheoryView() {
}
return (
<div className="flex flex-col h-full overflow-hidden bg-bg-0">
<div className="flex flex-col h-full overflow-hidden bg-bg-base">
<div className="flex-1 overflow-y-auto scrollbar-thin p-5">
{/* 헤더 */}
@ -29,11 +29,11 @@ export function BoomDeploymentTheoryView() {
</div>
<div>
<div className="text-[15px] font-bold"> </div>
<div className="text-[10px] mt-0.5 text-text-3">Oil Boom Deployment Optimization · · </div>
<div className="text-[10px] mt-0.5 text-fg-disabled">Oil Boom Deployment Optimization · · </div>
</div>
</div>
<button onClick={handleExportPDF}
className="px-3.5 py-1.5 rounded-md text-[10px] font-semibold cursor-pointer text-primary-blue"
className="px-3.5 py-1.5 rounded-md text-[10px] font-semibold cursor-pointer text-color-info"
style={{ border: '1px solid rgba(59,130,246,.3)', background: 'rgba(59,130,246,.08)' }}>
📤 PDF
</button>
@ -41,7 +41,7 @@ export function BoomDeploymentTheoryView() {
{/* 내부 네비게이션 */}
<div className="mb-5">
<div className="flex gap-[3px] rounded-lg p-1 bg-bg-3 border border-border">
<div className="flex gap-[3px] rounded-lg p-1 bg-bg-card border border-stroke">
{boomTabs.map(tab => (
<button
key={tab.id}
@ -49,8 +49,8 @@ export function BoomDeploymentTheoryView() {
className="flex-1 py-2 px-2 text-[12px] font-semibold rounded-md transition-all"
style={{
background: activePanel === tab.id ? 'rgba(249,115,22,.15)' : undefined,
color: activePanel === tab.id ? 'var(--orange)' : 'var(--t3)',
border: activePanel === tab.id ? '1px solid rgba(249,115,22,.3)' : '1px solid var(--bd)',
color: activePanel === tab.id ? 'var(--color-warning)' : 'var(--fg-disabled)',
border: activePanel === tab.id ? '1px solid rgba(249,115,22,.3)' : '1px solid var(--stroke-default)',
}}
>
{tab.label}
@ -79,22 +79,22 @@ function OverviewPanel() {
{/* 인트로 카드 */}
<div className="rounded-xl p-5 mb-4 relative overflow-hidden"
style={{ background: 'linear-gradient(135deg,rgba(249,115,22,.06),rgba(234,179,8,.04))', border: '1px solid rgba(249,115,22,.2)' }}>
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--orange),var(--yellow),var(--cyan))' }} />
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--color-warning),var(--color-caution),var(--color-accent))' }} />
<div className="grid grid-cols-2 gap-5">
<div>
<div className="text-[13px] font-bold mb-2">🛡 ?</div>
<div className="text-[11px] leading-[1.8] text-text-2">
<b className="text-status-orange"> </b> (··) , ( · ) <b className="text-primary-cyan"> </b> ·· .
<div className="text-[11px] leading-[1.8] text-fg-sub">
<b className="text-color-warning"> </b> (··) , ( · ) <b className="text-color-accent"> </b> ·· .
</div>
</div>
<div>
<div className="text-[13px] font-bold mb-2">🎯 WING </div>
<div className="flex flex-col gap-[5px] text-[11px] text-text-2">
<div className="flex flex-col gap-[5px] text-[11px] text-fg-sub">
{[
{ num: '①', color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', text: '차단 면적 최대화 — 예측 유출유 확산 경계와 오일펜스 교차 면적 극대화' },
{ num: '②', color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', text: '도달시간 최소화 — 유출유 해안·ESI 민감구역 도달 전 선제적 차단선 구축' },
{ num: '③', color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', text: '자원 효율 최적화 — 가용 오일펜스 길이·방제정 수·이동시간 제약 충족' },
{ num: '④', color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', text: '실패 안전성 확보 — 조류 초과 시 오일펜스 이탈 방지 방향각 자동 보정' },
{ num: '①', color: 'var(--color-warning)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', text: '차단 면적 최대화 — 예측 유출유 확산 경계와 오일펜스 교차 면적 극대화' },
{ num: '②', color: 'var(--color-accent)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', text: '도달시간 최소화 — 유출유 해안·ESI 민감구역 도달 전 선제적 차단선 구축' },
{ num: '③', color: 'var(--color-success)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', text: '자원 효율 최적화 — 가용 오일펜스 길이·방제정 수·이동시간 제약 충족' },
{ num: '④', color: 'var(--color-tertiary)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', text: '실패 안전성 확보 — 조류 초과 시 오일펜스 이탈 방지 방향각 자동 보정' },
].map((item, i) => (
<div key={i} className="px-2.5 py-1.5 rounded-md" style={{ background: item.bg, border: `1px solid ${item.bd}` }}>
<span className="font-bold" style={{ color: item.color }}>{item.num}</span> <b>{item.text}</b>
@ -106,16 +106,16 @@ function OverviewPanel() {
</div>
{/* 전체 흐름도 */}
<div className="rounded-md p-4 mb-4 bg-bg-3 border border-border">
<div className="rounded-md p-4 mb-4 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3.5"> WING </div>
<div className="flex items-center justify-center gap-0 flex-nowrap overflow-x-auto py-2">
{[
{ icon: '🌊', label: '확산예측', sub: 'KOSPS/POSEIDON\nOpenDrift', color: 'var(--orange)', bg: 'rgba(249,115,22,.08)', bd: 'rgba(249,115,22,.2)' },
{ icon: '📡', label: '환경입력', sub: '조류·풍향\n파고·수심', color: 'var(--cyan)', bg: 'rgba(6,182,212,.08)', bd: 'rgba(6,182,212,.2)' },
{ icon: '🗺️', label: '차단선 후보', sub: '격자탐색\n후보지점 생성', color: 'var(--yellow)', bg: 'rgba(234,179,8,.08)', bd: 'rgba(234,179,8,.2)' },
{ icon: '⚙️', label: '최적화 엔진', sub: '다목적\n유전알고리즘', color: 'var(--purple)', bg: 'rgba(168,85,247,.08)', bd: 'rgba(168,85,247,.3)', bold: true },
{ icon: '✅', label: '배치안 출력', sub: '좌표·형태\n방향·순서', color: 'var(--green)', bg: 'rgba(34,197,94,.08)', bd: 'rgba(34,197,94,.2)' },
{ icon: '🗺️', label: '지도 표시', sub: 'ESI 중첩\n방제자원 연계', color: 'var(--blue)', bg: 'rgba(59,130,246,.08)', bd: 'rgba(59,130,246,.2)' },
{ icon: '🌊', label: '확산예측', sub: 'KOSPS/POSEIDON\nOpenDrift', color: 'var(--color-warning)', bg: 'rgba(249,115,22,.08)', bd: 'rgba(249,115,22,.2)' },
{ icon: '📡', label: '환경입력', sub: '조류·풍향\n파고·수심', color: 'var(--color-accent)', bg: 'rgba(6,182,212,.08)', bd: 'rgba(6,182,212,.2)' },
{ icon: '🗺️', label: '차단선 후보', sub: '격자탐색\n후보지점 생성', color: 'var(--color-caution)', bg: 'rgba(234,179,8,.08)', bd: 'rgba(234,179,8,.2)' },
{ icon: '⚙️', label: '최적화 엔진', sub: '다목적\n유전알고리즘', color: 'var(--color-tertiary)', bg: 'rgba(168,85,247,.08)', bd: 'rgba(168,85,247,.3)', bold: true },
{ icon: '✅', label: '배치안 출력', sub: '좌표·형태\n방향·순서', color: 'var(--color-success)', bg: 'rgba(34,197,94,.08)', bd: 'rgba(34,197,94,.2)' },
{ icon: '🗺️', label: '지도 표시', sub: 'ESI 중첩\n방제자원 연계', color: 'var(--color-info)', bg: 'rgba(59,130,246,.08)', bd: 'rgba(59,130,246,.2)' },
].map((step, i) => (
<div key={i} className="flex items-center">
<div className="text-center min-w-[80px] rounded-lg px-3 py-2.5 text-[9px]" style={{
@ -124,9 +124,9 @@ function OverviewPanel() {
}}>
<div className="text-[15px] mb-1">{step.icon}</div>
<div className="font-bold" style={{ color: step.color }}>{step.label}</div>
<div className="text-text-3" style={{ whiteSpace: 'pre-line' }}>{step.sub}</div>
<div className="text-fg-disabled" style={{ whiteSpace: 'pre-line' }}>{step.sub}</div>
</div>
{i < 5 && <div className="px-1.5 text-text-3 text-[14px]"></div>}
{i < 5 && <div className="px-1.5 text-fg-disabled text-[14px]"></div>}
</div>
))}
</div>
@ -135,16 +135,16 @@ function OverviewPanel() {
{/* 오일펜스 종류 */}
<div className="grid grid-cols-3 gap-3 mb-4">
{[
{ icon: '⛽', title: '고형 오일펜스', color: 'var(--orange)', desc: '단단한 부체와 수중커튼으로 구성. 정적 배치. 항구·좁은 수로에 적합.', specs: ['내조류 한계: 0.5~1.0 knot', '높이: 30~60cm · 수중 30~60cm', '전개속도: 30~60m/hr'] },
{ icon: '🌊', title: '공기충전식 오일펜스', color: 'var(--blue)', desc: '공기로 부력 확보. 이동·보관 편리. 해상 광역 차단에 주로 사용.', specs: ['내조류 한계: 0.7~1.5 knot', '높이: 45~90cm · 수중 45~90cm', '전개속도: 100~300m/hr'] },
{ icon: '🔄', title: '자항식 오일펜스', color: 'var(--green)', desc: '방제정 예인 또는 자체 추진. U형·V형 동적 배치. 강조류 해역 적합.', specs: ['운용수심: 5m 이상', 'U형·V형·J형 동적 형태', '내조류: 조류각도 보정으로 극복'] },
{ icon: '⛽', title: '고형 오일펜스', color: 'var(--color-warning)', desc: '단단한 부체와 수중커튼으로 구성. 정적 배치. 항구·좁은 수로에 적합.', specs: ['내조류 한계: 0.5~1.0 knot', '높이: 30~60cm · 수중 30~60cm', '전개속도: 30~60m/hr'] },
{ icon: '🌊', title: '공기충전식 오일펜스', color: 'var(--color-info)', desc: '공기로 부력 확보. 이동·보관 편리. 해상 광역 차단에 주로 사용.', specs: ['내조류 한계: 0.7~1.5 knot', '높이: 45~90cm · 수중 45~90cm', '전개속도: 100~300m/hr'] },
{ icon: '🔄', title: '자항식 오일펜스', color: 'var(--color-success)', desc: '방제정 예인 또는 자체 추진. U형·V형 동적 배치. 강조류 해역 적합.', specs: ['운용수심: 5m 이상', 'U형·V형·J형 동적 형태', '내조류: 조류각도 보정으로 극복'] },
].map((boom, i) => (
<div key={i} className="rounded-md p-3.5 bg-bg-3 border border-border" style={{ borderTop: `3px solid ${boom.color}` }}>
<div key={i} className="rounded-md p-3.5 bg-bg-card border border-stroke" style={{ borderTop: `3px solid ${boom.color}` }}>
<div className="text-[11px] font-bold mb-2" style={{ color: boom.color }}>{boom.icon} {boom.title}</div>
<div className="text-[10px] mb-2 leading-[1.7] text-text-2">{boom.desc}</div>
<div className="flex flex-col gap-[3px] text-[9px] text-text-2">
<div className="text-[10px] mb-2 leading-[1.7] text-fg-sub">{boom.desc}</div>
<div className="flex flex-col gap-[3px] text-[9px] text-fg-sub">
{boom.specs.map((spec, j) => (
<div key={j} className="px-[7px] py-[3px] rounded-[3px] text-text-2" style={{ background: `${boom.color}11` }}>{spec}</div>
<div key={j} className="px-[7px] py-[3px] rounded-[3px] text-fg-sub" style={{ background: `${boom.color}11` }}>{spec}</div>
))}
</div>
</div>
@ -152,17 +152,17 @@ function OverviewPanel() {
</div>
{/* 핵심 제약조건 */}
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
<div className="text-[11px] font-bold mb-2.5 text-status-red"> </div>
<div className="grid grid-cols-3 gap-2.5 text-[10px] text-text-2">
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-[11px] font-bold mb-2.5 text-color-danger"> </div>
<div className="grid grid-cols-3 gap-2.5 text-[10px] text-fg-sub">
{[
{ icon: '🌊', title: '조류 제약', color: 'var(--red)', bg: 'rgba(239,68,68,.05)', bd: 'rgba(239,68,68,.15)', lines: ['조류속도 > 임계유속 시', '오일펜스 하단 통과 발생', 'U<0.7 knot 유지 필수', '임계각도 자동 계산 적용'] },
{ icon: '📏', title: '자원 제약', color: 'var(--yellow)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.15)', lines: ['가용 오일펜스 총 길이', '방제정 척수·이동시간', '앵커링 가능 수심 조건', '연결부 허용 장력'] },
{ icon: '⏱️', title: '시간 제약', color: 'var(--blue)', bg: 'rgba(59,130,246,.05)', bd: 'rgba(59,130,246,.15)', lines: ['유출유 도달 예측시간', '오일펜스 전개 소요시간', '방제정 현장 도착시간', '조석 변환 주기 고려'] },
{ icon: '🌊', title: '조류 제약', color: 'var(--color-danger)', bg: 'rgba(239,68,68,.05)', bd: 'rgba(239,68,68,.15)', lines: ['조류속도 > 임계유속 시', '오일펜스 하단 통과 발생', 'U<0.7 knot 유지 필수', '임계각도 자동 계산 적용'] },
{ icon: '📏', title: '자원 제약', color: 'var(--color-caution)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.15)', lines: ['가용 오일펜스 총 길이', '방제정 척수·이동시간', '앵커링 가능 수심 조건', '연결부 허용 장력'] },
{ icon: '⏱️', title: '시간 제약', color: 'var(--color-info)', bg: 'rgba(59,130,246,.05)', bd: 'rgba(59,130,246,.15)', lines: ['유출유 도달 예측시간', '오일펜스 전개 소요시간', '방제정 현장 도착시간', '조석 변환 주기 고려'] },
].map((c, i) => (
<div key={i} className="p-2.5 rounded-[7px]" style={{ background: c.bg, border: `1px solid ${c.bd}` }}>
<div className="font-bold mb-[5px]" style={{ color: c.color }}>{c.icon} {c.title}</div>
<div className="leading-[1.7] text-text-2">
<div className="leading-[1.7] text-fg-sub">
{c.lines.map((l, j) => <span key={j}>{j > 0 && <br />}{l}</span>)}
</div>
</div>
@ -178,44 +178,44 @@ function DeploymentTheoryPanel() {
return (
<>
{/* 차단 효율 이론 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3">📐 (Boom Containment Efficiency)</div>
<div className="grid grid-cols-2 gap-3.5">
<div>
<div className="text-[11px] font-bold mb-2 text-status-orange"> E(θ, U)</div>
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">
<div className="text-[11px] font-bold mb-2 text-color-warning"> E(θ, U)</div>
<div className="text-[10px] leading-[1.8] mb-2 text-fg-sub">
<b> (U)</b> <b> (θ)</b> . , .
</div>
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
E(θ,U) = 1 <span className="text-status-red">F<sub>loss</sub>(U<sub>n</sub>)</span><br />
U<sub>n</sub> = U · sin(θ) <span className="text-[9px] text-text-3">( )</span><br />
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-base font-mono" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
E(θ,U) = 1 <span className="text-color-danger">F<sub>loss</sub>(U<sub>n</sub>)</span><br />
U<sub>n</sub> = U · sin(θ) <span className="text-[9px] text-fg-disabled">( )</span><br />
E = 1 (U<sub>n</sub> U<sub>c</sub>)<br />
E = max(0, 1 (U<sub>n</sub>/U<sub>c</sub>)²) (U<sub>n</sub> &gt; U<sub>c</sub>)<br />
<span className="text-[9px] text-text-3">U<sub>c</sub>: ( 0.35m/s = 0.7 knot)</span>
<span className="text-[9px] text-fg-disabled">U<sub>c</sub>: ( 0.35m/s = 0.7 knot)</span>
</div>
</div>
<div>
<div className="text-[11px] font-bold mb-2 text-primary-cyan"> θ* </div>
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">
. <b></b> , <b className="text-primary-cyan">30°~45° </b> .
<div className="text-[11px] font-bold mb-2 text-color-accent"> θ* </div>
<div className="text-[10px] leading-[1.8] mb-2 text-fg-sub">
. <b></b> , <b className="text-color-accent">30°~45° </b> .
</div>
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(6,182,212,.2)' }}>
θ* = arcsin(U<sub>c</sub> / U) <span className="text-[9px] text-text-3">()</span><br />
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-base font-mono" style={{ border: '1px solid rgba(6,182,212,.2)' }}>
θ* = arcsin(U<sub>c</sub> / U) <span className="text-[9px] text-fg-disabled">()</span><br />
θ<sub>opt</sub> = argmax [A<sub>block</sub>(θ) · E(θ,U)]<br />
실용범위: 15° θ 60°<br />
<span className="text-[9px] text-text-3">, θ &lt; arcsin(U<sub>c</sub>/U) </span>
<span className="text-[9px] text-fg-disabled">, θ &lt; arcsin(U<sub>c</sub>/U) </span>
</div>
</div>
</div>
</div>
{/* V형·U형·J형 배치 패턴 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3">🔷 </div>
<div className="grid grid-cols-3 gap-3">
{/* V형 */}
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(6,182,212,.2)', borderTop: '3px solid var(--cyan)' }}>
<div className="text-[11px] font-bold mb-2 text-primary-cyan">V형 (Chevron)</div>
<div className="rounded-lg p-3.5 bg-bg-base" style={{ border: '1px solid rgba(6,182,212,.2)', borderTop: '3px solid var(--color-accent)' }}>
<div className="text-[11px] font-bold mb-2 text-color-accent">V형 (Chevron)</div>
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(6,182,212,.04)' }}>
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
<defs><marker id="arr1" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
@ -227,17 +227,17 @@ function DeploymentTheoryPanel() {
<text x="58" y="64" fill="rgba(6,182,212,.7)" fontSize="6"></text>
</svg>
</div>
<div className="text-[10px] leading-[1.7] mb-[7px] text-text-2"> V형. . .</div>
<div className="text-[10px] leading-[1.7] mb-[7px] text-fg-sub"> V형. . .</div>
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9] font-mono" style={{ background: 'rgba(6,182,212,.05)' }}>
A<sub>V</sub> = L²·sin(2α)/2<br />
<span className="text-text-3">α: 반개각, L: 편측 </span><br />
<span className="text-fg-disabled">α: 반개각, L: 편측 </span><br />
α = 30°~45°
</div>
</div>
{/* U형 */}
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(34,197,94,.2)', borderTop: '3px solid var(--green)' }}>
<div className="text-[11px] font-bold mb-2 text-status-green">U형 (Horseshoe)</div>
<div className="rounded-lg p-3.5 bg-bg-base" style={{ border: '1px solid rgba(34,197,94,.2)', borderTop: '3px solid var(--color-success)' }}>
<div className="text-[11px] font-bold mb-2 text-color-success">U형 (Horseshoe)</div>
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(34,197,94,.04)' }}>
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
<defs><marker id="arr2" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
@ -248,17 +248,17 @@ function DeploymentTheoryPanel() {
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6"></text>
</svg>
</div>
<div className="text-[10px] leading-[1.7] mb-[7px] text-text-2"> . . .</div>
<div className="text-[10px] leading-[1.7] mb-[7px] text-fg-sub"> . . .</div>
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9] font-mono" style={{ background: 'rgba(34,197,94,.05)' }}>
A<sub>U</sub> = π·r²/2 + 2r·h<br />
<span className="text-text-3">r: 반경, h: 직선부 </span><br />
<span className="text-fg-disabled">r: 반경, h: 직선부 </span><br />
전제: U &lt; 0.5 knot
</div>
</div>
{/* J형 */}
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(168,85,247,.2)', borderTop: '3px solid var(--purple)' }}>
<div className="text-[11px] font-bold mb-2 text-primary-purple">J형 (Skimming)</div>
<div className="rounded-lg p-3.5 bg-bg-base" style={{ border: '1px solid rgba(168,85,247,.2)', borderTop: '3px solid var(--color-tertiary)' }}>
<div className="text-[11px] font-bold mb-2 text-color-tertiary">J형 (Skimming)</div>
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(168,85,247,.04)' }}>
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
<defs><marker id="arr3" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
@ -270,10 +270,10 @@ function DeploymentTheoryPanel() {
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6"></text>
</svg>
</div>
<div className="text-[10px] leading-[1.7] mb-[7px] text-text-2">+ . . · .</div>
<div className="text-[10px] leading-[1.7] mb-[7px] text-fg-sub">+ . . · .</div>
<div className="rounded-[5px] p-[7px] text-[9px] leading-[1.9] font-mono" style={{ background: 'rgba(168,85,247,.05)' }}>
θ<sub>J</sub> = arcsin(U<sub>c</sub>/U) + δ<br />
<span className="text-text-3">δ: 안전여유각(5°~10°)</span><br />
<span className="text-fg-disabled">δ: 안전여유각(5°~10°)</span><br />
활용: U &gt; 0.7 knot
</div>
</div>
@ -281,24 +281,24 @@ function DeploymentTheoryPanel() {
</div>
{/* 다단 배치 이론 */}
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-[11px] font-bold mb-2.5">🔢 (Multi-Boom) </div>
<div className="grid grid-cols-2 gap-3">
<div className="text-[10px] leading-[1.8] text-text-2">
<div className="text-[10px] leading-[1.8] text-fg-sub">
<b> </b> . n개 :
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-0 font-mono">
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-base font-mono">
E<sub>total</sub> = 1 (1E<sub>i</sub>)<br />
<span className="text-[9px] text-text-3">E<sub>i</sub>: i번째 </span>
<span className="text-[9px] text-fg-disabled">E<sub>i</sub>: i번째 </span>
</div>
</div>
<div className="flex flex-col gap-[5px] text-[9px] text-text-2">
<div className="flex flex-col gap-[5px] text-[9px] text-fg-sub">
{[
{ color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', label: '2단 직렬', text: ': E_total = E₁+E₂E₁·E₂ (예: 70%+70% → 91%)' },
{ color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', label: '단간 거리', text: ': 부표 집적 방지를 위해 ≥ 200m 이격 권장' },
{ color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', label: '배치 우선순위', text: ': ESI 고등급 구역 보호 → 취수원 → 어항 순' },
{ color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', label: '조석 변화', text: ': 창조·낙조 전환 시 오일펜스 방향 재조정 필요' },
{ color: 'var(--color-success)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', label: '2단 직렬', text: ': E_total = E₁+E₂E₁·E₂ (예: 70%+70% → 91%)' },
{ color: 'var(--color-accent)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', label: '단간 거리', text: ': 부표 집적 방지를 위해 ≥ 200m 이격 권장' },
{ color: 'var(--color-tertiary)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', label: '배치 우선순위', text: ': ESI 고등급 구역 보호 → 취수원 → 어항 순' },
{ color: 'var(--color-warning)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', label: '조석 변화', text: ': 창조·낙조 전환 시 오일펜스 방향 재조정 필요' },
].map((item, i) => (
<div key={i} className="px-[9px] py-1.5 rounded-[5px] text-text-2" style={{ background: item.bg, border: `1px solid ${item.bd}` }}>
<div key={i} className="px-[9px] py-1.5 rounded-[5px] text-fg-sub" style={{ background: item.bg, border: `1px solid ${item.bd}` }}>
<b style={{ color: item.color }}>{item.label}</b>{item.text}
</div>
))}
@ -316,54 +316,54 @@ function OptimizationPanel() {
{/* 다목적 최적화 개요 */}
<div className="rounded-xl p-4 mb-3.5 relative overflow-hidden"
style={{ background: 'linear-gradient(135deg,rgba(168,85,247,.06),rgba(59,130,246,.04))', border: '1px solid rgba(168,85,247,.2)' }}>
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--purple),var(--blue))' }} />
<div className="absolute top-0 left-0 right-0 h-[3px]" style={{ background: 'linear-gradient(90deg,var(--color-tertiary),var(--color-info))' }} />
<div className="text-[13px] font-bold mb-2"> (Multi-Objective Optimization)</div>
<div className="text-[11px] leading-[1.8] text-text-2">
<b className="text-primary-purple"> </b> . , <b className="text-primary-cyan"> (Pareto Optimal) </b> .
<div className="text-[11px] leading-[1.8] text-fg-sub">
<b className="text-color-tertiary"> </b> . , <b className="text-color-accent"> (Pareto Optimal) </b> .
</div>
</div>
{/* 목적함수 정의 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3">📊 </div>
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="rounded-lg p-3 bg-bg-0" style={{ border: '1px solid rgba(34,197,94,.2)' }}>
<div className="text-[11px] font-bold mb-2 text-status-green">🎯 F(x)</div>
<div className="rounded-lg p-3 bg-bg-base" style={{ border: '1px solid rgba(34,197,94,.2)' }}>
<div className="text-[11px] font-bold mb-2 text-color-success">🎯 F(x)</div>
<div className="rounded-[5px] p-[9px] text-[10px] leading-[2.2] font-mono" style={{ background: 'rgba(34,197,94,.04)' }}>
<b className="text-status-green">:</b><br />
f(x) = Σ A<sub>blocked,i</sub> · w<sub>ESI,i</sub> <span className="text-[9px] text-text-3">( )</span><br />
f(x) = T<sub>deadline</sub> T<sub>deploy</sub> <span className="text-[9px] text-text-3">()</span><br />
<b className="text-status-red">:</b><br />
f(x) = Σ L<sub>boom,j</sub> <span className="text-[9px] text-text-3">( )</span><br />
f(x) = Σ D<sub>vessel,k</sub> <span className="text-[9px] text-text-3">( )</span>
<b className="text-color-success">:</b><br />
f(x) = Σ A<sub>blocked,i</sub> · w<sub>ESI,i</sub> <span className="text-[9px] text-fg-disabled">( )</span><br />
f(x) = T<sub>deadline</sub> T<sub>deploy</sub> <span className="text-[9px] text-fg-disabled">()</span><br />
<b className="text-color-danger">:</b><br />
f(x) = Σ L<sub>boom,j</sub> <span className="text-[9px] text-fg-disabled">( )</span><br />
f(x) = Σ D<sub>vessel,k</sub> <span className="text-[9px] text-fg-disabled">( )</span>
</div>
</div>
<div className="rounded-lg p-3 bg-bg-0" style={{ border: '1px solid rgba(239,68,68,.2)' }}>
<div className="text-[11px] font-bold mb-2 text-status-red">🚫 G(x)</div>
<div className="rounded-lg p-3 bg-bg-base" style={{ border: '1px solid rgba(239,68,68,.2)' }}>
<div className="text-[11px] font-bold mb-2 text-color-danger">🚫 G(x)</div>
<div className="rounded-[5px] p-[9px] text-[10px] leading-[2.2] font-mono" style={{ background: 'rgba(239,68,68,.04)' }}>
g: U·sin(θ<sub>i</sub>) U<sub>c</sub> i <span className="text-[9px] text-text-3">()</span><br />
g: Σ L<sub>j</sub> L<sub>max</sub> <span className="text-[9px] text-text-3">( )</span><br />
g: T<sub>deploy,i</sub> T<sub>arrive,i</sub> <span className="text-[9px] text-text-3">( )</span><br />
g: d(p<sub>i</sub>, shore) d<sub>min</sub> <span className="text-[9px] text-text-3">( )</span><br />
g: h(p<sub>i</sub>) h<sub>min</sub> <span className="text-[9px] text-text-3">( )</span>
g: U·sin(θ<sub>i</sub>) U<sub>c</sub> i <span className="text-[9px] text-fg-disabled">()</span><br />
g: Σ L<sub>j</sub> L<sub>max</sub> <span className="text-[9px] text-fg-disabled">( )</span><br />
g: T<sub>deploy,i</sub> T<sub>arrive,i</sub> <span className="text-[9px] text-fg-disabled">( )</span><br />
g: d(p<sub>i</sub>, shore) d<sub>min</sub> <span className="text-[9px] text-fg-disabled">( )</span><br />
g: h(p<sub>i</sub>) h<sub>min</sub> <span className="text-[9px] text-fg-disabled">( )</span>
</div>
</div>
</div>
{/* ESI 가중치 */}
<div className="rounded-lg p-3 bg-bg-0" style={{ border: '1px solid rgba(234,179,8,.2)' }}>
<div className="text-[10px] font-bold mb-2 text-status-yellow">🏖 ESI w<sub>ESI</sub> </div>
<div className="grid grid-cols-5 gap-[5px] text-[9px] text-text-2">
<div className="rounded-lg p-3 bg-bg-base" style={{ border: '1px solid rgba(234,179,8,.2)' }}>
<div className="text-[10px] font-bold mb-2 text-color-caution">🏖 ESI w<sub>ESI</sub> </div>
<div className="grid grid-cols-5 gap-[5px] text-[9px] text-fg-sub">
{[
{ grade: 'ESI 1~2', desc: '노출암반', w: 'w = 0.2', color: 'var(--green)', bg: 'rgba(34,197,94,.06)' },
{ grade: 'ESI 3~4', desc: '모래해변', w: 'w = 0.4', color: 'var(--cyan)', bg: 'rgba(6,182,212,.06)' },
{ grade: 'ESI 5~6', desc: '자갈·조약', w: 'w = 0.6', color: 'var(--yellow)', bg: 'rgba(234,179,8,.06)' },
{ grade: 'ESI 7~8', desc: '갯벌·조간대', w: 'w = 0.85', color: 'var(--orange)', bg: 'rgba(249,115,22,.06)' },
{ grade: 'ESI 9~10', desc: '맹그로브·습지', w: 'w = 1.0', color: 'var(--red)', bg: 'rgba(239,68,68,.08)', bd: 'rgba(239,68,68,.2)' },
{ grade: 'ESI 1~2', desc: '노출암반', w: 'w = 0.2', color: 'var(--color-success)', bg: 'rgba(34,197,94,.06)' },
{ grade: 'ESI 3~4', desc: '모래해변', w: 'w = 0.4', color: 'var(--color-accent)', bg: 'rgba(6,182,212,.06)' },
{ grade: 'ESI 5~6', desc: '자갈·조약', w: 'w = 0.6', color: 'var(--color-caution)', bg: 'rgba(234,179,8,.06)' },
{ grade: 'ESI 7~8', desc: '갯벌·조간대', w: 'w = 0.85', color: 'var(--color-warning)', bg: 'rgba(249,115,22,.06)' },
{ grade: 'ESI 9~10', desc: '맹그로브·습지', w: 'w = 1.0', color: 'var(--color-danger)', bg: 'rgba(239,68,68,.08)', bd: 'rgba(239,68,68,.2)' },
].map((esi, i) => (
<div key={i} className="p-1.5 rounded text-center" style={{ background: esi.bg, border: esi.bd ? `1px solid ${esi.bd}` : undefined }}>
<div className="font-bold" style={{ color: esi.color }}>{esi.grade}</div>
<div className="text-text-3">{esi.desc}</div>
<div className="text-fg-disabled">{esi.desc}</div>
<div className="font-bold">{esi.w}</div>
</div>
))}
@ -372,14 +372,14 @@ function OptimizationPanel() {
</div>
{/* NSGA-II 알고리즘 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
<div className="text-xs font-bold mb-3 text-primary-purple">🧬 NSGA-II (Non-dominated Sorting Genetic Algorithm II)</div>
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3 text-color-tertiary">🧬 NSGA-II (Non-dominated Sorting Genetic Algorithm II)</div>
<div className="grid grid-cols-2 gap-3">
<div>
<div className="text-[10px] leading-[1.8] mb-2 text-text-2">
WING의 <b className="text-primary-purple">NSGA-II</b>(Deb et al., 2002) . (Pareto Front) .
<div className="text-[10px] leading-[1.8] mb-2 text-fg-sub">
WING의 <b className="text-color-tertiary">NSGA-II</b>(Deb et al., 2002) . (Pareto Front) .
</div>
<div className="flex flex-col gap-1 text-[9px] text-text-2">
<div className="flex flex-col gap-1 text-[9px] text-fg-sub">
{[
'염색체 구조 : [배치지점 좌표, 방향각θ, 길이L, 형태, 배치순서]',
'집단 크기 : 100~200개체 · 세대수 50~200',
@ -387,15 +387,15 @@ function OptimizationPanel() {
'변이 연산 : 다항식 변이(Polynomial Mutation) · ηm=20',
'선택 방식 : 비지배 정렬 + 혼잡도 거리(Crowding Distance)',
].map((item, i) => (
<div key={i} className="px-2 py-[5px] rounded text-text-2" style={{ background: 'rgba(168,85,247,.05)', border: '1px solid rgba(168,85,247,.12)' }}>
<b className="text-primary-purple">{item.split(' : ')[0]}</b> : {item.split(' : ')[1]}
<div key={i} className="px-2 py-[5px] rounded text-fg-sub" style={{ background: 'rgba(168,85,247,.05)', border: '1px solid rgba(168,85,247,.12)' }}>
<b className="text-color-tertiary">{item.split(' : ')[0]}</b> : {item.split(' : ')[1]}
</div>
))}
</div>
</div>
<div>
<div className="text-[10px] font-bold mb-[7px] text-text-2">NSGA-II 5 </div>
<div className="flex flex-col gap-[5px] text-[9px] text-text-2">
<div className="text-[10px] font-bold mb-[7px] text-fg-sub">NSGA-II 5 </div>
<div className="flex flex-col gap-[5px] text-[9px] text-fg-sub">
{[
{ step: '①', title: '초기 집단 생성', desc: '확산예측 결과 기반 랜덤 + 휴리스틱 배치안 혼합 초기화' },
{ step: '②', title: '적합도 평가', desc: '유출유 확산 시뮬레이터로 각 배치안의 차단면적·도달시간 계산' },
@ -404,8 +404,8 @@ function OptimizationPanel() {
{ step: '⑤', title: '엘리트 선택', desc: '부모+자식 2N 집단에서 비지배 정렬+혼잡도 기준으로 N개 선택 → 수렴까지 반복' },
].map((item, i) => (
<div key={i} className="flex gap-2 px-[9px] py-1.5 rounded-[5px]" style={{ background: i === 4 ? 'rgba(168,85,247,.05)' : 'rgba(168,85,247,.04)', border: i === 4 ? '1px solid rgba(168,85,247,.12)' : undefined }}>
<span className="min-w-[20px] font-extrabold text-primary-purple">{item.step}</span>
<div className="leading-[1.6] text-text-2"><b>{item.title}</b> : {item.desc}</div>
<span className="min-w-[20px] font-extrabold text-color-tertiary">{item.step}</span>
<div className="leading-[1.6] text-fg-sub"><b>{item.title}</b> : {item.desc}</div>
</div>
))}
</div>
@ -414,29 +414,29 @@ function OptimizationPanel() {
</div>
{/* 보조 알고리즘 비교 */}
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-[11px] font-bold mb-2.5">🔬 </div>
<div className="overflow-x-auto">
<table className="w-full text-[10px] border-collapse">
<thead>
<tr style={{ background: 'rgba(255,255,255,.03)', borderBottom: '1px solid var(--bdL)' }}>
<tr style={{ background: 'rgba(255,255,255,.03)', borderBottom: '1px solid var(--stroke-light)' }}>
{['알고리즘', '유형', '장점', '단점', 'WING 활용'].map(h => (
<th key={h} className="py-[7px] px-2.5 font-semibold text-text-3" style={{ textAlign: h === '알고리즘' ? 'left' : 'center' }}>{h}</th>
<th key={h} className="py-[7px] px-2.5 font-semibold text-fg-disabled" style={{ textAlign: h === '알고리즘' ? 'left' : 'center' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{[
{ name: 'NSGA-II', color: 'var(--purple)', type: '다목적 GA', pros: '파레토 전면 탐색\n다양성 유지 우수', cons: '계산비용 높음\n수렴 느림', wing: '메인 엔진', wingColor: 'var(--cyan)' },
{ name: 'PSO', color: 'var(--orange)', type: '입자군집', pros: '빠른 수렴\n구현 단순', cons: '조기수렴\n다목적 취약', wing: '단일목적 빠른 배치', wingColor: 'var(--t2)' },
{ name: 'SA', color: 'var(--blue)', type: '모의담금질', pros: '전역 탈출 우수\n국소최적 회피', cons: '매개변수 민감\n느린 수렴', wing: '긴급 단순 배치', wingColor: 'var(--t2)' },
{ name: 'Greedy+휴리스틱', color: 'var(--green)', type: '결정론적', pros: '즉시 결과\n해석 용이', cons: '전역최적 미보장', wing: '실시간 초기 제안', wingColor: 'var(--green)' },
{ name: 'NSGA-II', color: 'var(--color-tertiary)', type: '다목적 GA', pros: '파레토 전면 탐색\n다양성 유지 우수', cons: '계산비용 높음\n수렴 느림', wing: '메인 엔진', wingColor: 'var(--color-accent)' },
{ name: 'PSO', color: 'var(--color-warning)', type: '입자군집', pros: '빠른 수렴\n구현 단순', cons: '조기수렴\n다목적 취약', wing: '단일목적 빠른 배치', wingColor: 'var(--fg-sub)' },
{ name: 'SA', color: 'var(--color-info)', type: '모의담금질', pros: '전역 탈출 우수\n국소최적 회피', cons: '매개변수 민감\n느린 수렴', wing: '긴급 단순 배치', wingColor: 'var(--fg-sub)' },
{ name: 'Greedy+휴리스틱', color: 'var(--color-success)', type: '결정론적', pros: '즉시 결과\n해석 용이', cons: '전역최적 미보장', wing: '실시간 초기 제안', wingColor: 'var(--color-success)' },
].map((row, i) => (
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,.04)', background: i % 2 === 1 ? 'rgba(255,255,255,.01)' : undefined }}>
<td className="py-[7px] px-2.5 font-bold" style={{ color: row.color }}>{row.name}</td>
<td className="py-[7px] px-2.5 text-center text-text-2">{row.type}</td>
<td className="py-[7px] px-2.5 text-center text-text-2 whitespace-pre-line">{row.pros}</td>
<td className="py-[7px] px-2.5 text-center text-text-2 whitespace-pre-line">{row.cons}</td>
<td className="py-[7px] px-2.5 text-center text-fg-sub">{row.type}</td>
<td className="py-[7px] px-2.5 text-center text-fg-sub whitespace-pre-line">{row.pros}</td>
<td className="py-[7px] px-2.5 text-center text-fg-sub whitespace-pre-line">{row.cons}</td>
<td className="py-[7px] px-2.5 text-center" style={{ color: row.wingColor }}>{row.wing}</td>
</tr>
))}
@ -453,54 +453,54 @@ function FluidDynamicsPanel() {
return (
<>
{/* 유동 수치 모델 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3">🌊 </div>
<div className="grid grid-cols-2 gap-3.5">
<div>
<div className="text-[11px] font-bold mb-2 text-primary-blue"> </div>
<div className="text-[10px] leading-[1.8] mb-2 text-text-2"> . (catenary형태) .</div>
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
<div className="text-[11px] font-bold mb-2 text-color-info"> </div>
<div className="text-[10px] leading-[1.8] mb-2 text-fg-sub"> . (catenary형태) .</div>
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-base font-mono" style={{ border: '1px solid rgba(59,130,246,.2)' }}>
F<sub>D</sub> = ½ · ρ · C<sub>D</sub> · A · U<sub>n</sub>²<br />
T = F<sub>D</sub> · L / (2·sin(α))<br />
<span className="text-[9px] text-text-3">C<sub>D</sub>: (1.2), A: 수중 </span><br />
<span className="text-[9px] text-text-3">T: 연결부 , α: 체인각도</span>
<span className="text-[9px] text-fg-disabled">C<sub>D</sub>: (1.2), A: 수중 </span><br />
<span className="text-[9px] text-fg-disabled">T: 연결부 , α: 체인각도</span>
</div>
</div>
<div>
<div className="text-[11px] font-bold mb-2 text-status-orange"> (Splash-over) </div>
<div className="text-[10px] leading-[1.8] mb-2 text-text-2"> Splash-over가 .</div>
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-0 font-mono" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
<div className="text-[11px] font-bold mb-2 text-color-warning"> (Splash-over) </div>
<div className="text-[10px] leading-[1.8] mb-2 text-fg-sub"> Splash-over가 .</div>
<div className="rounded-md p-2.5 text-[10px] leading-[2.1] bg-bg-base font-mono" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
Fr = U<sub>n</sub> / (g·Δρ/ρ·h)<br />
Splash-over: Fr &gt; 0.5~0.6<br />
<span className="text-[9px] text-text-3">Fr: 수정 Froude수, h: 오일펜스 </span><br />
<span className="text-[9px] text-text-3">Δρ/ρ: 기름- (~0.15)</span>
<span className="text-[9px] text-fg-disabled">Fr: 수정 Froude수, h: 오일펜스 </span><br />
<span className="text-[9px] text-fg-disabled">Δρ/ρ: 기름- (~0.15)</span>
</div>
</div>
</div>
</div>
{/* Catenary 변형 모델 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3">🔗 (Catenary) </div>
<div className="grid grid-cols-2 gap-3.5">
<div className="text-[10px] leading-[1.8] text-text-2">
<div className="text-[10px] leading-[1.8] text-fg-sub">
(Catenary) . , <b> </b> L<sub>eff</sub> .
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-0 font-mono">
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-base font-mono">
y(x) = a·cosh(x/a) a<br />
L<sub>arc</sub> = 2a·sinh(L<sub>span</sub>/(2a))<br />
L<sub>eff</sub> = L<sub>span</sub> · cos(φ<sub>max</sub>)<br />
<span className="text-[9px] text-text-3">a: catenary , φ: 최대 </span>
<span className="text-[9px] text-fg-disabled">a: catenary , φ: 최대 </span>
</div>
</div>
<div className="flex flex-col gap-1.5 text-[9px] text-text-2">
<div className="text-[10px] font-bold mb-1 text-text-2"> </div>
<div className="flex flex-col gap-1.5 text-[9px] text-fg-sub">
<div className="text-[10px] font-bold mb-1 text-fg-sub"> </div>
{[
{ cond: 'U < 0.3 knot', result: 'L_eff ≈ L (직선 유지)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)' },
{ cond: '0.3~0.7 knot', result: 'L_eff = 0.8~0.95 L (경미 변형)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.12)' },
{ cond: '0.7~1.0 knot', result: 'L_eff = 0.5~0.8 L (Catenary 현저)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)' },
{ cond: 'U > 1.0 knot', result: '기름 통과 위험 · 배치 재계산', bg: 'rgba(239,68,68,.05)', bd: 'rgba(239,68,68,.12)', danger: true },
].map((item, i) => (
<div key={i} className="px-[9px] py-1.5 rounded-[5px]" style={{ background: item.bg, border: `1px solid ${item.bd}`, color: item.danger ? 'var(--red)' : 'var(--t2)' }}>
<div key={i} className="px-[9px] py-1.5 rounded-[5px]" style={{ background: item.bg, border: `1px solid ${item.bd}`, color: item.danger ? 'var(--color-danger)' : 'var(--fg-sub)' }}>
{item.cond} {item.result}
</div>
))}
@ -509,12 +509,12 @@ function FluidDynamicsPanel() {
</div>
{/* 유막 포집 모델 */}
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-[11px] font-bold mb-2.5">🛢 </div>
<div className="grid grid-cols-2 gap-3 text-[10px] text-text-2">
<div className="grid grid-cols-2 gap-3 text-[10px] text-fg-sub">
<div>
<div className="font-bold mb-1.5 text-primary-cyan"> </div>
<div className="rounded-[5px] p-[9px] leading-[2] bg-bg-0 font-mono">
<div className="font-bold mb-1.5 text-color-accent"> </div>
<div className="rounded-[5px] p-[9px] leading-[2] bg-bg-base font-mono">
dV<sub>oil</sub>/dt = Q<sub>in</sub> Q<sub>out</sub> Q<sub>loss</sub><br />
Q<sub>in</sub> = U<sub>oil</sub>·h<sub>oil</sub>·L<sub>eff</sub><br />
Q<sub>out</sub> = Q<sub>skim</sub> ( )<br />
@ -522,8 +522,8 @@ function FluidDynamicsPanel() {
</div>
</div>
<div>
<div className="font-bold mb-1.5 text-status-orange"> </div>
<div className="text-[10px] leading-[1.7] text-text-2"> 70~80% Skimmer . overflow . WING이 .</div>
<div className="font-bold mb-1.5 text-color-warning"> </div>
<div className="text-[10px] leading-[1.7] text-fg-sub"> 70~80% Skimmer . overflow . WING이 .</div>
</div>
</div>
</div>
@ -534,25 +534,25 @@ function FluidDynamicsPanel() {
/* ──────────── PANEL 4: 현장 적용 ──────────── */
function FieldApplicationPanel() {
const steps = [
{ num: 1, color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.15)', numBg: 'rgba(249,115,22,.15)', numBd: 'rgba(249,115,22,.3)', title: '확산예측 결과 분석 — 위협 구역 및 도달시간 산출', desc: 'KOSPS·POSEIDON·OpenDrift 앙상블 예측 결과에서 유출유 확산 경계선(Pollution Boundary)과 각 ESI 구역별 도달 예상시간(T_arrive)을 산출합니다. 신뢰도 70% 이상 예측 경계를 기준으로 차단 전략 영역을 설정합니다.' },
{ num: 2, color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.15)', numBg: 'rgba(6,182,212,.15)', numBd: 'rgba(6,182,212,.3)', title: '해양환경 조건 확인 — 조류·파고·수심·기상 입력', desc: 'CHARRY 채리모델 조류예측값, KMA UM 풍속·풍향, 수심격자, NGSST 수온을 자동 연계하여 각 후보 배치지점의 U_n(법선방향 유속)을 계산합니다. 임계유속 0.7 knot를 초과하는 지점은 자동으로 J형·다단 배치로 변환합니다.' },
{ num: 3, color: 'var(--yellow)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.15)', numBg: 'rgba(234,179,8,.15)', numBd: 'rgba(234,179,8,.3)', title: '후보 차단선 격자 탐색 — 배치 가능지점 생성', desc: '확산 예측 경계를 따라 500m 간격 격자로 후보 배치지점을 생성합니다. 각 지점에서 조류 조건·수심·해안선 이격·방제정 접근 가능성을 동시 검토합니다. ESI 고등급 구역 전방 2km 이내 지점에 우선 가중치를 부여합니다.' },
{ num: 4, color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.15)', numBg: 'rgba(168,85,247,.15)', numBd: 'rgba(168,85,247,.3)', title: 'NSGA-II 최적화 실행 — 파레토 최적 배치안 산출', desc: '후보 배치지점·방향각·형태 조합을 염색체로 인코딩하여 NSGA-II 다목적 유전알고리즘을 실행합니다. 수렴 후 파레토 전면에서 3~5개 추천 배치안을 제시하며, 의사결정자가 차단 효율과 자원 사용량의 트레이드오프를 보고 선택합니다.' },
{ num: 5, color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.15)', numBg: 'rgba(34,197,94,.15)', numBd: 'rgba(34,197,94,.3)', title: '실시간 재최적화 — 조석 변환·조류 변화 대응', desc: '창조→낙조 전환 시(약 6시간 주기) 조류 방향 역전에 따른 오일펜스 재배치 알람을 발령합니다. 확산 예측이 갱신될 때마다 배치 최적화를 자동 재실행하여 방제대응 체계를 동적으로 업데이트합니다.' },
{ num: 1, color: 'var(--color-warning)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.15)', numBg: 'rgba(249,115,22,.15)', numBd: 'rgba(249,115,22,.3)', title: '확산예측 결과 분석 — 위협 구역 및 도달시간 산출', desc: 'KOSPS·POSEIDON·OpenDrift 앙상블 예측 결과에서 유출유 확산 경계선(Pollution Boundary)과 각 ESI 구역별 도달 예상시간(T_arrive)을 산출합니다. 신뢰도 70% 이상 예측 경계를 기준으로 차단 전략 영역을 설정합니다.' },
{ num: 2, color: 'var(--color-accent)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.15)', numBg: 'rgba(6,182,212,.15)', numBd: 'rgba(6,182,212,.3)', title: '해양환경 조건 확인 — 조류·파고·수심·기상 입력', desc: 'CHARRY 채리모델 조류예측값, KMA UM 풍속·풍향, 수심격자, NGSST 수온을 자동 연계하여 각 후보 배치지점의 U_n(법선방향 유속)을 계산합니다. 임계유속 0.7 knot를 초과하는 지점은 자동으로 J형·다단 배치로 변환합니다.' },
{ num: 3, color: 'var(--color-caution)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.15)', numBg: 'rgba(234,179,8,.15)', numBd: 'rgba(234,179,8,.3)', title: '후보 차단선 격자 탐색 — 배치 가능지점 생성', desc: '확산 예측 경계를 따라 500m 간격 격자로 후보 배치지점을 생성합니다. 각 지점에서 조류 조건·수심·해안선 이격·방제정 접근 가능성을 동시 검토합니다. ESI 고등급 구역 전방 2km 이내 지점에 우선 가중치를 부여합니다.' },
{ num: 4, color: 'var(--color-tertiary)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.15)', numBg: 'rgba(168,85,247,.15)', numBd: 'rgba(168,85,247,.3)', title: 'NSGA-II 최적화 실행 — 파레토 최적 배치안 산출', desc: '후보 배치지점·방향각·형태 조합을 염색체로 인코딩하여 NSGA-II 다목적 유전알고리즘을 실행합니다. 수렴 후 파레토 전면에서 3~5개 추천 배치안을 제시하며, 의사결정자가 차단 효율과 자원 사용량의 트레이드오프를 보고 선택합니다.' },
{ num: 5, color: 'var(--color-success)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.15)', numBg: 'rgba(34,197,94,.15)', numBd: 'rgba(34,197,94,.3)', title: '실시간 재최적화 — 조석 변환·조류 변화 대응', desc: '창조→낙조 전환 시(약 6시간 주기) 조류 방향 역전에 따른 오일펜스 재배치 알람을 발령합니다. 확산 예측이 갱신될 때마다 배치 최적화를 자동 재실행하여 방제대응 체계를 동적으로 업데이트합니다.' },
]
return (
<>
{/* 배치 5단계 절차 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-xs font-bold mb-3">🗺 WING 5</div>
<div className="flex flex-col gap-2">
{steps.map(step => (
<div key={step.num} className="flex gap-3 items-start p-3 rounded-lg" 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" style={{ background: step.numBg, border: `1px solid ${step.numBd}`, color: step.color }}>{step.num}</div>
<div className="text-[10px] text-text-2">
<div className="text-[10px] text-fg-sub">
<div className="font-bold mb-1">{step.title}</div>
<div className="leading-[1.7] text-text-2">{step.desc}</div>
<div className="leading-[1.7] text-fg-sub">{step.desc}</div>
</div>
</div>
))}
@ -560,17 +560,17 @@ function FieldApplicationPanel() {
</div>
{/* 해역별 적용 특성 */}
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-[11px] font-bold mb-2.5">📍 </div>
<div className="grid grid-cols-3 gap-2.5 text-[9px] text-text-2">
<div className="grid grid-cols-3 gap-2.5 text-[9px] text-fg-sub">
{[
{ icon: '🌊', title: '서해 (조차 대형)', color: 'var(--blue)', bg: 'rgba(59,130,246,.05)', bd: 'rgba(59,130,246,.12)', desc: '최대 조차 9m (인천), 조류 최대 3~5 knot. J형 배치 주력. 조석 전환 재배치 필수. 앵커링 수심 급변화 주의.' },
{ icon: '🌿', title: '남해 (다도해)', color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', desc: '복잡한 해안선·섬. 조류 1~2 knot. V형·U형 복합 배치. 좁은 수로 통제 우선. ESI 고등급 갯벌 보호.' },
{ icon: '🏔️', title: '동해 (심해형)', color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', desc: '조차 소(0.3m), 너울·파고 높음. 조류 0.5~1 knot. V형 집중. 고파랑 시 배치 제한. 수온약층 고려.' },
{ icon: '🌊', title: '서해 (조차 대형)', color: 'var(--color-info)', bg: 'rgba(59,130,246,.05)', bd: 'rgba(59,130,246,.12)', desc: '최대 조차 9m (인천), 조류 최대 3~5 knot. J형 배치 주력. 조석 전환 재배치 필수. 앵커링 수심 급변화 주의.' },
{ icon: '🌿', title: '남해 (다도해)', color: 'var(--color-success)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', desc: '복잡한 해안선·섬. 조류 1~2 knot. V형·U형 복합 배치. 좁은 수로 통제 우선. ESI 고등급 갯벌 보호.' },
{ icon: '🏔️', title: '동해 (심해형)', color: 'var(--color-warning)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', desc: '조차 소(0.3m), 너울·파고 높음. 조류 0.5~1 knot. V형 집중. 고파랑 시 배치 제한. 수온약층 고려.' },
].map((area, i) => (
<div key={i} className="p-2.5 rounded-[7px]" style={{ background: area.bg, border: `1px solid ${area.bd}` }}>
<div className="font-bold mb-[5px]" style={{ color: area.color }}>{area.icon} {area.title}</div>
<div className="leading-[1.7] text-text-2">{area.desc}</div>
<div className="leading-[1.7] text-fg-sub">{area.desc}</div>
</div>
))}
</div>
@ -584,7 +584,7 @@ function ReferencesPanel() {
const categories = [
{
title: '⚙️ 최적화 알고리즘',
color: 'var(--purple)',
color: 'var(--color-tertiary)',
bg: 'rgba(168,85,247,.1)',
bd: 'rgba(168,85,247,.25)',
refs: [
@ -595,7 +595,7 @@ function ReferencesPanel() {
},
{
title: '🌊 유체역학 이론',
color: 'var(--blue)',
color: 'var(--color-info)',
bg: 'rgba(59,130,246,.1)',
bd: 'rgba(59,130,246,.25)',
refs: [
@ -607,7 +607,7 @@ function ReferencesPanel() {
},
{
title: '📐 오일펜스 배치 설계',
color: 'var(--cyan)',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.1)',
bd: 'rgba(6,182,212,.25)',
refs: [
@ -616,7 +616,7 @@ function ReferencesPanel() {
},
{
title: '🗺️ 방제 운용 기준',
color: 'var(--green)',
color: 'var(--color-success)',
bg: 'rgba(34,197,94,.1)',
bd: 'rgba(34,197,94,.25)',
refs: [
@ -630,19 +630,19 @@ function ReferencesPanel() {
return (
<>
<div className="text-xs font-bold mb-1">📚 </div>
<div className="text-[10px] mb-3.5 text-text-3"> 12 · 4 </div>
<div className="text-[10px] mb-3.5 text-fg-disabled"> 12 · 4 </div>
{categories.map((cat, ci) => (
<div key={ci} className="mb-4">
<div className="text-[10px] font-bold mb-[7px] flex items-center gap-1.5" style={{ color: cat.color }}>
<span className="px-[7px] py-0.5 rounded" style={{ background: cat.bg, border: `1px solid ${cat.bd}` }}>{cat.title}</span>
</div>
<div className="flex flex-col gap-[5px] text-[9px] text-text-2">
<div className="flex flex-col gap-[5px] text-[9px] text-fg-sub">
{cat.refs.map((ref, ri) => (
<div key={ri} className="p-[9px] px-3 rounded-[7px] grid gap-2" style={{
gridTemplateColumns: '24px 1fr',
background: 'var(--bg3)',
border: ref.highlight ? `1px solid ${cat.bd}` : '1px solid var(--bd)',
background: 'var(--bg-card)',
border: ref.highlight ? `1px solid ${cat.bd}` : '1px solid var(--stroke-default)',
borderLeft: ref.highlight ? `3px solid ${cat.color}` : undefined,
}}>
<div className="w-[22px] h-[22px] rounded flex items-center justify-center text-[10px] flex-shrink-0" style={{
@ -654,8 +654,8 @@ function ReferencesPanel() {
</div>
<div>
<div className="font-bold mb-0.5">{ref.title}</div>
<div className="leading-[1.6] text-text-3">{ref.author}</div>
<div className="mt-0.5 text-text-2">{ref.desc}</div>
<div className="leading-[1.6] text-fg-disabled">{ref.author}</div>
<div className="mt-0.5 text-fg-sub">{ref.desc}</div>
</div>
</div>
))}

파일 보기

@ -33,15 +33,15 @@ const InfoLayerSection = ({
const effectiveLayers: Layer[] = layerTree ?? []
return (
<div className="border-b border-border">
<div className="border-b border-stroke">
<div
className="flex items-center justify-between p-4 hover:bg-[rgba(255,255,255,0.02)]"
>
<h3
onClick={onToggle}
className="text-[13px] font-bold text-text-1 font-korean cursor-pointer"
className="text-[13px] font-bold text-fg font-korean cursor-pointer"
>
📂
</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<button
@ -65,15 +65,15 @@ const InfoLayerSection = ({
padding: '4px 8px',
fontSize: '10px',
fontWeight: 600,
border: '1px solid var(--cyan)',
borderRadius: 'var(--rS)',
border: '1px solid var(--stroke-default)',
borderRadius: 'var(--radius-sm)',
background: 'transparent',
color: 'var(--cyan)',
color: 'var(--fg-sub)',
cursor: 'pointer',
transition: '0.15s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(6,182,212,0.1)'
e.currentTarget.style.background = 'var(--bg-surface-hover)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
@ -102,15 +102,15 @@ const InfoLayerSection = ({
padding: '4px 8px',
fontSize: '10px',
fontWeight: 600,
border: '1px solid var(--red)',
borderRadius: 'var(--rS)',
border: '1px solid var(--stroke-default)',
borderRadius: 'var(--radius-sm)',
background: 'transparent',
color: 'var(--red)',
color: 'var(--fg-sub)',
cursor: 'pointer',
transition: '0.15s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239,68,68,0.1)'
e.currentTarget.style.background = 'var(--bg-surface-hover)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
@ -120,7 +120,7 @@ const InfoLayerSection = ({
</button>
<span
onClick={onToggle}
className="text-[10px] text-text-3 cursor-pointer"
className="text-[10px] text-fg-disabled cursor-pointer"
>
{expanded ? '▼' : '▶'}
</span>
@ -130,9 +130,9 @@ const InfoLayerSection = ({
{expanded && (
<div className="px-4 pb-2">
{isLoading && effectiveLayers.length === 0 ? (
<p className="text-[11px] text-text-3 py-2"> ...</p>
<p className="text-[11px] text-fg-disabled py-2"> ...</p>
) : effectiveLayers.length === 0 ? (
<p className="text-[11px] text-text-3 py-2"> .</p>
<p className="text-[11px] text-fg-disabled py-2"> .</p>
) : (
<LayerTree
layers={effectiveLayers}

파일 보기

@ -110,6 +110,7 @@ export function LeftPanel({
onLayerBrightnessChange,
sensitiveResources = [],
onImageAnalysisResult,
validationErrors,
}: LeftPanelProps) {
const [expandedSections, setExpandedSections] = useState<ExpandedSections>({
predictionInput: true,
@ -127,9 +128,9 @@ export function LeftPanel({
}
return (
<div className="w-80 min-w-[320px] bg-bg-1 border-r border-border flex flex-col">
<div className="w-80 min-w-[320px] bg-bg-surface border-r border-stroke flex flex-col">
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent">
<div className="flex-1 overflow-y-scroll scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent" style={{ scrollbarGutter: 'stable' }}>
{/* Prediction Input Section */}
<PredictionInputSection
expanded={expandedSections.predictionInput}
@ -160,18 +161,19 @@ export function LeftPanel({
spillUnit={spillUnit}
onSpillUnitChange={onSpillUnitChange}
onImageAnalysisResult={onImageAnalysisResult}
validationErrors={validationErrors}
/>
{/* Incident Section */}
<div className="border-b border-border">
<div className="border-b border-stroke">
<div
onClick={() => toggleSection('incident')}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
>
<h3 className="text-[13px] font-bold text-text-2 font-korean">
<h3 className="text-[13px] font-bold text-fg-sub font-korean">
</h3>
<span className="text-[10px] text-text-3">
<span className="text-[10px] text-fg-disabled">
{expandedSections.incident ? '▼' : '▶'}
</span>
</div>
@ -184,18 +186,18 @@ export function LeftPanel({
const statusMap: Record<string, { label: string; style: string; dot: string }> = {
ACTIVE: {
label: '진행중',
style: 'bg-[rgba(239,68,68,0.15)] text-status-red border border-[rgba(239,68,68,0.3)]',
dot: 'bg-status-red animate-pulse',
style: 'bg-[rgba(239,68,68,0.15)] text-color-danger border border-[rgba(239,68,68,0.3)]',
dot: 'bg-color-danger animate-pulse',
},
INVESTIGATING: {
label: '조사중',
style: 'bg-[rgba(249,115,22,0.15)] text-status-orange border border-[rgba(249,115,22,0.3)]',
dot: 'bg-status-orange animate-pulse',
style: 'bg-[rgba(249,115,22,0.15)] text-color-warning border border-[rgba(249,115,22,0.3)]',
dot: 'bg-color-warning animate-pulse',
},
CLOSED: {
label: '종료',
style: 'bg-[rgba(100,116,139,0.15)] text-text-3 border border-[rgba(100,116,139,0.3)]',
dot: 'bg-text-3',
style: 'bg-[rgba(100,116,139,0.15)] text-fg-disabled border border-[rgba(100,116,139,0.3)]',
dot: 'bg-fg-disabled',
},
}
const s = statusMap[selectedAnalysis.acdntSttsCd] ?? statusMap['ACTIVE']
@ -210,53 +212,53 @@ export function LeftPanel({
{/* Info Grid */}
<div className="grid gap-1">
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-mono">{selectedAnalysis.acdntSn}</span>
<span className="text-[10px] text-fg-disabled min-w-[52px] font-korean"></span>
<span className="text-[11px] text-fg font-medium font-mono">{selectedAnalysis.acdntSn}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-korean">{selectedAnalysis.acdntNm || '—'}</span>
<span className="text-[10px] text-fg-disabled min-w-[52px] font-korean"></span>
<span className="text-[11px] text-fg font-medium font-korean">{selectedAnalysis.acdntNm || '—'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-mono">{selectedAnalysis.occurredAt ? selectedAnalysis.occurredAt.slice(0, 16).replace(' ', 'T') : '—'}</span>
<span className="text-[10px] text-fg-disabled min-w-[52px] font-korean"></span>
<span className="text-[11px] text-fg font-medium font-mono">{selectedAnalysis.occurredAt ? selectedAnalysis.occurredAt.slice(0, 16).replace(' ', 'T') : '—'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-korean">{selectedAnalysis.oilType || '—'}</span>
<span className="text-[10px] text-fg-disabled min-w-[52px] font-korean"></span>
<span className="text-[11px] text-fg font-medium font-korean">{selectedAnalysis.oilType || '—'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-mono">{selectedAnalysis.volume != null ? `${selectedAnalysis.volume.toFixed(2)} kl` : '—'}</span>
<span className="text-[10px] text-fg-disabled min-w-[52px] font-korean"></span>
<span className="text-[11px] text-fg font-medium font-mono">{selectedAnalysis.volume != null ? `${selectedAnalysis.volume.toFixed(2)} kl` : '—'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-text-1 font-medium font-korean">{selectedAnalysis.analyst || '—'}</span>
<span className="text-[10px] text-fg-disabled min-w-[52px] font-korean"></span>
<span className="text-[11px] text-fg font-medium font-korean">{selectedAnalysis.analyst || '—'}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[10px] text-text-3 min-w-[52px] font-korean"></span>
<span className="text-[11px] text-status-orange font-semibold font-korean">{selectedAnalysis.location || '—'}</span>
<span className="text-[10px] text-fg-disabled min-w-[52px] font-korean"></span>
<span className="text-[11px] text-color-warning font-semibold font-korean">{selectedAnalysis.location || '—'}</span>
</div>
</div>
</div>
) : (
<div className="px-4 pb-4">
<p className="text-[11px] text-text-3 font-korean text-center py-2"> .</p>
<p className="text-[11px] text-fg-disabled font-korean text-center py-2"> .</p>
</div>
)
)}
</div>
{/* Impact Resources Section */}
<div className="border-b border-border">
<div className="border-b border-stroke">
<div
onClick={() => toggleSection('impactResources')}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
>
<h3 className="text-[13px] font-bold text-text-2 font-korean">
<h3 className="text-[13px] font-bold text-fg-sub font-korean">
</h3>
<span className="text-[10px] text-text-3">
<span className="text-[10px] text-fg-disabled">
{expandedSections.impactResources ? '▼' : '▶'}
</span>
</div>
@ -264,7 +266,7 @@ export function LeftPanel({
{expandedSections.impactResources && (
<div className="px-4 pb-4">
{sensitiveResources.length === 0 ? (
<p className="text-[11px] text-text-3 font-korean"> </p>
<p className="text-[13px] text-fg-disabled text-center font-korean"> </p>
) : (
<div className="space-y-1.5">
{sensitiveResources.map(({ category, count, totalArea }) => {
@ -278,7 +280,7 @@ export function LeftPanel({
>
{meta.icon}
</span>
<span className="text-[11px] text-text-2 font-korean">{category}</span>
<span className="text-[11px] text-fg-sub font-korean">{category}</span>
</div>
<span className="text-[11px] text-primary font-bold font-mono">
{totalArea != null

파일 보기

@ -71,15 +71,15 @@ const OilBoomSection = ({
}
return (
<div className="border-b border-border">
<div className="border-b border-stroke">
<div
onClick={onToggle}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
>
<h3 className="text-[13px] font-bold text-text-2 font-korean">
🛡
<h3 className="text-[13px] font-bold text-fg-sub font-korean">
</h3>
<span className="text-[10px] text-text-3">
<span className="text-[10px] text-fg-disabled">
{expanded ? '▼' : '▶'}
</span>
</div>
@ -104,10 +104,10 @@ const OilBoomSection = ({
}}
style={{
padding: '6px 8px',
borderRadius: 'var(--rS)',
border: boomPlacementTab === tab.id ? '1px solid var(--orange)' : '1px solid var(--bd)',
background: boomPlacementTab === tab.id ? 'rgba(245,158,11,0.1)' : 'var(--bg0)',
color: boomPlacementTab === tab.id ? 'var(--orange)' : 'var(--t3)',
borderRadius: 'var(--radius-sm)',
border: boomPlacementTab === tab.id ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--stroke-default)',
background: boomPlacementTab === tab.id ? 'rgba(6,182,212,0.08)' : 'var(--bg-base)',
color: boomPlacementTab === tab.id ? 'var(--color-accent)' : 'var(--fg-disabled)',
transition: '0.15s',
}}
className="flex-1 text-[10px] font-semibold cursor-pointer"
@ -120,10 +120,10 @@ const OilBoomSection = ({
disabled={!hasData}
style={{
padding: '6px 10px',
borderRadius: 'var(--rS)',
border: '1px solid var(--bd)',
background: 'var(--bg0)',
color: hasData ? 'var(--red)' : 'var(--t3)',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--stroke-default)',
background: 'var(--bg-base)',
color: hasData ? 'var(--color-danger)' : 'var(--fg-disabled)',
cursor: hasData ? 'pointer' : 'not-allowed',
transition: '0.15s',
}}
@ -139,12 +139,12 @@ const OilBoomSection = ({
padding: '14px',
background: 'rgba(239,68,68,0.06)',
border: '1px solid rgba(239,68,68,0.3)',
borderRadius: 'var(--rM)',
borderRadius: 'var(--radius-md)',
}}>
<div className="text-[11px] font-bold text-text-1 font-korean mb-2">
<div className="text-[11px] font-bold text-fg font-korean mb-2">
</div>
<div className="text-[9px] text-text-3 font-korean mb-3">
<div className="text-[9px] text-fg-disabled font-korean mb-3">
. .
</div>
<div className="flex gap-2">
@ -153,9 +153,9 @@ const OilBoomSection = ({
style={{
padding: '6px 14px',
background: 'rgba(239,68,68,0.15)',
border: '1px solid var(--red)',
borderRadius: 'var(--rS)',
color: 'var(--red)',
border: '1px solid var(--color-danger)',
borderRadius: 'var(--radius-sm)',
color: 'var(--color-danger)',
transition: '0.15s',
}}
className="flex-1 text-[10px] font-bold cursor-pointer"
@ -166,10 +166,10 @@ const OilBoomSection = ({
onClick={() => setShowResetConfirm(false)}
style={{
padding: '6px 14px',
background: 'var(--bg0)',
border: '1px solid var(--bd)',
borderRadius: 'var(--rS)',
color: 'var(--t2)',
background: 'var(--bg-base)',
border: '1px solid var(--stroke-default)',
borderRadius: 'var(--radius-sm)',
color: 'var(--fg-sub)',
transition: '0.15s',
}}
className="flex-1 text-[10px] font-bold cursor-pointer"
@ -183,20 +183,20 @@ const OilBoomSection = ({
{/* Key Metrics */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '6px' }}>
{[
{ value: String(boomLines.length), label: '배치 라인', color: 'var(--orange)' },
{ value: boomLines.length > 0 ? `${(boomLines.reduce((s, l) => s + l.length, 0) / 1000).toFixed(1)}km` : '0km', label: '총 길이', color: 'var(--cyan)' },
{ value: boomLines.length > 0 ? `${Math.round(boomLines.reduce((s, l) => s + l.efficiency, 0) / boomLines.length)}%` : '—', label: '평균 효율', color: 'var(--orange)' },
{ value: String(boomLines.length), label: '배치 라인', color: 'var(--color-accent)' },
{ value: boomLines.length > 0 ? `${(boomLines.reduce((s, l) => s + l.length, 0) / 1000).toFixed(1)}km` : '0km', label: '총 길이', color: 'var(--color-accent)' },
{ value: boomLines.length > 0 ? `${Math.round(boomLines.reduce((s, l) => s + l.efficiency, 0) / boomLines.length)}%` : '—', label: '평균 효율', color: 'var(--color-accent)' },
].map((metric, idx) => (
<div key={idx} style={{
padding: '10px 8px',
background: 'var(--bg0)',
borderRadius: 'var(--rS)',
background: 'var(--bg-base)',
borderRadius: 'var(--radius-sm)',
textAlign: 'center',
}} className="border border-border">
}} className="border border-stroke">
<div style={{ color: metric.color }} className="text-lg font-bold font-mono mb-[2px]">
{metric.value}
</div>
<div className="text-[8px] text-text-3">
<div className="text-[8px] text-fg-disabled">
{metric.label}
</div>
</div>
@ -208,10 +208,10 @@ const OilBoomSection = ({
<>
{/* 전제조건 체크 */}
<div className="flex flex-col gap-1.5">
<div style={{ background: 'var(--bg0)', borderRadius: 'var(--rS)' }}
className="flex items-center gap-1.5 p-[6px_10px] border border-border text-[10px]">
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--red)' }} />
<span style={{ color: oilTrajectory.length > 0 ? 'var(--green)' : 'var(--t3)' }}>
<div style={{ background: 'var(--bg-base)', borderRadius: 'var(--radius-sm)' }}
className="flex items-center gap-1.5 p-[6px_10px] border border-stroke text-[10px]">
<span style={{ width: '8px', height: '8px', borderRadius: '50%', background: oilTrajectory.length > 0 ? 'var(--color-success)' : 'var(--color-danger)' }} />
<span style={{ color: oilTrajectory.length > 0 ? 'var(--color-success)' : 'var(--fg-disabled)' }}>
{oilTrajectory.length > 0 ? `(${oilTrajectory.length}개 입자)` : '없음'}
</span>
</div>
@ -219,7 +219,7 @@ const OilBoomSection = ({
{/* 알고리즘 설정 */}
<div>
<h4 className="text-[11px] font-bold text-primary-cyan mb-2" style={{ letterSpacing: '0.5px' }}>
<h4 className="text-[11px] font-bold text-fg-sub mb-2" style={{ letterSpacing: '0.5px' }}>
📊 V자형
</h4>
<div className="flex flex-col gap-2">
@ -230,10 +230,10 @@ const OilBoomSection = ({
{ label: '파고 보정 계수', key: 'waveHeightCorrectionFactor' as const, unit: 'x', value: algorithmSettings.waveHeightCorrectionFactor },
].map((setting) => (
<div key={setting.key} style={{
background: 'var(--bg0)',
borderRadius: 'var(--rS)',
}} className="flex items-center justify-between p-[6px_8px] border border-border">
<span className="text-[9px] text-text-3"> {setting.label}</span>
background: 'var(--bg-base)',
borderRadius: 'var(--radius-sm)',
}} className="flex items-center justify-between p-[6px_8px] border border-stroke">
<span className="text-[9px] text-fg-disabled"> {setting.label}</span>
<div className="flex items-center gap-[2px]">
<input
type="number"
@ -245,7 +245,7 @@ const OilBoomSection = ({
className="boom-setting-input"
step={setting.key === 'waveHeightCorrectionFactor' ? 0.1 : 1}
/>
<span className="text-[9px] text-status-orange">{setting.unit}</span>
<span className="text-[9px] text-fg-disabled">{setting.unit}</span>
</div>
</div>
))}
@ -257,19 +257,20 @@ const OilBoomSection = ({
onClick={handleRunSimulation}
disabled={oilTrajectory.length === 0}
style={{
background: oilTrajectory.length > 0 ? 'rgba(6,182,212,0.15)' : 'var(--bg0)',
border: oilTrajectory.length > 0 ? '2px solid var(--cyan)' : '1px solid var(--bd)',
borderRadius: 'var(--rS)',
color: oilTrajectory.length > 0 ? 'var(--cyan)' : 'var(--t3)',
background: 'transparent',
border: '1px solid var(--color-accent)',
borderRadius: 'var(--radius-sm)',
color: oilTrajectory.length > 0 ? 'var(--color-accent)' : 'var(--fg-disabled)',
cursor: oilTrajectory.length > 0 ? 'pointer' : 'not-allowed',
opacity: oilTrajectory.length > 0 ? 1 : 0.4,
transition: '0.15s',
}}
className="w-full p-[10px] text-[11px] font-bold"
>
🛡 V자형 +
V자형 +
</button>
<p className="text-[9px] text-text-3 leading-relaxed font-korean">
<p className="text-[9px] text-fg-disabled leading-relaxed font-korean">
1 (V형), U형 2 , 3 .
</p>
@ -279,23 +280,23 @@ const OilBoomSection = ({
{/* 전체 효율 */}
<div style={{
padding: '16px', background: 'rgba(6,182,212,0.05)',
border: '1px solid rgba(6,182,212,0.3)', borderRadius: 'var(--rM)', textAlign: 'center',
border: '1px solid rgba(6,182,212,0.3)', borderRadius: 'var(--radius-md)', textAlign: 'center',
}}>
<div className="text-[28px] font-bold text-primary-cyan font-mono">
<div className="text-[28px] font-bold text-color-accent font-mono">
{containmentResult.overallEfficiency}%
</div>
<div className="text-[10px] text-text-3 mt-[2px]"> </div>
<div className="text-[10px] text-fg-disabled mt-[2px]"> </div>
</div>
{/* 차단/통과 카운트 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
<div style={{ padding: '10px', background: 'var(--bg0)', borderRadius: 'var(--rS)', textAlign: 'center' }} className="border border-border">
<div className="text-base font-bold text-status-green font-mono">{containmentResult.blockedParticles}</div>
<div className="text-[8px] text-text-3"> </div>
<div style={{ padding: '10px', background: 'var(--bg-base)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }} className="border border-stroke">
<div className="text-base font-bold text-color-success font-mono">{containmentResult.blockedParticles}</div>
<div className="text-[8px] text-fg-disabled"> </div>
</div>
<div style={{ padding: '10px', background: 'var(--bg0)', borderRadius: 'var(--rS)', textAlign: 'center' }} className="border border-border">
<div className="text-base font-bold text-status-red font-mono">{containmentResult.passedParticles}</div>
<div className="text-[8px] text-text-3"> </div>
<div style={{ padding: '10px', background: 'var(--bg-base)', borderRadius: 'var(--radius-sm)', textAlign: 'center' }} className="border border-stroke">
<div className="text-base font-bold text-color-danger font-mono">{containmentResult.passedParticles}</div>
<div className="text-[8px] text-fg-disabled"> </div>
</div>
</div>
@ -303,18 +304,18 @@ const OilBoomSection = ({
<div className="boom-eff-bar">
<div className="boom-eff-fill" style={{
width: `${containmentResult.overallEfficiency}%`,
background: containmentResult.overallEfficiency >= 80 ? 'var(--green)' : containmentResult.overallEfficiency >= 50 ? 'var(--orange)' : 'var(--red)',
background: containmentResult.overallEfficiency >= 80 ? 'var(--color-success)' : containmentResult.overallEfficiency >= 50 ? 'var(--color-warning)' : 'var(--color-danger)',
}} />
</div>
{/* 라인별 분석 */}
<div>
<h4 className="text-[10px] font-bold text-text-3 mb-1.5"> </h4>
<h4 className="text-[10px] font-bold text-fg-sub mb-1.5"> </h4>
{containmentResult.perLineResults.map((r) => (
<div key={r.boomLineId} style={{ background: 'var(--bg0)', borderRadius: 'var(--rS)' }}
className="flex items-center justify-between p-[6px_8px] mb-1 border border-border text-[9px]">
<span className="text-text-2 flex-1">{r.boomLineName}</span>
<span style={{ color: r.efficiency >= 50 ? 'var(--green)' : 'var(--orange)', marginLeft: '8px' }} className="font-bold font-mono">
<div key={r.boomLineId} style={{ background: 'var(--bg-base)', borderRadius: 'var(--radius-sm)' }}
className="flex items-center justify-between p-[6px_8px] mb-1 border border-stroke text-[9px]">
<span className="text-fg-sub flex-1">{r.boomLineName}</span>
<span style={{ color: r.efficiency >= 50 ? 'var(--color-success)' : 'var(--color-warning)', marginLeft: '8px' }} className="font-bold font-mono">
{r.blocked} / {r.efficiency}%
</span>
</div>
@ -323,15 +324,15 @@ const OilBoomSection = ({
{/* 배치된 방어선 카드 */}
{boomLines.map((line, idx) => {
const priorityColor = line.priority === 'CRITICAL' ? 'var(--red)' : line.priority === 'HIGH' ? 'var(--orange)' : 'var(--yellow)'
const priorityColor = line.priority === 'CRITICAL' ? 'var(--color-danger)' : line.priority === 'HIGH' ? 'var(--color-warning)' : 'var(--color-caution)'
const priorityLabel = line.priority === 'CRITICAL' ? '긴급' : line.priority === 'HIGH' ? '중요' : '보통'
return (
<div key={line.id} style={{
padding: '10px', background: 'var(--bg0)',
borderLeft: `3px solid ${priorityColor}`, borderRadius: 'var(--rS)',
}} className="border border-border">
padding: '10px', background: 'var(--bg-base)',
borderLeft: `3px solid ${priorityColor}`, borderRadius: 'var(--radius-sm)',
}} className="border border-stroke">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold">
<span className="text-[11px] font-bold text-fg">
🛡 {idx + 1} ({line.type})
</span>
<span style={{
@ -344,17 +345,17 @@ const OilBoomSection = ({
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }} className="mb-1.5">
<div>
<span className="text-[8px] text-text-3"></span>
<div className="text-sm font-bold font-mono">{line.length.toFixed(0)}m</div>
<span className="text-[8px] text-fg-disabled"></span>
<div className="text-sm font-bold font-mono text-fg">{line.length.toFixed(0)}m</div>
</div>
<div>
<span className="text-[8px] text-text-3"></span>
<div className="text-sm font-bold font-mono">{line.angle.toFixed(0)}°</div>
<span className="text-[8px] text-fg-disabled"></span>
<div className="text-sm font-bold font-mono text-fg">{line.angle.toFixed(0)}°</div>
</div>
</div>
<div className="flex items-center gap-1">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: line.efficiency >= 80 ? 'var(--green)' : 'var(--orange)' }} />
<span style={{ color: line.efficiency >= 80 ? 'var(--green)' : 'var(--orange)' }} className="text-[9px] font-semibold">
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: line.efficiency >= 80 ? 'var(--color-success)' : 'var(--color-warning)' }} />
<span style={{ color: line.efficiency >= 80 ? 'var(--color-success)' : 'var(--color-warning)' }} className="text-[9px] font-semibold">
{line.efficiency}%
</span>
</div>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -127,6 +127,7 @@ export function OilSpillView() {
const [simulationProgress, setSimulationProgress] = useState(0)
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const [simulationError, setSimulationError] = useState<string | null>(null)
const [validationErrors, setValidationErrors] = useState<Set<string>>(new Set())
const [selectedModels, setSelectedModels] = useState<Set<PredictionModel>>(new Set(['OpenDrift']))
const [visibleModels, setVisibleModels] = useState<Set<PredictionModel>>(new Set(['OpenDrift']))
const [predictionTime, setPredictionTime] = useState(48)
@ -682,14 +683,30 @@ export function OilSpillView() {
: (selectedAnalysis?.acdntSn ?? analysisDetail?.acdnt?.acdntSn);
const effectiveCoord = overrides?.incidentCoord ?? incidentCoord;
if (!isDirectInput && !existingAcdntSn) return;
if (!effectiveCoord) return;
const effectiveModels = overrides?.models ?? selectedModels;
// ── 입력 유효성 검증 (border 하이라이트) ──
const errors = new Set<string>();
if (isDirectInput) {
if (!incidentName.trim()) errors.add('incidentName');
if (!accidentTime) errors.add('accidentTime');
if (!effectiveCoord || (effectiveCoord.lat === 0 && effectiveCoord.lon === 0)) errors.add('coord');
} else if (!existingAcdntSn) {
errors.add('incidentName');
}
if (!effectiveCoord) errors.add('coord');
if (effectiveModels.size === 0) errors.add('models');
if (errors.size > 0) {
setValidationErrors(errors);
return;
}
setValidationErrors(new Set());
const coord = effectiveCoord!; // 검증 통과 후 non-null 보장
const effectiveOilType = overrides?.oilType ?? oilType;
const effectiveSpillAmount = overrides?.spillAmount ?? spillAmount;
const effectiveSpillType = overrides?.spillType ?? spillType;
const effectivePredictionTime = overrides?.predictionTime ?? predictionTime;
const effectiveModels = overrides?.models ?? selectedModels;
setIsRunningSimulation(true);
setSimulationSummary(null);
@ -698,8 +715,8 @@ export function OilSpillView() {
try {
const payload: Record<string, unknown> = {
acdntSn: existingAcdntSn,
lat: effectiveCoord.lat,
lon: effectiveCoord.lon,
lat: coord.lat,
lon: coord.lon,
runTime: effectivePredictionTime,
matTy: effectiveOilType,
matVol: effectiveSpillAmount,
@ -733,8 +750,8 @@ export function OilSpillView() {
oilType,
volume: spillAmount,
location: '',
lat: effectiveCoord.lat,
lon: effectiveCoord.lon,
lat: coord.lat,
lon: coord.lon,
kospsStatus: 'pending',
poseidonStatus: 'pending',
opendriftStatus: 'pending',
@ -797,12 +814,12 @@ export function OilSpillView() {
setHydrDataByModel(newHydrDataByModel);
setSummaryByModel(newSummaryByModel);
setStepSummariesByModel(newStepSummariesByModel);
const booms = generateAIBoomLines(merged, effectiveCoord, algorithmSettings);
const booms = generateAIBoomLines(merged, coord, algorithmSettings);
setBoomLines(booms);
setSensitiveResources([]);
setCurrentStep(0);
setIsPlaying(true);
setFlyToCoord({ lon: effectiveCoord.lon, lat: effectiveCoord.lat });
setFlyToCoord({ lon: coord.lon, lat: coord.lat });
}
if (errors.length > 0 && merged.length === 0) {
@ -810,8 +827,8 @@ export function OilSpillView() {
} else {
simulationSucceeded = true;
const effectiveAcdntSn = data.acdntSn ?? selectedAnalysis?.acdntSn;
if (effectiveCoord) {
fetchWeatherSnapshotForCoord(effectiveCoord.lat, effectiveCoord.lon)
if (coord) {
fetchWeatherSnapshotForCoord(coord.lat, coord.lon)
.then(snapshot => {
useWeatherSnapshotStore.getState().setSnapshot(snapshot);
if (effectiveAcdntSn) {
@ -964,15 +981,15 @@ export function OilSpillView() {
enabledLayers={enabledLayers}
onToggleLayer={handleToggleLayer}
accidentTime={accidentTime}
onAccidentTimeChange={setAccidentTime}
onAccidentTimeChange={(v) => { setAccidentTime(v); setValidationErrors(prev => { const n = new Set(prev); n.delete('accidentTime'); return n; }); }}
incidentCoord={incidentCoord}
onCoordChange={setIncidentCoord}
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={setSelectedModels}
onModelsChange={(v) => { setSelectedModels(v); setValidationErrors(prev => { const n = new Set(prev); n.delete('models'); return n; }); }}
visibleModels={visibleModels}
onVisibleModelsChange={setVisibleModels}
hasResults={oilTrajectory.length > 0}
@ -985,7 +1002,7 @@ export function OilSpillView() {
spillAmount={spillAmount}
onSpillAmountChange={setSpillAmount}
incidentName={incidentName}
onIncidentNameChange={setIncidentName}
onIncidentNameChange={(v) => { setIncidentName(v); setValidationErrors(prev => { const n = new Set(prev); n.delete('incidentName'); return n; }); }}
spillUnit={spillUnit}
onSpillUnitChange={setSpillUnit}
boomLines={boomLines}
@ -1005,6 +1022,7 @@ export function OilSpillView() {
onLayerBrightnessChange={setLayerBrightness}
sensitiveResources={sensitiveResourceCategories}
onImageAnalysisResult={handleImageAnalysisResult}
validationErrors={validationErrors}
/>
)}
@ -1073,7 +1091,7 @@ export function OilSpillView() {
return (
<div className="absolute bottom-0 left-0 right-0 h-[72px] flex items-center px-5 gap-4" style={{
background: 'rgba(15,21,36,0.95)', backdropFilter: 'blur(16px)',
borderTop: '1px solid var(--bd)',
borderTop: '1px solid var(--stroke-default)',
zIndex: 1100
}}>
{/* 컨트롤 버튼 */}
@ -1084,7 +1102,7 @@ export function OilSpillView() {
].map((btn, i) => (
<button key={i} onClick={btn.action} style={{
width: '34px', height: '34px', borderRadius: 'var(--rS, 4px)',
border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t2)',
border: '1px solid var(--stroke-default)', background: 'var(--bg-card)', color: 'var(--fg-sub)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', fontSize: '14px', transition: '0.2s'
}}>{btn.icon}</button>
@ -1094,9 +1112,9 @@ export function OilSpillView() {
setIsPlaying(p => !p);
}} style={{
width: '34px', height: '34px', borderRadius: 'var(--rS, 4px)',
border: isPlaying ? '1px solid var(--cyan)' : '1px solid var(--bd)',
background: isPlaying ? 'var(--cyan)' : 'var(--bg3)',
color: isPlaying ? 'var(--bg0)' : 'var(--t2)',
border: isPlaying ? '1px solid var(--color-accent)' : '1px solid var(--stroke-default)',
background: isPlaying ? 'var(--color-accent)' : 'var(--bg-card)',
color: isPlaying ? 'var(--bg-base)' : 'var(--fg-sub)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', fontSize: '14px', transition: '0.2s'
}}>{isPlaying ? '⏸' : '▶'}</button>
@ -1106,7 +1124,7 @@ export function OilSpillView() {
].map((btn, i) => (
<button key={i} onClick={btn.action} style={{
width: '34px', height: '34px', borderRadius: 'var(--rS, 4px)',
border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t2)',
border: '1px solid var(--stroke-default)', background: 'var(--bg-card)', color: 'var(--fg-sub)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', fontSize: '12px', transition: '0.2s'
}}>{btn.icon}</button>
@ -1114,9 +1132,9 @@ export function OilSpillView() {
<div className="w-2" />
<button onClick={() => setPlaySpeed(playSpeed >= 4 ? 1 : playSpeed * 2)} style={{
width: '34px', height: '34px', borderRadius: 'var(--rS, 4px)',
border: '1px solid var(--bd)', background: 'var(--bg3)', color: 'var(--t2)',
border: '1px solid var(--stroke-default)', background: 'var(--bg-card)', color: 'var(--fg-sub)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fM)', transition: '0.2s'
cursor: 'pointer', fontSize: '11px', fontWeight: 600, fontFamily: 'var(--font-mono)', transition: '0.2s'
}}>{playSpeed}×</button>
</div>
@ -1130,8 +1148,8 @@ export function OilSpillView() {
return (
<span key={t} style={{
position: 'absolute', left: `${pos}%`, transform: 'translateX(-50%)',
fontSize: '10px', fontFamily: 'var(--fM)',
color: isActive ? 'var(--cyan)' : 'var(--t3)',
fontSize: '10px', fontFamily: 'var(--font-mono)',
color: isActive ? 'var(--color-accent)' : 'var(--fg-disabled)',
fontWeight: isActive ? 600 : 400, cursor: 'pointer', whiteSpace: 'nowrap'
}} onClick={() => setCurrentStep(t)}>{t}h</span>
)
@ -1141,7 +1159,7 @@ export function OilSpillView() {
{/* 슬라이더 트랙 */}
<div className="relative h-6 flex items-center">
<div
style={{ width: '100%', height: '4px', background: 'var(--bd)', borderRadius: '2px', position: 'relative', cursor: 'pointer' }}
style={{ width: '100%', height: '4px', background: 'var(--stroke-default)', borderRadius: '2px', position: 'relative', cursor: 'pointer' }}
onClick={(e) => {
if (timeSteps.length === 0) return;
const rect = e.currentTarget.getBoundingClientRect();
@ -1157,7 +1175,7 @@ export function OilSpillView() {
<div style={{
position: 'absolute', top: 0, left: 0,
width: `${progressPct}%`, height: '100%',
background: 'linear-gradient(90deg, var(--cyan), var(--blue))',
background: 'linear-gradient(90deg, var(--color-accent), var(--color-info))',
borderRadius: '2px', transition: 'width 0.15s'
}} />
{/* 스텝 마커 (각 타임스텝 위치에 틱 표시) */}
@ -1166,7 +1184,7 @@ export function OilSpillView() {
return (
<div key={`tick-${t}`} style={{
position: 'absolute', width: '2px', height: '10px',
background: t <= currentStep ? 'var(--cyan)' : 'var(--t3)',
background: t <= currentStep ? 'var(--color-accent)' : 'var(--fg-disabled)',
top: '-3px', left: `${pos}%`, opacity: 0.6
}} />
);
@ -1189,7 +1207,7 @@ export function OilSpillView() {
position: 'absolute', left: `${progressPct}%`, top: '50%',
transform: 'translate(-50%, -50%)',
width: '16px', height: '16px',
background: 'var(--cyan)', border: '3px solid var(--bg0)',
background: 'var(--color-accent)', border: '3px solid var(--bg-base)',
borderRadius: '50%', cursor: 'grab',
boxShadow: '0 0 10px rgba(6,182,212,0.4)', zIndex: 2,
transition: 'left 0.15s'
@ -1199,7 +1217,7 @@ export function OilSpillView() {
{/* 시간 정보 */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px', flexShrink: 0, minWidth: '200px' }}>
<div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--cyan)', fontFamily: 'var(--fM)' }}>
<div style={{ fontSize: '14px', fontWeight: 600, color: 'var(--color-accent)', fontFamily: 'var(--font-mono)' }}>
+{currentStep}h {(() => {
const base = accidentTime ? new Date(accidentTime) : new Date();
const d = new Date(base.getTime() + currentStep * 3600 * 1000);
@ -1214,11 +1232,11 @@ export function OilSpillView() {
return [
{ label: '풍화량', value: weatheredVal },
{ label: '면적', value: areaVal },
{ label: '차단율', value: boomLines.length > 0 ? `${Math.min(95, 70 + Math.round(progressPct * 0.2))}%` : '—', color: 'var(--boom)' },
{ label: '차단율', value: boomLines.length > 0 ? `${Math.min(95, 70 + Math.round(progressPct * 0.2))}%` : '—', color: 'var(--color-boom)' },
].map((s, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px' }}>
<span className="text-text-3">{s.label}</span>
<span style={{ color: s.color, fontWeight: 600, fontFamily: 'var(--fM)' }}>{s.value}</span>
<span className="text-fg-disabled">{s.label}</span>
<span style={{ color: s.color, fontWeight: 600, fontFamily: 'var(--font-mono)' }}>{s.value}</span>
</div>
));
})()}

파일 보기

@ -33,6 +33,7 @@ interface PredictionInputSectionProps {
spillUnit: string
onSpillUnitChange: (unit: string) => void
onImageAnalysisResult?: (result: ImageAnalyzeResult) => void
validationErrors?: Set<string>
}
const PredictionInputSection = ({
@ -63,6 +64,7 @@ const PredictionInputSection = ({
spillUnit,
onSpillUnitChange,
onImageAnalysisResult,
validationErrors,
}: PredictionInputSectionProps) => {
const [inputMode, setInputMode] = useState<'direct' | 'upload'>('direct')
const [uploadedFile, setUploadedFile] = useState<File | null>(null)
@ -112,15 +114,15 @@ const PredictionInputSection = ({
}
return (
<div className="border-b border-border">
<div className="border-b border-stroke">
<div
onClick={onToggle}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
>
<h3 className="text-[13px] font-bold text-text-2 font-korean">
<h3 className="text-[0.8125rem] font-bold text-fg-sub font-korean">
</h3>
<span className="text-[10px] text-text-3">
<span className="text-[0.625rem] text-fg-disabled">
{expanded ? '▼' : '▶'}
</span>
</div>
@ -128,14 +130,14 @@ const PredictionInputSection = ({
{expanded && (
<div className="px-4 pb-4 flex flex-col gap-[6px]">
{/* Input Mode Selection */}
<div className="flex items-center gap-[10px] text-[11px]">
<div className="flex items-center gap-[10px] text-[0.6875rem]">
<label className="flex items-center gap-[3px] cursor-pointer">
<input
type="radio"
name="prdType"
checked={inputMode === 'direct'}
onChange={() => setInputMode('direct')}
className="accent-[var(--cyan)] m-0 w-[11px] h-[11px]"
className="accent-[var(--color-accent)] m-0 w-[11px] h-[11px]"
/>
</label>
@ -145,7 +147,7 @@ const PredictionInputSection = ({
name="prdType"
checked={inputMode === 'upload'}
onChange={() => setInputMode('upload')}
className="accent-[var(--cyan)] m-0 w-[11px] h-[11px]"
className="accent-[var(--color-accent)] m-0 w-[11px] h-[11px]"
/>
</label>
@ -159,6 +161,7 @@ const PredictionInputSection = ({
placeholder="사고명 직접 입력"
value={incidentName}
onChange={(e) => onIncidentNameChange(e.target.value)}
style={validationErrors?.has('incidentName') ? { borderColor: 'var(--color-danger)' } : undefined}
/>
<input className="prd-i" placeholder="또는 사고 리스트에서 선택" readOnly />
</>
@ -170,24 +173,24 @@ const PredictionInputSection = ({
{/* 파일 선택 영역 */}
{!uploadedFile ? (
<label
className="flex items-center justify-center text-[11px] text-text-3 cursor-pointer"
className="flex items-center justify-center text-[0.6875rem] text-fg-disabled cursor-pointer"
style={{
padding: '20px',
background: 'var(--bg0)',
border: '2px dashed var(--bd)',
borderRadius: 'var(--rS)',
background: 'var(--bg-base)',
border: '2px dashed var(--stroke-default)',
borderRadius: 'var(--radius-sm)',
transition: '0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--cyan)'
e.currentTarget.style.borderColor = 'var(--color-accent)'
e.currentTarget.style.background = 'rgba(6,182,212,0.05)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--bd)'
e.currentTarget.style.background = 'var(--bg0)'
e.currentTarget.style.borderColor = 'var(--stroke-default)'
e.currentTarget.style.background = 'var(--bg-base)'
}}
>
📁
<input
ref={fileInputRef}
type="file"
@ -198,16 +201,16 @@ const PredictionInputSection = ({
</label>
) : (
<div
className="flex items-center justify-between font-mono text-[10px] bg-bg-0 border border-border"
style={{ padding: '8px 10px', borderRadius: 'var(--rS)' }}
className="flex items-center justify-between font-mono text-[0.625rem] bg-bg-base border border-stroke"
style={{ padding: '8px 10px', borderRadius: 'var(--radius-sm)' }}
>
<span className="text-text-2">📄 {uploadedFile.name}</span>
<span className="text-fg-sub">📄 {uploadedFile.name}</span>
<button
onClick={handleRemoveFile}
className="text-[10px] text-text-3 bg-transparent border-none cursor-pointer"
className="text-[0.625rem] text-fg-disabled bg-transparent border-none cursor-pointer"
style={{ padding: '2px 6px', transition: '0.15s' }}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--red)' }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--t3)' }}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--color-danger)' }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--fg-disabled)' }}
>
</button>
@ -217,23 +220,23 @@ const PredictionInputSection = ({
{/* 분석 실행 버튼 */}
<button
className="prd-btn pri"
style={{ padding: '7px', fontSize: '11px' }}
style={{ padding: '7px', fontSize: '0.6875rem' }}
onClick={handleAnalyze}
disabled={!uploadedFile || isAnalyzing}
>
{isAnalyzing ? '⏳ 분석 중...' : '🔍 이미지 분석 실행'}
{isAnalyzing ? '⏳ 분석 중...' : '이미지 분석 실행'}
</button>
{/* 에러 메시지 */}
{analyzeError && (
<div
className="text-[10px] font-semibold"
className="text-[0.625rem] font-semibold"
style={{
padding: '6px 8px',
background: 'rgba(239,68,68,0.1)',
border: '1px solid rgba(239,68,68,0.3)',
borderRadius: 'var(--rS)',
color: 'var(--red)',
borderRadius: 'var(--radius-sm)',
color: 'var(--color-danger)',
}}
>
{analyzeError}
@ -243,18 +246,18 @@ const PredictionInputSection = ({
{/* 분석 완료 메시지 */}
{analyzeResult && (
<div
className="text-[10px] font-semibold"
className="text-[0.625rem] font-semibold"
style={{
padding: '6px 8px',
background: 'rgba(34,197,94,0.1)',
border: '1px solid rgba(34,197,94,0.3)',
borderRadius: 'var(--rS)',
borderRadius: 'var(--radius-sm)',
color: '#22c55e',
lineHeight: 1.6,
}}
>
<br />
<span className="font-normal text-text-3">
<span className="font-normal text-fg-disabled">
{analyzeResult.lat.toFixed(4)} / {analyzeResult.lon.toFixed(4)}<br />
: {analyzeResult.oilType} / : {analyzeResult.area.toFixed(1)} m²
</span>
@ -265,10 +268,11 @@ const PredictionInputSection = ({
{/* 사고 발생 시각 */}
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-text-3 font-korean"> (KST)</label>
<label className="text-[0.625rem] text-fg-disabled font-korean"> (KST)</label>
<DateTimeInput
value={accidentTime}
onChange={onAccidentTimeChange}
error={validationErrors?.has('accidentTime')}
/>
</div>
@ -280,17 +284,19 @@ const PredictionInputSection = ({
isLatitude={true}
decimal={incidentCoord?.lat ?? 0}
onChange={(val) => onCoordChange({ lon: incidentCoord?.lon ?? 0, lat: val })}
error={validationErrors?.has('coord')}
/>
<button
className={`prd-map-btn${isSelectingLocation ? ' active' : ''}`}
onClick={onMapSelectClick}
style={{ gridRow: '1 / 3', gridColumn: 2, whiteSpace: 'nowrap', height: '100%', minWidth: 48, padding: '0 10px' }}
>📍<br/></button>
style={{ gridRow: '1 / 3', gridColumn: 2, whiteSpace: 'nowrap', alignSelf: 'stretch', minWidth: 48, padding: '0 10px' }}
><br/></button>
<DmsCoordInput
label="경도"
isLatitude={false}
decimal={incidentCoord?.lon ?? 0}
onChange={(val) => onCoordChange({ lat: incidentCoord?.lat ?? 0, lon: val })}
error={validationErrors?.has('coord')}
/>
</div>
</div>
@ -364,9 +370,9 @@ const PredictionInputSection = ({
{/* POSEIDON: 엔진 연동 완료. KOSPS: 준비 중 (ready: false) */}
<div className="grid grid-cols-3 gap-[3px]">
{([
{ id: 'KOSPS' as PredictionModel, color: 'var(--cyan)', ready: false },
{ id: 'POSEIDON' as PredictionModel, color: 'var(--red)', ready: true },
{ id: 'OpenDrift' as PredictionModel, color: 'var(--blue)', ready: true },
{ id: 'KOSPS' as PredictionModel, color: 'var(--color-accent)', ready: false },
{ id: 'POSEIDON' as PredictionModel, color: 'var(--color-danger)', ready: true },
{ id: 'OpenDrift' as PredictionModel, color: 'var(--color-info)', ready: true },
] as const).map(m => (
<div
key={m.id}
@ -384,7 +390,7 @@ const PredictionInputSection = ({
}
}}
>
<span className="prd-md" style={{ background: m.color }} />
{/* <span className="prd-md" style={{ background: m.color }} /> */}
{m.id}
</div>
))}
@ -395,15 +401,15 @@ const PredictionInputSection = ({
alert('앙상블 모델은 현재 준비중입니다.')
}}
>
<span className="prd-md" style={{ background: 'var(--purple)' }} />
<span className="prd-md" style={{ background: 'var(--color-tertiary)' }} />
</div>
*/}
</div>
{/* 모델 미선택 경고 */}
{selectedModels.size === 0 && (
<p className="text-[10px] text-status-red font-korean">
{/* 모델 미선택 경고 (실행 시 검증) */}
{validationErrors?.has('models') && (
<p className="text-[0.625rem] text-color-danger font-korean">
.
</p>
)}
@ -411,11 +417,11 @@ const PredictionInputSection = ({
{/* Run Button */}
<button
className="prd-btn pri mt-0.5"
style={{ padding: '7px', fontSize: '11px' }}
style={{ padding: '7px', fontSize: '0.6875rem' }}
onClick={onRunSimulation}
disabled={isRunningSimulation}
>
{isRunningSimulation ? '⏳ 실행 중...' : '🔬 확산예측 실행'}
{isRunningSimulation ? '⏳ 실행 중...' : '확산예측 실행'}
</button>
</div>
)}
@ -424,7 +430,7 @@ const PredictionInputSection = ({
}
// ── 커스텀 날짜/시간 선택 컴포넌트 ─────────────────────
function DateTimeInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
function DateTimeInput({ value, onChange, error }: { value: string; onChange: (v: string) => void; error?: boolean }) {
const [showCal, setShowCal] = useState(false)
const ref = useRef<HTMLDivElement>(null)
@ -487,21 +493,25 @@ function DateTimeInput({ value, onChange }: { value: string; onChange: (v: strin
const todayD = today.getDate()
return (
<div ref={ref} className="flex items-center gap-1 relative">
<div
ref={ref}
className="flex items-center gap-1 relative"
style={error ? { border: '1px solid var(--color-danger)', borderRadius: 6, padding: 2 } : undefined}
>
{/* 날짜 버튼 */}
<button
type="button"
onClick={() => setShowCal(!showCal)}
className="prd-i flex-1 flex items-center justify-between cursor-pointer"
style={{ padding: '5px 8px', fontSize: 10 }}
style={{ padding: '5px 8px', fontSize: '0.625rem' }}
>
<span className="font-mono" style={{ color: datePart ? 'var(--t1)' : 'var(--t3)' }}>{displayDate}</span>
<span className="text-[9px] opacity-60">📅</span>
<span className="font-mono" style={{ color: datePart ? 'var(--fg-default)' : 'var(--fg-disabled)' }}>{displayDate}</span>
<span className="text-[0.625rem] opacity-60">📅</span>
</button>
{/* 시 */}
<TimeDropdown value={hh} max={24} onChange={(v) => updateTime(v, mm)} />
<span className="text-[8px] text-text-3 font-bold">:</span>
<span className="text-[0.625rem] text-fg-disabled font-bold">:</span>
{/* 분 */}
<TimeDropdown value={mm} max={60} onChange={(v) => updateTime(hh, v)} />
@ -514,21 +524,21 @@ function DateTimeInput({ value, onChange }: { value: string; onChange: (v: strin
left: 0,
marginTop: 4,
width: 200,
background: 'var(--bg3)',
border: '1px solid var(--bd)',
background: 'var(--bg-card)',
border: '1px solid var(--stroke-default)',
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
}}
>
{/* 헤더 */}
<div className="flex items-center justify-between" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bd)' }}>
<button type="button" onClick={prevMonth} className="text-[10px] text-text-3 cursor-pointer px-1 hover:text-text-1"></button>
<span className="text-[10px] font-bold text-text-1 font-korean">{viewYear} {viewMonth + 1}</span>
<button type="button" onClick={nextMonth} className="text-[10px] text-text-3 cursor-pointer px-1 hover:text-text-1"></button>
<div className="flex items-center justify-between" style={{ padding: '6px 8px', borderBottom: '1px solid var(--stroke-default)' }}>
<button type="button" onClick={prevMonth} className="text-[0.625rem] text-fg-disabled cursor-pointer px-1 hover:text-fg"></button>
<span className="text-[0.625rem] font-bold text-fg font-korean">{viewYear} {viewMonth + 1}</span>
<button type="button" onClick={nextMonth} className="text-[0.625rem] text-fg-disabled cursor-pointer px-1 hover:text-fg"></button>
</div>
{/* 요일 */}
<div className="grid grid-cols-7 text-center" style={{ padding: '3px 4px 0' }}>
{['일', '월', '화', '수', '목', '금', '토'].map((d) => (
<span key={d} className="text-[8px] text-text-3 font-korean" style={{ padding: '2px 0' }}>{d}</span>
<span key={d} className="text-[0.625rem] text-fg-disabled font-korean" style={{ padding: '2px 0' }}>{d}</span>
))}
</div>
{/* 날짜 */}
@ -545,11 +555,11 @@ function DateTimeInput({ value, onChange }: { value: string; onChange: (v: strin
className="cursor-pointer rounded-sm"
style={{
padding: '3px 0',
fontSize: 9,
fontFamily: 'var(--fM)',
fontSize: '0.625rem',
fontFamily: 'var(--font-mono)',
fontWeight: isSelected ? 700 : 400,
color: isSelected ? '#fff' : isToday ? 'var(--cyan)' : 'var(--t2)',
background: isSelected ? 'var(--cyan)' : 'transparent',
color: isSelected ? '#fff' : isToday ? 'var(--color-accent)' : 'var(--fg-sub)',
background: isSelected ? 'var(--color-accent)' : 'transparent',
border: 'none',
}}
>
@ -573,12 +583,12 @@ function DateTimeInput({ value, onChange }: { value: string; onChange: (v: strin
onChange(`${now.getFullYear()}-${m}-${d}T${hh}:${mm}`)
setShowCal(false)
}}
className="w-full text-[8px] font-korean font-semibold cursor-pointer rounded-sm"
className="w-full text-[0.625rem] font-korean font-semibold cursor-pointer rounded-sm"
style={{
padding: '3px 0',
background: 'rgba(6,182,212,0.08)',
border: '1px solid rgba(6,182,212,0.2)',
color: 'var(--cyan)',
color: 'var(--color-accent)',
}}
>
@ -632,11 +642,11 @@ function TimeDropdown({ value, max, onChange }: { value: number; max: number; on
marginTop: 2,
width: 42,
maxHeight: 160,
background: 'var(--bg3)',
border: '1px solid var(--bd)',
background: 'var(--bg-card)',
border: '1px solid var(--stroke-default)',
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
scrollbarWidth: 'thin',
scrollbarColor: 'var(--bd) transparent',
scrollbarColor: 'var(--stroke-default) transparent',
}}
>
{Array.from({ length: max }, (_, i) => (
@ -649,7 +659,7 @@ function TimeDropdown({ value, max, onChange }: { value: number; max: number; on
style={{
padding: '4px 0',
fontSize: 9,
color: i === value ? 'var(--cyan)' : 'var(--t2)',
color: i === value ? 'var(--color-accent)' : 'var(--fg-sub)',
background: i === value ? 'rgba(6,182,212,0.15)' : 'transparent',
fontWeight: i === value ? 700 : 400,
border: 'none',
@ -670,11 +680,13 @@ function DmsCoordInput({
isLatitude,
decimal,
onChange,
error,
}: {
label: string
isLatitude: boolean
decimal: number
onChange: (val: number) => void
error?: boolean
}) {
const abs = Math.abs(decimal)
const d = Math.floor(abs)
@ -693,8 +705,11 @@ function DmsCoordInput({
return (
<div className="flex flex-col gap-0.5">
<span className="text-[8px] text-text-3 font-korean">{label}</span>
<div className="flex items-center gap-0.5">
<span className="text-[8px] text-fg-disabled font-korean">{label}</span>
<div
className="flex items-center gap-0.5"
style={error ? { border: '1px solid var(--color-danger)', borderRadius: 6, padding: 2 } : undefined}
>
<select
className="prd-i text-center"
value={dir}
@ -709,13 +724,13 @@ function DmsCoordInput({
</select>
<input className="prd-i text-center flex-1" type="number" min={0} max={isLatitude ? 90 : 180}
value={d} onChange={(e) => update(parseInt(e.target.value) || 0, m, s, dir)} style={fieldStyle} />
<span className="text-[9px] text-text-3">°</span>
<span className="text-[9px] text-fg-disabled">°</span>
<input className="prd-i text-center flex-1" type="number" min={0} max={59}
value={m} onChange={(e) => update(d, parseInt(e.target.value) || 0, s, dir)} style={fieldStyle} />
<span className="text-[9px] text-text-3">'</span>
<span className="text-[9px] text-fg-disabled">'</span>
<input className="prd-i text-center flex-1" type="number" min={0} max={59.99} step={0.01}
value={s} onChange={(e) => update(d, m, parseFloat(e.target.value) || 0, dir)} style={fieldStyle} />
<span className="text-[9px] text-text-3">"</span>
<span className="text-[9px] text-fg-disabled">"</span>
</div>
</div>
)

파일 보기

@ -117,14 +117,14 @@ export function RecalcModal({
>
<div style={{
width: '380px', maxHeight: 'calc(100vh - 120px)',
background: 'var(--bg1)',
background: 'var(--bg-surface)',
borderRadius: '14px',
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
}} className="border border-border overflow-hidden flex flex-col">
}} className="border border-stroke overflow-hidden flex flex-col">
{/* Header */}
<div style={{
padding: '16px 20px',
}} className="border-b border-border flex items-center gap-3">
}} className="border-b border-stroke flex items-center gap-3">
<div style={{
width: '36px', height: '36px', borderRadius: '10px',
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(6,182,212,0.2))',
@ -137,7 +137,7 @@ export function RecalcModal({
<h2 className="text-[15px] font-bold m-0">
</h2>
<div className="text-[10px] text-text-3 mt-[2px]">
<div className="text-[10px] text-fg-disabled mt-[2px]">
·
</div>
</div>
@ -145,10 +145,10 @@ export function RecalcModal({
onClick={onClose}
style={{
width: '28px', height: '28px', borderRadius: '6px',
background: 'var(--bg3)',
background: 'var(--bg-card)',
fontSize: '12px',
}}
className="border border-border text-text-3 cursor-pointer flex items-center justify-center"
className="border border-stroke text-fg-disabled cursor-pointer flex items-center justify-center"
>
</button>
@ -163,7 +163,7 @@ export function RecalcModal({
padding: '10px 12px', background: 'rgba(6,182,212,0.04)',
border: '1px solid rgba(6,182,212,0.15)', borderRadius: '8px',
}}>
<div className="text-[9px] font-bold text-primary-cyan mb-1.5">
<div className="text-[9px] font-bold text-color-accent mb-1.5">
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }} className="text-[9px]">
@ -239,7 +239,7 @@ export function RecalcModal({
<FieldGroup label="유출 위치 (좌표)">
<div className="flex gap-1.5">
<div className="flex-1">
<div className="text-[8px] text-text-3 mb-[3px]">
<div className="text-[8px] text-fg-disabled mb-[3px]">
(N)
</div>
<input
@ -251,7 +251,7 @@ export function RecalcModal({
/>
</div>
<div className="flex-1">
<div className="text-[8px] text-text-3 mb-[3px]">
<div className="text-[8px] text-fg-disabled mb-[3px]">
(E)
</div>
<input
@ -269,9 +269,9 @@ export function RecalcModal({
<FieldGroup label="예측 모델 선택">
<div className="flex flex-wrap gap-1.5">
{([
{ model: 'KOSPS' as PredictionModel, color: 'var(--cyan)', ready: false },
{ model: 'POSEIDON' as PredictionModel, color: 'var(--red)', ready: true },
{ model: 'OpenDrift' as PredictionModel, color: 'var(--blue)', ready: true },
{ model: 'KOSPS' as PredictionModel, color: 'var(--color-accent)', ready: false },
{ model: 'POSEIDON' as PredictionModel, color: 'var(--color-danger)', ready: true },
{ model: 'OpenDrift' as PredictionModel, color: 'var(--color-info)', ready: true },
]).map(({ model, color, ready }) => (
<button
key={model}
@ -296,17 +296,17 @@ export function RecalcModal({
{/* Footer */}
<div style={{
padding: '14px 20px',
}} className="border-t border-border flex gap-2">
}} className="border-t border-stroke flex gap-2">
<button
onClick={onClose}
disabled={phase !== 'editing'}
style={{
padding: '10px',
borderRadius: '8px',
background: 'var(--bg3)',
background: 'var(--bg-card)',
opacity: phase !== 'editing' ? 0.5 : 1,
}}
className="flex-1 text-[12px] font-semibold border border-border text-text-2 cursor-pointer"
className="flex-1 text-[12px] font-semibold border border-stroke text-fg-sub cursor-pointer"
>
</button>
@ -320,17 +320,17 @@ export function RecalcModal({
background: phase === 'done'
? 'rgba(34,197,94,0.15)'
: phase === 'running'
? 'var(--bg3)'
: 'linear-gradient(135deg, var(--orange), var(--cyan))',
? 'var(--bg-card)'
: 'linear-gradient(135deg, var(--color-warning), var(--color-accent))',
border: phase === 'done'
? '1px solid rgba(34,197,94,0.4)'
: phase === 'running'
? '1px solid var(--bd)'
? '1px solid var(--stroke-default)'
: 'none',
color: phase === 'done'
? 'var(--green)'
? 'var(--color-success)'
: phase === 'running'
? 'var(--orange)'
? 'var(--color-warning)'
: '#fff',
opacity: models.size === 0 && phase === 'editing' ? 0.5 : 1,
}}
@ -347,7 +347,7 @@ export function RecalcModal({
function FieldGroup({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<div className="text-[10px] font-bold text-text-2 mb-1.5">
<div className="text-[10px] font-bold text-fg-sub mb-1.5">
{label}
</div>
{children}
@ -358,7 +358,7 @@ function FieldGroup({ label, children }: { label: string; children: React.ReactN
function InfoItem({ label, value }: { label: string; value: string }) {
return (
<div className="flex justify-between py-[2px]">
<span className="text-text-3">{label}</span>
<span className="text-fg-disabled">{label}</span>
<span className="font-semibold font-mono">{value}</span>
</div>
)

파일 보기

@ -96,10 +96,10 @@ export function RightPanel({
}, [incidentCoord, centerPoints, summary, predictionTime])
return (
<div className="w-[300px] min-w-[300px] bg-bg-1 border-l border-border flex flex-col">
<div className="w-[300px] min-w-[300px] bg-bg-surface border-l border-stroke flex flex-col">
{/* Tab Header */}
<div className="flex border-b border-border">
<button className="flex-1 py-3 text-center text-xs font-semibold text-primary-cyan border-b-2 border-primary-cyan transition-all font-korean">
<div className="flex border-b border-stroke">
<button className="flex-1 py-3 text-center text-xs font-semibold text-color-accent border-b-2 border-color-accent transition-all font-korean">
</button>
</div>
@ -135,11 +135,11 @@ export function RightPanel({
</div>
{windHydrModelOptions.length > 1 && (
<div className="flex items-center gap-2 mt-1.5">
<span className="text-[9px] text-text-3 font-korean whitespace-nowrap"> </span>
<span className="text-[0.625rem] text-fg-disabled font-korean whitespace-nowrap"> </span>
<select
value={windHydrModel}
onChange={e => onWindHydrModelChange?.(e.target.value)}
className="flex-1 text-[9px] bg-bg-3 border border-border rounded px-1 py-0.5 text-text-2 font-korean"
className="flex-1 text-[0.625rem] bg-bg-card border border-stroke rounded px-1 py-0.5 text-fg-sub font-korean"
>
{windHydrModelOptions.map(m => (
<option key={m} value={m}>{m}</option>
@ -157,10 +157,10 @@ export function RightPanel({
<button
key={tab}
onClick={() => { onSwitchAnalysisTab?.(tab); onClearAnalysis?.() }}
className={`flex-1 py-1.5 px-1 rounded text-[9px] font-semibold font-korean border transition-colors ${
className={`flex-1 py-1.5 px-1 rounded text-[0.625rem] font-semibold font-korean border transition-colors ${
analysisTab === tab
? 'border-primary-cyan bg-[rgba(6,182,212,0.08)] text-primary-cyan'
: 'border-border bg-bg-3 text-text-3 hover:text-text-2'
? 'border-color-accent bg-[rgba(6,182,212,0.08)] text-color-accent'
: 'border-stroke bg-bg-card text-fg-disabled hover:text-fg-sub'
}`}
>
{tab === 'polygon' ? '다각형 분석' : '원 분석'}
@ -171,36 +171,36 @@ export function RightPanel({
{/* 다각형 패널 */}
{analysisTab === 'polygon' && (
<div>
<p className="text-[9px] text-text-3 font-korean mb-2 leading-relaxed">
<p className="text-[0.625rem] text-fg-disabled font-korean mb-2 leading-relaxed">
.
</p>
{!drawAnalysisMode && !analysisResult && (
<button
onClick={onStartPolygonDraw}
className="w-full py-2 rounded text-[10px] font-bold font-korean text-white mb-0 transition-opacity hover:opacity-90"
style={{ background: 'linear-gradient(135deg, var(--purple), var(--cyan))' }}
className="w-full py-2 rounded-sm text-[0.625rem] font-bold font-korean mb-0 transition-colors hover:bg-[rgba(6,182,212,0.08)]"
style={{ border: '1px solid var(--color-accent)', color: 'var(--color-accent)', background: 'transparent' }}
>
📐
</button>
)}
{drawAnalysisMode === 'polygon' && (
<div className="space-y-2">
<div className="text-[9px] text-purple-400 font-korean bg-[rgba(168,85,247,0.08)] rounded px-2 py-1.5 leading-relaxed">
<div className="text-[0.625rem] text-purple-400 font-korean bg-[rgba(168,85,247,0.08)] rounded px-2 py-1.5 leading-relaxed">
<br />
<span className="text-text-3"> {analysisPolygonPoints.length} </span>
<span className="text-fg-disabled"> {analysisPolygonPoints.length} </span>
</div>
<div className="flex gap-1.5">
<button
onClick={onRunPolygonAnalysis}
disabled={analysisPolygonPoints.length < 3}
className="flex-1 py-1.5 rounded text-[10px] font-bold font-korean text-white disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
style={{ background: 'linear-gradient(135deg, var(--purple), var(--cyan))' }}
className="flex-1 py-1.5 rounded text-[0.625rem] font-bold font-korean text-white disabled:opacity-40 disabled:cursor-not-allowed transition-opacity"
style={{ background: 'linear-gradient(135deg, var(--color-tertiary), var(--color-accent))' }}
>
</button>
<button
onClick={onCancelAnalysis}
className="py-1.5 px-2 rounded text-[10px] font-semibold font-korean border border-border text-text-3 hover:text-text-2 transition-colors"
className="py-1.5 px-2 rounded text-[0.625rem] font-semibold font-korean border border-stroke text-fg-disabled hover:text-fg-sub transition-colors"
>
</button>
@ -216,19 +216,19 @@ export function RightPanel({
{/* 원 분석 패널 */}
{analysisTab === 'circle' && (
<div>
<p className="text-[9px] text-text-3 font-korean mb-2 leading-relaxed">
<p className="text-[0.625rem] text-fg-disabled font-korean mb-2 leading-relaxed">
(NM) .
</p>
<div className="text-[9px] font-semibold text-text-2 font-korean mb-1.5"> (NM)</div>
<div className="text-[0.625rem] font-semibold text-fg-sub font-korean mb-1.5"> (NM)</div>
<div className="flex flex-wrap gap-1 mb-2">
{[1, 3, 5, 10, 15, 20, 30, 50].map((nm) => (
<button
key={nm}
onClick={() => onCircleRadiusChange?.(nm)}
className={`w-8 h-7 rounded text-[10px] font-semibold font-mono border transition-all ${
className={`w-8 h-7 rounded text-[0.625rem] font-semibold font-mono border transition-all ${
circleRadiusNm === nm
? 'border-primary-cyan bg-[rgba(6,182,212,0.1)] text-primary-cyan'
: 'border-border bg-bg-0 text-text-3 hover:text-text-2'
? 'border-color-accent bg-[rgba(6,182,212,0.1)] text-color-accent'
: 'border-stroke bg-bg-base text-fg-disabled hover:text-fg-sub'
}`}
>
{nm}
@ -236,7 +236,7 @@ export function RightPanel({
))}
</div>
<div className="flex items-center gap-1.5 mb-2.5">
<span className="text-[9px] text-text-3 font-korean whitespace-nowrap"> </span>
<span className="text-[0.625rem] text-fg-disabled font-korean whitespace-nowrap"> </span>
<input
type="number"
min="0.1"
@ -244,14 +244,14 @@ export function RightPanel({
step="0.1"
value={circleRadiusNm}
onChange={(e) => onCircleRadiusChange?.(parseFloat(e.target.value) || 0.1)}
className="w-14 text-center py-1 px-1 bg-bg-0 border border-border rounded text-[11px] font-mono text-text-1 outline-none focus:border-primary-cyan"
className="w-14 text-center py-1 px-1 bg-bg-base border border-stroke rounded text-[0.6875rem] font-mono text-fg outline-none focus:border-color-accent"
style={{ colorScheme: 'dark' }}
/>
<span className="text-[9px] text-text-3 font-korean">NM</span>
<span className="text-[0.625rem] text-fg-disabled font-korean">NM</span>
<button
onClick={onRunCircleAnalysis}
className="ml-auto py-1 px-3 rounded text-[9px] font-bold font-korean text-white transition-opacity hover:opacity-90"
style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}
className="ml-auto py-1 px-3 rounded-sm text-[0.625rem] font-bold font-korean transition-colors hover:bg-[rgba(6,182,212,0.08)]"
style={{ border: '1px solid var(--color-accent)', color: 'var(--color-accent)', background: 'transparent' }}
>
</button>
@ -265,36 +265,36 @@ export function RightPanel({
{/* 오염 종합 상황 */}
<Section title="오염 종합 상황" badge="위험" badgeColor="red">
<div className="grid grid-cols-2 gap-0.5 text-[9px]">
<StatBox label="유출량" value={spill?.volume != null ? spill.volume.toFixed(2) : '—'} unit={spill?.unit || 'kl'} color="var(--t1)" />
<StatBox label="풍화량" value={summary ? summary.weatheredVolume.toFixed(2) : '—'} unit="m³" color="var(--orange)" />
<StatBox label="해상잔존" value={summary ? summary.remainingVolume.toFixed(2) : '—'} unit="m³" color="var(--blue)" />
<StatBox label="연안부착" value={summary ? summary.beachedVolume.toFixed(2) : '—'} unit="m³" color="var(--red)" />
<div className="grid grid-cols-2 gap-0.5 text-[0.625rem]">
<StatBox label="유출량" value={spill?.volume != null ? spill.volume.toFixed(2) : '—'} unit={spill?.unit || 'kl'} color="var(--fg-default)" />
<StatBox label="풍화량" value={summary ? summary.weatheredVolume.toFixed(2) : '—'} unit="m³" color="var(--fg-default)" />
<StatBox label="해상잔존" value={summary ? summary.remainingVolume.toFixed(2) : '—'} unit="m³" color="var(--fg-default)" />
<StatBox label="연안부착" value={summary ? summary.beachedVolume.toFixed(2) : '—'} unit="m³" color="var(--fg-default)" />
<div className="col-span-2">
<StatBox label="오염해역면적" value={summary ? summary.pollutionArea.toFixed(2) : '—'} unit="km²" color="var(--cyan)" />
<StatBox label="오염해역면적" value={summary ? summary.pollutionArea.toFixed(2) : '—'} unit="km²" color="var(--fg-default)" />
</div>
</div>
</Section>
{/* 확산 예측 요약 */}
<Section title={`확산 예측 요약 (+${predictionTime ?? 18}h)`} badge="위험" badgeColor="red">
<div className="grid grid-cols-2 gap-0.5 text-[9px]">
<PredictionCard value={spreadSummary?.area != null ? `${spreadSummary.area.toFixed(1)} km²` : '—'} label="영향 면적" color="var(--red)" />
<PredictionCard value={spreadSummary?.distance != null ? `${spreadSummary.distance.toFixed(1)} km` : '—'} label="확산 거리" color="var(--orange)" />
<PredictionCard value={spreadSummary?.directionLabel ?? '—'} label="확산 방향" color="var(--cyan)" />
<PredictionCard value={spreadSummary?.speed != null ? `${spreadSummary.speed.toFixed(2)} m/s` : '—'} label="확산 속도" color="var(--t1)" />
<div className="grid grid-cols-2 gap-0.5 text-[0.625rem]">
<PredictionCard value={spreadSummary?.area != null ? `${spreadSummary.area.toFixed(1)} km²` : '—'} label="영향 면적" color="var(--fg-default)" />
<PredictionCard value={spreadSummary?.distance != null ? `${spreadSummary.distance.toFixed(1)} km` : '—'} label="확산 거리" color="var(--fg-default)" />
<PredictionCard value={spreadSummary?.directionLabel ?? '—'} label="확산 방향" color="var(--fg-default)" />
<PredictionCard value={spreadSummary?.speed != null ? `${spreadSummary.speed.toFixed(2)} m/s` : '—'} label="확산 속도" color="var(--fg-default)" />
</div>
</Section>
{/* 유출유 풍화 상태 */}
<Section title="유출유 풍화 상태">
<div className="flex flex-col gap-[3px] text-[8px]">
<div className="flex flex-col gap-[3px] text-[0.625rem]">
<>
<ProgressBar label="수면잔류" value={weatheringStatus.surface} color="var(--blue)" />
<ProgressBar label="증발" value={weatheringStatus.evaporation} color="var(--cyan)" />
<ProgressBar label="분산" value={weatheringStatus.dispersion} color="var(--green)" />
<ProgressBar label="펜스차단" value={weatheringStatus.boom} color="var(--boom)" />
<ProgressBar label="해안도달" value={weatheringStatus.beached} color="var(--red)" />
<ProgressBar label="수면잔류" value={weatheringStatus.surface} color="var(--color-info)" />
<ProgressBar label="증발" value={weatheringStatus.evaporation} color="var(--color-accent)" />
<ProgressBar label="분산" value={weatheringStatus.dispersion} color="var(--color-success)" />
<ProgressBar label="펜스차단" value={weatheringStatus.boom} color="var(--color-boom)" />
<ProgressBar label="해안도달" value={weatheringStatus.beached} color="var(--color-danger)" />
</>
</div>
</Section>
@ -315,20 +315,20 @@ export function RightPanel({
border: '1px solid rgba(6,182,212,0.2)',
}}>🚢</div>
<div className="flex-1">
<div className="text-[11px] font-bold text-text-1 font-korean">{vessel?.vesselNm || '—'}</div>
<div className="text-[8px] text-text-3 font-mono">IMO {vessel?.imoNo || '—'} · {vessel?.vesselTp || '—'}</div>
<div className="text-[0.6875rem] font-bold text-fg font-korean">{vessel?.vesselNm || '—'}</div>
<div className="text-[0.625rem] text-fg-disabled font-mono">IMO {vessel?.imoNo || '—'} · {vessel?.vesselTp || '—'}</div>
</div>
<span className="text-[7px] px-2 py-0.5 rounded bg-[rgba(239,68,68,0.12)] text-status-red font-bold"></span>
<span className="text-[0.625rem] px-2 py-0.5 rounded bg-[rgba(239,68,68,0.12)] text-color-danger font-bold"></span>
</div>
{/* 제원 */}
<div className="grid grid-cols-3 gap-1">
<SpecCard value={vessel?.loaM?.toFixed(1) || '—'} label="전장 LOA(m)" color="var(--purple)" />
<SpecCard value={vessel?.breadthM?.toFixed(1) || '—'} label="형폭 B(m)" color="var(--cyan)" />
<SpecCard value={vessel?.draftM?.toFixed(1) || '—'} label="흘수 d(m)" color="var(--green)" />
<SpecCard value={vessel?.loaM?.toFixed(1) || '—'} label="전장 LOA(m)" color="var(--fg-default)" />
<SpecCard value={vessel?.breadthM?.toFixed(1) || '—'} label="형폭 B(m)" color="var(--fg-default)" />
<SpecCard value={vessel?.draftM?.toFixed(1) || '—'} label="흘수 d(m)" color="var(--fg-default)" />
</div>
<div className="space-y-0.5 text-[9px] font-korean">
<div className="space-y-0.5 text-[0.625rem] font-korean">
<InfoRow label="총톤수(GT)" value={vessel?.gt ? `${vessel.gt.toLocaleString()}` : '—'} />
<InfoRow label="재화중량(DWT)" value={vessel?.dwt ? `${vessel.dwt.toLocaleString()}` : '—'} />
<InfoRow label="건조" value={vessel?.builtYr ? `${vessel.builtYr}` : '—'} />
@ -340,8 +340,8 @@ export function RightPanel({
{/* 충돌 상대 */}
{vessel2 && (
<div className="p-1.5 bg-[rgba(249,115,22,0.04)] border border-[rgba(249,115,22,0.12)] rounded">
<div className="text-[8px] font-bold text-status-orange font-korean mb-1"> : {vessel2.vesselNm}</div>
<div className="text-[8px] text-text-3 font-korean leading-relaxed">
<div className="text-[0.625rem] font-bold text-color-warning font-korean mb-1"> : {vessel2.vesselNm}</div>
<div className="text-[0.625rem] text-fg-disabled font-korean leading-relaxed">
{vessel2.flagCd} {vessel2.vesselTp} {vessel2.gt ? `${vessel2.gt.toLocaleString()}GT` : ''}
</div>
</div>
@ -378,34 +378,34 @@ export function RightPanel({
))}
</>
) : (
<div className="text-[9px] text-text-3 font-korean text-center py-4"> .</div>
<div className="text-[0.625rem] text-fg-disabled font-korean text-center py-4"> .</div>
)}
</div>
</CollapsibleSection>
</div>
{/* Bottom Action Buttons */}
<div className="flex gap-1.5 p-3 border-t border-border">
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean">
💾
<div className="flex gap-1.5 p-3 border-t border-stroke">
<button className="flex-1 py-2 px-1 rounded-sm text-[0.6875rem] font-semibold border border-color-accent text-color-accent font-korean hover:bg-[rgba(6,182,212,0.08)] transition-colors">
</button>
<button
onClick={onOpenRecalc}
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(249,115,22,0.1)] border border-[rgba(249,115,22,0.3)] text-status-orange font-korean"
className="flex-1 py-2 px-1 rounded-sm text-[0.6875rem] font-semibold border border-stroke text-fg-sub font-korean hover:bg-[var(--bg-surface-hover)] transition-colors"
>
🔄
</button>
<button
onClick={onOpenReport}
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-primary-cyan to-primary-blue text-white font-korean"
className="flex-1 py-2 px-1 rounded-sm text-[0.6875rem] font-semibold border border-stroke text-fg-sub font-korean hover:bg-[var(--bg-surface-hover)] transition-colors"
>
📄
</button>
<button
onClick={onOpenBacktrack}
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(168,85,247,0.1)] border border-[rgba(168,85,247,0.3)] text-purple-500 font-korean"
className="flex-1 py-2 px-1 rounded-sm text-[0.6875rem] font-semibold border border-[var(--color-tertiary)] text-[var(--color-tertiary)] font-korean hover:bg-[rgba(168,85,247,0.08)] transition-colors"
>
🔍
</button>
</div>
</div>
@ -425,15 +425,15 @@ function Section({
children: React.ReactNode
}) {
return (
<div className="bg-bg-3 border border-border rounded-md p-3.5 mb-2.5">
<div className="bg-bg-card border border-stroke rounded-md p-3.5 mb-2.5">
<div className="flex items-center justify-between mb-2.5">
<h4 className="text-xs font-semibold text-text-2 font-korean">{title}</h4>
<h4 className="text-xs font-semibold text-fg-sub font-korean">{title}</h4>
{badge && (
<span
className={`text-[10px] font-semibold px-2 py-1 rounded-full ${
className={`text-[0.625rem] font-semibold px-2 py-0.5 rounded-full ${
badgeColor === 'red'
? 'bg-[rgba(239,68,68,0.15)] text-status-red'
: 'bg-[rgba(34,197,94,0.15)] text-status-green'
? 'bg-[rgba(239,68,68,0.08)] text-color-danger border border-[rgba(239,68,68,0.25)]'
: 'bg-[rgba(34,197,94,0.08)] text-color-success border border-[rgba(34,197,94,0.25)]'
}`}
>
{badge}
@ -458,8 +458,8 @@ function ControlledCheckbox({
}) {
return (
<label
className={`flex items-center gap-1.5 text-[10px] font-korean cursor-pointer ${
disabled ? 'text-text-3 cursor-not-allowed opacity-40' : 'text-text-2'
className={`flex items-center gap-1.5 text-[0.625rem] font-korean cursor-pointer ${
disabled ? 'text-fg-disabled cursor-not-allowed opacity-40' : 'text-fg-sub'
}`}
>
<input
@ -467,7 +467,7 @@ function ControlledCheckbox({
checked={checked}
disabled={disabled}
onChange={(e) => onChange(e.target.checked)}
className="w-[13px] h-[13px] accent-[var(--cyan)]"
className="w-[13px] h-[13px] accent-[var(--color-accent)]"
/>
{children}
</label>
@ -486,12 +486,12 @@ function StatBox({
color: string
}) {
return (
<div className="flex justify-between px-2 py-1 bg-bg-0 border border-border rounded-[3px]">
<span className="text-text-3 font-korean">
<div className="flex justify-between px-2 py-1 bg-bg-base border border-stroke rounded-[3px]">
<span className="text-fg-disabled font-korean">
{label}
</span>
<span style={{ fontWeight: 700, color, fontFamily: 'var(--fM)' }}>
{value} <small className="font-normal text-text-3">{unit}</small>
<span style={{ fontWeight: 700, color, fontFamily: 'var(--font-mono)' }}>
{value} <small className="font-normal text-fg-disabled">{unit}</small>
</span>
</div>
)
@ -499,9 +499,9 @@ function StatBox({
function PredictionCard({ value, label, color }: { value: string; label: string; color: string }) {
return (
<div className="flex justify-between px-2 py-1 bg-bg-0 border border-border rounded-[3px] text-[9px]">
<span className="text-text-3 font-korean">{label}</span>
<span style={{ fontWeight: 700, color, fontFamily: 'var(--fM)' }}>{value}</span>
<div className="flex justify-between px-2 py-1 bg-bg-base border border-stroke rounded-[3px] text-[0.625rem]">
<span className="text-fg-disabled font-korean">{label}</span>
<span style={{ fontWeight: 700, color, fontFamily: 'var(--font-mono)' }}>{value}</span>
</div>
)
}
@ -509,7 +509,7 @@ function PredictionCard({ value, label, color }: { value: string; label: string;
function ProgressBar({ label, value, color }: { label: string; value: number; color: string }) {
return (
<div className="flex items-center gap-1">
<span className="text-text-3 font-korean" style={{ minWidth: '38px' }}>
<span className="text-fg-disabled font-korean" style={{ minWidth: '38px' }}>
{label}
</span>
<div
@ -521,7 +521,7 @@ function ProgressBar({ label, value, color }: { label: string; value: number; co
/>
</div>
<span
style={{ color, minWidth: '28px' }}
style={{ color: 'var(--fg-default)', minWidth: '28px' }}
className="font-semibold text-right font-mono"
>
{value}%
@ -542,13 +542,13 @@ function CollapsibleSection({
children: React.ReactNode
}) {
return (
<div className="bg-bg-3 border border-border rounded-md p-3.5 mb-2.5">
<div className="bg-bg-card border border-stroke rounded-md p-3.5 mb-2.5">
<div
className="flex items-center justify-between cursor-pointer mb-2"
onClick={onToggle}
>
<h4 className="text-xs font-semibold text-text-2 font-korean">{title}</h4>
<span className="text-[10px] text-text-3">{expanded ? '▾' : '▸'}</span>
<h4 className="text-xs font-semibold text-fg-sub font-korean">{title}</h4>
<span className="text-[0.625rem] text-fg-disabled">{expanded ? '▾' : '▸'}</span>
</div>
{expanded && children}
</div>
@ -557,14 +557,14 @@ function CollapsibleSection({
function SpecCard({ value, label, color }: { value: string; label: string; color: string }) {
return (
<div className="text-center py-[6px] px-0.5 bg-bg-0 border border-border rounded-md">
<div className="text-center py-[6px] px-0.5 bg-bg-base border border-stroke rounded-md">
<div
style={{ color }}
className="text-xs font-extrabold font-mono"
>
{value}
</div>
<div className="text-[7px] text-text-3 font-korean">
<div className="text-[0.625rem] text-fg-disabled font-korean">
{label}
</div>
</div>
@ -583,10 +583,10 @@ function InfoRow({
valueColor?: string
}) {
return (
<div className="flex justify-between py-[3px] px-[6px] bg-bg-0 rounded-[3px]">
<span className="text-text-3">{label}</span>
<div className="flex justify-between py-[3px] px-[6px] bg-bg-base rounded-[3px]">
<span className="text-fg-disabled">{label}</span>
<span
style={{ color: valueColor || 'var(--t1)' }}
style={{ color: valueColor || 'var(--fg-default)' }}
className={`font-semibold${mono ? ' font-mono' : ''}`}
>
{value}
@ -608,17 +608,17 @@ function InsuranceCard({
cyan: {
border: 'rgba(6,182,212,0.15)',
bg: 'rgba(6,182,212,0.02)',
text: 'var(--cyan)'
text: 'var(--color-accent)'
},
purple: {
border: 'rgba(168,85,247,0.15)',
bg: 'rgba(168,85,247,0.02)',
text: 'var(--purple)'
text: 'var(--color-tertiary)'
},
red: {
border: 'rgba(239,68,68,0.15)',
bg: 'rgba(239,68,68,0.02)',
text: 'var(--red)'
text: 'var(--color-danger)'
}
}
@ -635,19 +635,19 @@ function InsuranceCard({
>
<div
style={{ color: colors.text }}
className="text-[8px] font-bold font-korean mb-1"
className="text-[0.625rem] font-bold font-korean mb-1"
>
{title}
</div>
<div className="space-y-0.5 text-[8px] font-korean">
<div className="space-y-0.5 text-[0.625rem] font-korean">
{items.map((item, i) => (
<div
key={i}
className="flex justify-between py-0.5 px-1"
>
<span className="text-text-3">{item.label}</span>
<span className="text-fg-disabled">{item.label}</span>
<span
style={{ color: item.valueColor || 'var(--t1)' }}
style={{ color: item.valueColor || 'var(--fg-default)' }}
className={`font-semibold${item.mono ? ' font-mono' : ''}`}
>
{item.value}
@ -674,57 +674,57 @@ function PollResult({
}) {
const pollutedArea = (result.area * result.particlePercent / 100).toFixed(2)
return (
<div className="mt-1 p-2.5 bg-bg-0 border border-[rgba(168,85,247,0.2)] rounded-md" style={{ position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, var(--purple), var(--cyan))' }} />
<div className="mt-1 p-2.5 bg-bg-base border border-[rgba(168,85,247,0.2)] rounded-md" style={{ position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '2px', background: 'linear-gradient(90deg, var(--color-tertiary), var(--color-accent))' }} />
{radiusNm && (
<div className="flex justify-between items-center mb-2">
<span className="text-[10px] font-semibold text-text-1 font-korean"> </span>
<span className="text-[9px] font-semibold text-primary-cyan font-mono"> {radiusNm} NM</span>
<span className="text-[0.625rem] font-semibold text-fg font-korean"> </span>
<span className="text-[0.625rem] font-semibold text-color-accent font-mono"> {radiusNm} NM</span>
</div>
)}
<div className="grid grid-cols-3 gap-1 mb-2">
<div className="text-center py-1.5 px-1 bg-bg-3 rounded">
<div className="text-[13px] font-bold font-mono" style={{ color: 'var(--red)' }}>{result.area.toFixed(2)}</div>
<div className="text-[7px] text-text-3 font-korean mt-0.5">(km²)</div>
<div className="text-center py-1.5 px-1 bg-bg-card rounded">
<div className="text-[13px] font-bold font-mono" style={{ color: 'var(--color-danger)' }}>{result.area.toFixed(2)}</div>
<div className="text-[0.625rem] text-fg-disabled font-korean mt-0.5">(km²)</div>
</div>
<div className="text-center py-1.5 px-1 bg-bg-3 rounded">
<div className="text-[13px] font-bold font-mono" style={{ color: 'var(--orange)' }}>{result.particlePercent}%</div>
<div className="text-[7px] text-text-3 font-korean mt-0.5"></div>
<div className="text-center py-1.5 px-1 bg-bg-card rounded">
<div className="text-[13px] font-bold font-mono" style={{ color: 'var(--color-warning)' }}>{result.particlePercent}%</div>
<div className="text-[0.625rem] text-fg-disabled font-korean mt-0.5"></div>
</div>
<div className="text-center py-1.5 px-1 bg-bg-3 rounded">
<div className="text-[13px] font-bold font-mono" style={{ color: 'var(--cyan)' }}>{pollutedArea}</div>
<div className="text-[7px] text-text-3 font-korean mt-0.5">(km²)</div>
<div className="text-center py-1.5 px-1 bg-bg-card rounded">
<div className="text-[13px] font-bold font-mono" style={{ color: 'var(--color-accent)' }}>{pollutedArea}</div>
<div className="text-[0.625rem] text-fg-disabled font-korean mt-0.5">(km²)</div>
</div>
</div>
<div className="space-y-1 text-[9px] font-korean">
<div className="space-y-1 text-[0.625rem] font-korean">
{summary && (
<div className="flex justify-between">
<span className="text-text-3"></span>
<span className="font-semibold font-mono" style={{ color: 'var(--blue)' }}>{summary.remainingVolume.toFixed(2)} m³</span>
<span className="text-fg-disabled"></span>
<span className="font-semibold font-mono" style={{ color: 'var(--color-info)' }}>{summary.remainingVolume.toFixed(2)} m³</span>
</div>
)}
{summary && (
<div className="flex justify-between">
<span className="text-text-3"></span>
<span className="font-semibold font-mono" style={{ color: 'var(--red)' }}>{summary.beachedVolume.toFixed(2)} m³</span>
<span className="text-fg-disabled"></span>
<span className="font-semibold font-mono" style={{ color: 'var(--color-danger)' }}>{summary.beachedVolume.toFixed(2)} m³</span>
</div>
)}
<div className="flex justify-between">
<span className="text-text-3"> </span>
<span className="font-semibold font-mono" style={{ color: 'var(--orange)' }}>{result.sensitiveCount}</span>
<span className="text-fg-disabled"> </span>
<span className="font-semibold font-mono" style={{ color: 'var(--color-warning)' }}>{result.sensitiveCount}</span>
</div>
</div>
<div className="flex gap-1.5 mt-2">
<button
onClick={onClear}
className="flex-1 py-1.5 rounded text-[9px] font-semibold font-korean border border-border text-text-3 hover:text-text-2 transition-colors"
className="flex-1 py-1.5 rounded text-[0.625rem] font-semibold font-korean border border-stroke text-fg-disabled hover:text-fg-sub transition-colors"
>
</button>
{onRerun && (
<button
onClick={onRerun}
className="flex-1 py-1.5 rounded text-[9px] font-semibold font-korean border border-[rgba(168,85,247,0.3)] text-purple-400 hover:bg-[rgba(168,85,247,0.08)] transition-colors"
className="flex-1 py-1.5 rounded text-[0.625rem] font-semibold font-korean border border-[rgba(168,85,247,0.3)] text-purple-400 hover:bg-[rgba(168,85,247,0.08)] transition-colors"
>
</button>

파일 보기

@ -20,9 +20,9 @@ const SimulationErrorModal = ({ message, onClose }: SimulationErrorModalProps) =
<div
style={{
width: 360,
background: 'var(--bg1)',
background: 'var(--bg-surface)',
border: '1px solid rgba(239, 68, 68, 0.35)',
borderRadius: 'var(--rM)',
borderRadius: 'var(--radius-md)',
padding: '28px 24px',
display: 'flex',
flexDirection: 'column',
@ -53,10 +53,10 @@ const SimulationErrorModal = ({ message, onClose }: SimulationErrorModalProps) =
</svg>
</div>
<div>
<div style={{ color: 'var(--t1)', fontSize: 14, fontWeight: 600 }}>
<div style={{ color: 'var(--fg-default)', fontSize: 14, fontWeight: 600 }}>
</div>
<div style={{ color: 'var(--t3)', fontSize: 12, marginTop: 2 }}>
<div style={{ color: 'var(--fg-disabled)', fontSize: 12, marginTop: 2 }}>
</div>
</div>
@ -67,7 +67,7 @@ const SimulationErrorModal = ({ message, onClose }: SimulationErrorModalProps) =
style={{
background: 'rgba(239, 68, 68, 0.06)',
border: '1px solid rgba(239, 68, 68, 0.2)',
borderRadius: 'var(--rS)',
borderRadius: 'var(--radius-sm)',
padding: '10px 14px',
color: 'rgb(252, 165, 165)',
fontSize: 13,
@ -86,7 +86,7 @@ const SimulationErrorModal = ({ message, onClose }: SimulationErrorModalProps) =
padding: '8px 0',
background: 'rgba(239, 68, 68, 0.15)',
border: '1px solid rgba(239, 68, 68, 0.35)',
borderRadius: 'var(--rS)',
borderRadius: 'var(--radius-sm)',
color: 'rgb(252, 165, 165)',
fontSize: 13,
fontWeight: 600,

Some files were not shown because too many files have changed in this diff Show More