Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2298549a73 | ||
|
|
c0eb6b94cc | ||
|
|
c29d191a5e | ||
|
|
3a37192782 | ||
|
|
f3ca55d183 | ||
|
|
506eb78a80 | ||
|
|
42795e517b | ||
|
|
e9bfaa865a | ||
|
|
8a2ed7df58 | ||
|
|
00d6ff90c6 | ||
|
|
9bfd32b5c2 | ||
|
|
159fad40ef | ||
|
|
4aa4089ddd | ||
|
|
f6cb75ff1c | ||
|
|
61e4643503 | ||
|
|
6492ad5500 | ||
|
|
8ed0a86cf5 | ||
|
|
523925459c | ||
|
|
a11f760162 | ||
|
|
e35b73d067 | ||
|
|
2620e40872 | ||
|
|
c5f4f6ddd0 | ||
|
|
f332d64aa6 | ||
|
|
bb4d7b9d56 | ||
|
|
f6cefd6230 | ||
|
|
c6b45499c4 | ||
|
|
30bbf51e77 | ||
|
|
bb6dbfa617 | ||
|
|
1778c07ef9 | ||
|
|
075e3711a9 | ||
|
|
32b16055fb | ||
|
|
1daa925019 | ||
|
|
f58f9c3fe3 | ||
|
|
af98524806 | ||
|
|
ecf430dd33 | ||
|
|
892e329207 | ||
|
|
543fbb70cc | ||
|
|
3a191e8d06 | ||
|
|
402b9a5539 | ||
|
|
ef80915519 | ||
|
|
2e83898777 | ||
|
|
3d988b4644 | ||
|
|
bcdefaa828 | ||
|
|
0d48a0b579 | ||
|
|
c59c17f9f0 | ||
|
|
d5c967e9ad | ||
|
|
55793d8463 | ||
|
|
fc3ef2c59e | ||
|
|
accc3e0ef1 | ||
|
|
ba657e8311 | ||
|
|
53fef3bdbf | ||
|
|
21afb058cf | ||
|
|
5232703dd9 | ||
|
|
406ce4056a | ||
|
|
21e0ea086e | ||
|
|
f50a9aaf3a | ||
|
|
60e73212e8 | ||
|
|
6660c85150 | ||
|
|
2cfd57fdc3 | ||
|
|
5e5411a34f | ||
|
|
01852c1528 | ||
|
|
4488bd23c7 | ||
|
|
c2696c6eeb | ||
|
|
4ef86a3e6a | ||
|
|
badea83e9e | ||
|
|
fc2f119584 | ||
|
|
23a92ec38c | ||
|
|
fe2f3faee4 | ||
|
|
3e5a14fbc8 | ||
|
|
527b4293c4 | ||
|
|
4e2fe7aaa1 | ||
|
|
0a109111cb | ||
|
|
3b6e8ceedb | ||
|
|
99229a8453 | ||
|
|
383e5f8382 | ||
|
|
a11570b563 | ||
|
|
af1feb3920 | ||
|
|
a6df8d261a | ||
|
|
f96942beee | ||
|
|
7994698349 | ||
|
|
ab495c6572 | ||
|
|
804688b3c1 | ||
|
|
47c986d517 | ||
|
|
8435096e5a | ||
|
|
e30a7d184b | ||
|
|
b4e3d922b1 | ||
|
|
a028b6ae8b | ||
|
|
9565931042 | ||
|
|
1042391702 | ||
|
|
7c2ff7c037 | ||
|
|
ad0803f405 | ||
|
|
03675aaa60 | ||
|
|
c0c19c0425 | ||
|
|
0bbb27e3d3 | ||
|
|
214d283366 | ||
|
|
7d95ddf6a5 | ||
|
|
a19c5c269a | ||
|
|
d641866cc4 | ||
|
|
6102e0af6b | ||
|
|
225bae08d3 | ||
|
|
d8b5abb12f | ||
|
|
ba6d7f4177 | ||
|
|
d9a3da96bb | ||
|
|
87d1f33c05 | ||
|
|
56e9218646 | ||
|
|
2d5534b3a6 | ||
|
|
d68c6c151c | ||
|
|
dfb910f270 | ||
|
|
ddf3a5cfa0 | ||
|
|
5976b35c90 | ||
|
|
6b204fce15 | ||
|
|
f731d0f9f0 | ||
|
|
e231d0c4be | ||
|
|
e4328a2ed4 | ||
|
|
069e489d03 | ||
|
|
990dffc11c | ||
|
|
b4140d6457 | ||
|
|
91af5fd27c | ||
|
|
0bb16f9795 | ||
|
|
be7efb2ad0 | ||
|
|
fbd9575df7 | ||
|
|
6d967497ba | ||
|
|
7816b4e450 | ||
|
|
240c2f847c | ||
|
|
e39a102c14 |
@@ -1,8 +0,0 @@
|
||||
# 디폴트 무시된 파일
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 에디터 기반 HTTP 클라이언트 요청
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -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",
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
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_PORT = 11035
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
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_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 11035
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://wr.hasuon.or.kr
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000
|
||||
|
||||
@@ -30,9 +30,6 @@ export default function Home() {
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/sdmMng/sdmApplyList">수요처 신청 현황(개발중)</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/map/mainMap">지도팝업 (임시)</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
const Map = () => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
@@ -1,94 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수
|
||||
const copyStyles = (sourceDoc: Document, targetDoc: Document) => {
|
||||
Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
|
||||
try {
|
||||
if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS
|
||||
const newStyleEl = sourceDoc.createElement('style');
|
||||
Array.from(styleSheet.cssRules).forEach(cssRule => {
|
||||
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
|
||||
});
|
||||
targetDoc.head.appendChild(newStyleEl);
|
||||
} else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일
|
||||
const newLinkEl = sourceDoc.createElement('link');
|
||||
newLinkEl.rel = 'stylesheet';
|
||||
newLinkEl.href = styleSheet.href;
|
||||
targetDoc.head.appendChild(newLinkEl);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('스타일시트를 복사할 수 없습니다:', e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
interface NewWindowPortalProps {
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizable?: string;
|
||||
scrollbars?: string;
|
||||
}
|
||||
|
||||
const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "" , width = 900, height = 800, resizable = "yes", scrollbars= "yes" }) => {
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
||||
const newWindowRef = useRef<Window | null>(null);
|
||||
const onCloseRef = useRef(onClose);
|
||||
|
||||
// onClose 함수를 ref로 저장하여 의존성 문제 해결
|
||||
useEffect(() => {
|
||||
onCloseRef.current = onClose;
|
||||
}, [onClose]);
|
||||
|
||||
const handleUnload = useCallback(() => {
|
||||
onCloseRef.current();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 이미 창이 열려있으면 새로 열지 않음
|
||||
if (newWindowRef.current && !newWindowRef.current.closed) return;
|
||||
|
||||
// 1. 새 창을 열고 참조를 저장합니다.
|
||||
const popupOptions = `width=${width},height=${height},resizable=${resizable},scrollbars=${scrollbars}`;
|
||||
newWindowRef.current = window.open('', title, popupOptions);
|
||||
|
||||
if (newWindowRef.current) {
|
||||
newWindowRef.current.document.title = title;
|
||||
|
||||
// 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다.
|
||||
const div = newWindowRef.current.document.createElement('div');
|
||||
newWindowRef.current.document.body.appendChild(div);
|
||||
setContainer(div);
|
||||
|
||||
// 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요)
|
||||
copyStyles(document, newWindowRef.current.document);
|
||||
|
||||
// 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다.
|
||||
// const handleUnload = () => {
|
||||
// onClose();
|
||||
// };
|
||||
newWindowRef.current.addEventListener('unload', handleUnload);
|
||||
|
||||
// 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다.
|
||||
return () => {
|
||||
if (newWindowRef.current) {
|
||||
newWindowRef.current.removeEventListener('unload', handleUnload);
|
||||
if (!newWindowRef.current.closed) {
|
||||
newWindowRef.current.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.');
|
||||
onCloseRef.current();
|
||||
}
|
||||
}, [handleUnload, onClose, title]);
|
||||
|
||||
// `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다.
|
||||
return container ? createPortal(children, container) : null;
|
||||
};
|
||||
|
||||
export default NewWindowPortal;
|
||||
@@ -1 +0,0 @@
|
||||
export * from './map'
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
/**
|
||||
* 관망도 API
|
||||
*/
|
||||
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`
|
||||
|
||||
export type LyrStngProps = {
|
||||
lyr_sn: string
|
||||
lyr_nm: string
|
||||
upper_lyr_sn: string
|
||||
drawOrdr: string
|
||||
flterSql: string
|
||||
gobjTy: string
|
||||
lyrTpNm: string
|
||||
useYn: string
|
||||
minViewSize: string
|
||||
iconNm: string
|
||||
lblAtrbNm: string
|
||||
fontNm: string
|
||||
fontIntnColor: string
|
||||
fontExtnColor: string
|
||||
fontExtnWdth: string
|
||||
lnWdth: string
|
||||
lnColor: string
|
||||
polgnColor: string
|
||||
initEprssYn: string
|
||||
clickPsblYn: string
|
||||
}
|
||||
|
||||
export const mapService = {
|
||||
//레이어 설정 조회
|
||||
getLyrStngs: async () => {
|
||||
const response = await fetch(`${API_URL}/lyrStngs/WR_NETWORK`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { Toaster } from "react-hot-toast";
|
||||
import * as z from "zod";
|
||||
import SdmLogin from "../sdmLogin/page";
|
||||
|
||||
const sdmInfoFormSchema = z.object({
|
||||
const createFormSchema = () => z.object({
|
||||
sdmNm: z.string()
|
||||
.min(1, "수요처를 선택해주세요"),
|
||||
demandYmd: z.date().optional(),
|
||||
@@ -67,14 +67,14 @@ const sdmInfoFormSchema = z.object({
|
||||
candidateTitle: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof sdmInfoFormSchema>;
|
||||
export type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
|
||||
function SdmInfoInsertContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const {insertSdmInfoData, Sdm} = useDmStore((state) => state);
|
||||
const {successLogin} = Sdm;
|
||||
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
||||
const formSchema = sdmInfoFormSchema;
|
||||
const formSchema = createFormSchema();
|
||||
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [facilityName, setFacilityName] = useState<string>('');
|
||||
|
||||
@@ -21,7 +21,6 @@ export default function MyForm() {
|
||||
useEffect(() => {
|
||||
if (successNextPage && formDataRef.current) {
|
||||
const data = formDataRef.current;
|
||||
console.log(data);
|
||||
const queryParams = new URLSearchParams({
|
||||
dmdCoNm: data.dmdCoNm || '',
|
||||
ctpvCd: data.ctpvCd || '',
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import ModalSimilarPage from "@/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page";
|
||||
import type { FormData } from "@/app/pages/sdmMng/sdmInfoInsert/page";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import {
|
||||
CANDIDATE,
|
||||
DatePicker,
|
||||
FormField,
|
||||
FormLabel,
|
||||
FormUi,
|
||||
SelectBox,
|
||||
UiModal,
|
||||
} from "@common/business_components";
|
||||
import { CANDIDATE, DatePicker, FormField, FormLabel, FormUi, SelectBox, UiModal } from "@common/business_components";
|
||||
import { Input } from "@common/components";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
@@ -18,8 +10,6 @@ import { AccessibleFormField } from "./AccessibleFormField";
|
||||
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
|
||||
import SearchInputText from "../common/searhDemandInput/component";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import NewWindowPortal from "@/app/pages/map/popup/windowPotal";
|
||||
import Map from "@/app/pages/map/popup/sdmInfoMap";
|
||||
|
||||
export interface SdmInfoRegFormProps {
|
||||
form: UseFormReturn<FormData>;
|
||||
@@ -27,31 +17,21 @@ export interface SdmInfoRegFormProps {
|
||||
facilityName?: string;
|
||||
}
|
||||
|
||||
type FieldComponent = (
|
||||
field: ControllerRenderProps<FormData, keyof FormData>
|
||||
) => JSX.Element;
|
||||
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
|
||||
|
||||
export function SdmInfoRegForm({
|
||||
form,
|
||||
onCheckboxChange,
|
||||
facilityName,
|
||||
}: SdmInfoRegFormProps) {
|
||||
export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfoRegFormProps) {
|
||||
const { initializeCommonData, Sdm } = useDmStore((state) => state);
|
||||
const { useTypesData } = Sdm;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [sdmNmValue, setSdmNmValue] = useState<string>(
|
||||
searchParams.get("dmdCoNm") || ""
|
||||
);
|
||||
|
||||
const [sdmNmValue, setSdmNmValue] = useState<string>(searchParams.get('dmdCoNm') || '');
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const [isSimilarCase, setIsSimilarCase] = useState(false);
|
||||
const [key, setKey] = useState(0);
|
||||
// 후보지 팝업 오픈 상태
|
||||
const [isCandidatePopup, setIsCandidatePopup] = useState(false);
|
||||
|
||||
//모달 조회에 필요한 데이터
|
||||
const [sply, setSply] = useState<number>(0);
|
||||
const [usageCd, setUsageCd] = useState<string>("청소");
|
||||
const [usageCd, setUsageCd] = useState<string>('청소');
|
||||
const [money, setMoney] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,7 +41,7 @@ export function SdmInfoRegForm({
|
||||
useEffect(() => {
|
||||
if (useTypesData && useTypesData.length > 0) {
|
||||
setTimeout(() => {
|
||||
form.setValue("usePurpose", useTypesData[0]?.val?.toString() || "");
|
||||
form.setValue('usePurpose', useTypesData[0]?.val?.toString() || '');
|
||||
}, 500);
|
||||
}
|
||||
}, [useTypesData, form]);
|
||||
@@ -69,45 +49,32 @@ export function SdmInfoRegForm({
|
||||
useEffect(() => {
|
||||
if (facilityName) {
|
||||
setSdmNmValue(facilityName);
|
||||
form.setValue("sdmNm", facilityName);
|
||||
setKey((prev) => prev + 1);
|
||||
form.setValue('sdmNm', facilityName);
|
||||
setKey(prev => prev + 1);
|
||||
}
|
||||
}, [facilityName, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get("dmdCoNm")) {
|
||||
form.setValue("sdmNm", searchParams.get("dmdCoNm") || "");
|
||||
setSdmNmValue(searchParams.get("dmdCoNm") || "");
|
||||
setKey((prev) => prev + 1);
|
||||
}
|
||||
}, [searchParams, form]);
|
||||
|
||||
const searchCandidate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log(1);
|
||||
setIsCandidatePopup(true);
|
||||
};
|
||||
}
|
||||
|
||||
const searchSimilarCase = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSimilarCase(true);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
setIsBznmTotNoDisabled(checked);
|
||||
onCheckboxChange?.(checked);
|
||||
};
|
||||
|
||||
// 시설 선택시 이벤트
|
||||
const selectComp = useCallback(
|
||||
(rtnData: DemandCompType[]) => {
|
||||
const rtnCompData = rtnData?.[0];
|
||||
form.setValue("sdmNm", rtnCompData?.dmdCoNm || "");
|
||||
},
|
||||
[form]
|
||||
);
|
||||
const selectComp = useCallback((rtnData: DemandCompType[]) => {
|
||||
const rtnCompData = rtnData?.[0];
|
||||
form.setValue('sdmNm', rtnCompData?.dmdCoNm || '');
|
||||
}, [form]);
|
||||
|
||||
const formFields: Array<{
|
||||
name: keyof FormData;
|
||||
@@ -126,15 +93,15 @@ export function SdmInfoRegForm({
|
||||
<SearchInputText
|
||||
label="수요처명"
|
||||
setValue={(val) => {
|
||||
setSdmNmValue(val || "");
|
||||
field.onChange(val || "");
|
||||
setSdmNmValue(val || '');
|
||||
field.onChange(val || '');
|
||||
}}
|
||||
setRtnData={selectComp}
|
||||
initValue={facilityName || sdmNmValue}
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "demandYmd",
|
||||
@@ -143,7 +110,7 @@ export function SdmInfoRegForm({
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<div className="w-full">
|
||||
<DatePicker
|
||||
<DatePicker
|
||||
mode="single"
|
||||
selected={field.value as Date}
|
||||
onSelect={(date) => {
|
||||
@@ -155,12 +122,12 @@ export function SdmInfoRegForm({
|
||||
value: field.value as Date,
|
||||
onChange: (date: Date | undefined) => {
|
||||
field.onChange(date as Date);
|
||||
},
|
||||
}
|
||||
}}
|
||||
selectClass={undefined}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "demandQnty",
|
||||
@@ -168,8 +135,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="요구수량(㎥/d)을 입력하세요"
|
||||
<Input
|
||||
placeholder="요구수량(㎥/d)을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -181,7 +148,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "usePurpose",
|
||||
@@ -191,16 +158,16 @@ export function SdmInfoRegForm({
|
||||
component: (field) => (
|
||||
<SelectBox
|
||||
selectClass={undefined}
|
||||
label=""
|
||||
selectArray={useTypesData}
|
||||
label=""
|
||||
selectArray={useTypesData}
|
||||
onSelectValue={(val) => {
|
||||
field.onChange(val.toString());
|
||||
setUsageCd(val.toString());
|
||||
}}
|
||||
className=""
|
||||
className=""
|
||||
value={field.value as string}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "money",
|
||||
@@ -234,14 +201,14 @@ export function SdmInfoRegForm({
|
||||
</FormUi>
|
||||
}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "reqWaterQlty",
|
||||
label: "요구수질",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: undefined,
|
||||
component: undefined
|
||||
},
|
||||
{
|
||||
name: "grssClfg",
|
||||
@@ -249,8 +216,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총대장균군수를 입력하세요"
|
||||
<Input
|
||||
placeholder="총대장균군수를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -259,7 +226,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "tn",
|
||||
@@ -267,8 +234,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총질소를 입력하세요"
|
||||
<Input
|
||||
placeholder="총질소를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -277,7 +244,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "combResdch",
|
||||
@@ -285,8 +252,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="결합잔류염소를 입력하세요"
|
||||
<Input
|
||||
placeholder="결합잔류염소를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -295,7 +262,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "tph",
|
||||
@@ -303,8 +270,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총인을 입력하세요"
|
||||
<Input
|
||||
placeholder="총인을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -313,16 +280,16 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "trbt",
|
||||
label: "탁도(NTU)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="탁도를 입력하세요"
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="탁도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -331,7 +298,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "ph",
|
||||
@@ -339,8 +306,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="수소이온농도를 입력하세요"
|
||||
<Input
|
||||
placeholder="수소이온농도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -349,7 +316,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "bod",
|
||||
@@ -357,8 +324,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="생물학적 산소요구량을 입력하세요"
|
||||
<Input
|
||||
placeholder="생물학적 산소요구량을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -367,7 +334,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "clIonQnt",
|
||||
@@ -375,8 +342,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="염화물을 입력하세요"
|
||||
<Input
|
||||
placeholder="염화물을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -385,16 +352,16 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
},
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "smll",
|
||||
label: "냄새",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="냄새를 입력하세요"
|
||||
<Input
|
||||
placeholder="냄새를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -403,7 +370,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "cdtnv",
|
||||
@@ -411,8 +378,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="전기전도도를 입력하세요"
|
||||
<Input
|
||||
placeholder="전기전도도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -421,7 +388,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "crmty",
|
||||
@@ -429,8 +396,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="색도를 입력하세요"
|
||||
<Input
|
||||
placeholder="색도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -439,7 +406,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "rmrk",
|
||||
@@ -447,8 +414,8 @@ export function SdmInfoRegForm({
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="비고를 입력하세요"
|
||||
<Input
|
||||
placeholder="비고를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -457,7 +424,7 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "candidateTitle",
|
||||
@@ -466,16 +433,16 @@ export function SdmInfoRegForm({
|
||||
component: (field) => (
|
||||
<div>
|
||||
<div className="-mt-14 float-right flex gap-10">
|
||||
<Button className={"bg-gray-200"} onClick={searchCandidate}>
|
||||
<Button className={'bg-gray-200'} onClick={searchCandidate}>
|
||||
후보지 조회
|
||||
</Button>
|
||||
<Button className={"bg-gray-200"} onClick={searchSimilarCase}>
|
||||
<Button className={'bg-gray-200'} onClick={searchSimilarCase}>
|
||||
유사사례
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-center gap-10 mt-5">
|
||||
<Input
|
||||
placeholder="후보지1"
|
||||
<Input
|
||||
placeholder="후보지1"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -484,8 +451,8 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Input
|
||||
placeholder="후보지2"
|
||||
<Input
|
||||
placeholder="후보지2"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -494,8 +461,8 @@ export function SdmInfoRegForm({
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Input
|
||||
placeholder="후보지3"
|
||||
<Input
|
||||
placeholder="후보지3"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
@@ -506,32 +473,24 @@ export function SdmInfoRegForm({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const normalFields = formFields.filter(
|
||||
(f) => f.formIndex === 0 && f.component
|
||||
);
|
||||
const groupFields = formFields.filter(
|
||||
(f) => f.formIndex === 1 && f.component
|
||||
);
|
||||
const candidateFields = formFields.filter(
|
||||
(f) => f.formIndex === 2 && f.component
|
||||
);
|
||||
const normalFields = formFields.filter(f => f.formIndex === 0 && f.component);
|
||||
const groupFields = formFields.filter(f => f.formIndex === 1 && f.component);
|
||||
const candidateFields = formFields.filter(f => f.formIndex === 2 && f.component);
|
||||
|
||||
return (
|
||||
<>
|
||||
{normalFields.map(({ name, label, component, required }) =>
|
||||
{normalFields.map(({ name, label, component, required }) => (
|
||||
component ? (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) =>
|
||||
name === "money" ? (
|
||||
component(field)
|
||||
) : (
|
||||
render={({ field }) => (
|
||||
name === 'money' ? component(field) : (
|
||||
// <FormUi label={label}>
|
||||
// {component(field)}
|
||||
// </FormUi>
|
||||
@@ -543,10 +502,10 @@ export function SdmInfoRegForm({
|
||||
inputComponent={component(field)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
))}
|
||||
|
||||
{groupFields.length > 0 && (
|
||||
<div className="mt-10">
|
||||
@@ -576,7 +535,7 @@ export function SdmInfoRegForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{candidateFields.map(({ name, label, component, required }, index) =>
|
||||
{candidateFields.map(({ name, label, component, required }, index) => (
|
||||
component ? (
|
||||
<FormField
|
||||
key={name + index}
|
||||
@@ -596,13 +555,11 @@ export function SdmInfoRegForm({
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
))}
|
||||
|
||||
{isSimilarCase && (
|
||||
<UiModal
|
||||
body={
|
||||
<ModalSimilarPage sply={sply} usageCd={usageCd} money={money} />
|
||||
}
|
||||
body={<ModalSimilarPage sply={sply} usageCd={usageCd} money={money} />}
|
||||
bodyClass="sm:max-w-[100vw] w-[1400px] h-[900px]"
|
||||
open={isSimilarCase}
|
||||
onOpenChange={(open) => !open && setIsSimilarCase(false)}
|
||||
@@ -610,20 +567,6 @@ export function SdmInfoRegForm({
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 후보지 조회 새 창 포털 */}
|
||||
{isCandidatePopup && (
|
||||
<NewWindowPortal
|
||||
onClose={() => setIsCandidatePopup(false)}
|
||||
title="공급처 후보지 조회"
|
||||
width={window.screen.availWidth}
|
||||
height={window.screen.availHeight}
|
||||
resizable="no"
|
||||
scrollbars="yes"
|
||||
>
|
||||
<Map />
|
||||
</NewWindowPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,14 @@ import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { sha512 } from 'js-sha512';
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, type ControllerRenderProps } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
export function SdmLoginForm() {
|
||||
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
|
||||
// 스키마를 useMemo로 최적화
|
||||
const sdmLoginFormSchema = useMemo(() => z.object({
|
||||
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
|
||||
const createFormSchema = () => z.object({
|
||||
dmdInfoId: z.string()
|
||||
.min(1, "사업자등록번호를 입력해주세요")
|
||||
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다"),
|
||||
@@ -26,12 +24,11 @@ export function SdmLoginForm() {
|
||||
.max(16, "비밀번호는 16자 이내로 입력해주세요"),
|
||||
rgtr: z.string().optional(),
|
||||
dmdAplyPswd: z.string().optional(),
|
||||
}), [isBznmTotNoDisabled]);
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof sdmLoginFormSchema>;
|
||||
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
|
||||
|
||||
const formSchema = sdmLoginFormSchema;
|
||||
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const formSchema = createFormSchema();
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
|
||||
@@ -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.
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 970 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1016 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 757 B |
|
Before Width: | Height: | Size: 785 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 791 B |
|
Before Width: | Height: | Size: 455 B |
|
Before Width: | Height: | Size: 782 B |
|
Before Width: | Height: | Size: 782 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 217 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1009 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1009 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1017 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 167 B |
|
Before Width: | Height: | Size: 259 B |
|
Before Width: | Height: | Size: 167 B |
|
Before Width: | Height: | Size: 164 B |
|
Before Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 108 B |