[apps/cs] feat: 접속자 통계 페이지 레이아웃 작업
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
import { UserStatisticsPage } from "@/pages/user-statistics";
|
||||
export default UserStatisticsPage;
|
||||
@@ -0,0 +1 @@
|
||||
export { UserStatisticsApi } from "./user-statistics-service";
|
||||
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
type AccessLog,
|
||||
type AccessLogParams,
|
||||
type AccessLogService,
|
||||
type PageableList,
|
||||
} from "../model/user-statistics-interface";
|
||||
|
||||
// 목 데이터
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const UserStatisticsApi = new UserStatisticsApiImpl();
|
||||
@@ -0,0 +1 @@
|
||||
export { UserStatisticsPage } from "./ui/user-statistics-page";
|
||||
@@ -0,0 +1,148 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const chartData = {
|
||||
monthlyStatisticsData,
|
||||
monthlyStatisticsOptions,
|
||||
siteVisitorsData,
|
||||
siteVisitorsOptions,
|
||||
browserStatisticsData,
|
||||
browserStatisticsOptions,
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { type AccessLog } from "./user-statistics-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<AccessLog>();
|
||||
|
||||
export const columns = [
|
||||
columnHelper.accessor("no", {
|
||||
header: "No",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("accessTime", {
|
||||
header: "접속일시",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("accessIp", {
|
||||
header: "접속IP",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("accessRegion", {
|
||||
header: "접속지역",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("urlPath", {
|
||||
header: "URL경로",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("browser", {
|
||||
header: "브라우저",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("site", {
|
||||
header: "사이트",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("accessType", {
|
||||
header: "메뉴명",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
siteId?: string;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
export interface AccessLogService {
|
||||
getAccessLogList(params: AccessLogParams): Promise<PageableList<AccessLog>>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { SiteSelect } from "@/entities/site";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
ArcElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
type ChartData,
|
||||
} from "chart.js";
|
||||
import { Bar, Doughnut } from "react-chartjs-2";
|
||||
import { UserStatisticsApi } from "../api";
|
||||
import { chartData } from "../model/chart-data";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { type AccessLogParams } from "../model/user-statistics-interface";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
ArcElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
);
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
function UserStatisticsPage() {
|
||||
const defaultDateRange: DateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [keywordType, setKeywordType] = useState("");
|
||||
const [keyword, setKeyword] = 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 query = useQuery({
|
||||
queryKey: ["user-statistics", searchParams, pagination],
|
||||
queryFn: () =>
|
||||
UserStatisticsApi.getAccessLogList({
|
||||
...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 handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
switch (value) {
|
||||
case "1W":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "1M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(30, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "3M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(90, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
|
||||
// 검색 조건에 따라 검색 파라미터 설정
|
||||
const params: AccessLogParams = {
|
||||
sort: "accessTime,desc",
|
||||
accessDateStart: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
accessDateEnd: 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("");
|
||||
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",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">날짜</Label>
|
||||
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputDatePicker
|
||||
value={dateRange.from}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, from: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
~
|
||||
<InputDatePicker
|
||||
value={dateRange.to}
|
||||
onChange={(date) => {
|
||||
setDateRange({ ...dateRange, to: date });
|
||||
setPresetDateRange("");
|
||||
}}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">사이트</Label>
|
||||
<SiteSelect enabledAllSite className="w-[16rem]" />
|
||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={keywordType}
|
||||
onValueChange={(value) => setKeywordType(value)}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="urlPath">URL경로</SelectItem>
|
||||
<SelectItem value="ip">접속IP</SelectItem>
|
||||
<SelectItem value="region">접속지역</SelectItem>
|
||||
<SelectItem value="browser">브라우저</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* 차트 영역 */}
|
||||
<div className="grid grid-cols-2">
|
||||
{/* 일별 접속 현황 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-xl font-medium">일별 접속 현황</h3>
|
||||
</div>
|
||||
<div className="h-96 p-4">
|
||||
<Bar
|
||||
data={chartData.monthlyStatisticsData as ChartData<"bar">}
|
||||
options={chartData.monthlyStatisticsOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
{/* 사이트별 접속자 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-xl font-medium">사이트별 접속자</h3>
|
||||
</div>
|
||||
<div className="h-96 p-4">
|
||||
<Bar
|
||||
data={chartData.siteVisitorsData}
|
||||
options={chartData.siteVisitorsOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 브라우저별 접속자 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-xl font-medium">브라우저별 접속자</h3>
|
||||
</div>
|
||||
<div className="h-96 p-4">
|
||||
<Doughnut
|
||||
data={chartData.browserStatisticsData}
|
||||
options={chartData.browserStatisticsOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 접속 로그 상세 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">접속 로그 상세</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(parseInt(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
{pageSize}개씩 보기
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { UserStatisticsPage };
|
||||
Reference in New Issue
Block a user