Merge pull request 'refactor(frontend): admin Phase 1-B 디자인 시스템 하드코딩 색상 제거' (#59) from feature/admin-ds-phase1b into develop

This commit is contained in:
htlee 2026-04-16 11:36:45 +09:00
커밋 369aaec06f
12개의 변경된 파일157개의 추가작업 그리고 168개의 파일을 삭제

파일 보기

@ -4,6 +4,9 @@
## [Unreleased] ## [Unreleased]
### 변경
- **Admin 11개 페이지 디자인 시스템 하드코딩 색상 제거 (Phase 1-B)** — 129건 Tailwind 색상 → 시맨틱 토큰(text-label/text-heading/text-hint) + Badge intent 치환. raw `<button>``<Button>` 컴포넌트 교체. 미사용 import 정리
## [2026-04-16.4] ## [2026-04-16.4]
### 추가 ### 추가

파일 보기

@ -41,11 +41,11 @@ const AGENTS = [
]; ];
const AGENT_KPI = [ const AGENT_KPI = [
{ label: '활성 Agent', value: '4', color: 'text-green-400', bg: 'bg-green-500/10' }, { label: '활성 Agent', value: '4', color: 'text-label', bg: 'bg-green-500/10' },
{ label: '등록 Tool', value: '26', color: 'text-blue-400', bg: 'bg-blue-500/10' }, { label: '등록 Tool', value: '26', color: 'text-label', bg: 'bg-blue-500/10' },
{ label: '24h 호출', value: '2,656', color: 'text-purple-400', bg: 'bg-purple-500/10' }, { label: '24h 호출', value: '2,656', color: 'text-heading', bg: 'bg-purple-500/10' },
{ label: '차단 건수', value: '3', color: 'text-red-400', bg: 'bg-red-500/10' }, { label: '차단 건수', value: '3', color: 'text-heading', bg: 'bg-red-500/10' },
{ label: '승인 대기', value: '0', color: 'text-yellow-400', bg: 'bg-yellow-500/10' }, { label: '승인 대기', value: '0', color: 'text-label', bg: 'bg-yellow-500/10' },
]; ];
// ─── 화이트리스트 도구 ────────────────── // ─── 화이트리스트 도구 ──────────────────
@ -110,7 +110,7 @@ export function AIAgentSecurityPage() {
<PageContainer> <PageContainer>
<PageHeader <PageHeader
icon={Bot} icon={Bot}
iconColor="text-orange-400" iconColor="text-heading"
title="AI Agent 구축·운영 보안" title="AI Agent 구축·운영 보안"
description="SER-11 | AI Agent 화이트리스트·자동중단·민감명령 승인·MCP 최소권한·감사로그" description="SER-11 | AI Agent 화이트리스트·자동중단·민감명령 승인·MCP 최소권한·감사로그"
demo demo
@ -192,7 +192,7 @@ export function AIAgentSecurityPage() {
<div className="space-y-2"> <div className="space-y-2">
{KILL_SWITCH_RULES.map(r => ( {KILL_SWITCH_RULES.map(r => (
<div key={r.rule} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg"> <div key={r.rule} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<AlertTriangle className="w-4 h-4 text-red-400" /> <AlertTriangle className="w-4 h-4 text-heading" />
<div className="flex-1"> <div className="flex-1">
<div className="text-[11px] text-heading font-medium">{r.rule}</div> <div className="text-[11px] text-heading font-medium">{r.rule}</div>
<div className="text-[9px] text-hint">{r.desc}</div> <div className="text-[9px] text-hint">{r.desc}</div>
@ -216,7 +216,7 @@ export function AIAgentSecurityPage() {
<td className="py-2 px-2 text-heading font-medium">{c.command}</td> <td className="py-2 px-2 text-heading font-medium">{c.command}</td>
<td className="py-2 text-center"><Badge intent={getThreatLevelIntent(c.level)} size="sm">{c.level}</Badge></td> <td className="py-2 text-center"><Badge intent={getThreatLevelIntent(c.level)} size="sm">{c.level}</Badge></td>
<td className="py-2 text-muted-foreground">{c.approval}</td> <td className="py-2 text-muted-foreground">{c.approval}</td>
<td className="py-2 text-center">{c.hitl ? <Hand className="w-3.5 h-3.5 text-orange-400 mx-auto" /> : <span className="text-hint">-</span>}</td> <td className="py-2 text-center">{c.hitl ? <Hand className="w-3.5 h-3.5 text-label mx-auto" /> : <span className="text-hint">-</span>}</td>
<td className="py-2 text-center"><Badge intent="success" size="sm">{c.status}</Badge></td> <td className="py-2 text-center"><Badge intent="success" size="sm">{c.status}</Badge></td>
</tr> </tr>
))}</tbody> ))}</tbody>
@ -232,7 +232,7 @@ export function AIAgentSecurityPage() {
<div className="space-y-2"> <div className="space-y-2">
{IDENTITY_POLICIES.map(p => ( {IDENTITY_POLICIES.map(p => (
<div key={p.policy} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg"> <div key={p.policy} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Key className="w-4 h-4 text-orange-400" /> <Key className="w-4 h-4 text-label" />
<div className="flex-1"> <div className="flex-1">
<div className="text-[11px] text-heading font-medium">{p.policy}</div> <div className="text-[11px] text-heading font-medium">{p.policy}</div>
<div className="text-[9px] text-hint">{p.desc}</div> <div className="text-[9px] text-hint">{p.desc}</div>

파일 보기

@ -31,11 +31,11 @@ const TABS: { key: Tab; icon: React.ComponentType<{ className?: string }>; label
// ─── 보안 현황 KPI ────────────────── // ─── 보안 현황 KPI ──────────────────
const SECURITY_KPI = [ const SECURITY_KPI = [
{ label: '데이터 수집 보안', value: '정상', score: 95, icon: Database, color: 'text-green-400', bg: 'bg-green-500/10' }, { label: '데이터 수집 보안', value: '정상', score: 95, icon: Database, color: 'text-label', bg: 'bg-green-500/10' },
{ label: 'AI 학습 보안', value: '정상', score: 92, icon: Brain, color: 'text-blue-400', bg: 'bg-blue-500/10' }, { label: 'AI 학습 보안', value: '정상', score: 92, icon: Brain, color: 'text-label', bg: 'bg-blue-500/10' },
{ label: '입출력 필터링', value: '정상', score: 98, icon: Lock, color: 'text-purple-400', bg: 'bg-purple-500/10' }, { label: '입출력 필터링', value: '정상', score: 98, icon: Lock, color: 'text-heading', bg: 'bg-purple-500/10' },
{ label: '경계 보안', value: '주의', score: 85, icon: Server, color: 'text-yellow-400', bg: 'bg-yellow-500/10' }, { label: '경계 보안', value: '주의', score: 85, icon: Server, color: 'text-label', bg: 'bg-yellow-500/10' },
{ label: '취약점 점검', value: '정상', score: 90, icon: Search, color: 'text-cyan-400', bg: 'bg-cyan-500/10' }, { label: '취약점 점검', value: '정상', score: 90, icon: Search, color: 'text-label', bg: 'bg-cyan-500/10' },
]; ];
// ─── 데이터 수집 보안 ────────────────── // ─── 데이터 수집 보안 ──────────────────
@ -111,7 +111,7 @@ export function AISecurityPage() {
<PageContainer> <PageContainer>
<PageHeader <PageHeader
icon={Shield} icon={Shield}
iconColor="text-red-400" iconColor="text-heading"
title="AI 보안 관리" title="AI 보안 관리"
description="SER-10 | AI 데이터 수집·학습·입출력·경계 보안 및 취약점 관리" description="SER-10 | AI 데이터 수집·학습·입출력·경계 보안 및 취약점 관리"
demo demo
@ -159,7 +159,7 @@ export function AISecurityPage() {
['복구 계획', '3/3 활성', '완료'], ['복구 계획', '3/3 활성', '완료'],
].map(([k, v, s]) => ( ].map(([k, v, s]) => (
<div key={k} className="flex items-center gap-2 px-2 py-1.5 bg-surface-overlay rounded"> <div key={k} className="flex items-center gap-2 px-2 py-1.5 bg-surface-overlay rounded">
{s === '완료' ? <CheckCircle className="w-3 h-3 text-green-500" /> : <AlertTriangle className="w-3 h-3 text-yellow-500" />} {s === '완료' ? <CheckCircle className="w-3 h-3 text-label" /> : <AlertTriangle className="w-3 h-3 text-label" />}
<span className="text-heading flex-1">{k}</span> <span className="text-heading flex-1">{k}</span>
<span className="text-hint">{v}</span> <span className="text-hint">{v}</span>
</div> </div>
@ -218,7 +218,7 @@ export function AISecurityPage() {
<div className="space-y-2"> <div className="space-y-2">
{CONTAMINATION_CHECKS.map(c => ( {CONTAMINATION_CHECKS.map(c => (
<div key={c.check} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg"> <div key={c.check} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<CheckCircle className="w-4 h-4 text-green-500" /> <CheckCircle className="w-4 h-4 text-label" />
<div className="flex-1"> <div className="flex-1">
<div className="text-[11px] text-heading font-medium">{c.check}</div> <div className="text-[11px] text-heading font-medium">{c.check}</div>
<div className="text-[9px] text-hint">{c.desc}</div> <div className="text-[9px] text-hint">{c.desc}</div>
@ -239,7 +239,7 @@ export function AISecurityPage() {
<div className="space-y-2"> <div className="space-y-2">
{TRAINING_POLICIES.map(p => ( {TRAINING_POLICIES.map(p => (
<div key={p.policy} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg"> <div key={p.policy} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Lock className="w-4 h-4 text-blue-400" /> <Lock className="w-4 h-4 text-label" />
<div className="flex-1"> <div className="flex-1">
<div className="text-[11px] text-heading font-medium">{p.policy}</div> <div className="text-[11px] text-heading font-medium">{p.policy}</div>
<div className="text-[9px] text-hint">{p.desc}</div> <div className="text-[9px] text-hint">{p.desc}</div>
@ -266,7 +266,7 @@ export function AISecurityPage() {
<td className="py-2.5 px-2 text-heading font-medium">{f.name}</td> <td className="py-2.5 px-2 text-heading font-medium">{f.name}</td>
<td className="py-2.5 text-hint text-[9px]">{f.desc}</td> <td className="py-2.5 text-hint text-[9px]">{f.desc}</td>
<td className="py-2.5 text-center"><Badge intent="success" size="sm">{f.status}</Badge></td> <td className="py-2.5 text-center"><Badge intent="success" size="sm">{f.status}</Badge></td>
<td className="py-2.5 text-center text-red-400 font-bold">{f.blocked}</td> <td className="py-2.5 text-center text-heading font-bold">{f.blocked}</td>
<td className="py-2.5 text-center text-muted-foreground">{f.total.toLocaleString()}</td> <td className="py-2.5 text-center text-muted-foreground">{f.total.toLocaleString()}</td>
<td className="py-2.5 text-center text-muted-foreground">{(f.blocked / f.total * 100).toFixed(2)}%</td> <td className="py-2.5 text-center text-muted-foreground">{(f.blocked / f.total * 100).toFixed(2)}%</td>
</tr> </tr>
@ -283,7 +283,7 @@ export function AISecurityPage() {
<div className="space-y-2"> <div className="space-y-2">
{BOUNDARY_ITEMS.map(b => ( {BOUNDARY_ITEMS.map(b => (
<div key={b.item} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg"> <div key={b.item} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Server className="w-4 h-4 text-yellow-400" /> <Server className="w-4 h-4 text-label" />
<div className="flex-1"> <div className="flex-1">
<div className="text-[11px] text-heading font-medium">{b.item}</div> <div className="text-[11px] text-heading font-medium">{b.item}</div>
<div className="text-[9px] text-hint">{b.desc}</div> <div className="text-[9px] text-hint">{b.desc}</div>
@ -298,7 +298,7 @@ export function AISecurityPage() {
<div className="space-y-2"> <div className="space-y-2">
{EXPLAINABILITY.map(e => ( {EXPLAINABILITY.map(e => (
<div key={e.item} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg"> <div key={e.item} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<Eye className="w-4 h-4 text-purple-400" /> <Eye className="w-4 h-4 text-heading" />
<div className="flex-1"> <div className="flex-1">
<div className="text-[11px] text-heading font-medium">{e.item}</div> <div className="text-[11px] text-heading font-medium">{e.item}</div>
<div className="text-[9px] text-hint">{e.desc}</div> <div className="text-[9px] text-hint">{e.desc}</div>
@ -339,7 +339,7 @@ export function AISecurityPage() {
<div className="space-y-2"> <div className="space-y-2">
{RECOVERY_PLANS.map(r => ( {RECOVERY_PLANS.map(r => (
<div key={r.plan} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg"> <div key={r.plan} className="flex items-center gap-3 px-3 py-2.5 bg-surface-overlay rounded-lg">
<RefreshCw className="w-4 h-4 text-cyan-400" /> <RefreshCw className="w-4 h-4 text-label" />
<div className="flex-1"> <div className="flex-1">
<div className="text-[11px] text-heading font-medium">{r.plan}</div> <div className="text-[11px] text-heading font-medium">{r.plan}</div>
<div className="text-[9px] text-hint">{r.desc}</div> <div className="text-[9px] text-hint">{r.desc}</div>

파일 보기

@ -104,10 +104,10 @@ export function AccessControl() {
}; };
// ── 사용자 테이블 컬럼 ────────────── // ── 사용자 테이블 컬럼 ──────────────
// eslint-disable-next-line react-hooks/exhaustive-deps
const userColumns: DataColumn<AdminUser & Record<string, unknown>>[] = useMemo(() => [ const userColumns: DataColumn<AdminUser & Record<string, unknown>>[] = useMemo(() => [
{ key: 'userAcnt', label: '계정', width: '90px', { key: 'userAcnt', label: '계정', width: '90px',
render: (v) => <span className="text-cyan-400 font-mono text-[11px]">{v as string}</span> }, render: (v) => <span className="text-label font-mono text-[11px]">{v as string}</span> },
{ key: 'userNm', label: '이름', width: '80px', sortable: true, { key: 'userNm', label: '이름', width: '80px', sortable: true,
render: (v) => <span className="text-heading font-medium">{v as string}</span> }, render: (v) => <span className="text-heading font-medium">{v as string}</span> },
{ key: 'rnkpNm', label: '직급', width: '60px', { key: 'rnkpNm', label: '직급', width: '60px',
@ -133,7 +133,7 @@ export function AccessControl() {
}, },
}, },
{ key: 'failCnt', label: '실패', width: '50px', align: 'center', { key: 'failCnt', label: '실패', width: '50px', align: 'center',
render: (v) => <span className={`text-[10px] ${(v as number) > 0 ? 'text-red-400' : 'text-hint'}`}>{v as number}</span> }, render: (v) => <span className={`text-[10px] ${(v as number) > 0 ? 'text-heading' : 'text-hint'}`}>{v as number}</span> },
{ key: 'authProvider', label: '인증', width: '70px', { key: 'authProvider', label: '인증', width: '70px',
render: (v) => <span className="text-muted-foreground text-[10px]">{v as string}</span> }, render: (v) => <span className="text-muted-foreground text-[10px]">{v as string}</span> },
{ key: 'lastLoginDtm', label: '최종 로그인', width: '140px', sortable: true, { key: 'lastLoginDtm', label: '최종 로그인', width: '140px', sortable: true,
@ -147,12 +147,12 @@ export function AccessControl() {
render: (_v, row) => ( render: (_v, row) => (
<div className="flex items-center justify-center gap-1"> <div className="flex items-center justify-center gap-1">
<button type="button" onClick={() => setAssignTarget(row)} <button type="button" onClick={() => setAssignTarget(row)}
className="p-1 text-hint hover:text-purple-400" title="역할 배정"> className="p-1 text-hint hover:text-heading" title="역할 배정">
<UserCog className="w-3 h-3" /> <UserCog className="w-3 h-3" />
</button> </button>
{row.userSttsCd === 'LOCKED' && ( {row.userSttsCd === 'LOCKED' && (
<button type="button" onClick={() => handleUnlock(row.userId, row.userAcnt)} <button type="button" onClick={() => handleUnlock(row.userId, row.userAcnt)}
className="p-1 text-hint hover:text-green-400" title="잠금 해제"> className="p-1 text-hint hover:text-label" title="잠금 해제">
<Key className="w-3 h-3" /> <Key className="w-3 h-3" />
</button> </button>
)} )}
@ -166,7 +166,7 @@ export function AccessControl() {
{ key: 'createdAt', label: '일시', width: '160px', sortable: true, { key: 'createdAt', label: '일시', width: '160px', sortable: true,
render: (v) => <span className="text-muted-foreground font-mono text-[10px]">{formatDateTime(v as string)}</span> }, render: (v) => <span className="text-muted-foreground font-mono text-[10px]">{formatDateTime(v as string)}</span> },
{ key: 'userAcnt', label: '사용자', width: '90px', sortable: true, { key: 'userAcnt', label: '사용자', width: '90px', sortable: true,
render: (v) => <span className="text-cyan-400 font-mono">{(v as string) || '-'}</span> }, render: (v) => <span className="text-label font-mono">{(v as string) || '-'}</span> },
{ key: 'actionCd', label: '액션', width: '180px', sortable: true, { key: 'actionCd', label: '액션', width: '180px', sortable: true,
render: (v) => <span className="text-heading font-medium">{v as string}</span> }, render: (v) => <span className="text-heading font-medium">{v as string}</span> },
{ key: 'resourceType', label: '리소스', width: '110px', { key: 'resourceType', label: '리소스', width: '110px',
@ -180,24 +180,24 @@ export function AccessControl() {
}, },
}, },
{ key: 'failReason', label: '실패 사유', { key: 'failReason', label: '실패 사유',
render: (v) => <span className="text-red-400 text-[10px]">{(v as string) || '-'}</span> }, render: (v) => <span className="text-heading text-[10px]">{(v as string) || '-'}</span> },
], []); ], []);
return ( return (
<PageContainer size="lg"> <PageContainer size="lg">
<PageHeader <PageHeader
icon={Shield} icon={Shield}
iconColor="text-blue-400" iconColor="text-label"
title={t('accessControl.title')} title={t('accessControl.title')}
description={t('accessControl.desc')} description={t('accessControl.desc')}
actions={ actions={
<> <>
{userStats && ( {userStats && (
<div className="flex items-center gap-2 text-[10px] text-hint"> <div className="flex items-center gap-2 text-[10px] text-hint">
<UserCheck className="w-3.5 h-3.5 text-green-500" /> <UserCheck className="w-3.5 h-3.5 text-label" />
<span className="text-green-400 font-bold">{userStats.active}</span> <span className="text-label font-bold">{userStats.active}</span>
<span className="mx-1">|</span> <span className="mx-1">|</span>
<span className="text-red-400 font-bold">{userStats.locked}</span> <span className="text-heading font-bold">{userStats.locked}</span>
<span className="mx-1">|</span> <span className="mx-1">|</span>
<span className="text-heading font-bold">{userStats.total}</span> <span className="text-heading font-bold">{userStats.total}</span>
</div> </div>
@ -237,7 +237,7 @@ export function AccessControl() {
))} ))}
</div> </div>
{error && <div className="text-xs text-red-400">: {error}</div>} {error && <div className="text-xs text-heading">: {error}</div>}
{/* ── 역할 관리 (PermissionsPanel: 트리 + R/C/U/D 매트릭스) ── */} {/* ── 역할 관리 (PermissionsPanel: 트리 + R/C/U/D 매트릭스) ── */}
{tab === 'roles' && <PermissionsPanel />} {tab === 'roles' && <PermissionsPanel />}
@ -249,8 +249,8 @@ export function AccessControl() {
{userStats && ( {userStats && (
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
<StatCard label="총 사용자" value={userStats.total} color="text-heading" /> <StatCard label="총 사용자" value={userStats.total} color="text-heading" />
<StatCard label="활성" value={userStats.active} color="text-green-400" /> <StatCard label="활성" value={userStats.active} color="text-label" />
<StatCard label="잠금" value={userStats.locked} color="text-red-400" /> <StatCard label="잠금" value={userStats.locked} color="text-heading" />
<StatCard label="비활성" value={userStats.inactive} color="text-gray-400" /> <StatCard label="비활성" value={userStats.inactive} color="text-gray-400" />
</div> </div>
)} )}
@ -278,9 +278,9 @@ export function AccessControl() {
{auditStats && ( {auditStats && (
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
<StatCard label="전체 로그" value={auditStats.total} color="text-heading" /> <StatCard label="전체 로그" value={auditStats.total} color="text-heading" />
<StatCard label="24시간" value={auditStats.last24h} color="text-blue-400" /> <StatCard label="24시간" value={auditStats.last24h} color="text-label" />
<StatCard label="실패 (24시간)" value={auditStats.failed24h} color="text-red-400" /> <StatCard label="실패 (24시간)" value={auditStats.failed24h} color="text-heading" />
<StatCard label="액션 종류" value={auditStats.byAction.length} color="text-purple-400" /> <StatCard label="액션 종류" value={auditStats.byAction.length} color="text-heading" />
</div> </div>
)} )}
@ -292,7 +292,7 @@ export function AccessControl() {
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{auditStats.byAction.map((a) => ( {auditStats.byAction.map((a) => (
<Badge key={a.action} className="bg-surface-overlay text-muted-foreground border border-border text-[10px]"> <Badge key={a.action} className="bg-surface-overlay text-muted-foreground border border-border text-[10px]">
{a.action} <span className="text-cyan-400 font-bold ml-1">{a.count}</span> {a.action} <span className="text-label font-bold ml-1">{a.count}</span>
</Badge> </Badge>
))} ))}
</div> </div>

파일 보기

@ -37,7 +37,7 @@ export function AccessLogs() {
<PageContainer size="lg"> <PageContainer size="lg">
<PageHeader <PageHeader
icon={Activity} icon={Activity}
iconColor="text-cyan-400" iconColor="text-label"
title="접근 이력" title="접근 이력"
description="AccessLogFilter가 모든 HTTP 요청 비동기 기록" description="AccessLogFilter가 모든 HTTP 요청 비동기 기록"
actions={ actions={
@ -50,10 +50,10 @@ export function AccessLogs() {
{stats && ( {stats && (
<div className="grid grid-cols-5 gap-3"> <div className="grid grid-cols-5 gap-3">
<MetricCard label="전체 요청" value={stats.total} color="text-heading" /> <MetricCard label="전체 요청" value={stats.total} color="text-heading" />
<MetricCard label="24시간 내" value={stats.last24h} color="text-blue-400" /> <MetricCard label="24시간 내" value={stats.last24h} color="text-label" />
<MetricCard label="4xx (24h)" value={stats.error4xx} color="text-orange-400" /> <MetricCard label="4xx (24h)" value={stats.error4xx} color="text-heading" />
<MetricCard label="5xx (24h)" value={stats.error5xx} color="text-red-400" /> <MetricCard label="5xx (24h)" value={stats.error5xx} color="text-heading" />
<MetricCard label="평균 응답(ms)" value={Math.round(stats.avgDurationMs)} color="text-purple-400" /> <MetricCard label="평균 응답(ms)" value={Math.round(stats.avgDurationMs)} color="text-heading" />
</div> </div>
)} )}
@ -73,7 +73,7 @@ export function AccessLogs() {
{stats.topPaths.map((p) => ( {stats.topPaths.map((p) => (
<tr key={p.path} className="border-t border-border"> <tr key={p.path} className="border-t border-border">
<td className="py-1.5 text-heading font-mono text-[10px]">{p.path}</td> <td className="py-1.5 text-heading font-mono text-[10px]">{p.path}</td>
<td className="py-1.5 text-right text-cyan-400 font-bold">{p.count}</td> <td className="py-1.5 text-right text-label font-bold">{p.count}</td>
<td className="py-1.5 text-right text-muted-foreground">{p.avg_ms}</td> <td className="py-1.5 text-right text-muted-foreground">{p.avg_ms}</td>
</tr> </tr>
))} ))}
@ -83,7 +83,7 @@ export function AccessLogs() {
</Card> </Card>
)} )}
{error && <div className="text-xs text-red-400">: {error}</div>} {error && <div className="text-xs text-heading">: {error}</div>}
{loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>} {loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>}
@ -109,8 +109,8 @@ export function AccessLogs() {
<tr key={it.accessSn} className="border-t border-border hover:bg-surface-overlay/50"> <tr key={it.accessSn} className="border-t border-border hover:bg-surface-overlay/50">
<td className="px-3 py-2 text-hint font-mono">{it.accessSn}</td> <td className="px-3 py-2 text-hint font-mono">{it.accessSn}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{formatDateTime(it.createdAt)}</td> <td className="px-3 py-2 text-muted-foreground text-[10px]">{formatDateTime(it.createdAt)}</td>
<td className="px-3 py-2 text-cyan-400">{it.userAcnt || '-'}</td> <td className="px-3 py-2 text-label">{it.userAcnt || '-'}</td>
<td className="px-3 py-2 text-purple-400 font-mono">{it.httpMethod}</td> <td className="px-3 py-2 text-heading font-mono">{it.httpMethod}</td>
<td className="px-3 py-2 text-heading font-mono text-[10px] max-w-md truncate">{it.requestPath}</td> <td className="px-3 py-2 text-heading font-mono text-[10px] max-w-md truncate">{it.requestPath}</td>
<td className="px-3 py-2 text-center"> <td className="px-3 py-2 text-center">
<Badge intent={getHttpStatusIntent(it.statusCode)} size="sm">{it.statusCode}</Badge> <Badge intent={getHttpStatusIntent(it.statusCode)} size="sm">{it.statusCode}</Badge>

파일 보기

@ -67,7 +67,7 @@ export function AdminPanel() {
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<Card> <Card>
<CardHeader className="px-4 pt-3 pb-2"> <CardHeader className="px-4 pt-3 pb-2">
<CardTitle className="text-xs text-label flex items-center gap-1.5"><Database className="w-3.5 h-3.5 text-blue-400" /></CardTitle> <CardTitle className="text-xs text-label flex items-center gap-1.5"><Database className="w-3.5 h-3.5 text-label" /></CardTitle>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4 space-y-2"> <CardContent className="px-4 pb-4 space-y-2">
{[['PostgreSQL', 'v15.4 운영중'], ['TimescaleDB', 'v2.12 운영중'], ['Redis 캐시', 'v7.2 운영중'], ['Kafka', 'v3.6 클러스터 3노드']].map(([k, v]) => ( {[['PostgreSQL', 'v15.4 운영중'], ['TimescaleDB', 'v2.12 운영중'], ['Redis 캐시', 'v7.2 운영중'], ['Kafka', 'v3.6 클러스터 3노드']].map(([k, v]) => (
@ -77,7 +77,7 @@ export function AdminPanel() {
</Card> </Card>
<Card> <Card>
<CardHeader className="px-4 pt-3 pb-2"> <CardHeader className="px-4 pt-3 pb-2">
<CardTitle className="text-xs text-label flex items-center gap-1.5"><Shield className="w-3.5 h-3.5 text-green-400" /> </CardTitle> <CardTitle className="text-xs text-label flex items-center gap-1.5"><Shield className="w-3.5 h-3.5 text-label" /> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-4 space-y-2"> <CardContent className="px-4 pb-4 space-y-2">
{[['SSL 인증서', '2027-03-15 만료'], ['방화벽', '정상 동작'], ['IDS/IPS', '실시간 감시중'], ['백업', '금일 03:00 완료']].map(([k, v]) => ( {[['SSL 인증서', '2027-03-15 만료'], ['방화벽', '정상 동작'], ['IDS/IPS', '실시간 감시중'], ['백업', '금일 03:00 완료']].map(([k, v]) => (

파일 보기

@ -36,7 +36,7 @@ export function AuditLogs() {
<PageContainer size="lg"> <PageContainer size="lg">
<PageHeader <PageHeader
icon={FileSearch} icon={FileSearch}
iconColor="text-blue-400" iconColor="text-label"
title="감사 로그" title="감사 로그"
description="@Auditable AOP가 모든 운영자 의사결정 자동 기록" description="@Auditable AOP가 모든 운영자 의사결정 자동 기록"
actions={ actions={
@ -50,9 +50,9 @@ export function AuditLogs() {
{stats && ( {stats && (
<div className="grid grid-cols-4 gap-3"> <div className="grid grid-cols-4 gap-3">
<MetricCard label="전체 로그" value={stats.total} color="text-heading" /> <MetricCard label="전체 로그" value={stats.total} color="text-heading" />
<MetricCard label="24시간 내" value={stats.last24h} color="text-blue-400" /> <MetricCard label="24시간 내" value={stats.last24h} color="text-label" />
<MetricCard label="실패 (24시간)" value={stats.failed24h} color="text-red-400" /> <MetricCard label="실패 (24시간)" value={stats.failed24h} color="text-heading" />
<MetricCard label="액션 종류 (7일)" value={stats.byAction.length} color="text-purple-400" /> <MetricCard label="액션 종류 (7일)" value={stats.byAction.length} color="text-heading" />
</div> </div>
)} )}
@ -64,7 +64,7 @@ export function AuditLogs() {
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{stats.byAction.map((a) => ( {stats.byAction.map((a) => (
<Badge key={a.action} className="bg-surface-overlay text-muted-foreground border border-border text-[10px] px-2 py-1"> <Badge key={a.action} className="bg-surface-overlay text-muted-foreground border border-border text-[10px] px-2 py-1">
{a.action} <span className="text-cyan-400 font-bold ml-1">{a.count}</span> {a.action} <span className="text-label font-bold ml-1">{a.count}</span>
</Badge> </Badge>
))} ))}
</div> </div>
@ -72,7 +72,7 @@ export function AuditLogs() {
</Card> </Card>
)} )}
{error && <div className="text-xs text-red-400">: {error}</div>} {error && <div className="text-xs text-heading">: {error}</div>}
{loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>} {loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>}
@ -99,7 +99,7 @@ export function AuditLogs() {
<tr key={it.auditSn} className="border-t border-border hover:bg-surface-overlay/50"> <tr key={it.auditSn} className="border-t border-border hover:bg-surface-overlay/50">
<td className="px-3 py-2 text-hint font-mono">{it.auditSn}</td> <td className="px-3 py-2 text-hint font-mono">{it.auditSn}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{formatDateTime(it.createdAt)}</td> <td className="px-3 py-2 text-muted-foreground text-[10px]">{formatDateTime(it.createdAt)}</td>
<td className="px-3 py-2 text-cyan-400">{it.userAcnt || '-'}</td> <td className="px-3 py-2 text-label">{it.userAcnt || '-'}</td>
<td className="px-3 py-2 text-heading font-medium">{it.actionCd}</td> <td className="px-3 py-2 text-heading font-medium">{it.actionCd}</td>
<td className="px-3 py-2 text-muted-foreground">{it.resourceType ?? '-'} {it.resourceId ? `(${it.resourceId})` : ''}</td> <td className="px-3 py-2 text-muted-foreground">{it.resourceType ?? '-'} {it.resourceId ? `(${it.resourceId})` : ''}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
@ -107,7 +107,7 @@ export function AuditLogs() {
{it.result || '-'} {it.result || '-'}
</Badge> </Badge>
</td> </td>
<td className="px-3 py-2 text-red-400 text-[10px]">{it.failReason || '-'}</td> <td className="px-3 py-2 text-heading text-[10px]">{it.failReason || '-'}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{it.ipAddress || '-'}</td> <td className="px-3 py-2 text-muted-foreground text-[10px]">{it.ipAddress || '-'}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px] max-w-xs truncate"> <td className="px-3 py-2 text-muted-foreground text-[10px] max-w-xs truncate">
{it.detail ? JSON.stringify(it.detail) : '-'} {it.detail ? JSON.stringify(it.detail) : '-'}

파일 보기

@ -1,29 +1,27 @@
import { useState, useMemo } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; 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 { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button'; import { Button } from '@shared/components/ui/button';
import { TabBar, TabButton } from '@shared/components/ui/tabs'; import { TabBar, TabButton } from '@shared/components/ui/tabs';
import { PageContainer, PageHeader } from '@shared/components/layout'; import { PageContainer, PageHeader } from '@shared/components/layout';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable'; import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { SaveButton } from '@shared/components/common/SaveButton';
import { getConnectionStatusHex } from '@shared/constants/connectionStatuses'; import { getConnectionStatusHex } from '@shared/constants/connectionStatuses';
import type { BadgeIntent } from '@lib/theme/variants'; 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 { function jobStatusIntent(s: string): BadgeIntent {
if (s === '수행중') return 'success'; if (s === '수행중') return 'success';
if (s === '대기중') return 'warning'; if (s === '대기중') return 'warning';
if (s === '장애발생') return 'critical'; if (s === '장애발생') return 'critical';
return 'muted'; 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: 통합데이터 · * SFR-03: 통합데이터 ·
@ -115,7 +113,7 @@ const channelColumns: DataColumn<ChannelRecord>[] = [
render: (v) => <span className="text-hint font-mono">{v as string}</span>, render: (v) => <span className="text-hint font-mono">{v as string}</span>,
}, },
{ key: 'system', label: '정보시스템명', width: '100px', sortable: true, { key: 'system', label: '정보시스템명', width: '100px', sortable: true,
render: (v) => <span className="text-cyan-400 font-medium">{v as string}</span>, render: (v) => <span className="text-label font-medium">{v as string}</span>,
}, },
{ key: 'linkInfo', label: '연계정보', width: '65px' }, { key: 'linkInfo', label: '연계정보', width: '65px' },
{ key: 'storage', label: '저장장소', render: (v) => <span className="text-hint font-mono text-[9px]">{v as string}</span> }, { key: 'storage', label: '저장장소', render: (v) => <span className="text-hint font-mono text-[9px]">{v as string}</span> },
@ -126,7 +124,7 @@ const channelColumns: DataColumn<ChannelRecord>[] = [
render: (v) => { render: (v) => {
const s = v as string; const s = v as string;
return s === '수신대기중' return s === '수신대기중'
? <span className="text-orange-400 text-[9px]">{s}</span> ? <Badge intent="warning" size="xs">{s}</Badge>
: <span className="text-muted-foreground font-mono text-[10px]">{s}</span>; : <span className="text-muted-foreground font-mono text-[10px]">{s}</span>;
}, },
}, },
@ -157,13 +155,7 @@ function SignalTimeline({ source }: { source: SignalSource }) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* 라벨 */} {/* 라벨 */}
<div className="w-16 shrink-0 text-right"> <div className="w-16 shrink-0 text-right">
<div className="text-[11px] font-bold" style={{ <div className="text-[11px] font-bold text-label">{source.name}</div>
color: source.name === 'VTS' ? '#22c55e'
: source.name === 'VTS-AIS' ? '#3b82f6'
: source.name === 'V-PASS' ? '#a855f7'
: source.name === 'E-NAVI' ? '#ef4444'
: '#eab308',
}}>{source.name}</div>
<div className="text-[10px] text-hint">{source.rate}%</div> <div className="text-[10px] text-hint">{source.rate}%</div>
</div> </div>
{/* 타임라인 바 */} {/* 타임라인 바 */}
@ -232,20 +224,21 @@ const collectColumns: DataColumn<CollectJob>[] = [
{ key: 'successRate', label: '성공률', width: '70px', align: 'right', sortable: true, { key: 'successRate', label: '성공률', width: '70px', align: 'right', sortable: true,
render: (v) => { render: (v) => {
const n = v as number; const n = v as number;
const c = n >= 90 ? 'text-green-400' : n >= 70 ? 'text-yellow-400' : n > 0 ? 'text-red-400' : 'text-hint'; if (n === 0) return <span className="font-bold text-[11px] text-hint">-</span>;
return <span className={`font-bold text-[11px] ${c}`}>{n > 0 ? `${n}%` : '-'}</span>; const intent: BadgeIntent = n >= 90 ? 'success' : n >= 70 ? 'warning' : 'critical';
return <Badge intent={intent} size="xs">{n}%</Badge>;
}, },
}, },
{ key: 'id', label: '', width: '70px', align: 'center', sortable: false, { key: 'id', label: '', width: '70px', align: 'center', sortable: false,
render: (_v, row) => ( render: (_v, row) => (
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
{row.status === '정지' ? ( {row.status === '정지' ? (
<button type="button" className="p-1 text-hint hover:text-green-400" title="시작"><Play className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-heading" title="시작"><Play className="w-3 h-3" /></button>
) : row.status !== '장애발생' ? ( ) : row.status !== '장애발생' ? (
<button type="button" className="p-1 text-hint hover:text-orange-400" title="정지"><Square className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-heading" title="정지"><Square className="w-3 h-3" /></button>
) : null} ) : null}
<button type="button" className="p-1 text-hint hover:text-blue-400" title="편집"><Edit2 className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-label" title="편집"><Edit2 className="w-3 h-3" /></button>
<button type="button" className="p-1 text-hint hover:text-cyan-400" title="이력"><FileText className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-label" title="이력"><FileText className="w-3 h-3" /></button>
</div> </div>
), ),
}, },
@ -291,9 +284,9 @@ const loadColumns: DataColumn<LoadJob>[] = [
{ key: 'id', label: '', width: '70px', align: 'center', sortable: false, { key: 'id', label: '', width: '70px', align: 'center', sortable: false,
render: () => ( render: () => (
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<button type="button" className="p-1 text-hint hover:text-blue-400" title="편집"><Edit2 className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-label" title="편집"><Edit2 className="w-3 h-3" /></button>
<button type="button" className="p-1 text-hint hover:text-cyan-400" title="이력"><FileText className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-label" title="이력"><FileText className="w-3 h-3" /></button>
<button type="button" className="p-1 text-hint hover:text-orange-400" title="스토리지"><HardDrive className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-heading" title="스토리지"><HardDrive className="w-3 h-3" /></button>
</div> </div>
), ),
}, },
@ -387,7 +380,7 @@ export function DataHub() {
<PageContainer> <PageContainer>
<PageHeader <PageHeader
icon={Database} icon={Database}
iconColor="text-cyan-400" iconColor="text-label"
title={t('dataHub.title')} title={t('dataHub.title')}
description={t('dataHub.desc')} description={t('dataHub.desc')}
demo demo
@ -402,11 +395,11 @@ export function DataHub() {
<div className="flex gap-2"> <div className="flex gap-2">
{[ {[
{ label: '전체 채널', value: CHANNELS.length, icon: Layers, color: 'text-label', bg: 'bg-muted' }, { 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: 'ON', value: onCount, icon: Wifi, color: 'text-label', bg: 'bg-blue-500/10' },
{ label: 'OFF', value: offCount, icon: WifiOff, color: 'text-red-400', bg: 'bg-red-500/10' }, { label: 'OFF', value: offCount, icon: WifiOff, color: 'text-heading', bg: 'bg-red-500/10' },
{ label: '평균 수신율', value: '86.5%', icon: BarChart3, color: 'text-green-400', bg: 'bg-green-500/10' }, { label: '평균 수신율', value: '86.5%', icon: BarChart3, color: 'text-label', bg: 'bg-green-500/10' },
{ label: '데이터 소스', value: '5종', icon: Radio, color: 'text-purple-400', bg: 'bg-purple-500/10' }, { label: '데이터 소스', value: '5종', icon: Radio, color: 'text-heading', bg: 'bg-purple-500/10' },
{ label: '연계 방식', value: 'KAFKA', icon: Server, color: 'text-orange-400', bg: 'bg-orange-500/10' }, { label: '연계 방식', value: 'KAFKA', icon: Server, color: 'text-heading', bg: 'bg-orange-500/10' },
].map((kpi) => ( ].map((kpi) => (
<div key={kpi.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card"> <div key={kpi.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
<div className={`p-1.5 rounded-lg ${kpi.bg}`}> <div className={`p-1.5 rounded-lg ${kpi.bg}`}>
@ -509,11 +502,11 @@ export function DataHub() {
<div className="flex items-center gap-3 px-4 py-2 rounded-xl border border-border bg-card"> <div className="flex items-center gap-3 px-4 py-2 rounded-xl border border-border bg-card">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{hasPartialOff ? ( {hasPartialOff ? (
<AlertTriangle className="w-3.5 h-3.5 text-orange-400" /> <AlertTriangle className="w-3.5 h-3.5 text-heading" />
) : ( ) : (
<CheckCircle className="w-3.5 h-3.5 text-green-400" /> <CheckCircle className="w-3.5 h-3.5 text-label" />
)} )}
<span className={`text-[11px] font-bold ${hasPartialOff ? 'text-orange-400' : 'text-green-400'}`}> <span className="text-[11px] font-bold text-heading">
{hasPartialOff ? `일부 OFF (${offCount}/${CHANNELS.length})` : '전체 정상'} {hasPartialOff ? `일부 OFF (${offCount}/${CHANNELS.length})` : '전체 정상'}
</span> </span>
</div> </div>
@ -653,9 +646,9 @@ export function DataHub() {
<div className="flex items-center justify-between mt-3 pt-2 border-t border-border"> <div className="flex items-center justify-between mt-3 pt-2 border-t border-border">
<span className="text-[9px] text-hint"> {agent.taskCount} · heartbeat {agent.lastHeartbeat.slice(11)}</span> <span className="text-[9px] text-hint"> {agent.taskCount} · heartbeat {agent.lastHeartbeat.slice(11)}</span>
<div className="flex gap-0.5"> <div className="flex gap-0.5">
<button type="button" className="p-1 text-hint hover:text-blue-400" title="상태 상세"><Eye className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-label" title="상태 상세"><Eye className="w-3 h-3" /></button>
<button type="button" className="p-1 text-hint hover:text-yellow-400" title="이름 변경"><Edit2 className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-heading" title="이름 변경"><Edit2 className="w-3 h-3" /></button>
<button type="button" className="p-1 text-hint hover:text-red-400" title="삭제"><Trash2 className="w-3 h-3" /></button> <button type="button" className="p-1 text-hint hover:text-heading" title="삭제"><Trash2 className="w-3 h-3" /></button>
</div> </div>
</div> </div>
</CardContent> </CardContent>

파일 보기

@ -41,7 +41,7 @@ export function LoginHistoryView() {
<PageContainer size="lg"> <PageContainer size="lg">
<PageHeader <PageHeader
icon={LogIn} icon={LogIn}
iconColor="text-green-400" iconColor="text-label"
title="로그인 이력" title="로그인 이력"
description="성공/실패 로그인 시도 기록 (5회 실패 시 자동 잠금)" description="성공/실패 로그인 시도 기록 (5회 실패 시 자동 잠금)"
actions={ actions={
@ -55,10 +55,10 @@ export function LoginHistoryView() {
{stats && ( {stats && (
<div className="grid grid-cols-5 gap-3"> <div className="grid grid-cols-5 gap-3">
<MetricCard label="전체 시도" value={stats.total} color="text-heading" /> <MetricCard label="전체 시도" value={stats.total} color="text-heading" />
<MetricCard label="성공 (24h)" value={stats.success24h} color="text-green-400" /> <MetricCard label="성공 (24h)" value={stats.success24h} color="text-label" />
<MetricCard label="실패 (24h)" value={stats.failed24h} color="text-orange-400" /> <MetricCard label="실패 (24h)" value={stats.failed24h} color="text-label" />
<MetricCard label="잠금 (24h)" value={stats.locked24h} color="text-red-400" /> <MetricCard label="잠금 (24h)" value={stats.locked24h} color="text-heading" />
<MetricCard label="성공률 (24h)" value={stats.successRate} color="text-cyan-400" suffix="%" /> <MetricCard label="성공률 (24h)" value={stats.successRate} color="text-label" suffix="%" />
</div> </div>
)} )}
@ -71,7 +71,7 @@ export function LoginHistoryView() {
<CardContent className="px-4 pb-4 space-y-1"> <CardContent className="px-4 pb-4 space-y-1">
{stats.byUser.map((u) => ( {stats.byUser.map((u) => (
<div key={u.user_acnt} className="flex items-center justify-between text-[11px]"> <div key={u.user_acnt} className="flex items-center justify-between text-[11px]">
<span className="text-cyan-400 font-mono">{u.user_acnt}</span> <span className="text-label font-mono">{u.user_acnt}</span>
<span className="text-heading font-bold">{u.count}</span> <span className="text-heading font-bold">{u.count}</span>
</div> </div>
))} ))}
@ -86,9 +86,9 @@ export function LoginHistoryView() {
<div key={d.day} className="flex items-center justify-between text-[11px]"> <div key={d.day} className="flex items-center justify-between text-[11px]">
<span className="text-muted-foreground font-mono">{formatDate(d.day)}</span> <span className="text-muted-foreground font-mono">{formatDate(d.day)}</span>
<div className="flex gap-3"> <div className="flex gap-3">
<span className="text-green-400"> {d.success}</span> <span className="text-label"> {d.success}</span>
<span className="text-orange-400"> {d.failed}</span> <span className="text-label"> {d.failed}</span>
<span className="text-red-400"> {d.locked}</span> <span className="text-heading"> {d.locked}</span>
</div> </div>
</div> </div>
))} ))}
@ -98,7 +98,7 @@ export function LoginHistoryView() {
</div> </div>
)} )}
{error && <div className="text-xs text-red-400">: {error}</div>} {error && <div className="text-xs text-heading">: {error}</div>}
{loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>} {loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>}
@ -123,11 +123,11 @@ export function LoginHistoryView() {
<tr key={it.histSn} className="border-t border-border hover:bg-surface-overlay/50"> <tr key={it.histSn} className="border-t border-border hover:bg-surface-overlay/50">
<td className="px-3 py-2 text-hint font-mono">{it.histSn}</td> <td className="px-3 py-2 text-hint font-mono">{it.histSn}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{formatDateTime(it.loginDtm)}</td> <td className="px-3 py-2 text-muted-foreground text-[10px]">{formatDateTime(it.loginDtm)}</td>
<td className="px-3 py-2 text-cyan-400">{it.userAcnt}</td> <td className="px-3 py-2 text-label">{it.userAcnt}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<Badge intent={getLoginResultIntent(it.result)} size="sm">{getLoginResultLabel(it.result, tc, lang)}</Badge> <Badge intent={getLoginResultIntent(it.result)} size="sm">{getLoginResultLabel(it.result, tc, lang)}</Badge>
</td> </td>
<td className="px-3 py-2 text-red-400 text-[10px]">{it.failReason || '-'}</td> <td className="px-3 py-2 text-heading text-[10px]">{it.failReason || '-'}</td>
<td className="px-3 py-2 text-muted-foreground">{it.authProvider || '-'}</td> <td className="px-3 py-2 text-muted-foreground">{it.authProvider || '-'}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{it.loginIp || '-'}</td> <td className="px-3 py-2 text-muted-foreground text-[10px]">{it.loginIp || '-'}</td>
</tr> </tr>

파일 보기

@ -1,17 +1,16 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; 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 { Badge } from '@shared/components/ui/badge';
import { Button } from '@shared/components/ui/button'; import { Button } from '@shared/components/ui/button';
import { PageContainer, PageHeader } from '@shared/components/layout'; import { PageContainer, PageHeader } from '@shared/components/layout';
import { useAuth } from '@/app/auth/AuthContext'; import { useAuth } from '@/app/auth/AuthContext';
import type { BadgeIntent } from '@lib/theme/variants'; import type { BadgeIntent } from '@lib/theme/variants';
import { import {
Bell, Plus, Edit2, Trash2, Eye, EyeOff, Calendar, Bell, Plus, Edit2, Trash2, Eye,
Users, Megaphone, AlertTriangle, Info, Search, Filter, Megaphone, AlertTriangle, Info,
CheckCircle, Clock, Pin, Monitor, MessageSquare, X, Clock, Pin, Monitor, MessageSquare, X,
} from 'lucide-react'; } from 'lucide-react';
import { DataTable, type DataColumn } from '@shared/components/common/DataTable';
import { toDateParam } from '@shared/utils/dateFormat'; import { toDateParam } from '@shared/utils/dateFormat';
import { SaveButton } from '@shared/components/common/SaveButton'; import { SaveButton } from '@shared/components/common/SaveButton';
import type { SystemNotice, NoticeType, NoticeDisplay } from '@shared/components/common/NotificationBanner'; 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 }[] = [ const TYPE_OPTIONS: { key: NoticeType; label: string; icon: React.ElementType; color: string }[] = [
{ key: 'info', label: '정보', icon: Info, color: 'text-blue-400' }, { key: 'info', label: '정보', icon: Info, color: 'text-label' },
{ key: 'warning', label: '경고', icon: AlertTriangle, color: 'text-yellow-400' }, { key: 'warning', label: '경고', icon: AlertTriangle, color: 'text-label' },
{ key: 'urgent', label: '긴급', icon: Bell, color: 'text-red-400' }, { key: 'urgent', label: '긴급', icon: Bell, color: 'text-label' },
{ key: 'maintenance', label: '점검', icon: Megaphone, color: 'text-orange-400' }, { key: 'maintenance', label: '점검', icon: Megaphone, color: 'text-label' },
]; ];
const DISPLAY_OPTIONS: { key: NoticeDisplay; label: string; icon: React.ElementType }[] = [ const DISPLAY_OPTIONS: { key: NoticeDisplay; label: string; icon: React.ElementType }[] = [
@ -146,7 +145,7 @@ export function NoticeManagement() {
<PageContainer> <PageContainer>
<PageHeader <PageHeader
icon={Bell} icon={Bell}
iconColor="text-yellow-400" iconColor="text-label"
title={t('notices.title')} title={t('notices.title')}
description={t('notices.desc')} description={t('notices.desc')}
demo demo
@ -161,9 +160,9 @@ export function NoticeManagement() {
<div className="flex gap-2"> <div className="flex gap-2">
{[ {[
{ label: '전체 알림', count: notices.length, icon: Bell, color: 'text-label', bg: 'bg-muted' }, { 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: activeCount, icon: Eye, color: 'text-label', bg: 'bg-green-500/10' },
{ label: '예약됨', count: scheduledCount, icon: Clock, color: 'text-blue-400', bg: 'bg-blue-500/10' }, { label: '예약됨', count: scheduledCount, icon: Clock, color: 'text-label', bg: 'bg-blue-500/10' },
{ label: '긴급 알림', count: urgentCount, icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-500/10' }, { label: '긴급 알림', count: urgentCount, icon: AlertTriangle, color: 'text-label', bg: 'bg-red-500/10' },
].map((kpi) => ( ].map((kpi) => (
<div key={kpi.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card"> <div key={kpi.label} className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-card">
<div className={`p-1.5 rounded-lg ${kpi.bg}`}> <div className={`p-1.5 rounded-lg ${kpi.bg}`}>
@ -238,14 +237,14 @@ export function NoticeManagement() {
)} )}
</td> </td>
<td className="px-1 py-1.5 text-center"> <td className="px-1 py-1.5 text-center">
{n.pinned && <Pin className="w-3 h-3 text-yellow-400 inline" />} {n.pinned && <Pin className="w-3 h-3 text-label inline" />}
</td> </td>
<td className="px-1 py-1.5"> <td className="px-1 py-1.5">
<div className="flex items-center justify-center gap-0.5"> <div className="flex items-center justify-center gap-0.5">
<button type="button" onClick={() => openEdit(n)} disabled={!canUpdate} className="p-1 text-hint hover:text-blue-400 disabled:opacity-30 disabled:cursor-not-allowed" title={canUpdate ? '수정' : '수정 권한이 필요합니다'}> <button type="button" onClick={() => openEdit(n)} disabled={!canUpdate} className="p-1 text-hint hover:text-heading disabled:opacity-30 disabled:cursor-not-allowed" title={canUpdate ? '수정' : '수정 권한이 필요합니다'}>
<Edit2 className="w-3 h-3" /> <Edit2 className="w-3 h-3" />
</button> </button>
<button type="button" onClick={() => handleDelete(n.id)} disabled={!canDelete} className="p-1 text-hint hover:text-red-400 disabled:opacity-30 disabled:cursor-not-allowed" title={canDelete ? '삭제' : '삭제 권한이 필요합니다'}> <button type="button" onClick={() => handleDelete(n.id)} disabled={!canDelete} className="p-1 text-hint hover:text-heading disabled:opacity-30 disabled:cursor-not-allowed" title={canDelete ? '삭제' : '삭제 권한이 필요합니다'}>
<Trash2 className="w-3 h-3" /> <Trash2 className="w-3 h-3" />
</button> </button>
</div> </div>
@ -327,7 +326,7 @@ export function NoticeManagement() {
onClick={() => setForm({ ...form, display: opt.key })} onClick={() => setForm({ ...form, display: opt.key })}
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-[10px] transition-colors ${ className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-[10px] transition-colors ${
form.display === opt.key 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' : 'text-hint hover:bg-surface-overlay'
}`} }`}
> >
@ -375,7 +374,7 @@ export function NoticeManagement() {
onClick={() => toggleRole(role)} onClick={() => toggleRole(role)}
className={`px-3 py-1.5 rounded-lg text-[10px] transition-colors ${ className={`px-3 py-1.5 rounded-lg text-[10px] transition-colors ${
form.targetRoles.includes(role) 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' : 'text-hint border border-slate-700/30 hover:bg-surface-overlay'
}`} }`}
> >

파일 보기

@ -359,13 +359,13 @@ export function PermissionsPanel() {
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button type="button" onClick={load} <button type="button" onClick={load}
className="p-1.5 rounded text-hint hover:text-blue-400 hover:bg-surface-overlay" title="새로고침"> className="p-1.5 rounded text-hint hover:text-label hover:bg-surface-overlay" title="새로고침">
<RefreshCw className="w-3.5 h-3.5" /> <RefreshCw className="w-3.5 h-3.5" />
</button> </button>
</div> </div>
</div> </div>
{error && <div className="text-xs text-red-400">: {error}</div>} {error && <div className="text-xs text-heading">: {error}</div>}
{loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>} {loading && <div className="flex items-center justify-center py-12 text-muted-foreground"><Loader2 className="w-5 h-5 animate-spin" /></div>}
@ -379,13 +379,13 @@ export function PermissionsPanel() {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{canCreateRole && ( {canCreateRole && (
<button type="button" onClick={() => setShowCreate(!showCreate)} <button type="button" onClick={() => setShowCreate(!showCreate)}
className="p-1 text-hint hover:text-green-400" title="신규 역할"> className="p-1 text-hint hover:text-label" title="신규 역할">
<Plus className="w-3.5 h-3.5" /> <Plus className="w-3.5 h-3.5" />
</button> </button>
)} )}
{canDeleteRole && selectedRole && selectedRole.builtinYn !== 'Y' && ( {canDeleteRole && selectedRole && selectedRole.builtinYn !== 'Y' && (
<button type="button" onClick={handleDeleteRole} <button type="button" onClick={handleDeleteRole}
className="p-1 text-hint hover:text-red-400" title="역할 삭제"> className="p-1 text-hint hover:text-heading" title="역할 삭제">
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
)} )}
@ -442,7 +442,7 @@ export function PermissionsPanel() {
<button <button
type="button" type="button"
onClick={() => setEditingColor(isEditingColor ? null : String(r.roleSn))} onClick={() => setEditingColor(isEditingColor ? null : String(r.roleSn))}
className="text-[8px] text-hint hover:text-blue-400" className="text-[8px] text-hint hover:text-label"
title="색상 변경" title="색상 변경"
> >
@ -479,9 +479,9 @@ export function PermissionsPanel() {
{selectedRole ? `${selectedRole.roleNm} (${selectedRole.roleCd})` : '역할 선택'} {selectedRole ? `${selectedRole.roleNm} (${selectedRole.roleCd})` : '역할 선택'}
</div> </div>
<div className="text-[10px] text-hint mt-0.5"> <div className="text-[10px] text-hint mt-0.5">
: <span className="text-blue-400"> </span> / : <span className="text-label"> </span> /
<span className="text-blue-300/80 ml-1"> </span> / <span className="text-blue-300/80 ml-1"> </span> /
<span className="text-red-400 ml-1"> </span> / <span className="text-heading ml-1"> </span> /
<span className="text-gray-500 ml-1">× </span> / <span className="text-gray-500 ml-1">× </span> /
<span className="text-hint ml-1">· </span> <span className="text-hint ml-1">· </span>
</div> </div>

파일 보기

@ -6,9 +6,9 @@ import { Button } from '@shared/components/ui/button';
import { PageContainer, PageHeader } from '@shared/components/layout'; import { PageContainer, PageHeader } from '@shared/components/layout';
import type { BadgeIntent } from '@lib/theme/variants'; import type { BadgeIntent } from '@lib/theme/variants';
import { import {
Settings, Database, Search, ChevronDown, ChevronRight, Settings, Database, Search,
Map, Fish, Anchor, Ship, Globe, BarChart3, Download, Map, Fish, Anchor, Ship, Globe, BarChart3, Download,
Filter, RefreshCw, BookOpen, Layers, Hash, Info, Filter, RefreshCw, Hash, Info,
} from 'lucide-react'; } from 'lucide-react';
import { import {
AREA_CODES, SPECIES_CODES, FISHERY_CODES, VESSEL_TYPE_CODES, AREA_CODES, SPECIES_CODES, FISHERY_CODES, VESSEL_TYPE_CODES,
@ -149,7 +149,7 @@ export function SystemConfig() {
<PageContainer> <PageContainer>
<PageHeader <PageHeader
icon={Database} icon={Database}
iconColor="text-cyan-400" iconColor="text-label"
title={t('systemConfig.title')} title={t('systemConfig.title')}
description={t('systemConfig.desc')} description={t('systemConfig.desc')}
demo demo
@ -168,11 +168,11 @@ export function SystemConfig() {
{/* KPI 카드 */} {/* KPI 카드 */}
<div className="grid grid-cols-5 gap-3"> <div className="grid grid-cols-5 gap-3">
{[ {[
{ icon: Map, label: '해역분류', count: CODE_STATS.areas, color: 'text-blue-400', bg: 'bg-blue-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-green-400', bg: 'bg-green-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-purple-400', bg: 'bg-purple-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-orange-400', bg: 'bg-orange-500/10', desc: 'MDA 5개출처 통합' }, { 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-cyan-400', bg: 'bg-cyan-500/10', desc: '공통코드 총계' }, { icon: Globe, label: '전체 코드', count: CODE_STATS.total, color: 'text-label', bg: 'bg-cyan-500/10', desc: '공통코드 총계' },
].map((kpi) => ( ].map((kpi) => (
<Card key={kpi.label} className="bg-surface-raised border-border"> <Card key={kpi.label} className="bg-surface-raised border-border">
<CardContent className="p-3"> <CardContent className="p-3">
@ -270,7 +270,7 @@ export function SystemConfig() {
<tbody> <tbody>
{(pagedData as AreaCode[]).map((a) => ( {(pagedData as AreaCode[]).map((a) => (
<tr key={a.code} className="border-b border-border hover:bg-surface-overlay"> <tr key={a.code} className="border-b border-border hover:bg-surface-overlay">
<td className="px-4 py-2 text-cyan-400 font-mono font-medium">{a.code}</td> <td className="px-4 py-2 text-label font-mono font-medium">{a.code}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
{(() => { {(() => {
const intent: BadgeIntent = a.major === '서해' ? 'info' : a.major === '남해' ? 'success' : a.major === '동해' ? 'purple' : a.major === '제주' ? 'high' : 'cyan'; 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" className="border-b border-border hover:bg-surface-overlay cursor-pointer"
onClick={() => setExpandedRow(expandedRow === s.code ? null : s.code)} onClick={() => setExpandedRow(expandedRow === s.code ? null : s.code)}
> >
<td className="px-4 py-2 text-cyan-400 font-mono font-medium">{s.code}</td> <td className="px-4 py-2 text-label font-mono font-medium">{s.code}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<Badge intent="muted" size="sm">{s.major}</Badge> <Badge intent="muted" size="sm">{s.major}</Badge>
</td> </td>
@ -323,12 +323,12 @@ export function SystemConfig() {
<td className="px-4 py-2 text-muted-foreground text-[10px]">{s.area}</td> <td className="px-4 py-2 text-muted-foreground text-[10px]">{s.area}</td>
<td className="px-4 py-2 text-center"> <td className="px-4 py-2 text-center">
{s.active {s.active
? <span className="text-green-400 text-[9px]">Y</span> ? <span className="text-label text-[9px]">Y</span>
: <span className="text-hint text-[9px]">N</span> : <span className="text-hint text-[9px]">N</span>
} }
</td> </td>
<td className="px-4 py-2 text-center"> <td className="px-4 py-2 text-center">
{s.fishing && <span className="text-yellow-400"></span>} {s.fishing && <span className="text-label"></span>}
</td> </td>
</tr> </tr>
))} ))}
@ -357,7 +357,7 @@ export function SystemConfig() {
<tbody> <tbody>
{(pagedData as FisheryCode[]).map((f) => ( {(pagedData as FisheryCode[]).map((f) => (
<tr key={f.code} className="border-b border-border hover:bg-surface-overlay"> <tr key={f.code} className="border-b border-border hover:bg-surface-overlay">
<td className="px-4 py-2 text-cyan-400 font-mono font-medium">{f.code}</td> <td className="px-4 py-2 text-label font-mono font-medium">{f.code}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
{(() => { {(() => {
const intent: BadgeIntent = f.major === '근해어업' ? 'info' : f.major === '연안어업' ? 'success' : f.major === '양식어업' ? 'cyan' : f.major === '원양어업' ? 'purple' : f.major === '구획어업' ? 'high' : f.major === '마을어업' ? 'warning' : 'muted'; 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() {
<tbody> <tbody>
{(pagedData as VesselTypeCode[]).map((v) => ( {(pagedData as VesselTypeCode[]).map((v) => (
<tr key={v.code} className="border-b border-border hover:bg-surface-overlay"> <tr key={v.code} className="border-b border-border hover:bg-surface-overlay">
<td className="px-3 py-2 text-cyan-400 font-mono font-medium">{v.code}</td> <td className="px-3 py-2 text-label font-mono font-medium">{v.code}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
{(() => { {(() => {
const intent: BadgeIntent = v.major === '어선' ? 'info' : v.major === '여객선' ? 'success' : v.major === '화물선' ? 'high' : v.major === '유조선' ? 'critical' : v.major === '관공선' ? 'purple' : v.major === '함정' ? 'cyan' : 'muted'; 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() {
<td className="px-3 py-2 text-label text-[10px]">{v.mid}</td> <td className="px-3 py-2 text-label text-[10px]">{v.mid}</td>
<td className="px-3 py-2 text-heading font-medium">{v.name}</td> <td className="px-3 py-2 text-heading font-medium">{v.name}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`text-[9px] font-mono ${ <span className="text-[9px] font-mono text-label">{v.source}</span>
v.source === 'AIS' ? 'text-cyan-400'
: v.source === 'GIC' ? 'text-green-400'
: v.source === 'RRA' ? 'text-blue-400'
: v.source === 'PMS' ? 'text-orange-400'
: 'text-muted-foreground'
}`}>{v.source}</span>
</td> </td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{v.tonnage}</td> <td className="px-3 py-2 text-muted-foreground text-[10px]">{v.tonnage}</td>
<td className="px-3 py-2 text-muted-foreground text-[10px]">{v.purpose}</td> <td className="px-3 py-2 text-muted-foreground text-[10px]">{v.purpose}</td>
@ -431,23 +425,25 @@ export function SystemConfig() {
{/* 페이지네이션 (코드 탭에서만) */} {/* 페이지네이션 (코드 탭에서만) */}
{tab !== 'settings' && totalPages > 1 && ( {tab !== 'settings' && totalPages > 1 && (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<button type="button" <Button
variant="secondary"
size="sm"
onClick={() => setPage(Math.max(0, page - 1))} onClick={() => setPage(Math.max(0, page - 1))}
disabled={page === 0} disabled={page === 0}
className="px-3 py-1.5 rounded-lg text-[11px] bg-surface-overlay border border-border text-muted-foreground hover:text-heading disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
> >
</button> </Button>
<span className="text-[11px] text-hint"> <span className="text-[11px] text-hint">
{page + 1} / {totalPages} ({totalItems.toLocaleString()}) {page + 1} / {totalPages} ({totalItems.toLocaleString()})
</span> </span>
<button type="button" <Button
variant="secondary"
size="sm"
onClick={() => setPage(Math.min(totalPages - 1, page + 1))} onClick={() => setPage(Math.min(totalPages - 1, page + 1))}
disabled={page >= totalPages - 1} disabled={page >= totalPages - 1}
className="px-3 py-1.5 rounded-lg text-[11px] bg-surface-overlay border border-border text-muted-foreground hover:text-heading disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
> >
</button> </Button>
</div> </div>
)} )}
@ -466,7 +462,7 @@ export function SystemConfig() {
<Card key={section} className="bg-surface-raised border-border"> <Card key={section} className="bg-surface-raised border-border">
<CardHeader className="px-4 pt-3 pb-2"> <CardHeader className="px-4 pt-3 pb-2">
<CardTitle className="text-xs text-label flex items-center gap-1.5"> <CardTitle className="text-xs text-label flex items-center gap-1.5">
<meta.icon className="w-3.5 h-3.5 text-cyan-400" /> <meta.icon className="w-3.5 h-3.5 text-label" />
{meta.title} {meta.title}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -474,9 +470,7 @@ export function SystemConfig() {
{items.map((item) => ( {items.map((item) => (
<div key={item.key} className="flex justify-between items-center text-[11px]"> <div key={item.key} className="flex justify-between items-center text-[11px]">
<span className="text-hint">{item.label}</span> <span className="text-hint">{item.label}</span>
<span className={`font-medium ${ <span className="font-medium text-label">{item.value}</span>
item.value === '활성' ? 'text-green-400' : 'text-label'
}`}>{item.value}</span>
</div> </div>
))} ))}
</CardContent> </CardContent>