From d7233992fed24ad3f5cf38c1191028f8e0cfafe7 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 25 Jul 2025 14:30:33 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[apps/cs]=20feat:=20=EC=A0=91=EC=86=8D?= =?UTF-8?q?=EC=9E=90=20=ED=86=B5=EA=B3=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user-statistics/page.tsx | 2 + .../cs/src/pages/user-statistics/api/index.ts | 1 + .../api/user-statistics-service.ts | 138 ++++++++ apps/cs/src/pages/user-statistics/index.ts | 1 + .../pages/user-statistics/model/chart-data.ts | 148 ++++++++ .../user-statistics/model/table-columns.ts | 39 +++ .../model/user-statistics-interface.ts | 56 +++ .../ui/user-statistics-page.tsx | 330 ++++++++++++++++++ 8 files changed, 715 insertions(+) create mode 100644 apps/cs/app/(with-base-layout)/user-statistics/page.tsx create mode 100644 apps/cs/src/pages/user-statistics/api/index.ts create mode 100644 apps/cs/src/pages/user-statistics/api/user-statistics-service.ts create mode 100644 apps/cs/src/pages/user-statistics/index.ts create mode 100644 apps/cs/src/pages/user-statistics/model/chart-data.ts create mode 100644 apps/cs/src/pages/user-statistics/model/table-columns.ts create mode 100644 apps/cs/src/pages/user-statistics/model/user-statistics-interface.ts create mode 100644 apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx diff --git a/apps/cs/app/(with-base-layout)/user-statistics/page.tsx b/apps/cs/app/(with-base-layout)/user-statistics/page.tsx new file mode 100644 index 000000000..125900022 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/user-statistics/page.tsx @@ -0,0 +1,2 @@ +import { UserStatisticsPage } from "@/pages/user-statistics"; +export default UserStatisticsPage; diff --git a/apps/cs/src/pages/user-statistics/api/index.ts b/apps/cs/src/pages/user-statistics/api/index.ts new file mode 100644 index 000000000..802b91e30 --- /dev/null +++ b/apps/cs/src/pages/user-statistics/api/index.ts @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..10c90c024 --- /dev/null +++ b/apps/cs/src/pages/user-statistics/api/user-statistics-service.ts @@ -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> { + // 실제 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(); diff --git a/apps/cs/src/pages/user-statistics/index.ts b/apps/cs/src/pages/user-statistics/index.ts new file mode 100644 index 000000000..6d4fc3a25 --- /dev/null +++ b/apps/cs/src/pages/user-statistics/index.ts @@ -0,0 +1 @@ +export { UserStatisticsPage } from "./ui/user-statistics-page"; diff --git a/apps/cs/src/pages/user-statistics/model/chart-data.ts b/apps/cs/src/pages/user-statistics/model/chart-data.ts new file mode 100644 index 000000000..c836a4e5d --- /dev/null +++ b/apps/cs/src/pages/user-statistics/model/chart-data.ts @@ -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, +}; diff --git a/apps/cs/src/pages/user-statistics/model/table-columns.ts b/apps/cs/src/pages/user-statistics/model/table-columns.ts new file mode 100644 index 000000000..c0d53380e --- /dev/null +++ b/apps/cs/src/pages/user-statistics/model/table-columns.ts @@ -0,0 +1,39 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { type AccessLog } from "./user-statistics-interface"; + +const columnHelper = createColumnHelper(); + +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(), + }), +]; 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 new file mode 100644 index 000000000..124d91044 --- /dev/null +++ b/apps/cs/src/pages/user-statistics/model/user-statistics-interface.ts @@ -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>; +} + +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; +} 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 new file mode 100644 index 000000000..4c45e04e2 --- /dev/null +++ b/apps/cs/src/pages/user-statistics/ui/user-statistics-page.tsx @@ -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({ + pageIndex: 0, + pageSize: 10, + }); + const [keywordType, setKeywordType] = useState(""); + const [keyword, setKeyword] = 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 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) => { + 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 ( +
+
+
+ + + { + setDateRange({ ...dateRange, from: date }); + setPresetDateRange(""); + }} + /> + ~ + { + setDateRange({ ...dateRange, to: date }); + setPresetDateRange(""); + }} + /> + + + + + setKeyword(e.target.value)} + className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" + /> + + + +
+ + {/* 차트 영역 */} +
+ {/* 일별 접속 현황 */} +
+
+

일별 접속 현황

+
+
+ } + options={chartData.monthlyStatisticsOptions} + /> +
+
+ +
+ {/* 사이트별 접속자 */} +
+
+

사이트별 접속자

+
+
+ +
+
+ + {/* 브라우저별 접속자 */} +
+
+

브라우저별 접속자

+
+
+ +
+
+
+
+ + {/* 접속 로그 상세 */} +
+
+
+

접속 로그 상세

+
+ +
+
+
+
{tableComponent}
+
+
+ ); +} + +export { UserStatisticsPage }; From 78eb2d8fcda3b36e0f7e28f692720133cd56b53d Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 25 Jul 2025 15:11:41 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[apps/cs]=20chore:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=93=B1=EB=A1=9D=20=ED=8F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/src/pages/image/ui/create-page.tsx | 1 + apps/cs/src/pages/image/ui/image-form.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/cs/src/pages/image/ui/create-page.tsx b/apps/cs/src/pages/image/ui/create-page.tsx index e7dda81d5..3bb249d25 100644 --- a/apps/cs/src/pages/image/ui/create-page.tsx +++ b/apps/cs/src/pages/image/ui/create-page.tsx @@ -23,6 +23,7 @@ function CreatePage() { resolver: zodResolver(imageFormSchema), defaultValues: { atflYn: "Y", + useYn: "Y", }, }); diff --git a/apps/cs/src/pages/image/ui/image-form.tsx b/apps/cs/src/pages/image/ui/image-form.tsx index 1de7820c5..7430a21e1 100644 --- a/apps/cs/src/pages/image/ui/image-form.tsx +++ b/apps/cs/src/pages/image/ui/image-form.tsx @@ -124,6 +124,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) { {...field} type="number" min={0} + max={9999} value={field.value ?? ""} onChange={(e) => field.onChange(parseInt(e.target.value)) From bd999eb0f38c69865fa9c8ca89639d3e4f0f0126 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 25 Jul 2025 18:38:56 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[apps/cs]=20chore:=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/admin/ui/add-admin-dialog.tsx | 5 +- apps/cs/src/pages/apilog/ui/apilog-page.tsx | 26 ++++----- apps/cs/src/pages/groups/ui/group-form.tsx | 4 +- apps/cs/src/pages/groups/ui/groups-dialog.tsx | 10 ++-- .../src/pages/loginlog/ui/loginlog-page.tsx | 26 ++++----- .../pages/privacylog/ui/privacylog-page.tsx | 26 ++++----- apps/cs/src/pages/recent/ui/recent-page.tsx | 35 ++++++------ .../ui/user-statistics-page.tsx | 54 +++++++++---------- apps/cs/src/pages/users/ui/users-page.tsx | 42 +++++++-------- .../src/shared/ui/input-date-range-picker.tsx | 47 ++++++++++++++++ 10 files changed, 149 insertions(+), 126 deletions(-) create mode 100644 apps/cs/src/shared/ui/input-date-range-picker.tsx diff --git a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx index 327473932..859af4058 100644 --- a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx +++ b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx @@ -47,7 +47,7 @@ function AddAdminDialog() { const [open, setOpen] = useState(false); const [userSearchQuery, setUserSearchQuery] = useState({ - keywordType: "", + keywordType: "userName", keyword: "", }); const [selectedGroupId, setSelectedGroupId] = useState(""); @@ -193,6 +193,7 @@ function AddAdminDialog() {
{ + setDateRange(dateRange); + setPresetDateRange(""); + }; + return (
@@ -148,20 +153,9 @@ function ApiLogPage() { 3개월 - { - setDateRange({ ...dateRange, from: date }); - setPresetDateRange(""); - }} - /> - ~ - { - setDateRange({ ...dateRange, to: date }); - setPresetDateRange(""); - }} +
- +
- +
diff --git a/apps/cs/src/pages/groups/ui/groups-dialog.tsx b/apps/cs/src/pages/groups/ui/groups-dialog.tsx index 427ba4b1b..9fca4ebdb 100644 --- a/apps/cs/src/pages/groups/ui/groups-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/groups-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { memo, useState } from "react"; +import { memo, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; @@ -43,7 +43,7 @@ const GroupsDialog = memo( const { data: group } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId], queryFn: () => GroupApi.findGroupById(groupId!), - enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출 + enabled: !!groupId && open, // open 시점 groupId가 있을 때만 상세 조회 API 호출 }); const { mutateAsync: createGroup } = useMutationWrapper({ @@ -77,7 +77,7 @@ const GroupsDialog = memo( delYn: group?.delYn ?? "", authrtId: group?.authrtId ?? "", siteId: group?.siteId ?? "", - mngrYn: group?.mngrYn ?? "", + mngrYn: group?.mngrYn ?? "Y", }, }); @@ -91,6 +91,10 @@ const GroupsDialog = memo( if (await confirm(confirmMessage)) callback(data); }; + useEffect(() => { + if (open) form.reset(); + }, [open]); + return ( {children} diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx index a9748b28d..7ae7990a1 100644 --- a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx +++ b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx @@ -7,7 +7,6 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { Button, Input, - InputDatePicker, Label, Select, SelectContent, @@ -16,10 +15,11 @@ import { SelectValue, LoadingWithImage, } from "@common/components"; +import { columns } from "@/pages/loginlog/model/table-columns"; import { LoginlogApi, type LoginlogParams } from "@/entities/loginlog"; import { useDataTable } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; -import { columns } from "@/pages/loginlog/model/table-columns"; +import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; type DateRange = { from?: Date | undefined; @@ -141,6 +141,11 @@ function LoginlogPage() { setDateRange(presetDateRange || defaultDateRange); }; + const handleDateRangeChange = (dateRange: DateRange) => { + setDateRange(dateRange); + setPresetDateRange(""); + }; + return (
@@ -156,20 +161,9 @@ function LoginlogPage() { 3개월 - { - setDateRange({ ...dateRange, from: date }); - setPresetDateRange(""); - }} - /> - ~ - { - setDateRange({ ...dateRange, to: date }); - setPresetDateRange(""); - }} + { + setDateRange(dateRange); + setPresetDateRange(""); + }; + return (
@@ -147,20 +152,9 @@ function PrivacyLogPage() { 3개월 - { - setDateRange({ ...dateRange, from: date }); - setPresetDateRange(""); - }} - /> - ~ - { - setDateRange({ ...dateRange, to: date }); - setPresetDateRange(""); - }} + ({}); const [presetDateRange, setPresetDateRange] = useState(""); - const [dateRange, setDateRange] = useState({ + const [dateRange, setDateRange] = useState({ from: undefined, to: undefined, }); @@ -169,7 +169,7 @@ function RecentPage() { }; const handlePresetDateRange = (value: string) => { - let presetDateRange: DateRange | undefined; + let presetDateRange: DateRange; switch (value) { case "1W": presetDateRange = { @@ -190,12 +190,20 @@ function RecentPage() { }; break; default: - presetDateRange = undefined; + presetDateRange = { + from: undefined, + to: undefined, + }; } setPresetDateRange(value); setDateRange(presetDateRange); }; + const handleDateRangeChange = (dateRange: DateRange) => { + setDateRange(dateRange); + setPresetDateRange(""); + }; + const onSoftDeletePost = async () => { const { resolved, value: delRsn } = await prompt("삭제 사유를 입력해주세요."); @@ -260,20 +268,9 @@ function RecentPage() { 3개월 - { - setDateRange({ ...dateRange, from: date }); - setPresetDateRange(""); - }} - /> - ~ - { - setDateRange({ ...dateRange, to: date }); - setPresetDateRange(""); - }} + - { - setDateRange({ ...dateRange, from: date }); - setPresetDateRange(""); - }} - /> - ~ - { - setDateRange({ ...dateRange, to: date }); - setPresetDateRange(""); - }} + diff --git a/apps/cs/src/pages/users/ui/users-page.tsx b/apps/cs/src/pages/users/ui/users-page.tsx index e119738fa..5362b8b42 100644 --- a/apps/cs/src/pages/users/ui/users-page.tsx +++ b/apps/cs/src/pages/users/ui/users-page.tsx @@ -9,7 +9,6 @@ import { Button, Checkbox, Input, - InputDatePicker, Label, Select, SelectContent, @@ -25,6 +24,7 @@ import { } from "@/entities/user"; import { ENDPOINT } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; +import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker"; import { columns } from "../model/table-columns"; type DateRange = { @@ -40,7 +40,7 @@ function UsersPage() { pageSize: 10, }); const [presetDateRange, setPresetDateRange] = useState(""); - const [dateRange, setDateRange] = useState({ + const [dateRange, setDateRange] = useState({ from: undefined, to: undefined, }); @@ -62,7 +62,7 @@ function UsersPage() { }); const { data: userSttsStats } = useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.USERS], + queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"], queryFn: () => UserApi.selectUserSttsStats(), }); @@ -145,7 +145,7 @@ function UsersPage() { }; const handlePresetDateRange = (value: string) => { - let presetDateRange: DateRange | undefined; + let presetDateRange: DateRange; switch (value) { case "1M": presetDateRange = { @@ -166,15 +166,24 @@ function UsersPage() { }; break; default: - presetDateRange = undefined; + presetDateRange = { + from: undefined, + to: undefined, + }; } setPresetDateRange(value); setDateRange(presetDateRange); }; - const getUserSttsStatsCnt = (code: string): number => { - if (!Array.isArray(userSttsStats)) return 0; - return userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0; + const handleDateRangeChange = (dateRange: DateRange) => { + setDateRange(dateRange); + setPresetDateRange(""); + }; + + const getUserSttsStatsCnt = (code: string): string => { + if (!Array.isArray(userSttsStats)) return "0"; + const count = userSttsStats.find((item) => item.sttsCd === code)?.cnt ?? 0; + return count.toLocaleString(); }; const handleUserStateCodeChange = (code: string, checked: boolean) => { @@ -292,20 +301,9 @@ function UsersPage() { 6개월 - { - setDateRange({ ...dateRange, from: date }); - setPresetDateRange(""); - }} - /> - ~ - { - setDateRange({ ...dateRange, to: date }); - setPresetDateRange(""); - }} +
diff --git a/apps/cs/src/shared/ui/input-date-range-picker.tsx b/apps/cs/src/shared/ui/input-date-range-picker.tsx new file mode 100644 index 000000000..9f574661e --- /dev/null +++ b/apps/cs/src/shared/ui/input-date-range-picker.tsx @@ -0,0 +1,47 @@ +import { InputDatePicker } from "@common/components"; +import { useEffect } from "react"; +import { useAlertStore } from "../lib"; + +type DateRange = { + from?: Date; + to?: Date; +}; + +type InputDateRangePickerProps = { + dateRange: DateRange; + setDateRange: (dateRange: DateRange) => void; +}; + +function InputDateRangePicker({ + dateRange, + setDateRange, +}: InputDateRangePickerProps) { + const alert = useAlertStore((state) => state.setMessage); + + useEffect(() => { + if (dateRange.from && dateRange.to && dateRange.from > dateRange.to) { + alert("시작일보다 종료일이 커야 합니다."); + setDateRange({ ...dateRange, to: dateRange.from }); + } + }, [dateRange]); + + return ( + <> + { + setDateRange({ ...dateRange, from: date }); + }} + /> + ~ + { + setDateRange({ ...dateRange, to: date }); + }} + /> + + ); +} + +export { InputDateRangePicker }; From 9a62ef6276a5496a720c4a41a1cadd65ed69609c Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 25 Jul 2025 18:44:30 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[apps/cs]=20chore:=20=EC=88=AB=EC=9E=90?= =?UTF-8?q?=EC=97=90=20toLocaleString()=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/dashboard/ui/dashboard-page.tsx | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx index 4d657a5fc..4bae35f7c 100644 --- a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx @@ -110,10 +110,16 @@ function DashboardPage() {

총 회원수

- {data?.memberTotCnt ?? "-"} + {data?.memberTotCnt + ? data.memberTotCnt.toLocaleString() + : "-"}

- 금일 신규 가입자 {data?.memberTodayCnt ?? "-"}명 + 금일 신규 가입자 + {data?.memberTodayCnt + ? data.memberTodayCnt.toLocaleString() + : "-"} + 명

{/* 차트 영역 */} @@ -131,10 +137,12 @@ function DashboardPage() {

전체 게시물

- {data?.pstTotCnt ?? "-"} + {data?.pstTotCnt ? data.pstTotCnt.toLocaleString() : "-"}

- 금일 +{data?.pstTodayCnt ?? "-"} 등록 + 금일 + + {data?.pstTodayCnt ? data.pstTodayCnt.toLocaleString() : "-"}{" "} + 등록

{/* 차트 영역 */} @@ -152,10 +160,16 @@ function DashboardPage() {

오늘 접속자 수

- {data?.accessTodayCnt ?? "-"} + {data?.accessTodayCnt + ? data.accessTodayCnt.toLocaleString() + : "-"}

- 전일 대비 +{data?.accessBefDayPer ?? "-"}% + 전일 대비 + + {data?.accessBefDayPer + ? data.accessBefDayPer.toLocaleString() + : "-"} + %

{/* 차트 영역 */} @@ -173,7 +187,7 @@ function DashboardPage() {

API 호출 건수

- {data?.apiTodayCnt ?? "-"} + {data?.apiTodayCnt ? data.apiTodayCnt.toLocaleString() : "-"}

금일 누적

@@ -221,7 +235,10 @@ function DashboardPage() {
- {data?.waitMemberCnt}건 + {data?.waitMemberCnt + ? data.waitMemberCnt.toLocaleString() + : "0"} + 건
신규 가입 승인 대기 @@ -253,7 +270,9 @@ function DashboardPage() { 메뉴 승인
-
3건
+
+ {(3).toLocaleString()}건 +
매뉴 수정 승인 대기
@@ -283,7 +302,10 @@ function DashboardPage() {
- {data?.waitCntnCnt}건 + {data?.waitCntnCnt + ? data.waitCntnCnt.toLocaleString() + : "0"} + 건
컨텐츠 수정 승인 대기 @@ -303,19 +325,19 @@ function DashboardPage() {
신규 사용자 가입 - 1분 전 + {(1).toLocaleString()}분 전
메뉴 수정 요청 - 5분 전 + {(5).toLocaleString()}분 전
게시물 신고 - 10분 전 + {(10).toLocaleString()}분 전
컨텐츠 승인 요청 - 15분 전 + {(15).toLocaleString()}분 전
@@ -328,20 +350,22 @@ function DashboardPage() {
- 3월 정기 시스템 점검 + + {(3).toLocaleString()}월 정기 시스템 점검 + 내일
서버 업데이트 예정 - 3일 후 + {(3).toLocaleString()}일 후
백업 스케줄 - 5일 후 + {(5).toLocaleString()}일 후
정기 보안 점검 - 7일 후 + {(7).toLocaleString()}일 후