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:
부모
52749638ef
커밋
4ee8f05dfd
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user