refactor(frontend): admin 계열 PageContainer/PageHeader 적용

- AdminPanel: PageContainer + PageHeader(demo)
- AuditLogs/AccessLogs/LoginHistoryView: size=lg + primary Button
- AccessControl: size=lg + 우측 stats 유지 + ghost 새로고침 Button
- DataHub: PageContainer + demo 배지 + secondary 새로고침
- NoticeManagement: primary '새 알림 등록' Button
- SystemConfig: secondary 2개 액션 Button

인라인 <button>/<div className="p-5 space-y-4"> 패턴을 쇼케이스 공통 컴포넌트로
치환. admin 계열 9개 파일 중 7개 완료 (PermissionsPanel은 서브 컴포넌트라 제외).
UserRoleAssignDialog는 dialog라 제외.
This commit is contained in:
htlee 2026-04-08 11:48:41 +09:00
부모 52749638ef
커밋 4ee8f05dfd
8개의 변경된 파일143개의 추가작업 그리고 150개의 파일을 삭제

파일 보기

@ -2,6 +2,8 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } 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, Users, UserCheck, Key, Lock, FileText, Loader2, RefreshCw, UserCog,
@ -183,33 +185,36 @@ export function AccessControl() {
], []);
return (
<div className="space-y-4 p-6">
<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-blue-400" />
{t('accessControl.title')}
</h2>
<p className="text-[10px] text-hint mt-0.5">{t('accessControl.desc')}</p>
</div>
<div className="flex items-center gap-2">
{userStats && (
<div className="flex items-center gap-2 text-[10px] text-hint">
<UserCheck className="w-3.5 h-3.5 text-green-500" />
<span className="text-green-400 font-bold">{userStats.active}</span>
<span className="mx-1">|</span>
<span className="text-red-400 font-bold">{userStats.locked}</span>
<span className="mx-1">|</span>
<span className="text-heading font-bold">{userStats.total}</span>
</div>
)}
<button type="button"
onClick={() => { if (tab === 'users') loadUsers(); else if (tab === 'audit') loadAudit(); }}
className="p-1.5 rounded text-hint hover:text-blue-400 hover:bg-surface-overlay" title="새로고침">
<RefreshCw className="w-3.5 h-3.5" />
</button>
</div>
</div>
<PageContainer size="lg">
<PageHeader
icon={Shield}
iconColor="text-blue-400"
title={t('accessControl.title')}
description={t('accessControl.desc')}
actions={
<>
{userStats && (
<div className="flex items-center gap-2 text-[10px] text-hint">
<UserCheck className="w-3.5 h-3.5 text-green-500" />
<span className="text-green-400 font-bold">{userStats.active}</span>
<span className="mx-1">|</span>
<span className="text-red-400 font-bold">{userStats.locked}</span>
<span className="mx-1">|</span>
<span className="text-heading font-bold">{userStats.total}</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={() => { if (tab === 'users') loadUsers(); else if (tab === 'audit') loadAudit(); }}
title="새로고침"
icon={<RefreshCw className="w-3.5 h-3.5" />}
>
<span className="sr-only"></span>
</Button>
</>
}
/>
{/* 탭 */}
<div className="flex gap-1">
@ -354,7 +359,7 @@ export function AccessControl() {
]} />
</div>
)}
</div>
</PageContainer>
);
}

파일 보기

@ -1,7 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import { Loader2, RefreshCw } from 'lucide-react';
import { Loader2, RefreshCw, Activity } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } 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 { fetchAccessLogs, fetchAccessStats, type AccessLog, type AccessStats } from '@/services/adminApi';
import { formatDateTime } from '@shared/utils/dateFormat';
import { getHttpStatusIntent } from '@shared/constants/httpStatusCodes';
@ -32,16 +34,18 @@ export function AccessLogs() {
useEffect(() => { load(); }, [load]);
return (
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-heading"> </h1>
<p className="text-xs text-hint mt-1">AccessLogFilter가 HTTP </p>
</div>
<button type="button" onClick={load} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded flex items-center gap-1">
<RefreshCw className="w-3.5 h-3.5" />
</button>
</div>
<PageContainer size="lg">
<PageHeader
icon={Activity}
iconColor="text-cyan-400"
title="접근 이력"
description="AccessLogFilter가 모든 HTTP 요청 비동기 기록"
actions={
<Button variant="primary" size="sm" onClick={load} icon={<RefreshCw className="w-3.5 h-3.5" />}>
</Button>
}
/>
{stats && (
<div className="grid grid-cols-5 gap-3">
@ -120,7 +124,7 @@ export function AccessLogs() {
</CardContent>
</Card>
)}
</div>
</PageContainer>
);
}

파일 보기

@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card';
import { Settings, Server, HardDrive, Shield, Clock, Database } from 'lucide-react';
import { PageContainer, PageHeader } from '@shared/components/layout';
import { Settings, Server, Shield, Database } from 'lucide-react';
/*
* , ,
@ -30,17 +31,13 @@ function UsageBar({ value }: { value: number }) {
export function AdminPanel() {
const { t } = useTranslation('admin');
return (
<div className="p-5 space-y-4">
<div>
<h2 className="text-lg font-bold text-heading whitespace-nowrap flex items-center gap-2">
<Settings className="w-5 h-5 text-muted-foreground" />
{t('adminPanel.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('adminPanel.desc')}</p>
</div>
<PageContainer>
<PageHeader
icon={Settings}
title={t('adminPanel.title')}
description={t('adminPanel.desc')}
demo
/>
{/* 서버 상태 */}
<div className="grid grid-cols-3 gap-3">
@ -89,6 +86,6 @@ export function AdminPanel() {
</CardContent>
</Card>
</div>
</div>
</PageContainer>
);
}

파일 보기

@ -1,7 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import { Loader2, RefreshCw } from 'lucide-react';
import { Loader2, RefreshCw, FileSearch } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } 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 { fetchAuditLogs, fetchAuditStats, type AuditLog, type AuditStats } from '@/services/adminApi';
import { formatDateTime } from '@shared/utils/dateFormat';
@ -31,16 +33,18 @@ export function AuditLogs() {
useEffect(() => { load(); }, [load]);
return (
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-heading"> </h1>
<p className="text-xs text-hint mt-1">@Auditable AOP가 </p>
</div>
<button type="button" onClick={load} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded flex items-center gap-1">
<RefreshCw className="w-3.5 h-3.5" />
</button>
</div>
<PageContainer size="lg">
<PageHeader
icon={FileSearch}
iconColor="text-blue-400"
title="감사 로그"
description="@Auditable AOP가 모든 운영자 의사결정 자동 기록"
actions={
<Button variant="primary" size="sm" onClick={load} icon={<RefreshCw className="w-3.5 h-3.5" />}>
</Button>
}
/>
{/* 통계 카드 */}
{stats && (
@ -115,7 +119,7 @@ export function AuditLogs() {
</CardContent>
</Card>
)}
</div>
</PageContainer>
);
}

파일 보기

@ -2,6 +2,8 @@ import { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } 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 { SaveButton } from '@shared/components/common/SaveButton';
import { getConnectionStatusHex } from '@shared/constants/connectionStatuses';
@ -380,28 +382,19 @@ export function DataHub() {
);
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">
<Database className="w-5 h-5 text-cyan-400" />
{t('dataHub.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('dataHub.desc')}
</p>
</div>
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-surface-overlay border border-border rounded-lg text-[10px] text-muted-foreground hover:text-heading transition-colors">
<RefreshCw className="w-3 h-3" />
<PageContainer>
<PageHeader
icon={Database}
iconColor="text-cyan-400"
title={t('dataHub.title')}
description={t('dataHub.desc')}
demo
actions={
<Button variant="secondary" size="sm" icon={<RefreshCw className="w-3 h-3" />}>
</button>
</div>
</div>
</Button>
}
/>
{/* KPI */}
<div className="flex gap-2">
@ -670,6 +663,6 @@ export function DataHub() {
</div>
</div>
)}
</div>
</PageContainer>
);
}

파일 보기

@ -1,7 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import { Loader2, RefreshCw } from 'lucide-react';
import { Loader2, RefreshCw, LogIn } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } 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 { fetchLoginHistory, fetchLoginStats, type LoginHistory, type LoginStats } from '@/services/adminApi';
import { formatDateTime, formatDate } from '@shared/utils/dateFormat';
import { getLoginResultIntent, getLoginResultLabel } from '@shared/constants/loginResultStatuses';
@ -36,16 +38,18 @@ export function LoginHistoryView() {
useEffect(() => { load(); }, [load]);
return (
<div className="p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-heading"> </h1>
<p className="text-xs text-hint mt-1">/ (5 )</p>
</div>
<button type="button" onClick={load} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs rounded flex items-center gap-1">
<RefreshCw className="w-3.5 h-3.5" />
</button>
</div>
<PageContainer size="lg">
<PageHeader
icon={LogIn}
iconColor="text-green-400"
title="로그인 이력"
description="성공/실패 로그인 시도 기록 (5회 실패 시 자동 잠금)"
actions={
<Button variant="primary" size="sm" onClick={load} icon={<RefreshCw className="w-3.5 h-3.5" />}>
</Button>
}
/>
{/* 통계 카드 */}
{stats && (
@ -133,7 +137,7 @@ export function LoginHistoryView() {
</CardContent>
</Card>
)}
</div>
</PageContainer>
);
}

파일 보기

@ -2,6 +2,8 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } 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 {
Bell, Plus, Edit2, Trash2, Eye, EyeOff, Calendar,
Users, Megaphone, AlertTriangle, Info, Search, Filter,
@ -135,29 +137,19 @@ export function NoticeManagement() {
const urgentCount = notices.filter((n) => n.type === 'urgent' && n.startDate <= now && n.endDate >= now).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">
<Bell className="w-5 h-5 text-yellow-400" />
{t('notices.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('notices.desc')}
</p>
</div>
<button
onClick={openNew}
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 transition-colors"
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
<PageContainer>
<PageHeader
icon={Bell}
iconColor="text-yellow-400"
title={t('notices.title')}
description={t('notices.desc')}
demo
actions={
<Button variant="primary" size="md" onClick={openNew} icon={<Plus className="w-3.5 h-3.5" />}>
</Button>
}
/>
{/* KPI — 가로 한 줄 */}
<div className="flex gap-2">
@ -423,6 +415,6 @@ export function NoticeManagement() {
</div>
</div>
)}
</div>
</PageContainer>
);
}

파일 보기

@ -2,6 +2,8 @@ import { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } 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 {
Settings, Database, Search, ChevronDown, ChevronRight,
Map, Fish, Anchor, Ship, Globe, BarChart3, Download,
@ -143,32 +145,24 @@ export function SystemConfig() {
: [];
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">
<Database className="w-5 h-5 text-cyan-400" />
{t('systemConfig.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('systemConfig.desc')}
</p>
</div>
<div className="flex items-center gap-2">
<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 transition-colors">
<Download 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 transition-colors">
<RefreshCw className="w-3 h-3" />
</button>
</div>
</div>
<PageContainer>
<PageHeader
icon={Database}
iconColor="text-cyan-400"
title={t('systemConfig.title')}
description={t('systemConfig.desc')}
demo
actions={
<>
<Button variant="secondary" size="sm" icon={<Download className="w-3 h-3" />}>
</Button>
<Button variant="secondary" size="sm" icon={<RefreshCw className="w-3 h-3" />}>
</Button>
</>
}
/>
{/* KPI 카드 */}
<div className="grid grid-cols-5 gap-3">
@ -505,6 +499,6 @@ export function SystemConfig() {
})}
</div>
)}
</div>
</PageContainer>
);
}