From 12f7fe53ad759a518b44bfaaa479732ff8340c51 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 29 Jul 2025 13:23:48 +0900 Subject: [PATCH] =?UTF-8?q?[apps/cs]=20feat:=20=EC=A0=91=EC=86=8D=EC=9E=90?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B0=94?= =?UTF-8?q?=EC=9D=B8=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs/src/pages/user-statistics/api/index.ts | 1 - .../api/user-statistics-service.ts | 147 ++--------- .../pages/user-statistics/model/chart-data.ts | 245 +++++++----------- .../model/user-statistics-interface.ts | 84 +++--- .../ui/user-statistics-page.tsx | 163 +++++------- apps/cs/src/shared/config/api.ts | 1 + 6 files changed, 220 insertions(+), 421 deletions(-) delete mode 100644 apps/cs/src/pages/user-statistics/api/index.ts diff --git a/apps/cs/src/pages/user-statistics/api/index.ts b/apps/cs/src/pages/user-statistics/api/index.ts deleted file mode 100644 index 802b91e30..000000000 --- a/apps/cs/src/pages/user-statistics/api/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UserStatisticsApi } from "./user-statistics-service"; diff --git a/apps/cs/src/pages/user-statistics/api/user-statistics-service.ts b/apps/cs/src/pages/user-statistics/api/user-statistics-service.ts index 10c90c024..d0522267b 100644 --- a/apps/cs/src/pages/user-statistics/api/user-statistics-service.ts +++ b/apps/cs/src/pages/user-statistics/api/user-statistics-service.ts @@ -1,138 +1,21 @@ +import { client } from "@/shared/api"; import { - type AccessLog, - type AccessLogParams, - type AccessLogService, - type PageableList, + type UserStatisticsService, + type ConnectionStatisticsParams, + type ConnectionStatistics, } from "../model/user-statistics-interface"; +import { ENDPOINT } from "@/shared/config"; -// 목 데이터 -const mockAccessLogs: AccessLog[] = [ - { - no: 1, - accessTime: "2024-01-15 14:30:25", - accessIp: "192.168.1.100", - accessRegion: "서울특별시", - urlPath: "/index", - browser: "Chrome", - site: "SITE1", - accessType: "메인", - }, - { - no: 2, - accessTime: "2024-01-15 14:28:15", - accessIp: "203.241.50.123", - accessRegion: "부산광역시", - urlPath: "/board/list", - browser: "Edge", - site: "SITE2", - accessType: "게시판", - }, - { - no: 3, - accessTime: "2024-01-15 14:25:42", - accessIp: "175.223.45.67", - accessRegion: "대구광역시", - urlPath: "/statistics", - browser: "Safari", - site: "SITE1", - accessType: "통계", - }, - { - no: 4, - accessTime: "2024-01-15 14:22:18", - accessIp: "211.252.87.45", - accessRegion: "인천광역시", - urlPath: "/contents/view", - browser: "Firefox", - site: "SITE3", - accessType: "컨텐츠", - }, - { - no: 5, - accessTime: "2024-01-15 14:20:33", - accessIp: "121.254.12.89", - accessRegion: "광주광역시", - urlPath: "/search", - browser: "Chrome", - site: "SITE1", - accessType: "검색", - }, -]; - -// 목 API 서비스 -class UserStatisticsApiImpl implements AccessLogService { - async getAccessLogList( - params: AccessLogParams, - ): Promise> { - // 실제 API 호출 대신 목 데이터 반환 - const { page = 0, size = 10 } = params; - - // 필터링 로직 (실제로는 서버에서 처리) - let filteredLogs = [...mockAccessLogs]; - - if (params.urlPath) { - filteredLogs = filteredLogs.filter((log) => - log.urlPath.toLowerCase().includes(params.urlPath!.toLowerCase()), - ); - } - - if (params.accessIp) { - filteredLogs = filteredLogs.filter((log) => - log.accessIp.includes(params.accessIp!), - ); - } - - if (params.accessRegion) { - filteredLogs = filteredLogs.filter((log) => - log.accessRegion.includes(params.accessRegion!), - ); - } - - if (params.browser) { - filteredLogs = filteredLogs.filter( - (log) => log.browser.toLowerCase() === params.browser!.toLowerCase(), - ); - } - - if (params.siteId) { - filteredLogs = filteredLogs.filter((log) => log.site === params.siteId); - } - - // 페이지네이션 - const start = page * size; - const end = start + size; - const paginatedLogs = filteredLogs.slice(start, end); - - // 목 응답 생성 - return { - content: paginatedLogs, - pageable: { - pageNumber: page, - pageSize: size, - sort: { - empty: true, - sorted: false, - unsorted: true, - }, - offset: page * size, - paged: true, - unpaged: false, - }, - last: (page + 1) * size >= filteredLogs.length, - totalPages: Math.ceil(filteredLogs.length / size), - totalElements: filteredLogs.length, - size, - number: page, - sort: { - empty: true, - sorted: false, - unsorted: true, - }, - first: page === 0, - numberOfElements: paginatedLogs.length, - empty: paginatedLogs.length === 0, - }; +class UserStatisticsApiService implements UserStatisticsService { + async getConnectionStatistics( + params: ConnectionStatisticsParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONNECTION_LOG}/stat`, + { params }, + ); + return response.data; } } -export const UserStatisticsApi = new UserStatisticsApiImpl(); +export const UserStatisticsApi = new UserStatisticsApiService(); diff --git a/apps/cs/src/pages/user-statistics/model/chart-data.ts b/apps/cs/src/pages/user-statistics/model/chart-data.ts index c836a4e5d..8c4241cfd 100644 --- a/apps/cs/src/pages/user-statistics/model/chart-data.ts +++ b/apps/cs/src/pages/user-statistics/model/chart-data.ts @@ -1,148 +1,103 @@ -import { type ChartData, type ChartOptions } from "chart.js"; - -// 일별 접속 현황 차트 데이터 -export const monthlyStatisticsData = { - labels: [ - "1일", - "2일", - "3일", - "4일", - "5일", - "6일", - "7일", - "8일", - "9일", - "10일", - "11일", - "12일", - "13일", - "14일", - "15일", - ], - datasets: [ - { - type: "bar" as const, - label: "페이지뷰", - data: [ - 850, 1000, 750, 750, 700, 750, 780, 800, 850, 900, 1200, 1700, 2300, - 2100, 2600, - ], - backgroundColor: "rgba(54, 162, 235, 0.8)", - order: 2, - }, - { - type: "line" as const, - label: "신규 접속자", - data: [ - 700, 600, 500, 550, 500, 550, 500, 550, 600, 850, 1200, 1500, 1700, - 2300, 2200, - ], - borderColor: "rgba(255, 99, 132, 0.8)", - borderWidth: 2, - tension: 0.4, - pointRadius: 3, - order: 0, - }, - { - type: "line" as const, - label: "총 접속자", - data: [ - 200, 300, 400, 500, 600, 500, 550, 500, 400, 500, 350, 800, 900, 1200, - 1300, - ], - borderColor: "rgba(75, 192, 192, 0.8)", - borderWidth: 2, - tension: 0.4, - pointRadius: 3, - order: 1, - }, - ], -}; - -export const monthlyStatisticsOptions: ChartOptions<"bar"> = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: "top" as const, - }, - tooltip: { - mode: "index" as const, - intersect: false, - }, - }, -}; - -// 사이트별 접속자 차트 데이터 -export const siteVisitorsData: ChartData<"bar"> = { - labels: ["SITE1", "SITE2", "SITE3", "SITE4", "SITE5"], - datasets: [ - { - label: "사이트별 접속자", - data: [2500, 2000, 1500, 1200, 800], - backgroundColor: [ - "rgba(54, 162, 235, 0.8)", - "rgba(153, 102, 255, 0.8)", - "rgba(255, 99, 132, 0.8)", - "rgba(255, 159, 64, 0.8)", - "rgba(255, 205, 86, 0.8)", - ], - borderWidth: 1, - }, - ], -}; - -export const siteVisitorsOptions: ChartOptions<"bar"> = { - responsive: true, - maintainAspectRatio: false, - indexAxis: "y" as const, - plugins: { - legend: { - display: false, - }, - }, -}; - -// 브라우저별 접속자 차트 데이터 -export const browserStatisticsData: ChartData<"doughnut"> = { - labels: ["Chrome", "Edge", "Safari", "Firefox", "Internet Explorer"], - datasets: [ - { - data: [45, 25, 15, 10, 5], - backgroundColor: [ - "rgba(54, 162, 235, 0.8)", - "rgba(153, 102, 255, 0.8)", - "rgba(255, 99, 132, 0.8)", - "rgba(255, 159, 64, 0.8)", - "rgba(255, 205, 86, 0.8)", - ], - borderColor: [ - "rgba(54, 162, 235, 1)", - "rgba(153, 102, 255, 1)", - "rgba(255, 99, 132, 1)", - "rgba(255, 159, 64, 1)", - "rgba(255, 205, 86, 1)", - ], - borderWidth: 1, - }, - ], -}; - -export const browserStatisticsOptions: ChartOptions<"doughnut"> = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: "bottom" as const, - }, - }, -}; +import type { ChartData, ChartOptions } from "chart.js"; +import { getCssVariableValue } from "@common/components"; +import type { ConnectionStatistics } from "./user-statistics-interface"; export const chartData = { - monthlyStatisticsData, - monthlyStatisticsOptions, - siteVisitorsData, - siteVisitorsOptions, - browserStatisticsData, - browserStatisticsOptions, + day: ( + data: ConnectionStatistics, + ): { + data: ChartData<"line">; + options: ChartOptions<"line">; + } => { + return { + data: { + labels: data.dayList.map((item) => item.dayYmd), + datasets: [ + { + type: "line", + label: "접속자 수", + data: data.dayList.map((item) => item.accessCnt), + borderColor: getCssVariableValue("--chart-1"), + }, + ], + }, + options: { + maintainAspectRatio: false, + scales: { + y: { + ticks: { + precision: 0, + }, + }, + }, + }, + }; + }, + site: ( + data: ConnectionStatistics, + ): { + data: ChartData<"bar">; + options: ChartOptions<"bar">; + } => { + return { + data: { + labels: data.siteList.map((item) => item.siteNm), + datasets: [ + { + type: "bar", + label: "사이트별 접속자", + data: data.siteList.map((item) => item.accessCnt), + backgroundColor: data.siteList.map((_, index) => + getCssVariableValue(`--chart-${(index % 5) + 1}`), + ), + }, + ], + }, + options: { + maintainAspectRatio: false, + indexAxis: "y", + scales: { + x: { + ticks: { + precision: 0, + }, + }, + }, + plugins: { + legend: { + display: false, + }, + }, + }, + }; + }, + browser: ( + data: ConnectionStatistics, + ): { + data: ChartData<"doughnut">; + options: ChartOptions<"doughnut">; + } => { + return { + data: { + labels: data.browserList.map((item) => item.wbsrKndNm), + datasets: [ + { + type: "doughnut", + data: data.browserList.map((item) => item.accessCnt), + backgroundColor: data.browserList.map((_, index) => + getCssVariableValue(`--chart-${(index % 5) + 1}`), + ), + }, + ], + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: { + position: "bottom", + }, + }, + }, + }; + }, }; diff --git a/apps/cs/src/pages/user-statistics/model/user-statistics-interface.ts b/apps/cs/src/pages/user-statistics/model/user-statistics-interface.ts index 124d91044..42d247185 100644 --- a/apps/cs/src/pages/user-statistics/model/user-statistics-interface.ts +++ b/apps/cs/src/pages/user-statistics/model/user-statistics-interface.ts @@ -1,56 +1,40 @@ -export interface AccessLog { - no: number; - accessTime: string; - accessIp: string; - accessRegion: string; - urlPath: string; - browser: string; - site: string; - accessType: string; -} - -export interface AccessLogParams { - page?: number; - size?: number; - urlPath?: string; - accessIp?: string; - accessRegion?: string; - browser?: string; - accessDateStart?: string; - accessDateEnd?: string; +export interface ConnectionStatisticsParams { + keywordType?: string; + keyword?: string; siteId?: string; - sort?: string; + cntnIp?: string; + cntnRgn?: string; + wbsrKnd?: string; + urlPath?: string; + cntnDtStart?: string; + cntnDtEnd?: string; } -export interface AccessLogService { - getAccessLogList(params: AccessLogParams): Promise>; +interface DayConnectionStatistics { + dayYmd: string; + accessCnt: number; } -export interface PageableList { - content: T[]; - pageable: { - pageNumber: number; - pageSize: number; - sort: { - empty: boolean; - sorted: boolean; - unsorted: boolean; - }; - offset: number; - paged: boolean; - unpaged: boolean; - }; - last: boolean; - totalPages: number; - totalElements: number; - size: number; - number: number; - sort: { - empty: boolean; - sorted: boolean; - unsorted: boolean; - }; - first: boolean; - numberOfElements: number; - empty: boolean; +interface SiteConnectionStatistics { + siteId: string; + siteNm: string; + accessCnt: number; +} + +interface BrowserConnectionStatistics { + wbsrKnd: string; + wbsrKndNm: string; + accessCnt: number; +} + +export interface ConnectionStatistics { + dayList: DayConnectionStatistics[]; + siteList: SiteConnectionStatistics[]; + browserList: BrowserConnectionStatistics[]; +} + +export interface UserStatisticsService { + getConnectionStatistics( + params: ConnectionStatisticsParams, + ): Promise; } diff --git a/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx b/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx index 9fedb47f4..4fe1d3fc0 100644 --- a/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx +++ b/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx @@ -15,9 +15,8 @@ import { Filler, Legend, Tooltip, - type ChartData, } from "chart.js"; -import { Bar, Doughnut } from "react-chartjs-2"; +import { Bar, Doughnut, Line } from "react-chartjs-2"; import { Button, Input, @@ -30,11 +29,13 @@ import { } from "@common/components"; import { SiteSelect } from "@/entities/site"; import { useDataTable } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { NoData } from "@/shared/ui/no-data"; import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; -import { UserStatisticsApi } from "../api"; +import { UserStatisticsApi } from "../api/user-statistics-service"; import { chartData } from "../model/chart-data"; import { columns } from "../model/table-columns"; -import { type AccessLogParams } from "../model/user-statistics-interface"; +import { type ConnectionStatisticsParams } from "../model/user-statistics-interface"; ChartJS.register( CategoryScale, @@ -65,44 +66,41 @@ function UserStatisticsPage() { }); const [keywordType, setKeywordType] = useState(""); const [keyword, setKeyword] = useState(""); + const [siteId, setSiteId] = useState(""); const [presetDateRange, setPresetDateRange] = useState("1W"); const [dateRange, setDateRange] = useState(defaultDateRange); - const [searchParams, setSearchParams] = useState({ - urlPath: "", - accessIp: "", - accessRegion: "", - browser: "", - accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"), - accessDateEnd: dayjs().format("YYYY-MM-DD"), - sort: "accessTime,desc", + const [searchParams, setSearchParams] = useState({ + keywordType: "", + keyword: "", + siteId: "", + cntnDtStart: dayjs(defaultDateRange.from).format("YYYY-MM-DD"), + cntnDtEnd: dayjs(defaultDateRange.to).format("YYYY-MM-DD"), }); const query = useQuery({ - queryKey: ["user-statistics", searchParams, pagination], + queryKey: [ENDPOINT.CMS_SERVICE.CONNECTION_LOG, searchParams], queryFn: () => - UserStatisticsApi.getAccessLogList({ + UserStatisticsApi.getConnectionStatistics({ ...searchParams, - page: pagination.pageIndex, - size: pagination.pageSize, }), placeholderData: keepPreviousData, }); - const { tableComponent, tableInstance: table } = useDataTable({ - query, - tableOptions: { - columns, - state: { - pagination, - }, - manualPagination: true, - getCoreRowModel: getCoreRowModel(), - onPaginationChange: setPagination, - }, - dataTableOptions: { - viewPagination: true, - }, - }); + // const { tableComponent, tableInstance: table } = useDataTable({ + // query, + // tableOptions: { + // columns, + // state: { + // pagination, + // }, + // manualPagination: true, + // getCoreRowModel: getCoreRowModel(), + // onPaginationChange: setPagination, + // }, + // dataTableOptions: { + // viewPagination: true, + // }, + // }); const handlePresetDateRange = (value: string) => { let presetDateRange: DateRange; @@ -139,54 +137,30 @@ function UserStatisticsPage() { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - setPagination({ - ...pagination, - pageIndex: 0, - }); - - // 검색 조건에 따라 검색 파라미터 설정 - const params: AccessLogParams = { - sort: "accessTime,desc", - accessDateStart: dateRange.from + const params: ConnectionStatisticsParams = { + keywordType, + keyword, + siteId, + cntnDtStart: dateRange.from ? dayjs(dateRange.from).format("YYYY-MM-DD") : "", - accessDateEnd: dateRange.to - ? dayjs(dateRange.to).format("YYYY-MM-DD") - : "", + cntnDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "", }; - - // 검색어 타입에 따라 파라미터 설정 - switch (keywordType) { - case "urlPath": - params.urlPath = keyword; - break; - case "ip": - params.accessIp = keyword; - break; - case "region": - params.accessRegion = keyword; - break; - case "browser": - params.browser = keyword; - break; - } - setSearchParams(params); }; const handleReset = () => { setKeywordType(""); setKeyword(""); + setSiteId(""); setDateRange(defaultDateRange); setPresetDateRange("1W"); setSearchParams({ - urlPath: "", - accessIp: "", - accessRegion: "", - browser: "", - accessDateStart: dayjs().subtract(7, "day").format("YYYY-MM-DD"), - accessDateEnd: dayjs().format("YYYY-MM-DD"), - sort: "accessTime,desc", + keywordType: "", + keyword: "", + siteId: "", + cntnDtStart: dayjs(defaultDateRange.from).format("YYYY-MM-DD"), + cntnDtEnd: dayjs(defaultDateRange.to).format("YYYY-MM-DD"), }); }; @@ -210,7 +184,11 @@ function UserStatisticsPage() { setDateRange={handleDateRangeChange} /> - + setSiteId(value)} + /> - {/* 차트 영역 */}
- {/* 일별 접속 현황 */}
-

일별 접속 현황

+

일별 접속 현황

- } - options={chartData.monthlyStatisticsOptions} - /> + {query.data?.dayList.length ? ( + + ) : ( + + )}
- {/* 사이트별 접속자 */}
-

사이트별 접속자

+

사이트별 접속자

- + {query.data?.siteList.length ? ( + + ) : ( + + )}
- {/* 브라우저별 접속자 */}
-

브라우저별 접속자

+

브라우저별 접속자

- + {query.data?.browserList.length ? ( + + ) : ( + + )}
@@ -295,7 +272,7 @@ function UserStatisticsPage() {

접속 로그 상세

- + */}
-
{tableComponent}
+ {/*
{tableComponent}
*/} ); diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 6db62f1a0..ce6f61381 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -55,6 +55,7 @@ export const ENDPOINT = { MANAGER_IP: "/cms-service/api/v1/mgrip", APILOG: "/cms-service/api/v1/api-logs", SITE_AUTHORITY: "/cms-service/api/v1/site-authrt", + CONNECTION_LOG: "/cms-service/api/v1/cntnlog", }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs",