From d72799011d38f186931d08205b8a65d5fcce5d58 Mon Sep 17 00:00:00 2001 From: htlee Date: Sat, 14 Feb 2026 23:00:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=B1=EC=97=94=EB=93=9C=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EA=B0=95=ED=99=94=20=EB=B0=8F=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StatsResponse/LoginHistory 타입을 백엔드 스키마에 동기화 - 통계 카드 7개(사용자 상태별, 오늘 로그인, 전체 롤) + 로그인 이력 테이블 추가 - 권한 삭제 API 경로 수정 (/admin/roles/permissions/) - 사용자 관리에 관리자 지정/해제 토글 및 ADMIN 뱃지 추가 - 가이드/홈 페이지에 활동 추적(POST /activity/track) 연동 - CiCdGuide.tsx ESLint no-useless-escape 에러 수정 Co-Authored-By: Claude Opus 4.6 --- src/content/CiCdGuide.tsx | 4 +- src/pages/GuidePage.tsx | 9 ++- src/pages/HomePage.tsx | 6 ++ src/pages/admin/PermissionManagement.tsx | 2 +- src/pages/admin/StatsPage.tsx | 81 ++++++++++++------------ src/pages/admin/UserManagement.tsx | 34 +++++++++- src/types/index.ts | 15 ++--- 7 files changed, 97 insertions(+), 54 deletions(-) diff --git a/src/content/CiCdGuide.tsx b/src/content/CiCdGuide.tsx index 512b216..db35391 100644 --- a/src/content/CiCdGuide.tsx +++ b/src/content/CiCdGuide.tsx @@ -291,8 +291,8 @@ jobs: steps: - name: Checkout run: | - git clone --depth=1 --branch=\$\{GITHUB_REF_NAME\} \\ - http://gitea:3000/\$\{GITHUB_REPOSITORY\}.git . + git clone --depth=1 --branch=${'${GITHUB_REF_NAME}'} \\ + http://gitea:3000/${'${GITHUB_REPOSITORY}'}.git . - name: Configure Maven settings run: | diff --git a/src/pages/GuidePage.tsx b/src/pages/GuidePage.tsx index 5ba3bf9..5cd2ab1 100644 --- a/src/pages/GuidePage.tsx +++ b/src/pages/GuidePage.tsx @@ -1,5 +1,6 @@ -import { lazy, Suspense } from 'react'; +import { lazy, Suspense, useEffect } from 'react'; import { useParams } from 'react-router'; +import { api } from '../utils/api'; const CONTENT_MAP: Record> = { 'env-intro': lazy(() => import('../content/DevEnvIntro')), @@ -17,6 +18,12 @@ export function GuidePage() { const { section } = useParams<{ section: string }>(); const Content = section ? CONTENT_MAP[section] : null; + useEffect(() => { + if (section) { + api.post('/activity/track', { pagePath: `/dev/${section}` }).catch(() => {}); + } + }, [section]); + if (!Content) { return (
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index e57d86a..8b1f8f5 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,9 +1,15 @@ +import { useEffect } from 'react'; import { Link } from 'react-router'; import { useAuth } from '../auth/useAuth'; +import { api } from '../utils/api'; export function HomePage() { const { user } = useAuth(); + useEffect(() => { + api.post('/activity/track', { pagePath: '/' }).catch(() => {}); + }, []); + return (

diff --git a/src/pages/admin/PermissionManagement.tsx b/src/pages/admin/PermissionManagement.tsx index e19a465..2a13000 100644 --- a/src/pages/admin/PermissionManagement.tsx +++ b/src/pages/admin/PermissionManagement.tsx @@ -54,7 +54,7 @@ export function PermissionManagement() { const handleDelete = async (permissionId: number) => { try { - await api.delete(`/admin/permissions/${permissionId}`); + await api.delete(`/admin/roles/permissions/${permissionId}`); fetchPermissions(); } catch { // 에러 처리 diff --git a/src/pages/admin/StatsPage.tsx b/src/pages/admin/StatsPage.tsx index d554e6a..a710bcc 100644 --- a/src/pages/admin/StatsPage.tsx +++ b/src/pages/admin/StatsPage.tsx @@ -1,23 +1,31 @@ import { useCallback, useEffect, useState } from 'react'; -import type { PageStat, StatsResponse } from '../../types'; +import type { LoginHistory, StatsResponse } from '../../types'; import { api } from '../../utils/api'; +const LOGIN_HISTORY_LIMIT = 20; + export function StatsPage() { const [stats, setStats] = useState(null); - const [pageStats] = useState([]); + const [loginHistory, setLoginHistory] = useState([]); const [loading, setLoading] = useState(true); - const fetchStats = useCallback(async () => { + const fetchData = useCallback(async () => { try { - const data = await api.get('/admin/stats'); - setStats(data); + const [statsData, historyData] = await Promise.all([ + api.get('/admin/stats'), + api.get('/activity/login-history'), + ]); + setStats(statsData); + setLoginHistory(historyData.slice(0, LOGIN_HISTORY_LIMIT)); } catch { - // API 미연동 시 기본값 setStats({ totalUsers: 0, activeUsers: 0, pendingUsers: 0, - totalPages: 7, + rejectedUsers: 0, + disabledUsers: 0, + todayLogins: 0, + totalRoles: 0, }); } finally { setLoading(false); @@ -25,8 +33,8 @@ export function StatsPage() { }, []); useEffect(() => { - fetchStats(); - }, [fetchStats]); + fetchData(); + }, [fetchData]); if (loading) { return ( @@ -40,7 +48,10 @@ export function StatsPage() { { label: '전체 사용자', value: stats?.totalUsers ?? 0, color: 'text-accent' }, { label: '활성 사용자', value: stats?.activeUsers ?? 0, color: 'text-success' }, { label: '승인 대기', value: stats?.pendingUsers ?? 0, color: 'text-warning' }, - { label: '가이드 페이지', value: stats?.totalPages ?? 0, color: 'text-info' }, + { label: '거절', value: stats?.rejectedUsers ?? 0, color: 'text-danger' }, + { label: '비활성', value: stats?.disabledUsers ?? 0, color: 'text-text-muted' }, + { label: '오늘 로그인', value: stats?.todayLogins ?? 0, color: 'text-info' }, + { label: '전체 롤', value: stats?.totalRoles ?? 0, color: 'text-accent' }, ]; return ( @@ -48,7 +59,7 @@ export function StatsPage() {

통계

{/* 통계 카드 */} -
+
{statCards.map((card) => (

{card.label}

@@ -57,46 +68,38 @@ export function StatsPage() { ))}
- {/* 인기 페이지 */} -

인기 페이지

+ {/* 로그인 이력 */} +

최근 로그인 이력

- - - + + + - {pageStats.length === 0 ? ( + {loginHistory.length === 0 ? ( ) : ( - pageStats.map((page) => { - const maxViews = Math.max(...pageStats.map((p) => p.viewCount), 1); - const percent = Math.round((page.viewCount / maxViews) * 100); - return ( - - - - - - ); - }) + loginHistory.map((log) => ( + + + + + + )) )}
페이지조회수비율시간IP 주소User-Agent
- 아직 통계 데이터가 없습니다. 백엔드 API 연동 후 데이터가 표시됩니다. + 로그인 이력이 없습니다.
{page.pagePath}{page.viewCount} -
-
-
-
- {percent}% -
-
+ {new Date(log.loginAt).toLocaleString('ko-KR')} + + {log.ipAddress} + + {log.userAgent} +
diff --git a/src/pages/admin/UserManagement.tsx b/src/pages/admin/UserManagement.tsx index 9f11384..9767203 100644 --- a/src/pages/admin/UserManagement.tsx +++ b/src/pages/admin/UserManagement.tsx @@ -40,6 +40,19 @@ export function UserManagement() { } }; + const handleToggleAdmin = async (userId: number, isAdmin: boolean) => { + try { + if (isAdmin) { + await api.delete(`/admin/users/${userId}/admin`); + } else { + await api.post(`/admin/users/${userId}/admin`); + } + fetchData(); + } catch { + // 에러 처리 + } + }; + const handleRoleSave = async () => { if (!roleModalUser) return; try { @@ -136,7 +149,14 @@ export function UserManagement() {
)}
-

{user.name}

+

+ {user.name} + {user.isAdmin && ( + + ADMIN + + )} +

{user.email}

@@ -185,6 +205,18 @@ export function UserManagement() { > 롤 배정 + {user.status === 'ACTIVE' && ( + + )}
diff --git a/src/types/index.ts b/src/types/index.ts index d776727..8d0c292 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -61,20 +61,15 @@ export interface StatsResponse { totalUsers: number; activeUsers: number; pendingUsers: number; - totalPages: number; -} - -export interface PageStat { - pagePath: string; - viewCount: number; - lastAccessed: string; + rejectedUsers: number; + disabledUsers: number; + todayLogins: number; + totalRoles: number; } export interface LoginHistory { id: number; - userId: number; - userName: string; - email: string; loginAt: string; ipAddress: string; + userAgent: string; } -- 2.45.2