Merge branch 'iipbs/sn/dev' into iipbs/sn/ii

This commit is contained in:
swcho
2025-07-25 00:28:27 +09:00
31 changed files with 1297 additions and 200 deletions
+33 -21
View File
@@ -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();
+49
View File
@@ -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 };
+37 -24
View File
@@ -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]"
+1
View File
@@ -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>
+33 -25
View File
@@ -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}
+32 -19
View File
@@ -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">
+1
View File
@@ -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",
+5
View File
@@ -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.
+5
View File
@@ -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.
+8 -8
View File
@@ -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>