From da4dc86e9014d05cb7108b669473123b8562b5e0 Mon Sep 17 00:00:00 2001 From: htlee Date: Wed, 8 Apr 2026 12:36:07 +0900 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=EC=9D=B8=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EB=B2=84=ED=8A=BC/=ED=95=98=EB=93=9C=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EC=83=89=EC=83=81=20=EC=A0=84=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase C-2 (인라인 + ) : row.status !== '장애발생' ? ( - + ) : null} - - + + ), }, @@ -290,9 +291,9 @@ const loadColumns: DataColumn[] = [ { key: 'id', label: '', width: '70px', align: 'center', sortable: false, render: () => (
- - - + + +
), }, @@ -418,7 +419,7 @@ export function DataHub() { {/* 탭 */} -
+ {[ { key: 'signal' as Tab, icon: Activity, label: '선박신호 수신 현황' }, { key: 'monitor' as Tab, icon: Server, label: '선박위치정보 모니터링' }, @@ -426,18 +427,17 @@ export function DataHub() { { key: 'load' as Tab, icon: HardDrive, label: '적재 작업 관리' }, { key: 'agents' as Tab, icon: Network, label: '연계서버 모니터링' }, ].map((t) => ( - + ))} -
+ {/* ── ① 선박신호 수신 현황 ── */} {tab === 'signal' && ( @@ -455,10 +455,9 @@ export function DataHub() { className="bg-surface-overlay border border-slate-700/50 rounded-lg px-3 py-1.5 text-[11px] text-heading focus:outline-none focus:border-cyan-500/50" /> - + @@ -527,17 +526,15 @@ export function DataHub() { {/* 상태 필터 */}
{(['', 'ON', 'OFF'] as const).map((f) => ( - + ))}
@@ -560,19 +557,21 @@ export function DataHub() {
서버 타입: {(['', 'SQL', 'FILE', 'FTP'] as const).map((f) => ( - + setCollectTypeFilter(f)} className="px-2.5 py-1 text-[10px]"> + {f || '전체'} + ))} 상태: {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( - + setCollectStatusFilter(f)} className="px-2.5 py-1 text-[10px]"> + {f || '전체'} + ))} - +
+ +
@@ -585,17 +584,17 @@ export function DataHub() {
상태: {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( - + setLoadStatusFilter(f)} className="px-2.5 py-1 text-[10px]"> + {f || '전체'} + ))}
- - + +
종류: {(['', '수집', '적재'] as const).map((f) => ( - + setAgentRoleFilter(f)} className="px-2.5 py-1 text-[10px]"> + {f || '전체'} + ))} 상태: {(['', '수행중', '대기중', '장애발생', '정지'] as const).map((f) => ( - + setAgentStatusFilter(f)} className="px-2.5 py-1 text-[10px]"> + {f || '전체'} + ))} - +
+ +
{/* 연계서버 카드 그리드 */} @@ -651,9 +652,9 @@ export function DataHub() {
작업 {agent.taskCount}건 · heartbeat {agent.lastHeartbeat.slice(11)}
- - - + + +
diff --git a/frontend/src/features/admin/NoticeManagement.tsx b/frontend/src/features/admin/NoticeManagement.tsx index 7608264..6888d59 100644 --- a/frontend/src/features/admin/NoticeManagement.tsx +++ b/frontend/src/features/admin/NoticeManagement.tsx @@ -237,10 +237,10 @@ export function NoticeManagement() {
- -
@@ -261,7 +261,7 @@ export function NoticeManagement() { {editingId ? '알림 수정' : '새 알림 등록'} - @@ -296,7 +296,7 @@ export function NoticeManagement() {
{TYPE_OPTIONS.map((opt) => ( - - + +
)} @@ -464,11 +467,15 @@ export function PermissionsPanel() { {canUpdatePerm && selectedRole && ( - + )} diff --git a/frontend/src/features/admin/SystemConfig.tsx b/frontend/src/features/admin/SystemConfig.tsx index b97f823..d731be9 100644 --- a/frontend/src/features/admin/SystemConfig.tsx +++ b/frontend/src/features/admin/SystemConfig.tsx @@ -194,7 +194,7 @@ export function SystemConfig() { {/* 탭 */}
{TAB_ITEMS.map((t) => ( -
diff --git a/frontend/src/features/ai-operations/AIModelManagement.tsx b/frontend/src/features/ai-operations/AIModelManagement.tsx index e7b19df..898b61e 100644 --- a/frontend/src/features/ai-operations/AIModelManagement.tsx +++ b/frontend/src/features/ai-operations/AIModelManagement.tsx @@ -298,7 +298,7 @@ export function AIModelManagement() { { key: 'engines' as Tab, icon: Shield, label: '7대 탐지 엔진' }, { key: 'api' as Tab, icon: Globe, label: '예측 결과 API' }, ].map((t) => ( - @@ -317,7 +317,7 @@ export function AIModelManagement() {
정확도 93.2% (+3.1%) · 오탐률 7.8% (-2.1%) · 다크베셀 탐지 강화
- + @@ -331,7 +331,7 @@ export function AIModelManagement() { {rules.map((rule, i) => ( - @@ -880,7 +880,7 @@ export function AIModelManagement() {
격자별 위험도 조회 (파라미터: 좌표 범위, 시간) - +
 {`GET /api/v1/predictions/grid
diff --git a/frontend/src/features/ai-operations/MLOpsPage.tsx b/frontend/src/features/ai-operations/MLOpsPage.tsx
index e82fa1a..4b5819d 100644
--- a/frontend/src/features/ai-operations/MLOpsPage.tsx
+++ b/frontend/src/features/ai-operations/MLOpsPage.tsx
@@ -133,7 +133,7 @@ export function MLOpsPage() {
           { key: 'llmops' as Tab, icon: Brain, label: 'LLMOps' },
           { key: 'platform' as Tab, icon: Settings, label: '플랫폼 관리' },
         ]).map(t => (
-          
@@ -197,7 +197,7 @@ export function MLOpsPage() {
           
             
실험 목록
- +
{EXPERIMENTS.map(e => ( @@ -288,7 +288,7 @@ export function MLOpsPage() { {MODELS.filter(m => m.status === 'APPROVED').map(m => (
{m.name} {m.ver} - +
))}
@@ -313,8 +313,8 @@ export function MLOpsPage() { "version": "v2.1.0" }`} />
- - + +
@@ -352,7 +352,7 @@ export function MLOpsPage() { { key: 'worker' as LLMSubTab, label: '배포 워커' }, { key: 'llmtest' as LLMSubTab, label: 'LLM 테스트' }, ]).map(t => ( - ))}
@@ -381,7 +381,7 @@ export function MLOpsPage() {
{k}
{v}
))} - +
@@ -417,7 +417,7 @@ export function MLOpsPage() {
{k}{v}
))} - +
HPS 시도 결과
Best: Trial #3 (F1=0.912)
@@ -505,7 +505,7 @@ export function MLOpsPage() {
- +
diff --git a/frontend/src/features/detection/ChinaFishing.tsx b/frontend/src/features/detection/ChinaFishing.tsx index fcd62dd..4bba840 100644 --- a/frontend/src/features/detection/ChinaFishing.tsx +++ b/frontend/src/features/detection/ChinaFishing.tsx @@ -292,7 +292,7 @@ export function ChinaFishing() { {/* ── 모드 탭 (AI 대시보드 / 환적 탐지) ── */}
{modeTabs.map((tab) => ( - +
@@ -623,7 +623,7 @@ export function ChinaFishing() {
기상 예보 - +
@@ -646,7 +646,7 @@ export function ChinaFishing() {
VTS연계 현황 - +
{VTS_ITEMS.map((vts) => ( @@ -664,10 +664,10 @@ export function ChinaFishing() { ))}
- -
diff --git a/frontend/src/features/detection/DarkVesselDetection.tsx b/frontend/src/features/detection/DarkVesselDetection.tsx index 445cc39..ccd8568 100644 --- a/frontend/src/features/detection/DarkVesselDetection.tsx +++ b/frontend/src/features/detection/DarkVesselDetection.tsx @@ -84,7 +84,7 @@ export function DarkVesselDetection() { { key: 'status', label: '상태', width: '70px', align: 'center', sortable: true, render: v => {getVesselSurveillanceLabel(v as string, tc, lang)} }, { key: 'label', label: '라벨', width: '60px', align: 'center', - render: v => { const l = v as string; return l === '-' ? : {l}; } }, + render: v => { const l = v as string; return l === '-' ? : {l}; } }, ], [tc, lang]); const [darkItems, setDarkItems] = useState([]); diff --git a/frontend/src/features/detection/GearIdentification.tsx b/frontend/src/features/detection/GearIdentification.tsx index 9900a4f..a606bb4 100644 --- a/frontend/src/features/detection/GearIdentification.tsx +++ b/frontend/src/features/detection/GearIdentification.tsx @@ -648,7 +648,7 @@ export function GearIdentification() {

- - diff --git a/frontend/src/features/statistics/ReportManagement.tsx b/frontend/src/features/statistics/ReportManagement.tsx index b1ea820..9afd932 100644 --- a/frontend/src/features/statistics/ReportManagement.tsx +++ b/frontend/src/features/statistics/ReportManagement.tsx @@ -81,7 +81,7 @@ export function ReportManagement() {
증거 파일 업로드 (사진·영상·문서) - +
@@ -120,8 +120,8 @@ export function ReportManagement() {
증거 {r.evidence}건
- - + +
))} @@ -135,7 +135,7 @@ export function ReportManagement() {
보고서 미리보기
-
diff --git a/frontend/src/features/surveillance/LiveMapView.tsx b/frontend/src/features/surveillance/LiveMapView.tsx index 27a71cf..9b81840 100644 --- a/frontend/src/features/surveillance/LiveMapView.tsx +++ b/frontend/src/features/surveillance/LiveMapView.tsx @@ -289,7 +289,7 @@ export function LiveMapView() { {/* 지도 영역 */}
- + {/* 범례 */}
선박 범례
diff --git a/frontend/src/features/surveillance/MapControl.tsx b/frontend/src/features/surveillance/MapControl.tsx index 90d07e9..60a0657 100644 --- a/frontend/src/features/surveillance/MapControl.tsx +++ b/frontend/src/features/surveillance/MapControl.tsx @@ -300,7 +300,7 @@ export function MapControl() { { key: 'kcg' as Tab, label: '해경', icon: Anchor }, { key: 'ntm' as Tab, label: '항행통보', icon: Bell }, ]).map(t => ( - @@ -309,7 +309,7 @@ export function MapControl() {
{['', '서해', '남해', '동해', '제주'].map(s => ( - @@ -340,7 +340,7 @@ export function MapControl() { 구분: {NTM_CATEGORIES.map(c => ( - ))}
diff --git a/frontend/src/features/vessel/VesselDetail.tsx b/frontend/src/features/vessel/VesselDetail.tsx index b6930bc..1e65326 100644 --- a/frontend/src/features/vessel/VesselDetail.tsx +++ b/frontend/src/features/vessel/VesselDetail.tsx @@ -180,7 +180,7 @@ export function VesselDetail() { setSearchMmsi(e.target.value)} placeholder="MMSI 입력" className="flex-1 bg-surface-overlay border border-slate-700/50 rounded px-2 py-1 text-[10px] text-label focus:outline-none" /> -
@@ -414,19 +414,19 @@ export function VesselDetail() { {/* ── 우측 도구바 ── */}
{RIGHT_TOOLS.map((t) => ( - ))}
- +
- +
- - - + + +
); diff --git a/frontend/src/shared/components/ui/tabs.tsx b/frontend/src/shared/components/ui/tabs.tsx new file mode 100644 index 0000000..f54c874 --- /dev/null +++ b/frontend/src/shared/components/ui/tabs.tsx @@ -0,0 +1,78 @@ +import { type ButtonHTMLAttributes, type ReactNode } from 'react'; +import { cn } from '@lib/utils/cn'; + +/** + * TabBar — 탭 버튼 그룹 공통 컴포넌트 + * + * 3가지 스타일: + * - 'underline' (기본): border-b 밑줄 탭 (MLOps, RiskMap 스타일) + * - 'pill': 라운드 pill 탭 (ChinaFishing 스타일) + * - 'segmented': 배경 그룹 segmented control 스타일 + * + * 사용: + * + * setTab('a')}>탭 A + * setTab('b')}>탭 B + * + */ + +export interface TabBarProps { + variant?: 'underline' | 'pill' | 'segmented'; + children: ReactNode; + className?: string; +} + +export function TabBar({ variant = 'underline', children, className }: TabBarProps) { + const variantClass = { + underline: 'flex gap-0 border-b border-border', + pill: 'flex items-center gap-1', + segmented: 'flex items-center gap-1 bg-surface-raised rounded-lg p-1 border border-border w-fit', + }[variant]; + return
{children}
; +} + +export interface TabButtonProps extends ButtonHTMLAttributes { + active?: boolean; + variant?: 'underline' | 'pill' | 'segmented'; + icon?: ReactNode; + children: ReactNode; +} + +/** + * TabButton — 단일 탭 버튼. active 상태에 따라 스타일 변화. + */ +export function TabButton({ + active = false, + variant = 'underline', + icon, + children, + className, + type = 'button', + ...props +}: TabButtonProps) { + const variantClass = { + underline: cn( + 'flex items-center gap-1.5 px-4 py-2 text-[11px] font-medium border-b-2 transition-colors', + active ? 'text-blue-400 border-blue-400' : 'text-hint border-transparent hover:text-label', + ), + pill: cn( + 'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[11px] font-medium transition-colors', + active + ? 'bg-blue-600 text-on-vivid' + : 'text-muted-foreground hover:bg-secondary hover:text-foreground', + ), + segmented: cn( + 'flex items-center gap-1.5 px-4 py-2 rounded-md text-[11px] font-medium transition-colors', + active + ? 'bg-blue-600 text-on-vivid' + : 'text-muted-foreground hover:text-foreground hover:bg-surface-overlay', + ), + }[variant]; + + return ( + + ); +}