diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 08f97e8..f6e3ef0 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,9 @@ ## [Unreleased] +### 변경 +- **Admin 11개 페이지 디자인 시스템 하드코딩 색상 제거 (Phase 1-B)** — 129건 Tailwind 색상 → 시맨틱 토큰(text-label/text-heading/text-hint) + Badge intent 치환. raw ` {row.userSttsCd === 'LOCKED' && ( )} @@ -166,7 +166,7 @@ export function AccessControl() { { key: 'createdAt', label: '일시', width: '160px', sortable: true, render: (v) => {formatDateTime(v as string)} }, { key: 'userAcnt', label: '사용자', width: '90px', sortable: true, - render: (v) => {(v as string) || '-'} }, + render: (v) => {(v as string) || '-'} }, { key: 'actionCd', label: '액션', width: '180px', sortable: true, render: (v) => {v as string} }, { key: 'resourceType', label: '리소스', width: '110px', @@ -180,24 +180,24 @@ export function AccessControl() { }, }, { key: 'failReason', label: '실패 사유', - render: (v) => {(v as string) || '-'} }, + render: (v) => {(v as string) || '-'} }, ], []); return ( {userStats && (
- - 활성 {userStats.active}명 + + 활성 {userStats.active}| - 잠금 {userStats.locked} + 잠금 {userStats.locked} |{userStats.total}
@@ -237,7 +237,7 @@ export function AccessControl() { ))} - {error &&
에러: {error}
} + {error &&
에러: {error}
} {/* ── 역할 관리 (PermissionsPanel: 트리 + R/C/U/D 매트릭스) ── */} {tab === 'roles' && } @@ -249,8 +249,8 @@ export function AccessControl() { {userStats && (
- - + +
)} @@ -278,9 +278,9 @@ export function AccessControl() { {auditStats && (
- - - + + +
)} @@ -292,7 +292,7 @@ export function AccessControl() {
{auditStats.byAction.map((a) => ( - {a.action} {a.count} + {a.action} {a.count} ))}
diff --git a/frontend/src/features/admin/AccessLogs.tsx b/frontend/src/features/admin/AccessLogs.tsx index 6e367c3..acde808 100644 --- a/frontend/src/features/admin/AccessLogs.tsx +++ b/frontend/src/features/admin/AccessLogs.tsx @@ -37,7 +37,7 @@ export function AccessLogs() { - - - - + + + + )} @@ -73,7 +73,7 @@ export function AccessLogs() { {stats.topPaths.map((p) => ( {p.path} - {p.count} + {p.count} {p.avg_ms} ))} @@ -83,7 +83,7 @@ export function AccessLogs() { )} - {error &&
에러: {error}
} + {error &&
에러: {error}
} {loading &&
} @@ -109,8 +109,8 @@ export function AccessLogs() { {it.accessSn} {formatDateTime(it.createdAt)} - {it.userAcnt || '-'} - {it.httpMethod} + {it.userAcnt || '-'} + {it.httpMethod} {it.requestPath} {it.statusCode} diff --git a/frontend/src/features/admin/AdminPanel.tsx b/frontend/src/features/admin/AdminPanel.tsx index e83874c..14916ad 100644 --- a/frontend/src/features/admin/AdminPanel.tsx +++ b/frontend/src/features/admin/AdminPanel.tsx @@ -67,7 +67,7 @@ export function AdminPanel() {
- 데이터베이스 + 데이터베이스 {[['PostgreSQL', 'v15.4 운영중'], ['TimescaleDB', 'v2.12 운영중'], ['Redis 캐시', 'v7.2 운영중'], ['Kafka', 'v3.6 클러스터 3노드']].map(([k, v]) => ( @@ -77,7 +77,7 @@ export function AdminPanel() { - 보안 현황 + 보안 현황 {[['SSL 인증서', '2027-03-15 만료'], ['방화벽', '정상 동작'], ['IDS/IPS', '실시간 감시중'], ['백업', '금일 03:00 완료']].map(([k, v]) => ( diff --git a/frontend/src/features/admin/AuditLogs.tsx b/frontend/src/features/admin/AuditLogs.tsx index 08d9625..59af1f0 100644 --- a/frontend/src/features/admin/AuditLogs.tsx +++ b/frontend/src/features/admin/AuditLogs.tsx @@ -36,7 +36,7 @@ export function AuditLogs() { - - - + + +
)} @@ -64,7 +64,7 @@ export function AuditLogs() {
{stats.byAction.map((a) => ( - {a.action} {a.count} + {a.action} {a.count} ))}
@@ -72,7 +72,7 @@ export function AuditLogs() { )} - {error &&
에러: {error}
} + {error &&
에러: {error}
} {loading &&
} @@ -99,7 +99,7 @@ export function AuditLogs() { {it.auditSn} {formatDateTime(it.createdAt)} - {it.userAcnt || '-'} + {it.userAcnt || '-'} {it.actionCd} {it.resourceType ?? '-'} {it.resourceId ? `(${it.resourceId})` : ''} @@ -107,7 +107,7 @@ export function AuditLogs() { {it.result || '-'} - {it.failReason || '-'} + {it.failReason || '-'} {it.ipAddress || '-'} {it.detail ? JSON.stringify(it.detail) : '-'} diff --git a/frontend/src/features/admin/DataHub.tsx b/frontend/src/features/admin/DataHub.tsx index 7c1b6cf..38a57a4 100644 --- a/frontend/src/features/admin/DataHub.tsx +++ b/frontend/src/features/admin/DataHub.tsx @@ -1,29 +1,27 @@ -import { useState, useMemo } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; +import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { TabBar, TabButton } from '@shared/components/ui/tabs'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; -import { SaveButton } from '@shared/components/common/SaveButton'; import { getConnectionStatusHex } from '@shared/constants/connectionStatuses'; import type { BadgeIntent } from '@lib/theme/variants'; +import { + Database, RefreshCw, Wifi, WifiOff, Radio, + Activity, Server, ArrowDownToLine, AlertTriangle, + CheckCircle, BarChart3, Layers, Plus, Play, Square, + Trash2, Edit2, Eye, FileText, HardDrive, FolderOpen, + Network, +} from 'lucide-react'; -/** 수집/적재 작업 상태 → BadgeIntent 매핑 (DataHub 로컬 전용) */ function jobStatusIntent(s: string): BadgeIntent { if (s === '수행중') return 'success'; if (s === '대기중') return 'warning'; if (s === '장애발생') return 'critical'; return 'muted'; } -import { - Database, RefreshCw, Calendar, Wifi, WifiOff, Radio, - Activity, Server, ArrowDownToLine, Clock, AlertTriangle, - CheckCircle, XCircle, BarChart3, Layers, Plus, Play, Square, - Trash2, Edit2, Eye, FileText, HardDrive, Upload, FolderOpen, - Network, X, ChevronRight, Info, -} from 'lucide-react'; /* * SFR-03: 통합데이터 허브 수집·연계 관리 @@ -115,7 +113,7 @@ const channelColumns: DataColumn[] = [ render: (v) => {v as string}, }, { key: 'system', label: '정보시스템명', width: '100px', sortable: true, - render: (v) => {v as string}, + render: (v) => {v as string}, }, { key: 'linkInfo', label: '연계정보', width: '65px' }, { key: 'storage', label: '저장장소', render: (v) => {v as string} }, @@ -126,7 +124,7 @@ const channelColumns: DataColumn[] = [ render: (v) => { const s = v as string; return s === '수신대기중' - ? {s} + ? {s} : {s}; }, }, @@ -157,13 +155,7 @@ function SignalTimeline({ source }: { source: SignalSource }) {
{/* 라벨 */}
-
{source.name}
+
{source.name}
{source.rate}%
{/* 타임라인 바 */} @@ -232,20 +224,21 @@ const collectColumns: DataColumn[] = [ { key: 'successRate', label: '성공률', width: '70px', align: 'right', sortable: true, render: (v) => { const n = v as number; - const c = n >= 90 ? 'text-green-400' : n >= 70 ? 'text-yellow-400' : n > 0 ? 'text-red-400' : 'text-hint'; - return {n > 0 ? `${n}%` : '-'}; + if (n === 0) return -; + const intent: BadgeIntent = n >= 90 ? 'success' : n >= 70 ? 'warning' : 'critical'; + return {n}%; }, }, { key: 'id', label: '', width: '70px', align: 'center', sortable: false, render: (_v, row) => (
{row.status === '정지' ? ( - + ) : row.status !== '장애발생' ? ( - + ) : null} - - + +
), }, @@ -291,9 +284,9 @@ const loadColumns: DataColumn[] = [ { key: 'id', label: '', width: '70px', align: 'center', sortable: false, render: () => (
- - - + + +
), }, @@ -387,7 +380,7 @@ export function DataHub() { {[ { label: '전체 채널', value: CHANNELS.length, icon: Layers, color: 'text-label', bg: 'bg-muted' }, - { label: 'ON', value: onCount, icon: Wifi, color: 'text-blue-400', bg: 'bg-blue-500/10' }, - { label: 'OFF', value: offCount, icon: WifiOff, color: 'text-red-400', bg: 'bg-red-500/10' }, - { label: '평균 수신율', value: '86.5%', icon: BarChart3, color: 'text-green-400', bg: 'bg-green-500/10' }, - { label: '데이터 소스', value: '5종', icon: Radio, color: 'text-purple-400', bg: 'bg-purple-500/10' }, - { label: '연계 방식', value: 'KAFKA', icon: Server, color: 'text-orange-400', bg: 'bg-orange-500/10' }, + { label: 'ON', value: onCount, icon: Wifi, color: 'text-label', bg: 'bg-blue-500/10' }, + { label: 'OFF', value: offCount, icon: WifiOff, color: 'text-heading', bg: 'bg-red-500/10' }, + { label: '평균 수신율', value: '86.5%', icon: BarChart3, color: 'text-label', bg: 'bg-green-500/10' }, + { label: '데이터 소스', value: '5종', icon: Radio, color: 'text-heading', bg: 'bg-purple-500/10' }, + { label: '연계 방식', value: 'KAFKA', icon: Server, color: 'text-heading', bg: 'bg-orange-500/10' }, ].map((kpi) => (
@@ -509,11 +502,11 @@ export function DataHub() {
{hasPartialOff ? ( - + ) : ( - + )} - + {hasPartialOff ? `일부 OFF (${offCount}/${CHANNELS.length})` : '전체 정상'}
@@ -653,9 +646,9 @@ export function DataHub() {
작업 {agent.taskCount}건 · heartbeat {agent.lastHeartbeat.slice(11)}
- - - + + +
diff --git a/frontend/src/features/admin/LoginHistoryView.tsx b/frontend/src/features/admin/LoginHistoryView.tsx index 0578536..6325856 100644 --- a/frontend/src/features/admin/LoginHistoryView.tsx +++ b/frontend/src/features/admin/LoginHistoryView.tsx @@ -41,7 +41,7 @@ export function LoginHistoryView() { - - - - + + + +
)} @@ -71,7 +71,7 @@ export function LoginHistoryView() { {stats.byUser.map((u) => (
- {u.user_acnt} + {u.user_acnt} {u.count}회
))} @@ -86,9 +86,9 @@ export function LoginHistoryView() {
{formatDate(d.day)}
- 성공 {d.success} - 실패 {d.failed} - 잠금 {d.locked} + 성공 {d.success} + 실패 {d.failed} + 잠금 {d.locked}
))} @@ -98,7 +98,7 @@ export function LoginHistoryView() {
)} - {error &&
에러: {error}
} + {error &&
에러: {error}
} {loading &&
} @@ -123,11 +123,11 @@ export function LoginHistoryView() { {it.histSn} {formatDateTime(it.loginDtm)} - {it.userAcnt} + {it.userAcnt} {getLoginResultLabel(it.result, tc, lang)} - {it.failReason || '-'} + {it.failReason || '-'} {it.authProvider || '-'} {it.loginIp || '-'} diff --git a/frontend/src/features/admin/NoticeManagement.tsx b/frontend/src/features/admin/NoticeManagement.tsx index 002c2ac..8d1a5b0 100644 --- a/frontend/src/features/admin/NoticeManagement.tsx +++ b/frontend/src/features/admin/NoticeManagement.tsx @@ -1,17 +1,16 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'; +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 { useAuth } from '@/app/auth/AuthContext'; import type { BadgeIntent } from '@lib/theme/variants'; import { - Bell, Plus, Edit2, Trash2, Eye, EyeOff, Calendar, - Users, Megaphone, AlertTriangle, Info, Search, Filter, - CheckCircle, Clock, Pin, Monitor, MessageSquare, X, + Bell, Plus, Edit2, Trash2, Eye, + Megaphone, AlertTriangle, Info, + Clock, Pin, Monitor, MessageSquare, X, } from 'lucide-react'; -import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { toDateParam } from '@shared/utils/dateFormat'; import { SaveButton } from '@shared/components/common/SaveButton'; import type { SystemNotice, NoticeType, NoticeDisplay } from '@shared/components/common/NotificationBanner'; @@ -59,10 +58,10 @@ const INITIAL_NOTICES: SystemNotice[] = [ ]; const TYPE_OPTIONS: { key: NoticeType; label: string; icon: React.ElementType; color: string }[] = [ - { key: 'info', label: '정보', icon: Info, color: 'text-blue-400' }, - { key: 'warning', label: '경고', icon: AlertTriangle, color: 'text-yellow-400' }, - { key: 'urgent', label: '긴급', icon: Bell, color: 'text-red-400' }, - { key: 'maintenance', label: '점검', icon: Megaphone, color: 'text-orange-400' }, + { key: 'info', label: '정보', icon: Info, color: 'text-label' }, + { key: 'warning', label: '경고', icon: AlertTriangle, color: 'text-label' }, + { key: 'urgent', label: '긴급', icon: Bell, color: 'text-label' }, + { key: 'maintenance', label: '점검', icon: Megaphone, color: 'text-label' }, ]; const DISPLAY_OPTIONS: { key: NoticeDisplay; label: string; icon: React.ElementType }[] = [ @@ -146,7 +145,7 @@ export function NoticeManagement() { {[ { label: '전체 알림', count: notices.length, icon: Bell, color: 'text-label', bg: 'bg-muted' }, - { label: '현재 노출 중', count: activeCount, icon: Eye, color: 'text-green-400', bg: 'bg-green-500/10' }, - { label: '예약됨', count: scheduledCount, icon: Clock, color: 'text-blue-400', bg: 'bg-blue-500/10' }, - { label: '긴급 알림', count: urgentCount, icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-500/10' }, + { label: '현재 노출 중', count: activeCount, icon: Eye, color: 'text-label', bg: 'bg-green-500/10' }, + { label: '예약됨', count: scheduledCount, icon: Clock, color: 'text-label', bg: 'bg-blue-500/10' }, + { label: '긴급 알림', count: urgentCount, icon: AlertTriangle, color: 'text-label', bg: 'bg-red-500/10' }, ].map((kpi) => (
@@ -238,14 +237,14 @@ export function NoticeManagement() { )} - {n.pinned && } + {n.pinned && }
- -
@@ -327,7 +326,7 @@ export function NoticeManagement() { onClick={() => setForm({ ...form, display: opt.key })} className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-[10px] transition-colors ${ form.display === opt.key - ? 'bg-blue-600/20 text-blue-400 font-bold' + ? 'bg-blue-600/20 text-label font-bold' : 'text-hint hover:bg-surface-overlay' }`} > @@ -375,7 +374,7 @@ export function NoticeManagement() { onClick={() => toggleRole(role)} className={`px-3 py-1.5 rounded-lg text-[10px] transition-colors ${ form.targetRoles.includes(role) - ? 'bg-blue-600/20 text-blue-400 border border-blue-500/30 font-bold' + ? 'bg-surface-overlay text-heading border border-border font-bold' : 'text-hint border border-slate-700/30 hover:bg-surface-overlay' }`} > diff --git a/frontend/src/features/admin/PermissionsPanel.tsx b/frontend/src/features/admin/PermissionsPanel.tsx index d0f04be..55d9c9e 100644 --- a/frontend/src/features/admin/PermissionsPanel.tsx +++ b/frontend/src/features/admin/PermissionsPanel.tsx @@ -359,13 +359,13 @@ export function PermissionsPanel() {
- {error &&
에러: {error}
} + {error &&
에러: {error}
} {loading &&
} @@ -379,13 +379,13 @@ export function PermissionsPanel() {
{canCreateRole && ( )} {canDeleteRole && selectedRole && selectedRole.builtinYn !== 'Y' && ( )} @@ -442,7 +442,7 @@ export function PermissionsPanel() {
- 셀 의미: ✓ 명시 허용 / + 셀 의미: ✓ 명시 허용 / ✓ 상속 허용 / - — 명시 거부 / + — 명시 거부 / × 강제 거부 / · 미지정
diff --git a/frontend/src/features/admin/SystemConfig.tsx b/frontend/src/features/admin/SystemConfig.tsx index 5e2138a..f1f76d2 100644 --- a/frontend/src/features/admin/SystemConfig.tsx +++ b/frontend/src/features/admin/SystemConfig.tsx @@ -6,9 +6,9 @@ import { Button } from '@shared/components/ui/button'; import { PageContainer, PageHeader } from '@shared/components/layout'; import type { BadgeIntent } from '@lib/theme/variants'; import { - Settings, Database, Search, ChevronDown, ChevronRight, + Settings, Database, Search, Map, Fish, Anchor, Ship, Globe, BarChart3, Download, - Filter, RefreshCw, BookOpen, Layers, Hash, Info, + Filter, RefreshCw, Hash, Info, } from 'lucide-react'; import { AREA_CODES, SPECIES_CODES, FISHERY_CODES, VESSEL_TYPE_CODES, @@ -149,7 +149,7 @@ export function SystemConfig() { {[ - { icon: Map, label: '해역분류', count: CODE_STATS.areas, color: 'text-blue-400', bg: 'bg-blue-500/10', desc: '해양경찰청 관할 기준' }, - { icon: Fish, label: '어종 코드', count: CODE_STATS.species, color: 'text-green-400', bg: 'bg-green-500/10', desc: '국립수산과학원 기준' }, - { icon: Anchor, label: '어업유형', count: CODE_STATS.fishery, color: 'text-purple-400', bg: 'bg-purple-500/10', desc: '수산업법 허가·면허' }, - { icon: Ship, label: '선박유형', count: CODE_STATS.vesselTypes, color: 'text-orange-400', bg: 'bg-orange-500/10', desc: 'MDA 5개출처 통합' }, - { icon: Globe, label: '전체 코드', count: CODE_STATS.total, color: 'text-cyan-400', bg: 'bg-cyan-500/10', desc: '공통코드 총계' }, + { icon: Map, label: '해역분류', count: CODE_STATS.areas, color: 'text-label', bg: 'bg-blue-500/10', desc: '해양경찰청 관할 기준' }, + { icon: Fish, label: '어종 코드', count: CODE_STATS.species, color: 'text-label', bg: 'bg-green-500/10', desc: '국립수산과학원 기준' }, + { icon: Anchor, label: '어업유형', count: CODE_STATS.fishery, color: 'text-heading', bg: 'bg-purple-500/10', desc: '수산업법 허가·면허' }, + { icon: Ship, label: '선박유형', count: CODE_STATS.vesselTypes, color: 'text-label', bg: 'bg-orange-500/10', desc: 'MDA 5개출처 통합' }, + { icon: Globe, label: '전체 코드', count: CODE_STATS.total, color: 'text-label', bg: 'bg-cyan-500/10', desc: '공통코드 총계' }, ].map((kpi) => ( @@ -270,7 +270,7 @@ export function SystemConfig() { {(pagedData as AreaCode[]).map((a) => ( - {a.code} + {a.code} {(() => { const intent: BadgeIntent = a.major === '서해' ? 'info' : a.major === '남해' ? 'success' : a.major === '동해' ? 'purple' : a.major === '제주' ? 'high' : 'cyan'; @@ -313,7 +313,7 @@ export function SystemConfig() { className="border-b border-border hover:bg-surface-overlay cursor-pointer" onClick={() => setExpandedRow(expandedRow === s.code ? null : s.code)} > - {s.code} + {s.code} {s.major} @@ -323,12 +323,12 @@ export function SystemConfig() { {s.area} {s.active - ? Y + ? Y : N } - {s.fishing && } + {s.fishing && } ))} @@ -357,7 +357,7 @@ export function SystemConfig() { {(pagedData as FisheryCode[]).map((f) => ( - {f.code} + {f.code} {(() => { const intent: BadgeIntent = f.major === '근해어업' ? 'info' : f.major === '연안어업' ? 'success' : f.major === '양식어업' ? 'cyan' : f.major === '원양어업' ? 'purple' : f.major === '구획어업' ? 'high' : f.major === '마을어업' ? 'warning' : 'muted'; @@ -399,7 +399,7 @@ export function SystemConfig() { {(pagedData as VesselTypeCode[]).map((v) => ( - {v.code} + {v.code} {(() => { const intent: BadgeIntent = v.major === '어선' ? 'info' : v.major === '여객선' ? 'success' : v.major === '화물선' ? 'high' : v.major === '유조선' ? 'critical' : v.major === '관공선' ? 'purple' : v.major === '함정' ? 'cyan' : 'muted'; @@ -409,13 +409,7 @@ export function SystemConfig() { {v.mid} {v.name} - {v.source} + {v.source} {v.tonnage} {v.purpose} @@ -431,23 +425,25 @@ export function SystemConfig() { {/* 페이지네이션 (코드 탭에서만) */} {tab !== 'settings' && totalPages > 1 && (
- + {page + 1} / {totalPages} 페이지 ({totalItems.toLocaleString()}건) - +
)} @@ -466,7 +462,7 @@ export function SystemConfig() { - + {meta.title} @@ -474,9 +470,7 @@ export function SystemConfig() { {items.map((item) => (
{item.label} - {item.value} + {item.value}
))}