[apps/cs] feat: 접속자통계 데이터 바인딩

This commit is contained in:
2025-07-29 13:23:48 +09:00
parent 387362d42b
commit 12f7fe53ad
6 changed files with 220 additions and 421 deletions
@@ -1 +0,0 @@
export { UserStatisticsApi } from "./user-statistics-service";
@@ -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<PageableList<AccessLog>> {
// 실제 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<ConnectionStatistics> {
const response = await client.get<ConnectionStatistics>(
`${ENDPOINT.CMS_SERVICE.CONNECTION_LOG}/stat`,
{ params },
);
return response.data;
}
}
export const UserStatisticsApi = new UserStatisticsApiImpl();
export const UserStatisticsApi = new UserStatisticsApiService();
@@ -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",
},
},
},
};
},
};
@@ -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<PageableList<AccessLog>>;
interface DayConnectionStatistics {
dayYmd: string;
accessCnt: number;
}
export interface PageableList<T> {
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<ConnectionStatistics>;
}
@@ -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<DateRange>(defaultDateRange);
const [searchParams, setSearchParams] = useState<AccessLogParams>({
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<ConnectionStatisticsParams>({
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<HTMLFormElement>) => {
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}
/>
<Label className="ml-4 mr-2 text-label">사이트</Label>
<SiteSelect enabledAllSite className="w-[16rem]" />
<SiteSelect
className="w-[16rem]"
value={siteId}
onValueChange={(value) => setSiteId(value)}
/>
<Label className="ml-4 mr-2 text-label">검색</Label>
<Select
value={keywordType}
@@ -221,9 +199,9 @@ function UserStatisticsPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value="urlPath">URL경로</SelectItem>
<SelectItem value="ip">접속IP</SelectItem>
<SelectItem value="region">접속지역</SelectItem>
<SelectItem value="browser">브라우저</SelectItem>
<SelectItem value="cntnIp">접속IP</SelectItem>
<SelectItem value="cntnRgn">접속지역</SelectItem>
<SelectItem value="wbsrKnd">브라우저</SelectItem>
</SelectContent>
</Select>
<Input
@@ -245,45 +223,44 @@ function UserStatisticsPage() {
</form>
</div>
{/* 차트 영역 */}
<div className="grid grid-cols-2">
{/* 일별 접속 현황 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-xl font-medium">일별 접속 현황</h3>
<h3 className="text-2xl font-medium">일별 접속 현황</h3>
</div>
<div className="h-96 p-4">
<Bar
data={chartData.monthlyStatisticsData as ChartData<"bar">}
options={chartData.monthlyStatisticsOptions}
/>
{query.data?.dayList.length ? (
<Line {...chartData.day(query.data)} />
) : (
<NoData />
)}
</div>
</div>
<div className="grid grid-cols-2">
{/* 사이트별 접속자 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-xl font-medium">사이트별 접속자</h3>
<h3 className="text-2xl font-medium">사이트별 접속자</h3>
</div>
<div className="h-96 p-4">
<Bar
data={chartData.siteVisitorsData}
options={chartData.siteVisitorsOptions}
/>
{query.data?.siteList.length ? (
<Bar {...chartData.site(query.data)} />
) : (
<NoData />
)}
</div>
</div>
{/* 브라우저별 접속자 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-xl font-medium">브라우저별 접속자</h3>
<h3 className="text-2xl font-medium">브라우저별 접속자</h3>
</div>
<div className="h-96 p-4">
<Doughnut
data={chartData.browserStatisticsData}
options={chartData.browserStatisticsOptions}
/>
{query.data?.browserList.length ? (
<Doughnut {...chartData.browser(query.data)} />
) : (
<NoData />
)}
</div>
</div>
</div>
@@ -295,7 +272,7 @@ function UserStatisticsPage() {
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">접속 로그 상세</h3>
<div className="flex gap-2">
<Select
{/* <Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(parseInt(value));
@@ -311,11 +288,11 @@ function UserStatisticsPage() {
</SelectItem>
))}
</SelectContent>
</Select>
</Select> */}
</div>
</div>
</div>
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
{/* <div className="card !m-[1.2rem] !p-0">{tableComponent}</div> */}
</div>
</div>
);
+1
View File
@@ -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",