refactor(frontend): enforcement/field-ops/patrol PageContainer/PageHeader 적용
- EnforcementHistory/EventList/EnforcementPlan: primary Button 액션 - EventList: Select 공통 컴포넌트로 등급 필터 치환 - AIAlert/ShipAgent/MobileService: PageContainer + PageHeader(demo) - PatrolRoute/FleetOptimization: primary Button 액션 2개씩 Phase B-3 완료. 총 10개 파일.
This commit is contained in:
부모
a1c521792d
커밋
2976796652
@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import { FileText, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||
import { useEnforcementStore } from '@stores/enforcementStore';
|
||||
@ -121,14 +122,13 @@ export function EnforcementHistory() {
|
||||
const DATA: Record[] = records as Record[];
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-blue-400" />
|
||||
{t('history.title')}
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('history.desc')}</p>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={FileText}
|
||||
iconColor="text-blue-400"
|
||||
title={t('history.title')}
|
||||
description={t('history.desc')}
|
||||
/>
|
||||
|
||||
{/* KPI 카드 — backend enum 코드(PUNISHED/REFERRED/FALSE_POSITIVE) 기반 비교 */}
|
||||
<div className="flex gap-2">
|
||||
@ -188,6 +188,6 @@ export function EnforcementHistory() {
|
||||
exportFilename="단속이력"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { Select } from '@shared/components/ui/select';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import { FileUpload } from '@shared/components/common/FileUpload';
|
||||
import {
|
||||
@ -135,45 +138,41 @@ export function EventList() {
|
||||
const kpiTotal = (stats['TOTAL'] as number | undefined) ?? EVENTS.length;
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Radar className="w-5 h-5 text-blue-400" />
|
||||
{t('eventList.title')}
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">
|
||||
{t('eventList.desc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 등급 필터 */}
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Radar}
|
||||
iconColor="text-blue-400"
|
||||
title={t('eventList.title')}
|
||||
description={t('eventList.desc')}
|
||||
actions={
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<Filter className="w-3.5 h-3.5 text-hint" />
|
||||
<select
|
||||
<Select
|
||||
size="sm"
|
||||
value={levelFilter}
|
||||
onChange={(e) => setLevelFilter(e.target.value)}
|
||||
title="등급 필터"
|
||||
className="bg-surface-overlay border border-slate-700/50 rounded-lg px-3 py-1.5 text-[11px] text-label focus:outline-none focus:border-blue-500/50"
|
||||
className="w-32"
|
||||
>
|
||||
<option value="">전체 등급</option>
|
||||
<option value="CRITICAL">CRITICAL</option>
|
||||
<option value="HIGH">HIGH</option>
|
||||
<option value="MEDIUM">MEDIUM</option>
|
||||
<option value="LOW">LOW</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setShowUpload(!showUpload)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 bg-surface-overlay border border-border rounded-lg text-[10px] text-muted-foreground hover:text-heading transition-colors"
|
||||
icon={<Upload className="w-3 h-3" />}
|
||||
>
|
||||
<Upload className="w-3 h-3" />
|
||||
파일 업로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* KPI 요약 */}
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
@ -248,6 +247,6 @@ export function EventList() {
|
||||
exportFilename="이벤트목록"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import { Send, Loader2, AlertTriangle } from 'lucide-react';
|
||||
import { getAlerts, type PredictionAlert } from '@/services/event';
|
||||
@ -140,34 +141,37 @@ export function AIAlert() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-5 flex items-center justify-center gap-2 text-muted-foreground">
|
||||
<PageContainer>
|
||||
<div className="flex items-center justify-center gap-2 text-muted-foreground py-8">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>알림 데이터 로딩 중...</span>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-5 flex items-center justify-center gap-2 text-red-400">
|
||||
<PageContainer>
|
||||
<div className="flex items-center justify-center gap-2 text-red-400 py-8">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<span>알림 조회 실패: {error}</span>
|
||||
<button type="button" onClick={fetchAlerts} className="ml-2 text-xs underline text-cyan-400">
|
||||
재시도
|
||||
</button>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Send className="w-5 h-5 text-yellow-400" />
|
||||
{t('aiAlert.title')}
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('aiAlert.desc')}</p>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Send}
|
||||
iconColor="text-yellow-400"
|
||||
title={t('aiAlert.title')}
|
||||
description={t('aiAlert.desc')}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ l: '총 발송', v: totalElements, c: 'text-heading' },
|
||||
@ -191,6 +195,6 @@ export function AIAlert() {
|
||||
searchKeys={['channel', 'recipient']}
|
||||
exportFilename="AI알림이력"
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { Smartphone, MapPin, Bell, Wifi, WifiOff, Shield, AlertTriangle, Navigation } from 'lucide-react';
|
||||
import { BaseMap, createMarkerLayer, createPolylineLayer, useMapLayers, type MapHandle, type MarkerData } from '@lib/map';
|
||||
import { useEventStore } from '@stores/eventStore';
|
||||
@ -52,11 +53,13 @@ export function MobileService() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2"><Smartphone className="w-5 h-5 text-blue-400" />{t('mobileService.title')}</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('mobileService.desc')}</p>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Smartphone}
|
||||
iconColor="text-blue-400"
|
||||
title={t('mobileService.title')}
|
||||
description={t('mobileService.desc')}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* 모바일 프리뷰 */}
|
||||
<Card>
|
||||
@ -143,6 +146,6 @@ export function MobileService() {
|
||||
</CardContent></Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import { Monitor, Ship, Wifi, WifiOff, RefreshCw, MapPin, Clock, CheckCircle } from 'lucide-react';
|
||||
import { Monitor } from 'lucide-react';
|
||||
import { getDeviceStatusIntent, getDeviceStatusLabel } from '@shared/constants/deviceStatuses';
|
||||
import { useSettingsStore } from '@stores/settingsStore';
|
||||
|
||||
@ -44,16 +44,14 @@ export function ShipAgent() {
|
||||
], [tc, lang]);
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Monitor className="w-5 h-5 text-cyan-400" />{t('shipAgent.title')}
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-500/15 border border-yellow-500/30 rounded text-[10px] text-yellow-400 font-normal">
|
||||
<span>⚠</span><span>데모 데이터 (백엔드 API 미구현)</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('shipAgent.desc')}</p>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Monitor}
|
||||
iconColor="text-cyan-400"
|
||||
title={t('shipAgent.title')}
|
||||
description={t('shipAgent.desc')}
|
||||
demo
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{[{ l: '전체 Agent', v: DATA.length, c: 'text-heading' }, { l: '온라인', v: DATA.filter(d => d.status === '온라인').length, c: 'text-green-400' }, { l: '오프라인', v: DATA.filter(d => d.status === '오프라인').length, c: 'text-red-400' }, { l: '미배포', v: DATA.filter(d => d.status === '미배포').length, c: 'text-muted-foreground' }].map(k => (
|
||||
<div key={k.l} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
|
||||
@ -62,6 +60,6 @@ export function ShipAgent() {
|
||||
))}
|
||||
</div>
|
||||
<DataTable data={DATA} columns={cols} pageSize={10} searchPlaceholder="함정명, Agent ID 검색..." searchKeys={['ship', 'id']} exportFilename="함정Agent" />
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { BaseMap, STATIC_LAYERS, createMarkerLayer, createPolylineLayer, createZoneLayer, useMapLayers, type MapHandle } from '@lib/map';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { Users, Ship, Target, BarChart3, Play, CheckCircle, AlertTriangle, Layers, RefreshCw } from 'lucide-react';
|
||||
import { usePatrolStore } from '@stores/patrolStore';
|
||||
|
||||
@ -96,24 +98,31 @@ export function FleetOptimization() {
|
||||
useMapLayers(mapRef, buildLayers, [ships, COVERAGE, FLEET_ROUTES]);
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-purple-400" />{t('fleetOptimization.title')}
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-500/15 border border-yellow-500/30 rounded text-[10px] text-yellow-400 font-normal">
|
||||
<span>⚠</span>
|
||||
<span>데모 데이터 (백엔드 API 미구현)</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('fleetOptimization.desc')}</p>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<button onClick={() => setSimRunning(true)} className="flex items-center gap-1 px-3 py-1.5 bg-purple-600 hover:bg-purple-500 text-on-vivid text-[10px] font-bold rounded-lg"><Play className="w-3 h-3" />시뮬레이션</button>
|
||||
<button onClick={() => setApproved(true)} disabled={!simRunning}
|
||||
className="flex items-center gap-1 px-3 py-1.5 bg-green-600 hover:bg-green-500 disabled:opacity-30 text-on-vivid text-[10px] font-bold rounded-lg"><CheckCircle className="w-3 h-3" />최종 승인</button>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Users}
|
||||
iconColor="text-purple-400"
|
||||
title={t('fleetOptimization.title')}
|
||||
description={t('fleetOptimization.desc')}
|
||||
demo
|
||||
actions={
|
||||
<>
|
||||
<Button variant="primary" size="sm" onClick={() => setSimRunning(true)} icon={<Play className="w-3 h-3" />}>
|
||||
시뮬레이션
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setApproved(true)}
|
||||
disabled={!simRunning}
|
||||
icon={<CheckCircle className="w-3 h-3" />}
|
||||
className="bg-green-600 hover:bg-green-500 border-green-700"
|
||||
>
|
||||
최종 승인
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* KPI */}
|
||||
<div className="flex gap-2">
|
||||
@ -218,6 +227,6 @@ export function FleetOptimization() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import type maplibregl from 'maplibre-gl';
|
||||
import { BaseMap, STATIC_LAYERS, createMarkerLayer, createPolylineLayer, useMapLayers, type MapHandle } from '@lib/map';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { Navigation, Ship, MapPin, Clock, Wind, Anchor, Play, BarChart3, Target, Settings, CheckCircle, Share2 } from 'lucide-react';
|
||||
import { usePatrolStore } from '@stores/patrolStore';
|
||||
|
||||
@ -93,23 +95,24 @@ export function PatrolRoute() {
|
||||
if (!currentShip || !route) return null;
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
|
||||
<Navigation className="w-5 h-5 text-cyan-400" />{t('patrolRoute.title')}
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 bg-yellow-500/15 border border-yellow-500/30 rounded text-[10px] text-yellow-400 font-normal">
|
||||
<span>⚠</span>
|
||||
<span>데모 데이터 (백엔드 API 미구현)</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('patrolRoute.desc')}</p>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<button className="flex items-center gap-1 px-3 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-on-vivid text-[10px] font-bold rounded-lg"><Play className="w-3 h-3" />경로 생성</button>
|
||||
<button className="flex items-center gap-1 px-3 py-1.5 bg-surface-overlay border border-border rounded-lg text-[10px] text-muted-foreground hover:text-heading"><Share2 className="w-3 h-3" />공유</button>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Navigation}
|
||||
iconColor="text-cyan-400"
|
||||
title={t('patrolRoute.title')}
|
||||
description={t('patrolRoute.desc')}
|
||||
demo
|
||||
actions={
|
||||
<>
|
||||
<Button variant="primary" size="sm" icon={<Play className="w-3 h-3" />}>
|
||||
경로 생성
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" icon={<Share2 className="w-3 h-3" />}>
|
||||
공유
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{/* 함정 선택 */}
|
||||
@ -215,6 +218,6 @@ export function PatrolRoute() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from '@shared/components/ui/card';
|
||||
import { Badge } from '@shared/components/ui/badge';
|
||||
import { Button } from '@shared/components/ui/button';
|
||||
import { PageContainer, PageHeader } from '@shared/components/layout';
|
||||
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
|
||||
import { Shield, AlertTriangle, Ship, Plus, Calendar, Users } from 'lucide-react';
|
||||
import { BaseMap, STATIC_LAYERS, createMarkerLayer, createRadiusLayer, useMapLayers, type MapHandle } from '@lib/map';
|
||||
@ -118,14 +120,18 @@ export function EnforcementPlan() {
|
||||
const totalCrew = PLANS.reduce((sum, p) => sum + p.crew, 0);
|
||||
|
||||
return (
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2"><Shield className="w-5 h-5 text-orange-400" />{t('enforcementPlan.title')}</h2>
|
||||
<p className="text-[10px] text-hint mt-0.5">{t('enforcementPlan.desc')}</p>
|
||||
</div>
|
||||
<button className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-on-vivid text-[11px] font-bold rounded-lg"><Plus className="w-3.5 h-3.5" />단속 계획 수립</button>
|
||||
</div>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
icon={Shield}
|
||||
iconColor="text-orange-400"
|
||||
title={t('enforcementPlan.title')}
|
||||
description={t('enforcementPlan.desc')}
|
||||
actions={
|
||||
<Button variant="primary" size="md" icon={<Plus className="w-3.5 h-3.5" />}>
|
||||
단속 계획 수립
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 로딩/에러 상태 */}
|
||||
{loading && (
|
||||
@ -185,6 +191,6 @@ export function EnforcementPlan() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user