Merge branch 'iipbs/sn/dev' into iipbs/sn/ii
This commit is contained in:
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -21,7 +21,7 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/apilog/model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ function ApiLogPage() {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [apiHttpPmhd, setApiHttpPmhd] = useState("");
|
||||
@@ -84,8 +85,10 @@ function ApiLogPage() {
|
||||
...searchParams,
|
||||
userId,
|
||||
apiHttpPmhd,
|
||||
dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||
dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||
dmndDtStart: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
dmndDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -126,6 +129,7 @@ function ApiLogPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
@@ -134,23 +138,31 @@ function ApiLogPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<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">사용자ID</Label>
|
||||
<Input
|
||||
value={userId}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
|
||||
|
||||
export class DashboardApiService implements DashboardService {
|
||||
public async getDashboard(): Promise<Dashboard> {
|
||||
const response = await client.get<Dashboard>(
|
||||
ENDPOINT.CMS_SERVICE.DASHBOARD,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardApi = new DashboardApiService();
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ChartData, ChartDataset } from "chart.js";
|
||||
import { getCssVariableValue } from "@common/components";
|
||||
import type {
|
||||
MenuStatisticsContent,
|
||||
SiteStatisticsContent,
|
||||
} from "../model/dashboard-interface";
|
||||
|
||||
export function processSiteAccessData(
|
||||
siteAccessList: SiteStatisticsContent[],
|
||||
): ChartData<"line">["datasets"] {
|
||||
const siteGroups: Record<string, ChartDataset> = siteAccessList.reduce(
|
||||
(acc, item) => {
|
||||
if (!acc[item.siteNm]) {
|
||||
acc[item.siteNm] = {
|
||||
label: item.siteNm,
|
||||
data: Array(24).fill(0), // 24시간 데이터 초기화
|
||||
borderColor: getCssVariableValue(
|
||||
`--chart-${Math.floor(Math.random() * 5) + 1}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 시간대별 접속 카운트 누적
|
||||
const hourIndex = parseInt(item.accessHour, 10);
|
||||
if (!isNaN(hourIndex) && hourIndex >= 0 && hourIndex < 24) {
|
||||
acc[item.siteNm]!.data[hourIndex] = item.accessCnt;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ChartDataset>,
|
||||
);
|
||||
|
||||
return Object.values(siteGroups) as ChartData<"line">["datasets"];
|
||||
}
|
||||
|
||||
export function processMenuAccessData(
|
||||
menuAccessList: MenuStatisticsContent[],
|
||||
): ChartData<"bar">["datasets"] {
|
||||
const data = menuAccessList.map((item) => item.accessCnt);
|
||||
|
||||
return [
|
||||
{
|
||||
label: "메뉴별 접속 횟수",
|
||||
data,
|
||||
backgroundColor: "rgb(32, 148, 250)",
|
||||
},
|
||||
] as ChartData<"bar">["datasets"];
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { type ChartData, type ChartOptions } from "chart.js";
|
||||
|
||||
const commonData = {
|
||||
labels: ["1", "2", "3", "4", "5", "6", "7"],
|
||||
datasets: [
|
||||
{
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
tension: 0.5,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const commonOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const chartData: Record<
|
||||
string,
|
||||
{ data: ChartData<"line" | "bar">; options: ChartOptions<"line" | "bar"> }
|
||||
> = {
|
||||
memberChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [8, 10, 7, 12, 9, 13, 12],
|
||||
borderColor: "rgba(255, 255, 255, 0.6)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
scales: {
|
||||
...commonOptions.scales,
|
||||
y: {
|
||||
...commonOptions.scales.y,
|
||||
min: 5,
|
||||
max: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
postChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [5200, 5350, 5450, 5520, 5600, 5650, 5678],
|
||||
borderColor: "rgba(86, 138, 255, 1)",
|
||||
backgroundColor: "rgba(86, 138, 255, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
},
|
||||
},
|
||||
visitorChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [1800, 2100, 1950, 2300, 2150, 2250, 2345],
|
||||
borderColor: "rgba(149, 131, 252)",
|
||||
backgroundColor: "rgba(149, 131, 252, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
},
|
||||
},
|
||||
apiChartData: {
|
||||
data: {
|
||||
...commonData,
|
||||
datasets: [
|
||||
{
|
||||
...commonData.datasets[0],
|
||||
data: [41000, 42500, 43200, 44100, 44800, 45200, 45678],
|
||||
borderColor: "rgba(220, 103, 206)",
|
||||
backgroundColor: "rgba(220, 103, 206, 0.1)",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
},
|
||||
},
|
||||
siteAccessChartData: {
|
||||
data: {
|
||||
labels: Array.from({ length: 24 }, (_, i) =>
|
||||
i.toString().padStart(2, "0"),
|
||||
),
|
||||
datasets: [],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top",
|
||||
},
|
||||
tooltip: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
text: "시간대",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
text: "접속자 수",
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
menuAccessChartData: {
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
},
|
||||
options: {
|
||||
...commonOptions,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top",
|
||||
},
|
||||
tooltip: {
|
||||
mode: "index",
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
text: "메뉴명",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
text: "접속 횟수",
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface SiteStatisticsContent {
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
accessHour: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
export interface MenuStatisticsContent {
|
||||
menuCd: number;
|
||||
menuNm: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
memberTotCnt: number;
|
||||
memberTodayCnt: number;
|
||||
pstTotCnt: number;
|
||||
pstTodayCnt: number;
|
||||
accessTodayCnt: number;
|
||||
accessBefDayPer: number;
|
||||
apiTodayCnt: number;
|
||||
waitMemberCnt: number;
|
||||
waitCntnCnt: number;
|
||||
notiList: [];
|
||||
schdlList: [];
|
||||
siteAccessList: SiteStatisticsContent[];
|
||||
menuAccessList: MenuStatisticsContent[];
|
||||
}
|
||||
|
||||
export interface DashboardService {
|
||||
getDashboard(): Promise<Dashboard>;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge } from "@common/components";
|
||||
import type { RecentPost } from "@/entities/post";
|
||||
|
||||
const noticePostTableColumns: ColumnDef<RecentPost>[] = [
|
||||
{
|
||||
header: "번호",
|
||||
cell: ({ row, table }) => {
|
||||
if (row.original.upndFixYn === "Y") {
|
||||
return (
|
||||
<Badge
|
||||
color="orange"
|
||||
className="flex size-8 items-center justify-center"
|
||||
>
|
||||
<i className="diveicon di-volume-up-fill" />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
},
|
||||
{
|
||||
accessorKey: "wrtrNm",
|
||||
header: "작성자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "작성일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
accessorKey: "inqNocs",
|
||||
header: "조회수",
|
||||
},
|
||||
];
|
||||
|
||||
const recentPostTableColumns: ColumnDef<RecentPost>[] = [
|
||||
{
|
||||
header: "번호",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
},
|
||||
{
|
||||
accessorKey: "wrtrNm",
|
||||
header: "작성자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "작성일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
{
|
||||
accessorKey: "inqNocs",
|
||||
header: "조회수",
|
||||
},
|
||||
];
|
||||
|
||||
export { noticePostTableColumns, recentPostTableColumns };
|
||||
@@ -1,5 +1,409 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
type ChartData,
|
||||
} from "chart.js";
|
||||
import { Bar, Line } from "react-chartjs-2";
|
||||
import { PostApi, type RecentPost } from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { DashboardApi } from "../api/dashboard-service";
|
||||
import { chartData } from "../model/chart-data";
|
||||
import {
|
||||
noticePostTableColumns,
|
||||
recentPostTableColumns,
|
||||
} from "../model/table-columns";
|
||||
import { processMenuAccessData, processSiteAccessData } from "../lib/utils";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
);
|
||||
|
||||
function DashboardPage() {
|
||||
return <></>;
|
||||
const router = useRouter();
|
||||
const { data } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD],
|
||||
queryFn: () => DashboardApi.getDashboard(),
|
||||
});
|
||||
|
||||
const noticePostQuery = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
page: 0,
|
||||
size: 5,
|
||||
keywordType: "bbsTypeCd",
|
||||
keyword: "BBSNOTICE",
|
||||
}),
|
||||
});
|
||||
|
||||
const recentPostQuery = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
page: 0,
|
||||
size: 5,
|
||||
}),
|
||||
});
|
||||
|
||||
const handleNoticePostRowClick = (row: RecentPost) => {
|
||||
router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`);
|
||||
};
|
||||
|
||||
const handleRecentPostRowClick = (row: RecentPost) => {
|
||||
router.push(
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const { tableComponent: noticePostTableComponent } = useDataTable({
|
||||
query: noticePostQuery,
|
||||
tableOptions: {
|
||||
columns: noticePostTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleNoticePostRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
const { tableComponent: recentPostTableComponent } = useDataTable({
|
||||
query: recentPostQuery,
|
||||
tableOptions: {
|
||||
columns: recentPostTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleRecentPostRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* 첫번째 라인 */}
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
{/* 총 회원수 카드 */}
|
||||
<div className="card card-border h-80 !bg-primary !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter text-white">
|
||||
<h3 className="text-2xl font-medium">총 회원수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.memberTotCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
금일 신규 가입자 {data?.memberTodayCnt ?? "-"}명
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.memberChartData!.data as ChartData<"line">}
|
||||
options={chartData.memberChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 전체 게시물 카드 */}
|
||||
<div className="card card-border h-80 !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||
<h3 className="text-2xl font-medium">전체 게시물</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.pstTotCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
금일 +{data?.pstTodayCnt ?? "-"} 등록
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.postChartData!.data as ChartData<"line">}
|
||||
options={chartData.postChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 오늘 접속자 수 카드 */}
|
||||
<div className="card card-border h-80 !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||
<h3 className="text-2xl font-medium">오늘 접속자 수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.accessTodayCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">
|
||||
전일 대비 +{data?.accessBefDayPer ?? "-"}%
|
||||
</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.visitorChartData!.data as ChartData<"line">}
|
||||
options={chartData.visitorChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* API 호출 건수 카드 */}
|
||||
<div className="card card-border h-80 !p-0">
|
||||
<div className="relative h-full">
|
||||
<div className="absolute left-0 top-0 p-6 tracking-tighter">
|
||||
<h3 className="text-2xl font-medium">API 호출 건수</h3>
|
||||
<div className="mt-2 text-6xl font-bold">
|
||||
{data?.apiTodayCnt ?? "-"}
|
||||
</div>
|
||||
<p className="mt-1 text-2xl opacity-80">금일 누적</p>
|
||||
</div>
|
||||
{/* 차트 영역 */}
|
||||
<div className="h-full">
|
||||
<Line
|
||||
data={chartData.apiChartData!.data as ChartData<"line">}
|
||||
options={chartData.apiChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 두번째 라인 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 승인 대기 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">승인 대기 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex gap-4">
|
||||
{/* 사용자 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-red-500"
|
||||
>
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-medium">사용자 승인</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">
|
||||
{data?.waitMemberCnt}건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
신규 가입 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 매뉴 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-blue-500"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-medium">메뉴 승인</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">3건</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
매뉴 수정 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 컨텐츠 승인 */}
|
||||
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-yellow-500"
|
||||
>
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-2xl font-medium">컨텐츠 승인</span>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="text-5xl font-bold">
|
||||
{data?.waitCntnCnt}건
|
||||
</div>
|
||||
<div className="mt-2 text-2xl text-gray-500">
|
||||
컨텐츠 수정 승인 대기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 실시간 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">실시간 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">신규 사용자 가입</span>
|
||||
<span>1분 전</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">메뉴 수정 요청</span>
|
||||
<span>5분 전</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">게시물 신고</span>
|
||||
<span>10분 전</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">컨텐츠 승인 요청</span>
|
||||
<span>15분 전</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 일정 알림 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">일정 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">3월 정기 시스템 점검</span>
|
||||
<span>내일</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">서버 업데이트 예정</span>
|
||||
<span>3일 후</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-border pb-4">
|
||||
<span className="font-medium">백업 스케줄</span>
|
||||
<span>5일 후</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">정기 보안 점검</span>
|
||||
<span>7일 후</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 세번째 라인 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 접속자 통계 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">접속자 통계</h3>
|
||||
</div>
|
||||
<div className="h-80 p-4">
|
||||
<Line
|
||||
data={{
|
||||
...chartData.siteAccessChartData!.data,
|
||||
datasets: processSiteAccessData(data?.siteAccessList ?? []),
|
||||
}}
|
||||
options={chartData.siteAccessChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 메뉴별 접속 통계 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">메뉴별 접속 통계</h3>
|
||||
</div>
|
||||
<div className="h-80 p-4">
|
||||
<Bar
|
||||
data={{
|
||||
labels:
|
||||
data?.menuAccessList?.map((item) => item.menuNm) || [],
|
||||
datasets: processMenuAccessData(data?.menuAccessList || []),
|
||||
}}
|
||||
options={chartData.menuAccessChartData!.options}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 네번째 라인 */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{/* 공지사항 */}
|
||||
<div className="card card-border !p-0">
|
||||
<div className="card-header !m-0 !border-none">
|
||||
<h3 className="text-2xl font-medium">공지사항</h3>
|
||||
</div>
|
||||
{noticePostTableComponent}
|
||||
</div>
|
||||
{/* 신규 게시글 */}
|
||||
<div className="card card-border !p-0">
|
||||
<div className="card-header !m-0 !border-none">
|
||||
<h3 className="text-2xl font-medium">신규 게시글</h3>
|
||||
</div>
|
||||
{recentPostTableComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DashboardPage };
|
||||
|
||||
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -22,7 +22,7 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/loginlog/model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -32,13 +32,12 @@ function LoginlogPage() {
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [isManualLoading, setIsManualLoading] = useState(false);
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
||||
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
||||
@@ -48,6 +47,7 @@ function LoginlogPage() {
|
||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||
sort: "lgnDt,desc",
|
||||
});
|
||||
const [isManualLoading, setIsManualLoading] = useState(false);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
|
||||
@@ -88,8 +88,12 @@ function LoginlogPage() {
|
||||
...searchParams,
|
||||
userId,
|
||||
errOcrnYn,
|
||||
searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||
searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||
searchDateFrom: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
searchDateTo: dateRange.to
|
||||
? dayjs(dateRange.to).format("YYYY-MM-DD")
|
||||
: "",
|
||||
});
|
||||
query.refetch().finally(() => {
|
||||
setIsManualLoading(false);
|
||||
@@ -133,6 +137,7 @@ function LoginlogPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
@@ -141,23 +146,31 @@ function LoginlogPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<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">사용자ID</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
|
||||
@@ -110,6 +110,7 @@ function MenuPage() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
});
|
||||
form.reset();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -24,7 +24,7 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "@/pages/privacylog/model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ function PrivacyLogPage() {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
|
||||
@@ -85,8 +86,10 @@ function PrivacyLogPage() {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
inqInfoKeyword,
|
||||
inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"),
|
||||
inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"),
|
||||
inqDtStart: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
inqDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,6 +128,7 @@ function PrivacyLogPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange || defaultDateRange);
|
||||
};
|
||||
|
||||
@@ -133,30 +137,37 @@ function PrivacyLogPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">조회기간</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(value) => setDateRange(value || defaultDateRange)}
|
||||
>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<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>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
|
||||
value={inqInfoKeyword}
|
||||
onChange={(e) => setInqInfoKeyword(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -37,7 +37,7 @@ import { columns } from "../model/table-columns";
|
||||
import { MovePostDialog } from "./move-post-dialog";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -51,22 +51,21 @@ function RecentPage() {
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
const [tableSort, setTableSort] = useState("regDt,desc");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
const [keywordType, setKeywordType] = useState("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [bbsId, setBbsId] = useState<string | null>(null);
|
||||
const [delYn, setDelYn] = useState("N");
|
||||
|
||||
const [searchParams, setSearchParams] = useState<RecentPostParams>({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
@@ -193,6 +192,7 @@ function RecentPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
@@ -250,21 +250,31 @@ function RecentPage() {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">날짜</Label>
|
||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
|
||||
<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>
|
||||
<Select
|
||||
value={bbsId?.toString() || ""}
|
||||
@@ -281,7 +291,6 @@ function RecentPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
@@ -292,7 +301,6 @@ function RecentPage() {
|
||||
<SelectItem value="Y">삭제</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={keywordType}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -28,7 +28,7 @@ import { useDataTable } from "@/shared/lib";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ function UsersPage() {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange | undefined>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
@@ -167,6 +168,7 @@ function UsersPage() {
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setPresetDateRange(value);
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
@@ -277,23 +279,34 @@ function UsersPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="mr-2 text-label">기간</Label>
|
||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder="기간 선택"
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
<SelectItem value="6M">6개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
<Select
|
||||
value={presetDateRange}
|
||||
onValueChange={handlePresetDateRange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
<SelectItem value="6M">6개월</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("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -23,6 +23,7 @@ export const ENDPOINT = {
|
||||
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
|
||||
},
|
||||
CMS_SERVICE: {
|
||||
DASHBOARD: "/cms-service/api/v1/dashboard",
|
||||
SITES: "/cms-service/api/v1/sites",
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -1,13 +1,13 @@
|
||||
NEXT_PUBLIC_SITE_ID = SN
|
||||
NEXT_PUBLIC_PORT = 11302
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302
|
||||
NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver_swl
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.rsonesoft.com #자료관 데이터
|
||||
NEXT_PUBLIC_CLAIM_NAME = Authorization
|
||||
NEXT_PUBLIC_AUTH_USER_ID = token-id
|
||||
NEXT_PUBLIC_ACCESS_TOKEN = access-token
|
||||
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = https://sn.hasuon.or.kr
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_ADDR_URL = http://con.mind-one.co.kr:11082
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.rsonesoft.com #자료관 데이터
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import { getData, getDcData } from '@/components/lib/api';
|
||||
import moment from "moment";
|
||||
import { library } from 'webpack';
|
||||
|
||||
export type TInftwtAnls = {
|
||||
inftwtAnlsSn:number;
|
||||
@@ -64,7 +65,20 @@ export type TOilqData = {
|
||||
mtrngDt: string; // 계측일시
|
||||
oilq: number; // 유량
|
||||
}
|
||||
|
||||
export type TRainData = {
|
||||
rainInfwtAnlsSn: number;
|
||||
mtrngDt: string;
|
||||
amrf: number; // 강우량
|
||||
}
|
||||
type lineBarChartDataProps = {
|
||||
label : string;
|
||||
lineVal : number;
|
||||
barVal : number;
|
||||
barVal0 : number;
|
||||
barVal1 : number;
|
||||
lineVal0 : number;
|
||||
lineVal1 : number;
|
||||
}
|
||||
const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string, hidden?: boolean }[] => {
|
||||
const yyyymms1 = yyyymms.map(dateStr => {
|
||||
const year = dateStr.slice(0, 4);
|
||||
@@ -223,10 +237,11 @@ export const selectRdiiAnls = async(aplcnYm:String) => { // 침입수분석
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => {
|
||||
export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; lineVal0: number, barVal0:number }[] => {
|
||||
const options = arr.map(anlsRslt => ({
|
||||
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"),
|
||||
val: anlsRslt.predcOilq,
|
||||
lineVal0: anlsRslt.predcOilq,
|
||||
barVal0: 0
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
@@ -237,7 +252,7 @@ export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: str
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // 침입수분석 결과
|
||||
export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // RDII분석 결과
|
||||
try {
|
||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
||||
@@ -245,10 +260,32 @@ export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string)
|
||||
}
|
||||
const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${pplMngNo}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
console.log("selectRdiiAnlsRslt", result.content);
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const selectRainData = async(rainInfwtAnlsSn:number) => { // 강우데이터
|
||||
try {
|
||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0)) {
|
||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
console.log("selectRainData", result.content);
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const getChartDataRainData = (arr: TRainData[]): { label: string; lineVal0: number, barVal0: number }[] => {
|
||||
const options = arr.map(data => ({
|
||||
label: moment(data.mtrngDt, "YYYY-MM-DD HH:mm:ss").format("MM-DD HH시"),
|
||||
lineVal0: 0,
|
||||
barVal0: data.amrf,
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
@@ -11,7 +11,13 @@ import { useRdiiChartModalStore } from '../chartModalStore'; // 모달 상태
|
||||
type ModalOpenProp = {
|
||||
pplMngNo: string, // 관로관리번호
|
||||
}
|
||||
type DataType = {label: string; val: number};
|
||||
// 차트
|
||||
type lineBarChartData = {
|
||||
label : string;
|
||||
lineVal0 : number;
|
||||
barVal0 : number;
|
||||
}
|
||||
|
||||
|
||||
const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore(); // 모달 상태 관리 스토어
|
||||
@@ -23,10 +29,9 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
|
||||
const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<any[]>([{ val: 0, text: '선택' }]); // 강우유입수분석정보 options
|
||||
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null); // 강우유입수분석정보
|
||||
const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState<number>(0); // 강우유입수분석 정보 선택시 사용
|
||||
const [chartData, setChartData] = useState<{ chartData1: DataType[]; }>({
|
||||
chartData1: [], //chartData2: [],
|
||||
});
|
||||
const [chartData, setChartData] = useState<lineBarChartData[]>([]);
|
||||
const [rdiiQty, setRdiiQty] = useState<number>(0); // 강우유입수량
|
||||
const [rainTotal, setRainTotal] = useState<number>(0); // 적용강우량
|
||||
const [totalVolume, setTotalVolume] = useState<number>(0); // 총체적 m³/mm
|
||||
const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기
|
||||
pplMngNo = "L"+pplMngNo; // "L13101003";
|
||||
@@ -42,10 +47,16 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
|
||||
setPopupOpen(true); // 팝업 열기
|
||||
};
|
||||
|
||||
const chartForm = CHART_FORM.LINE;
|
||||
const chartForm = CHART_FORM.LINEBAR;
|
||||
const chartConfig = {
|
||||
chartData1: { label: 'RDII', color: "red", },
|
||||
//chartData2: { label: '순오수량(m³/h)', color: "orange", },
|
||||
barVal0: {
|
||||
label: '강수량',
|
||||
color: "#33AAFF",
|
||||
},
|
||||
lineVal0: {
|
||||
label: 'RDII',
|
||||
color: "#FF0000",
|
||||
},
|
||||
} as ChartConfig;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -56,8 +67,9 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
|
||||
setRdiiAnlsInfos([{ val: 0, text: '선택' }]); // 강우유입수분석정보 초기화
|
||||
setRdiiAnls(null); // 강우유입수분석정보 초기화
|
||||
setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화
|
||||
setChartData({chartData1: [], }); // 차트 데이터 초기화
|
||||
setChartData([]); // 차트 데이터 초기화
|
||||
setRdiiQty(0); // 강우유입수량 초기화
|
||||
setRainTotal(0); // 적용강우량 초기화
|
||||
setTotalVolume(0); // 총체적 m³/mm 초기화
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
@@ -99,23 +111,34 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
|
||||
setPopupOpen(false);
|
||||
}
|
||||
const rdiiRsltChart = () => { // 그래프 & 팝업 닫기
|
||||
fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수분석 결과 조회
|
||||
const rsltData1 = fn.getChartDataRdiiAnlsRslt(data ?? []);
|
||||
//const rsltData2 = fn.getChartDataRdiiAnlsRsltMinq(data ?? []);
|
||||
setChartData(prev => ({
|
||||
chartData1: rsltData1, // 순오수량(m³/h)
|
||||
// chartData2: rsltData2 // RDII(m³/h) 강우유입수분석결과
|
||||
}));
|
||||
});
|
||||
fetchFilteredData();
|
||||
fn.selectPplRdiiQty(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수량 조회
|
||||
setRdiiQty(Math.round(data.rdiih*24)); // cmd: m³/d 단위로 변환
|
||||
if (data.raintotal && data.rainTotal !== 0) {
|
||||
setRainTotal(data.raintotal); // 총강우량mm
|
||||
setTotalVolume(Math.round(data.rdiih * data.sustaintime / data.raintotal)); // 총체적 m³/mm
|
||||
}
|
||||
});
|
||||
setPopupOpen(false);
|
||||
};
|
||||
|
||||
const fetchFilteredData: () => Promise<void> = async () => {
|
||||
const rsltData = await fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo);
|
||||
const rsltDataformatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []); // format 변경
|
||||
const rainData = await fn.selectRainData(rainInfwtAnlsSn);
|
||||
const rainDataformatted = fn.getChartDataRainData(rainData ?? []); // format 변경
|
||||
console.log('rainDataformatted :', rainDataformatted);
|
||||
const rainMap = new Map(rainDataformatted.map(item => [item.label, item.barVal0]));
|
||||
|
||||
rsltDataformatted.forEach(item => {
|
||||
const matchedVal = rainMap.get(item.label);
|
||||
if (matchedVal !== undefined) {
|
||||
item.barVal0 = matchedVal;
|
||||
}
|
||||
});
|
||||
console.log('rsltDataformatted :', rsltDataformatted);
|
||||
setChartData(rsltDataformatted); // 차트 데이터 설정
|
||||
}
|
||||
const renderBody = () => (
|
||||
<>
|
||||
<div className="text-black text-3xl font-bold mb-5">RDII분석결과</div>
|
||||
@@ -143,6 +166,11 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => {
|
||||
<span className="w-[30px] inline-block text-right text-black text-xl font-bold"> {rdiiQty}</span>
|
||||
<span className="ml-2 text-black"> m³/d</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<span className="w-[95px] inline-block text-right text-black text-xl">적용강우량 : </span>
|
||||
<span className="w-[30px] inline-block text-right text-black text-xl font-bold"> {rainTotal}</span>
|
||||
<span className="ml-2 text-black"> mm</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<span className="w-[95px] inline-block text-right text-black text-xl">강우량대비RDII : </span>
|
||||
<span className="w-[30px] inline-block text-right text-black text-xl font-bold"> {totalVolume}</span>
|
||||
|
||||
Reference in New Issue
Block a user