Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a67f549f | ||
|
|
c0888ea2a4 | ||
|
|
1ead18b53c | ||
|
|
07db5b1fbd | ||
|
|
6110abebb4 | ||
|
|
05e9082831 | ||
|
|
76a634cf9a | ||
|
|
793c673414 | ||
|
|
2a76c279eb | ||
|
|
5d1ba3fd87 | ||
|
|
99db9a0b77 | ||
|
|
4126bc2234 | ||
|
|
d4f1ec095e | ||
|
|
bbf3ce3948 | ||
|
|
fb5b8356c7 | ||
|
|
1333bd040a | ||
|
|
013c07ef59 | ||
|
|
29fd59a321 | ||
|
|
3bc152c341 | ||
|
|
ea274f8f7d | ||
|
|
7ac53e56d7 | ||
|
|
7b1c12e952 | ||
|
|
17dafe6530 | ||
|
|
ef331363f7 | ||
|
|
4f10864c2c | ||
|
|
4b5ceb0e0a | ||
|
|
4fa50161fd | ||
|
|
3d27e07ccb | ||
|
|
ca1ca71a62 | ||
|
|
bdd6c7b804 | ||
|
|
04b8082f04 | ||
|
|
7bf8ec1d35 | ||
|
|
3451b19b96 | ||
|
|
ae7c1ea1c4 | ||
|
|
9c76b955e2 | ||
|
|
6c4bfe2798 | ||
|
|
98189cd14a | ||
|
|
823ce262bb | ||
|
|
34358400e8 | ||
|
|
da9d069a9a | ||
|
|
58a03678e4 | ||
|
|
f5c281cf86 | ||
|
|
c72bcea7d9 | ||
|
|
49cd160e76 | ||
|
|
5ca93a25ca | ||
|
|
55554fc781 | ||
|
|
8e78c10304 | ||
|
|
0eb74b46ba | ||
|
|
f21dde8f7d | ||
|
|
1fc6707bb3 | ||
|
|
44c5d66afa | ||
|
|
04edb49c9f | ||
|
|
e3eb542e59 | ||
|
|
428b199dd3 | ||
|
|
3c4a6efeea | ||
|
|
dae7024043 | ||
|
|
64e5c26e8c | ||
|
|
84ae60edd6 | ||
|
|
d4c9989069 | ||
|
|
6cda413913 | ||
|
|
a11eaf031d | ||
|
|
3c701359fd | ||
|
|
85ba9fe0ab | ||
|
|
71077f4c8e | ||
|
|
4c7ff53d78 | ||
|
|
b31691dd9c | ||
|
|
0131cae0d7 | ||
|
|
f37b19eab6 | ||
|
|
7c1db5e0e0 | ||
|
|
2c495ab8fd | ||
|
|
ced1b7084d | ||
|
|
b0c871eff7 | ||
|
|
bd83fa3417 | ||
|
|
c0b63a655e | ||
|
|
cc21aaede1 | ||
|
|
0c71c80a25 | ||
|
|
49e42757bc | ||
|
|
0964f442be | ||
|
|
73ceeb1b38 | ||
|
|
814365d7de | ||
|
|
8c1a976b5b | ||
|
|
66db81071e | ||
|
|
6df6e612bc | ||
|
|
3fee2a781f | ||
|
|
55839f0913 | ||
|
|
f68131fc13 | ||
|
|
7b9081a125 | ||
|
|
54d3a35b38 | ||
|
|
ba42b74578 | ||
|
|
e19057d026 | ||
|
|
f4c64168d1 |
@@ -1,17 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type Row,
|
||||
type RowSelectionState,
|
||||
type Table,
|
||||
type Row,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { GroupApi } from "@/entities/group";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import {
|
||||
type AdminUserListResponse,
|
||||
UserApi,
|
||||
type UserSearchParams,
|
||||
} from "@/entities/user";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
@@ -19,18 +31,10 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
import { GroupApi } from "@/entities/group";
|
||||
import {
|
||||
UserApi,
|
||||
type UserSearchParams,
|
||||
type AdminUserListResponse,
|
||||
} from "@/entities/user";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useAlertStore } from "@/shared/lib";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
import { AddAdminDialog } from "./add-admin-dialog";
|
||||
|
||||
function AdminPage() {
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { type Banner } from "../model/banner-interface";
|
||||
import { createColumns } from "../model/banner-table-columns";
|
||||
import { type Banner, type BannerParams } from "../model/banner-interface";
|
||||
|
||||
import { PreviewBannerDialog } from "./preview-banner-dialog";
|
||||
|
||||
type PostingTarget = {
|
||||
@@ -22,50 +27,52 @@ type PostingTarget = {
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
bnrTtl: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function BannerPage() {
|
||||
const router = useRouter();
|
||||
const imageRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [bnrTtl, setBnrTtl] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<BannerParams>({
|
||||
bnrTtl: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [bnrTtl, setBnrTtl] = useState(params.bnrTtl);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, params],
|
||||
queryFn: () =>
|
||||
BannerApi.getBannerList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
bnrTtl: params.bnrTtl,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bnrTtl,
|
||||
});
|
||||
setParams({ bnrTtl, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setBnrTtl("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bnrTtl: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Banner) => {
|
||||
@@ -92,7 +99,7 @@ function BannerPage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Board, BoardApi } from "@/entities/board";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -18,60 +25,57 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { BoardApi, type BoardParams } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { type Board } from "@/entities/board";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function BbsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [bbsTypeCd, setBbsTypeCd] = useState("");
|
||||
const [bbsNm, setBbsNm] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<BoardParams>({
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [bbsTypeCd, setBbsTypeCd] = useState(params.bbsTypeCd);
|
||||
const [bbsNm, setBbsNm] = useState(params.bbsNm);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, params],
|
||||
queryFn: () =>
|
||||
BoardApi.getBoardList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
bbsTypeCd: params.bbsTypeCd,
|
||||
bbsNm: params.bbsNm,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bbsTypeCd,
|
||||
bbsNm,
|
||||
});
|
||||
setParams({ bbsTypeCd, bbsNm, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setBbsTypeCd("");
|
||||
setBbsNm("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Board) => {
|
||||
@@ -89,7 +93,7 @@ function BbsPage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { UpCode } from "@/entities/code";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { UpCodeManagementDialog } from "../ui/upcode-dialog";
|
||||
|
||||
export const columns: ColumnDef<UpCode>[] = [
|
||||
@@ -24,7 +27,7 @@ export const columns: ColumnDef<UpCode>[] = [
|
||||
header: "상위코드명",
|
||||
},
|
||||
{
|
||||
accessorKey: "cdSe",
|
||||
accessorKey: "cdSeNm",
|
||||
header: "코드구분",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,60 +2,64 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import {
|
||||
ContentsApi,
|
||||
type Contents,
|
||||
type ContentsParams,
|
||||
} from "@/entities/content";
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Contents, ContentsApi } from "@/entities/content";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
contsTtl: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function ContentsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [contsTtl, setContsTtl] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<ContentsParams>({
|
||||
contsTtl: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [contsTtl, setContsTtl] = useState(params.contsTtl);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, params],
|
||||
queryFn: () =>
|
||||
ContentsApi.getContentsList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
contsTtl: params.contsTtl,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
contsTtl,
|
||||
});
|
||||
setParams({ contsTtl, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setContsTtl("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
contsTtl: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Contents) => {
|
||||
@@ -69,7 +73,7 @@ function ContentsPage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -354,7 +354,7 @@ function DashboardPage() {
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">실시간 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="p-4" style={{ height: "120px", overflowY: "auto" }}>
|
||||
<div className="space-y-4 text-foreground">
|
||||
{noticeUserList?.map((item) => (
|
||||
<div
|
||||
@@ -373,7 +373,7 @@ function DashboardPage() {
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">일정 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="p-4" style={{ height: "120px", overflowY: "auto" }}>
|
||||
<div className="space-y-4 text-foreground">
|
||||
{scheduleList && scheduleList.length > 0 ? (
|
||||
scheduleList.map((schedule, index) => {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
function FilePage() {
|
||||
|
||||
@@ -2,68 +2,73 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { type Image } from "../model/image-interface";
|
||||
import { columns } from "../model/image-table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function ImagePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<ImageParams>({
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, params],
|
||||
queryFn: () =>
|
||||
ImageApi.getImageList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
setParams({ keyword, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Image) => {
|
||||
@@ -81,7 +86,7 @@ function ImagePage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -2,45 +2,76 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
AuthorizationsApi,
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type Authorizations,
|
||||
type AuthorizationsParams,
|
||||
AuthorizationsApi,
|
||||
} from "@/entities/authorization";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { columns } from "../model/authorizations-table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function ModulePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [authorizationName, setAuthorizationName] = useState("");
|
||||
const [httpMethodCode, setHttpMethodCode] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<AuthorizationsParams>({
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [authorizationName, setAuthorizationName] = useState(
|
||||
params.authorizationName,
|
||||
);
|
||||
const [httpMethodCode, setHttpMethodCode] = useState(params.httpMethodCode);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, params],
|
||||
queryFn: () =>
|
||||
AuthorizationsApi.getAuthorizationsList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
authorizationName: params.authorizationName,
|
||||
httpMethodCode: params.httpMethodCode,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setParams({ authorizationName, httpMethodCode, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setAuthorizationName("");
|
||||
setHttpMethodCode("");
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Authorizations) => {
|
||||
router.push(`/module/register/${row.authorizationNo}`);
|
||||
};
|
||||
@@ -52,7 +83,7 @@ function ModulePage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
@@ -62,29 +93,6 @@ function ModulePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
authorizationName,
|
||||
httpMethodCode,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setAuthorizationName("");
|
||||
setHttpMethodCode("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
|
||||
@@ -2,70 +2,75 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { PopupApi } from "../api/popup-service";
|
||||
import type { Popup } from "../model/popup-interface";
|
||||
import { columns } from "../model/table-columns";
|
||||
import type { Popup, PopupParams } from "../model/popup-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function PopupPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<PopupParams>({
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, params],
|
||||
queryFn: () =>
|
||||
PopupApi.getPopupList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
setParams({ keyword, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Popup) => {
|
||||
@@ -82,7 +87,7 @@ function PopupPage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
useAlertStore,
|
||||
useDataTable,
|
||||
useSearchParamsState,
|
||||
useSelectPrompt,
|
||||
} from "@/shared/lib";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import {
|
||||
type DeletePost,
|
||||
deleteReasonMessages,
|
||||
PostApi,
|
||||
type RecentPost,
|
||||
} from "@/entities/post";
|
||||
import { SortSelect } from "@/widgets/post";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -20,76 +40,72 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { SortSelect } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
deleteReasonMessages,
|
||||
type DeletePost,
|
||||
type RecentPost,
|
||||
type RecentPostParams,
|
||||
} from "@/entities/post";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
import { MovePostDialog } from "./move-post-dialog";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
from?: Date | null | undefined;
|
||||
to?: Date | null | undefined;
|
||||
};
|
||||
|
||||
const selectData = Object.entries(deleteReasonMessages).map(
|
||||
([label, value]) => ({ label, value }),
|
||||
);
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
bbsId: "",
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function parseDateRange(frDateYmd: string, toDateYmd: string): DateRange {
|
||||
return {
|
||||
from: frDateYmd ? dayjs(frDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
to: toDateYmd ? dayjs(toDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function RecentPage() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { prompt } = useSelectPrompt(selectData);
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
|
||||
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>({
|
||||
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: "",
|
||||
bbsId: null,
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [isDeletePostData, setIsDeletePostData] = useState(false);
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
||||
parseDateRange(params.frDateYmd, params.toDateYmd),
|
||||
);
|
||||
const [keywordType, setKeywordType] = useState(params.keywordType);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [bbsId, setBbsId] = useState(params.bbsId);
|
||||
const [delYn, setDelYn] = useState(params.delYn);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.BOARD_SERVICE.POSTS,
|
||||
searchParams,
|
||||
pagination,
|
||||
tableSort,
|
||||
],
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: tableSort,
|
||||
keywordType: params.keywordType,
|
||||
keyword: params.keyword,
|
||||
bbsId: params.bbsId ? parseInt(params.bbsId) : null,
|
||||
delYn: params.delYn,
|
||||
frDateYmd: params.frDateYmd,
|
||||
toDateYmd: params.toDateYmd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
@@ -99,9 +115,22 @@ function RecentPage() {
|
||||
queryFn: () => BoardApi.getBoardList(),
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const isDeletePostData = params.delYn === "Y";
|
||||
|
||||
const handleRowClick = (row: RecentPost) => {
|
||||
const from = `${pathname}${searchParams?.toString() ? `?${searchParams.toString()}` : ""}`;
|
||||
router.push(
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}?from=${encodeURIComponent(from)}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,7 +145,7 @@ function RecentPage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
@@ -127,45 +156,32 @@ function RecentPage() {
|
||||
|
||||
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDeletePostData(delYn === "Y");
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
setParams({
|
||||
keywordType,
|
||||
keyword,
|
||||
bbsId: bbsId ? parseInt(bbsId) : null,
|
||||
bbsId,
|
||||
delYn,
|
||||
frDateYmd: dateRange?.from
|
||||
? dayjs(dateRange.from).format("YYYYMMDD")
|
||||
: "",
|
||||
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
|
||||
page: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeywordType("");
|
||||
setKeyword("");
|
||||
setBbsId(null);
|
||||
setBbsId("");
|
||||
setDelYn("N");
|
||||
setPresetDateRange("");
|
||||
setDateRange({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
bbsId: null,
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
});
|
||||
setIsDeletePostData(false);
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
@@ -273,10 +289,7 @@ function RecentPage() {
|
||||
setDateRange={handleDateRangeChange}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||
<Select
|
||||
value={bbsId?.toString() || ""}
|
||||
onValueChange={(value) => setBbsId(value ? value : null)}
|
||||
>
|
||||
<Select value={bbsId} onValueChange={setBbsId}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
@@ -289,7 +302,7 @@ function RecentPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
||||
<Select value={delYn} onValueChange={setDelYn}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
@@ -376,10 +389,10 @@ function RecentPage() {
|
||||
</>
|
||||
)}
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(parseInt(value));
|
||||
}}
|
||||
value={params.size.toString()}
|
||||
onValueChange={(value) =>
|
||||
setParams({ size: parseInt(value), page: 0 })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
@@ -392,7 +405,10 @@ function RecentPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SortSelect value={tableSort} onValueChange={setTableSort} />
|
||||
<SortSelect
|
||||
value={params.sort}
|
||||
onValueChange={(value) => setParams({ sort: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,39 +2,68 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Role, RoleApi } from "@/entities/role";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keywordType: "roleName",
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function RolePage() {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<RoleParams>({
|
||||
keywordType: "roleName",
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES, params],
|
||||
queryFn: () =>
|
||||
RoleApi.getRoles({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keywordType: params.keywordType,
|
||||
keyword: params.keyword,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setParams({ keyword, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Role) => {
|
||||
router.push(`/role/register/${row.roleId}`);
|
||||
};
|
||||
@@ -48,7 +77,7 @@ function RolePage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
@@ -56,26 +85,6 @@ function RolePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
|
||||
@@ -1,54 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import "@common/assets/styles/grid.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { SiteApi, type Site, type SiteParams } from "@/entities/site";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Site, SiteApi } from "@/entities/site";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
siteNm: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function SitePage() {
|
||||
const router = useRouter();
|
||||
const [siteNm, setSiteNm] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<SiteParams>({
|
||||
siteNm: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [siteNm, setSiteNm] = useState(params.siteNm);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, params],
|
||||
queryFn: () =>
|
||||
SiteApi.getSiteList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
siteNm: params.siteNm,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
siteNm,
|
||||
});
|
||||
setParams({ siteNm, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSiteNm("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
siteNm: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Site) => {
|
||||
@@ -62,7 +73,7 @@ function SitePage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { SurveyApi } from "../api/survey-service";
|
||||
import type { Survey, SurveyParams } from "../model/survey-interface";
|
||||
import type { Survey } from "../model/survey-interface";
|
||||
import { columns } from "../model/survey-table-columns";
|
||||
|
||||
type PostingTarget = {
|
||||
@@ -22,32 +26,53 @@ type PostingTarget = {
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
srvyTtl: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function SurveyPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [srvyTtl, setSrvyTtl] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<SurveyParams>({
|
||||
srvyTtl: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [srvyTtl, setSrvyTtl] = useState(params.srvyTtl);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, params],
|
||||
queryFn: () =>
|
||||
SurveyApi.getSurveyList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
srvyTtl: params.srvyTtl,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setParams({ srvyTtl, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSrvyTtl("");
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Survey) => {
|
||||
router.push(`/survey/${row.srvySn}`);
|
||||
};
|
||||
@@ -62,7 +87,7 @@ function SurveyPage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
@@ -71,26 +96,6 @@ function SurveyPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
srvyTtl,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSrvyTtl("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
srvyTtl: "",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
|
||||
@@ -2,58 +2,70 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { TemplateApi } from "../api/template-service";
|
||||
import type { Template } from "../model/template-interface";
|
||||
import { columns } from "../model/template-table-columns";
|
||||
import type { Template, TemplateParams } from "../model/template-interface";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
tmpltSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function TemplatePage() {
|
||||
const router = useRouter();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [tmpltSeCd, setTmpltSeCd] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<TemplateParams>({
|
||||
keyword: "",
|
||||
tmpltSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [tmpltSeCd, setTmpltSeCd] = useState(params.tmpltSeCd);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, params],
|
||||
queryFn: () =>
|
||||
TemplateApi.getTemplateList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
tmpltSeCd: params.tmpltSeCd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
tmpltSeCd,
|
||||
});
|
||||
setParams({ keyword, tmpltSeCd, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setTmpltSeCd("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
tmpltSeCd: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Template) => {
|
||||
@@ -67,7 +79,7 @@ function TemplatePage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -2,63 +2,70 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { TermsApi } from "../api/terms-service";
|
||||
import type { Terms } from "../model/terms-interface";
|
||||
import { columns } from "../model/terms-table-columns";
|
||||
import type { TermsParams, Terms } from "../model/terms-interface";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
trmsSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function TermsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [trmsSeCd, setTrmsSeCd] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<TermsParams>({
|
||||
keyword: "",
|
||||
trmsSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [trmsSeCd, setTrmsSeCd] = useState(params.trmsSeCd);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, params],
|
||||
queryFn: () =>
|
||||
TermsApi.getTermsList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
trmsSeCd: params.trmsSeCd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
trmsSeCd,
|
||||
});
|
||||
setParams({ keyword, trmsSeCd, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setTrmsSeCd("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
trmsSeCd: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Terms) => {
|
||||
@@ -72,7 +79,7 @@ function TermsPage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { CodeApi } from "@/entities/code";
|
||||
import {
|
||||
UserApi,
|
||||
type UserListResponse,
|
||||
type UserSearchParams,
|
||||
} from "@/entities/user";
|
||||
import { UserApi, type UserListResponse } from "@/entities/user";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -35,34 +35,39 @@ type DateRange = {
|
||||
to?: Date | null;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keywordType: "all",
|
||||
keyword: "",
|
||||
userStateCode: "",
|
||||
aprvYn: "",
|
||||
grpId: "",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function parseDateRange(frDateYmd: string, toDateYmd: string): DateRange {
|
||||
return {
|
||||
from: frDateYmd ? dayjs(frDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
to: toDateYmd ? dayjs(toDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function UsersPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
const [keywordType, setKeywordType] = useState("all");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [userStateCode, setUserStateCode] = useState("");
|
||||
const [aprvYn, setAprvYn] = useState("");
|
||||
const [grpId, setGrpId] = useState("");
|
||||
|
||||
const [searchParams, setSearchParams] = useState<UserSearchParams>({
|
||||
keywordType: "all",
|
||||
keyword: "",
|
||||
userStateCode: "",
|
||||
aprvYn: "",
|
||||
grpId: "",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
||||
parseDateRange(params.frDateYmd, params.toDateYmd),
|
||||
);
|
||||
const [keywordType, setKeywordType] = useState(params.keywordType);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [userStateCode, setUserStateCode] = useState(params.userStateCode);
|
||||
const [aprvYn, setAprvYn] = useState(params.aprvYn);
|
||||
const [grpId, setGrpId] = useState(params.grpId);
|
||||
|
||||
const { data: userSttsStats } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"],
|
||||
@@ -70,13 +75,21 @@ function UsersPage() {
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, params],
|
||||
queryFn: () =>
|
||||
UserApi.findPageUser({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keywordType: params.keywordType,
|
||||
keyword: params.keyword,
|
||||
userStateCode: params.userStateCode,
|
||||
aprvYn: params.aprvYn,
|
||||
grpId: params.grpId,
|
||||
frDateYmd: params.frDateYmd,
|
||||
toDateYmd: params.toDateYmd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { data: userStateCodes } = useQuery({
|
||||
@@ -84,6 +97,16 @@ function UsersPage() {
|
||||
queryFn: () => CodeApi.getCode("USERSTTS"),
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleRowClick = (row: UserListResponse) => {
|
||||
router.push(`/users/${row.userUnqId}`);
|
||||
};
|
||||
@@ -97,7 +120,7 @@ function UsersPage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
@@ -105,14 +128,9 @@ function UsersPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
setParams({
|
||||
keywordType,
|
||||
keyword,
|
||||
userStateCode,
|
||||
@@ -122,6 +140,7 @@ function UsersPage() {
|
||||
? dayjs(dateRange.from).format("YYYYMMDD")
|
||||
: "",
|
||||
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
|
||||
page: 0,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -131,20 +150,12 @@ function UsersPage() {
|
||||
setUserStateCode("");
|
||||
setAprvYn("");
|
||||
setGrpId("");
|
||||
setPresetDateRange("");
|
||||
setDateRange({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keywordType: "all",
|
||||
keyword: "",
|
||||
userStateCode: "",
|
||||
aprvYn: "",
|
||||
grpId: "",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
@@ -351,12 +362,9 @@ function UsersPage() {
|
||||
<h3 className="text-2xl font-medium text-label">회원 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
defaultValue="regDt,desc"
|
||||
value={params.sort}
|
||||
onValueChange={(value) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sort: value,
|
||||
});
|
||||
setParams({ sort: value });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { useMutationWrapper } from "./use-mutation-wrapper";
|
||||
export { getCookie, removeCookie, setCookie } from "./cookie";
|
||||
export { useAlertStore } from "./use-alert-store";
|
||||
export { useDataTable } from "./use-data-table";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
export { useMutationWrapper } from "./use-mutation-wrapper";
|
||||
export { usePrompt } from "./use-prompt";
|
||||
export { useSearchParamsState } from "./use-search-params-state";
|
||||
export { useSelectPrompt } from "./use-select-prompt";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* 조회 조건을 URL searchParams와 동기화하는 훅.
|
||||
*
|
||||
* - URL이 단일 소스 오브 트루스(SSOT)이며, 새로고침·뒤로가기 시에도 조건이 유지된다.
|
||||
* - 기본값과 동일한 값은 URL에서 제거해 쿼리스트링을 깨끗하게 유지한다.
|
||||
* - `window.history.replaceState` 기반 shallow 갱신으로 서버 라운드트립 없이 동작한다.
|
||||
* - 숫자 기본값이 주어진 키는 자동으로 `Number` 파싱한다.
|
||||
*
|
||||
* @example
|
||||
* const { params, setParams, resetParams } = useSearchParamsState({
|
||||
* keyword: "",
|
||||
* page: 0,
|
||||
* size: 10,
|
||||
* });
|
||||
*/
|
||||
export function useSearchParamsState<T extends Record<string, ParamValue>>(
|
||||
defaults: T,
|
||||
): UseSearchParamsStateReturn<T> {
|
||||
const searchParams = useSearchParams();
|
||||
const defaultsRef = useRef(defaults);
|
||||
|
||||
const params = useMemo(
|
||||
() => parseParams(searchParams, defaultsRef.current),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const setParams = useCallback((partial: Partial<T>) => {
|
||||
const next = new URLSearchParams(window.location.search);
|
||||
for (const [key, value] of Object.entries(partial)) {
|
||||
if (value === undefined || value === defaultsRef.current[key]) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.set(key, String(value));
|
||||
}
|
||||
}
|
||||
replaceUrl(next);
|
||||
}, []);
|
||||
|
||||
const resetParams = useCallback(() => {
|
||||
const next = new URLSearchParams(window.location.search);
|
||||
for (const key of Object.keys(defaultsRef.current)) {
|
||||
next.delete(key);
|
||||
}
|
||||
replaceUrl(next);
|
||||
}, []);
|
||||
|
||||
return { params, setParams, resetParams };
|
||||
}
|
||||
|
||||
function parseParams<T extends Record<string, ParamValue>>(
|
||||
searchParams: ReturnType<typeof useSearchParams>,
|
||||
defaults: T,
|
||||
): T {
|
||||
const result = { ...defaults };
|
||||
|
||||
for (const key of Object.keys(defaults) as (keyof T)[]) {
|
||||
const raw = searchParams?.get(key as string) ?? null;
|
||||
if (raw === null) continue;
|
||||
|
||||
if (typeof defaults[key] === "number") {
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isNaN(parsed)) result[key] = parsed as T[keyof T];
|
||||
} else {
|
||||
result[key] = raw as T[keyof T];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function replaceUrl(searchParams: URLSearchParams) {
|
||||
const qs = searchParams.toString();
|
||||
const url = qs
|
||||
? `${window.location.pathname}?${qs}`
|
||||
: window.location.pathname;
|
||||
window.history.replaceState(null, "", url);
|
||||
}
|
||||
|
||||
type ParamValue = string | number | null;
|
||||
|
||||
interface UseSearchParamsStateReturn<T> {
|
||||
params: T;
|
||||
setParams: (partial: Partial<T>) => void;
|
||||
resetParams: () => void;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { toast } from "sonner";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { useClipboard } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { deleteReasonMessages } from "@/entities/post";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useMenuStore, useSelectPrompt, useUserStore } from "@/shared/lib";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { deleteReasonMessages } from "@/entities/post";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
type PostActionProps = {
|
||||
bbsId: string;
|
||||
@@ -21,11 +23,14 @@ const selectData = Object.entries(deleteReasonMessages).map(
|
||||
function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { copyText, copied } = useClipboard();
|
||||
const { prompt } = useSelectPrompt(selectData);
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const postPageLink = `/bbs/${bbsInfo?.bbsTypeCd.toLowerCase()}/${bbsInfo?.bbsId}`;
|
||||
const from = searchParams?.get("from");
|
||||
const backLink = from ?? postPageLink;
|
||||
|
||||
const onDelete = async () => {
|
||||
const { resolved, value } = await prompt("삭제 사유를 입력해주세요.");
|
||||
@@ -65,7 +70,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push(postPageLink)}
|
||||
onClick={() => router.push(backLink)}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
|
||||
@@ -18,8 +18,8 @@ NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:800
|
||||
NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
|
||||
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
|
||||
NEXT_PUBLIC_GATEWAY_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
CONTENTS_INTRO = 31
|
||||
CONTENTS_EXAMPLE = 32
|
||||
CONTENTS_INTRO = 24
|
||||
CONTENTS_EXAMPLE = 25
|
||||
|
||||
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
|
||||
|
||||
@@ -365,7 +365,7 @@ export default function SimilarCaseModal({
|
||||
</tr>
|
||||
<tr>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
총대장균군수<br />(개/100ml)
|
||||
총대장균군수<br />(개/100mL)
|
||||
</th>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
용도별 결합 잔류염소<br />(mg/L)
|
||||
|
||||
@@ -428,7 +428,7 @@ export default function DemandRegisterPage() {
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<FormRow label="요구수량(㎥/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} />
|
||||
</FormRow>
|
||||
<Divider />
|
||||
@@ -438,7 +438,7 @@ export default function DemandRegisterPage() {
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<FormRow label="희망단가(원/㎥)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
@@ -464,7 +464,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
leftLabel="총대장균군수(개/100㎖)"
|
||||
leftRequired
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function DemandStatusPage() {
|
||||
{ label: "요구시기", value: selected.detail.reqDate, required: true },
|
||||
{ label: "요구수량(㎡/d)", value: selected.detail.reqQty },
|
||||
{ label: "사용용도", value: selected.detail.usePurpose },
|
||||
{ label: "희망단가(원)", value: selected.detail.price },
|
||||
{ label: "희망단가(원/㎥)", value: selected.detail.price },
|
||||
];
|
||||
}, [selected]);
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function DemandStatusPage() {
|
||||
if (!selected) return [];
|
||||
const w = selected.detail.water;
|
||||
return [
|
||||
{ label: "총대장균군수(개/100mℓ)", value: w.coliform },
|
||||
{ label: "총대장균군수(개/100㎖)", value: w.coliform },
|
||||
{ label: "결합잔류염소(mg/L)", value: "-" },
|
||||
{ label: "탁도(NTU)", value: w.turbidity },
|
||||
|
||||
|
||||
@@ -44,17 +44,17 @@ export default function DemandRegisterPage() {
|
||||
const [coliform, setColiform] = useState<string>(""); // 총대장균군수
|
||||
const [noDetect, setNoDetect] = useState<boolean>(false);
|
||||
|
||||
const [tn, setTn] = useState<string>("");
|
||||
const [residualCl, setResidualCl] = useState<string>("");
|
||||
const [tp, setTp] = useState<string>("");
|
||||
const [turbidity, setTurbidity] = useState<string>("");
|
||||
const [ph, setPh] = useState<PHRange>({ min: "", max: "" });
|
||||
const [bod, setBod] = useState<string>("");
|
||||
const [chloride, setChloride] = useState<string>("");
|
||||
const [odor, setOdor] = useState<string>("불쾌하지 않을 것");
|
||||
const [ec, setEc] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("");
|
||||
const [remark, setRemark] = useState<string>("");
|
||||
const [tn, setTn] = useState<string>(""); // 총질소(T-N)
|
||||
const [residualCl, setResidualCl] = useState<string>(""); // 결합잔류염소
|
||||
const [tp, setTp] = useState<string>(""); // 총인(T-P)
|
||||
const [turbidity, setTurbidity] = useState<string>(""); // 탁도
|
||||
const [ph, setPh] = useState<PHRange>({ min: "", max: "" }); // 수소이온농도(pH)
|
||||
const [bod, setBod] = useState<string>(""); // 생물학적산소요구량(BOD)
|
||||
const [chloride, setChloride] = useState<string>(""); // 염화물
|
||||
const [odor, setOdor] = useState<string>("불쾌하지 않을 것"); // 냄새
|
||||
const [ec, setEc] = useState<string>(""); // 전기전도도
|
||||
const [color, setColor] = useState<string>(""); // 색도
|
||||
const [remark, setRemark] = useState<string>(""); // 비고
|
||||
|
||||
// 공급처 후보지 선택(필수아님)
|
||||
const [cand1, setCand1] = useState("");
|
||||
@@ -373,7 +373,8 @@ export default function DemandRegisterPage() {
|
||||
switch (value) {
|
||||
// 청소화장실용도
|
||||
case '01':
|
||||
setTp('0.2');
|
||||
setNoDetect(true); // 총대장균군수 불검출
|
||||
setResidualCl('0.2'); // 잔류염소
|
||||
setTurbidity('2');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
@@ -381,23 +382,35 @@ export default function DemandRegisterPage() {
|
||||
break;
|
||||
// 세척살수용도
|
||||
case '02':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setColiform('1000');
|
||||
setTurbidity('2'); // 탁도
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 조경용도
|
||||
case '03':
|
||||
setTp('0.2');
|
||||
setColiform('1000');
|
||||
setTurbidity('2');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setChloride('250');
|
||||
setChloride('250'); // 염화물
|
||||
break;
|
||||
// 지하수
|
||||
// 친수용도
|
||||
case '04':
|
||||
setNoDetect(true); // 총대장균군수 불검출
|
||||
setResidualCl('0.1'); // 잔류염소
|
||||
setTurbidity('2');
|
||||
setBod('3');
|
||||
setColor('10');
|
||||
setTn('10'); // 총질소
|
||||
setTp('0.5'); // 총인
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setChloride('0.5');
|
||||
break;
|
||||
// 지하수충전
|
||||
case '05':
|
||||
break;
|
||||
// 농업용도
|
||||
// 농업용수
|
||||
case '06':
|
||||
setTurbidity('5');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
@@ -405,16 +418,18 @@ export default function DemandRegisterPage() {
|
||||
setEc('2000');
|
||||
setFarmText("이하 (** 간접:2000,직접:700 )");
|
||||
break;
|
||||
// 하천 등 유지보수
|
||||
// 하천 등 유지용수
|
||||
case '07':
|
||||
setColiform('1000');
|
||||
setTn('10');
|
||||
setBod('5');
|
||||
setTn('20');
|
||||
setTp('0.5');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setChloride('0.5');
|
||||
break;
|
||||
// 공업
|
||||
// 공업용수
|
||||
case '08':
|
||||
setBod('20');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
break;
|
||||
default:
|
||||
@@ -467,7 +482,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<FormRow label="요구수량(㎥/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
|
||||
@@ -490,7 +505,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<FormRow label="희망단가(원/㎥)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
@@ -519,7 +534,7 @@ export default function DemandRegisterPage() {
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
leftLabel="총대장균군수(개/100㎖)"
|
||||
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -30,6 +30,19 @@ const formatValue = (value: string | number | null | undefined) => {
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatNumberWithComma = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
|
||||
const normalized =
|
||||
typeof value === "string" ? Number(value.replace(/,/g, "")) : Number(value);
|
||||
|
||||
if (Number.isNaN(normalized)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return normalized.toLocaleString("ko-KR");
|
||||
};
|
||||
|
||||
const formatBelow = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (Number(value) === -1) return "불검출";
|
||||
@@ -234,9 +247,9 @@ export default function DemandStatusPage() {
|
||||
|
||||
return [
|
||||
{ label: "요구시기", key: "dmndTmng", value: formatDate(selectedSdmInfoStatus.dmndTmng), required: true },
|
||||
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatValue(selectedSdmInfoStatus.dmndQty) },
|
||||
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatNumberWithComma(selectedSdmInfoStatus.dmndQty) },
|
||||
{ label: "사용용도", key: "useUsgCd", value: formatValue(selectedSdmInfoStatus.useUsgCd) },
|
||||
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatValue(selectedSdmInfoStatus.wntUntprc) },
|
||||
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatNumberWithComma(selectedSdmInfoStatus.wntUntprc) },
|
||||
];
|
||||
}, [selectedSdmInfoStatus]);
|
||||
|
||||
@@ -244,7 +257,7 @@ export default function DemandStatusPage() {
|
||||
if (!selectedSdmInfoStatus) return [];
|
||||
|
||||
return [
|
||||
{ label: "총대장균군수(개/100mL)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
|
||||
{ label: "총대장균군수(개/100㎖)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
|
||||
{ label: "결합잔류염소(mg/L)", key:"combResdch", value: formatBelow(selectedSdmInfoStatus.combResdch) },
|
||||
{ label: "탁도(NTU)", key:"trbt", value: formatBelow(selectedSdmInfoStatus.trbt) },
|
||||
|
||||
@@ -520,7 +533,7 @@ export default function DemandStatusPage() {
|
||||
|
||||
{!selectedSdmInfoStatus ? (
|
||||
<div className="mt-3 border border-neutral-200 px-6 py-10 text-center text-sm text-slate-400">
|
||||
상세 정보를 표시할 신청건이 없습니다.
|
||||
조회된 결과가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 border border-neutral-200">
|
||||
|
||||
@@ -17,7 +17,7 @@ type IntroCardItem = {
|
||||
|
||||
const INTRO_CARDS: IntroCardItem[] = [
|
||||
{
|
||||
title: "물 재이용 법",
|
||||
title: "물재이용법",
|
||||
desc: "물의 재이용 촉진 및 지원에 관한 법률을 시행하고, 일부 법률은 개정되었습니다.",
|
||||
image: card01,
|
||||
href: "/intro#law-plan",
|
||||
|
||||
@@ -770,7 +770,7 @@ export const SdmReducer = (set: any): SdmActions => {
|
||||
"/cms-service/api/v1/lwpo-cds/?lwpoCdNm=&sort=regDt,desc&upCd=WR018&page=0&size=100"
|
||||
);
|
||||
|
||||
const comUseTypes = upCd.content.filter(el=>el.lwpoCdNm != '친수용수').map((el: any) => ({ text: el.lwpoCdNm, val: el.lwpoCd }));
|
||||
const comUseTypes = upCd.content.map((el: any) => ({ text: el.lwpoCdNm, val: el.lwpoCd }));
|
||||
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
|
||||
@@ -83,14 +83,14 @@ const MOCK: Record<TabKey, CaseItem[]> = {
|
||||
sewage: [
|
||||
{
|
||||
id: "sewage-1",
|
||||
title: "구리시 하수 재이용",
|
||||
title: "구리시 하수처리수 재이용",
|
||||
desc: "고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화",
|
||||
imageSrc: "/images/landing/business/sewage/guri_01.png",
|
||||
href: "/example#example-sewage-guri",
|
||||
},
|
||||
{
|
||||
id: "sewage-2",
|
||||
title: "부천시 하수재이용",
|
||||
title: "부천시 하수처리수 재이용",
|
||||
desc: "굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공",
|
||||
imageSrc: "/images/landing/business/sewage/bucheon_01.png",
|
||||
href: "/example#example-sewage-bucheon",
|
||||
@@ -104,42 +104,42 @@ const MOCK: Record<TabKey, CaseItem[]> = {
|
||||
},
|
||||
{
|
||||
id: "sewage-4",
|
||||
title: "파주시 하수 재이용 시설",
|
||||
title: "파주시 하수처리수 재이용",
|
||||
desc: "운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)",
|
||||
imageSrc: "/images/landing/business/sewage/paju_01.png",
|
||||
href: "/example#example-sewage-paju",
|
||||
},
|
||||
{
|
||||
id: "sewage-5",
|
||||
title: "포항시 하수 재이용 시설",
|
||||
title: "포항시 하수처리수 재이용",
|
||||
desc: "포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등), 포항하수처리장 하수처리수를 재처리하여 공업용수로 공급",
|
||||
imageSrc: "/images/landing/business/sewage/pohang_01.png",
|
||||
href: "/example#example-sewage-pohang",
|
||||
},
|
||||
{
|
||||
id: "sewage-6",
|
||||
title: "울산광역시 하수 재이용 시설",
|
||||
title: "울산광역시 하수처리수 재이용",
|
||||
desc: "18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급",
|
||||
imageSrc: "/images/landing/business/sewage/ulsan_01.png",
|
||||
href: "/example#example-sewage-ulsan",
|
||||
},
|
||||
{
|
||||
id: "sewage-7",
|
||||
title: "여수시 하수 재이용 시설",
|
||||
title: "여수시 하수처리수 재이용",
|
||||
desc: "수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)",
|
||||
imageSrc: "/images/landing/business/sewage/yeosu_01.png",
|
||||
href: "/example#example-sewage-yeosu",
|
||||
},
|
||||
{
|
||||
id: "sewage-8",
|
||||
title: "보령시 하수 재이용 시설",
|
||||
title: "보령시 하수처리수 재이용",
|
||||
desc: "한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진, 하수처리수 재이용을 통한 수자원 절약 및 지역 물 부족 해소",
|
||||
imageSrc: "/images/landing/business/sewage/boryeong_01.png",
|
||||
href: "/example#example-sewage-boryeong",
|
||||
},
|
||||
{
|
||||
id: "sewage-9",
|
||||
title: "아산시 하수 재이용 시설",
|
||||
title: "아산시 하수처리수 재이용",
|
||||
desc: "신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모",
|
||||
imageSrc: "/images/landing/business/sewage/asan_01.png",
|
||||
href: "/example#example-sewage-asan",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
// },
|
||||
// {
|
||||
// name: "money",
|
||||
// label: "희망단가(원)",
|
||||
// label: "희망단가(원/㎥)",
|
||||
// formIndex: 0,
|
||||
// required: true,
|
||||
// component: (field) => (
|
||||
@@ -375,7 +375,7 @@
|
||||
// render={({ field }) =>
|
||||
// name === "money" ? (
|
||||
// <div>
|
||||
// <Label className="text-black">희망단가(원)*</Label>
|
||||
// <Label className="text-black">희망단가(원/㎥)*</Label>
|
||||
// {component(field)}
|
||||
// </div>
|
||||
// ) : (
|
||||
@@ -411,7 +411,7 @@
|
||||
// return (
|
||||
// <FormUi
|
||||
// label={
|
||||
// !isDisabled ? `총대장균군수(개/mg)*` : "총대장균군수(개/mg)"
|
||||
// !isDisabled ? `총대장균군수(개/100㎖)*` : "총대장균군수(개/100㎖)"
|
||||
// }
|
||||
// >
|
||||
// <div className="flex items-center gap-2">
|
||||
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 60 KiB |
@@ -237,7 +237,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
onSelectValue={(val) => setFlowPanel(val.toString())}
|
||||
className="mx-3 w-[150px]"
|
||||
label=""
|
||||
selectClass="w-full p-1 border rounded bg-white"
|
||||
selectClass="w-full rounded border border-input bg-form p-1"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -251,7 +251,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => handleStartDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={startDate}
|
||||
/>
|
||||
@@ -262,7 +262,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => handleEndDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={endDate}
|
||||
/>
|
||||
@@ -275,4 +275,4 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
</div>
|
||||
</ListDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
|
||||
return (
|
||||
<ListDiv className="!h-[100px]">
|
||||
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r">
|
||||
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0">
|
||||
<Table>
|
||||
<thead>
|
||||
<TableRow>
|
||||
@@ -237,7 +237,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
onSelectValue={(val) => setFlowPanel(val.toString())}
|
||||
className="mx-3 w-[150px]"
|
||||
label=""
|
||||
selectClass="w-full p-1 border rounded bg-white"
|
||||
selectClass="w-full rounded border border-input bg-form p-1"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -251,7 +251,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => handleStartDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
defaultMonth={startDate}
|
||||
/>
|
||||
</div>
|
||||
@@ -261,7 +261,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => handleEndDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
defaultMonth={endDate}
|
||||
/>
|
||||
</div>
|
||||
@@ -273,4 +273,4 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
</div>
|
||||
</ListDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,55 +5,55 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
return (
|
||||
layoutNum === 'A' ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : layoutNum === 'R' ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'유역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'유역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
@@ -99,22 +99,22 @@ export default function SearchInputText({label=undefined, setValue=()=>{}, sidoS
|
||||
|
||||
return (
|
||||
<div className={'relative'} ref={wrapperRef}>
|
||||
{label && <Label className="w-20 text-xl text-gray-600 justify-self-center">{label}</Label>}
|
||||
{label && <Label className="w-20 justify-self-center text-xl text-label">{label}</Label>}
|
||||
<input
|
||||
type="text"
|
||||
value={textValue}
|
||||
onChange={handleInputChange}
|
||||
onFocus={onInputFocus}
|
||||
className="w-[200px] ml-6 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="ml-6 w-[200px] rounded-md border border-input bg-form px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder={`${label}을(를) 입력하세요`}
|
||||
/>
|
||||
{isOpen && processFacilityList.length > 0 && (
|
||||
<div className="absolute z-10 w-[300px] px-2 py-1 !mt-5 ml-20 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto">
|
||||
<div className="absolute z-10 ml-20 !mt-5 h-[90px] w-[300px] overflow-auto rounded-md border border-border bg-popover px-2 py-1 text-popover-foreground shadow-lg">
|
||||
<div className="max-h-72">
|
||||
{processFacilityList.map((item : facilityListProps, index : number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full text-left pl-1 pr-10 py-2 hover:bg-gray-100 cursor-pointer overflow-auto"
|
||||
className="w-full cursor-pointer overflow-auto py-2 pl-1 pr-10 text-left hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => handleSuggestionClick(item.bscFctCd, item.bscFctNm, Number(item.fctCpc), item.trtFrfNm, item.efflWqltStndrdNm)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -17,7 +17,7 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
|
||||
<div className={"mt-"+divHeight}>
|
||||
{commentArray.map((item, index) => (
|
||||
<React.Fragment key={"comment" + index}>
|
||||
<span className="text-2xl font-bold text-gray-500">
|
||||
<span className="text-2xl font-bold text-muted-foreground">
|
||||
{commentStartPoint} {item}
|
||||
</span>
|
||||
<br/>
|
||||
@@ -28,4 +28,4 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
|
||||
}
|
||||
}
|
||||
|
||||
export default UnderComment;
|
||||
export default UnderComment;
|
||||
|
||||
@@ -127,15 +127,18 @@ const Map = () => {
|
||||
}
|
||||
const el = overlayContainerRef.current;
|
||||
el.className =
|
||||
"ol-custom-overlay shadow-xl rounded-2xl bg-white/95 backdrop-blur p-3 border border-gray-200 min-w-[220px] max-w-[350px] ";
|
||||
"ol-custom-overlay shadow-xl rounded-2xl backdrop-blur p-3 min-w-[220px] max-w-[350px]";
|
||||
el.style.backgroundColor = "color-mix(in srgb, var(--popover) 95%, transparent)";
|
||||
el.style.color = "var(--popover-foreground)";
|
||||
el.style.border = "1px solid var(--border)";
|
||||
el.innerHTML = `
|
||||
<div class="font-semibold text-lg text-gray-800 mb-2 text-center bg-green-400">${title}</div>
|
||||
<table class="w-full text-base text-gray-800">
|
||||
<div class="mb-2 rounded-md px-2 py-1 text-center text-lg font-semibold" style="background-color: var(--secondary); color: var(--secondary-foreground);">${title}</div>
|
||||
<table class="w-full text-base" style="color: var(--foreground);">
|
||||
${Object.entries(props)
|
||||
.map(
|
||||
([k, v]) => `
|
||||
<tr>
|
||||
<td class="py-1 pr-2 text-gray-500 whitespace-nowrap">${k}</td>
|
||||
<td class="whitespace-nowrap py-1 pr-2" style="color: var(--muted-foreground);">${k}</td>
|
||||
<td class="py-1 font-medium">${safe(v)}</td>
|
||||
</tr>`
|
||||
)
|
||||
|
||||
@@ -54,23 +54,23 @@ export default function DrainageEquipment({searchYear = '', fcltyList = []}: Dra
|
||||
const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
|
||||
return (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'part1' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> :
|
||||
head.id === 'part2' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> :
|
||||
head.id == 'nowSum' ? <TableHead rowSpan={2} colSpan={3} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'현재\n(기준년도)'}</TableHead> :
|
||||
head.id == 'stepOneSum' ? <TableHead rowSpan={1} colSpan={12} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'변경(금회계획)'}</TableHead> :
|
||||
head.id == 'etc' ? <TableHead rowSpan={3} key={head.id}>비고</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'stepOneSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>1단계</TableHead> :
|
||||
head.id === 'stepTwoSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>2단계</TableHead> :
|
||||
head.id === 'stepThreeSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>3단계</TableHead> :
|
||||
head.id === 'stepFourSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>4단계</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function DashBoardChartModal({item='', period='', title='',modalD
|
||||
if (label) {
|
||||
config[label] = {
|
||||
label: label,
|
||||
color : "hsl(var(--chart-1))"
|
||||
color : "var(--chart-1)"
|
||||
};
|
||||
}
|
||||
return config;
|
||||
|
||||
@@ -32,12 +32,12 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s
|
||||
return ( <>
|
||||
<TableRow>{
|
||||
upHeadList.map((head: TableUpperProps) =>
|
||||
(!head.upSequnce?<TableHead rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={7} style={{ whiteSpace: 'pre-line' }}>{'시설계획\n(금회)'}</TableHead>:null))}
|
||||
(!head.upSequnce?<TableHead className="fo-facility-info-header-surface" rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe'&& head.upSequnce=== 1?<TableHead className="fo-facility-info-header-surface" key={head.id} rowSpan={1} colSpan={7} style={{ whiteSpace: 'pre-line' }}>{'시설계획\n(금회)'}</TableHead>:null))}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{upHeadList.map((head: TableUpperProps, index: number) =>
|
||||
(head.upName === 'upChangeRe'?<TableHead key={head.id} style={{ whiteSpace: 'pre-line' }}>{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}</TableHead>:null))}
|
||||
(head.upName === 'upChangeRe'?<TableHead className="fo-facility-info-header-surface" key={head.id} style={{ whiteSpace: 'pre-line' }}>{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}</TableHead>:null))}
|
||||
</TableRow>
|
||||
</> )
|
||||
}
|
||||
@@ -52,20 +52,20 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s
|
||||
const headMakeColSpanOperationStatus = (upHeadList: TableUpperProps[], searchYear: string) => {
|
||||
return ( <>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
|
||||
<TableHead className="fo-facility-info-header-surface" rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>{
|
||||
upHeadList.map((head: TableUpperProps) =>
|
||||
(head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>:
|
||||
!head.upSequnce?<TableHead style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>:
|
||||
head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))}
|
||||
(head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>:
|
||||
!head.upSequnce?<TableHead className="fo-facility-info-header-surface" style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>:
|
||||
head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))}
|
||||
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead className="fo-facility-info-header-surface" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))}
|
||||
</TableRow>
|
||||
</> )
|
||||
}
|
||||
@@ -239,18 +239,18 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
:((index === 6 || index === 7 || index === 8) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree' || head.id === 'stepFour'))?
|
||||
null
|
||||
:((index === 5) && (head.id === 'provisionalApproval' || head.id === 'stepTwo'))?
|
||||
<TableCell colSpan={3} className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
<TableCell colSpan={3} className="fo-facility-info-header-surface text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:((index === 5) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree'|| head.id === 'stepFour'))?
|
||||
null
|
||||
:(index < 5 || index > 5)&& (index!= 4) && (head.id ==='provisionalApproval'||head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour')
|
||||
?<TableCell className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:(head.id === "provisionalApproval" && index === 4)
|
||||
?<TableCell colSpan={6} className="text-right border border-[var(--border)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)}</TableCell>
|
||||
?<TableCell colSpan={6} className="text-right border border-border text-label" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)}</TableCell>
|
||||
:((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') )
|
||||
?null
|
||||
:(head.id === 'item' && index != 5)?
|
||||
<TableCell className="text-center text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:<TableCell className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
<TableCell className="text-center text-label" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:<TableCell className="fo-facility-info-header-surface text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -278,15 +278,15 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
(head.id === 'rdiRain'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el['rdiRain']}</TableCell>:
|
||||
(head.id === 'rdiRain'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}></TableCell>:
|
||||
(head.id === 'waterQlty')?<TableCell className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="bg-green-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="bg-yellow-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="bg-green-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="bg-blue-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="bg-green-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
@@ -60,7 +60,14 @@ export default function DashBoardChart({searchYear = '', fcltyList = []}: facili
|
||||
/**
|
||||
* 차트 Config
|
||||
*/
|
||||
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#00b50c", "#D4A5A5"];
|
||||
const colors = [
|
||||
"var(--chart-5)",
|
||||
"var(--chart-2)",
|
||||
"var(--chart-1)",
|
||||
"var(--chart-3)",
|
||||
"var(--primary)",
|
||||
"var(--chart-6)",
|
||||
];
|
||||
|
||||
//유입하수량-강수량
|
||||
const chartConfig_inflowRain = {
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function RainManageTab({ searchYear = '', fcltyList = [] }: facil
|
||||
widthFixed={true}
|
||||
>
|
||||
</UiTable>
|
||||
<span className="text-2xl font-bold text-gray-500">☞ 일수에 강우영향일 포함됨</span>
|
||||
<span className="text-2xl font-bold text-muted-foreground">☞ 일수에 강우영향일 포함됨</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 대시보드 - 표로보기 상단구역 페에지
|
||||
* 대시보드 - 표로보기 상단구역 페이지
|
||||
*
|
||||
* - 대시보드 - 표로보기의 3개의 탭 상단에 조회년도, 행정정보, 시설명의 구역을 컴포넌트
|
||||
*/
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => handleStartDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={startDate}
|
||||
/>
|
||||
@@ -209,7 +209,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => handleEndDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={endDate}
|
||||
/>
|
||||
@@ -239,4 +239,4 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
|
||||
</ListDiv>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,27 +17,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
return (
|
||||
layoutNum === 0 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -45,27 +45,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 1 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -73,29 +73,29 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 2 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
}else if(head.upName==="upChangeRe2"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -103,25 +103,25 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 3 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe2"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -129,23 +129,23 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 4 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={4} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={4} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -153,21 +153,21 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 5 ? ( // 총인설비 수질현황 헤더
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="w-[180px] bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="w-[180px] bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1" && head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe2" && head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
@@ -237,7 +237,7 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
|
||||
const raw = row.infoRnDay;
|
||||
const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw);
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return value > 3 ? "bg-slate-50" : "";
|
||||
return value > 3 ? "fo-rain-impact-row" : "";
|
||||
},
|
||||
[activeTab],
|
||||
);
|
||||
|
||||
@@ -5,77 +5,77 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
return (
|
||||
layoutNum === 0 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={19}>{'시설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={19}>{'시설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => (
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : layoutNum === 1 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={3}>{'유역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={3}>{'유역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => (
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'환경청'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'환경청'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' ||head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
@@ -84,4 +84,4 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
);
|
||||
}
|
||||
|
||||
export { HeadMakeColSpan };
|
||||
export { HeadMakeColSpan };
|
||||
|
||||
@@ -116,7 +116,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
|
||||
<SelectBox
|
||||
labelClass="w-[40px]"
|
||||
className="mr-10 w-[200px]"
|
||||
selectClass="rounded-lg bg-white"
|
||||
selectClass="rounded-lg"
|
||||
label="시도"
|
||||
selectArray={sidoData}
|
||||
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
|
||||
@@ -125,7 +125,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
|
||||
<SelectBox
|
||||
labelClass="w-[65px]"
|
||||
className="mr-10 w-[200px]"
|
||||
selectClass="rounded-lg bg-white"
|
||||
selectClass="rounded-lg"
|
||||
label="시군구"
|
||||
selectArray={filteredSigunData}
|
||||
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
|
||||
@@ -134,7 +134,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
|
||||
<SelectBox
|
||||
labelClass="w-[140px]"
|
||||
className="mr-3 w-[250px]"
|
||||
selectClass="rounded-lg bg-white"
|
||||
selectClass="rounded-lg"
|
||||
label="하수처리시설"
|
||||
selectArray={facilityData}
|
||||
onSelectValue={(val) => onSelectValue(val.toString(), "fcltCd")}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# apps/fo 테마 적용 작업계획
|
||||
|
||||
작성일: 2026-06-25
|
||||
대상 앱: `D:\iipbs\frontend\apps\fo`
|
||||
|
||||
## 1. 목적
|
||||
|
||||
- `apps/fo` 프로그램 페이지 내부에 직접 박혀 있는 색상 하드코딩을 식별한다.
|
||||
- 가능한 범위는 `packages/common/assets/src/styles/base.css`의 CSS 변수 영향권으로 옮긴다.
|
||||
- 즉 `bg-white`, `text-gray-*`, `border-gray-*`, `bg-blue-*`, `bg-green-*`, `bg-red-*` 같은 직접 색상 표현을 `background`, `foreground`, `label`, `border`, `secondary`, `accent`, `primary`, `destructive` 계열 토큰으로 치환한다.
|
||||
|
||||
## 2. 전제
|
||||
|
||||
- `apps/fo`는 [tailwind.config.ts](D:/iipbs/frontend/apps/fo/tailwind.config.ts:1)에서 공통 preset을 사용한다.
|
||||
- 공통 토큰은 [packages/common/assets/src/styles/base.css](D:/iipbs/frontend/packages/common/assets/src/styles/base.css:1)에 정의되어 있다.
|
||||
- `packages/common/components`, `packages/common/assets`는 기준 레이어이며, 앱 작업은 이 기준을 따르는 방향으로 진행한다.
|
||||
- 단, 이번 요청의 1차 목표는 `apps/fo` 내부 코드에 박혀 있는 색상을 줄이는 것이다.
|
||||
|
||||
## 3. 확인된 하드코딩 유형
|
||||
|
||||
현재 `apps/fo`에서 확인된 대표 패턴은 아래와 같다.
|
||||
|
||||
- 검색 입력/선택 UI
|
||||
- `apps/fo/app/components/common/SearchInput.tsx`
|
||||
- `apps/fo/app/pages/map/searchBar/searchBar.tsx`
|
||||
- `apps/fo/app/components/common/FacilityTermModalHeader.tsx`
|
||||
- `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx`
|
||||
- 대시보드/안내문구
|
||||
- `apps/fo/app/components/common/UnderComment.tsx`
|
||||
- `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx`
|
||||
- 대시보드 표 셀 상태색
|
||||
- `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx`
|
||||
이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다.
|
||||
- 지도 오버레이/팝업
|
||||
- `apps/fo/app/pages/(map-layout)/map/page.tsx`
|
||||
- 테스트/샘플성 버튼
|
||||
- `apps/fo/app/pages/(with-layout)/test/page.tsx`
|
||||
- 모달 입력창
|
||||
- `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx`
|
||||
|
||||
## 4. 기본 원칙
|
||||
|
||||
### 4.1 바로 토큰 치환 가능한 경우
|
||||
|
||||
아래는 우선 토큰으로 바로 바꾼다.
|
||||
|
||||
- `bg-white` -> `bg-background` 또는 `bg-card` 또는 `bg-popover`
|
||||
- `text-gray-900`, `text-gray-800` -> `text-foreground`
|
||||
- `text-gray-700`, `text-gray-600` -> `text-label` 또는 `text-foreground`
|
||||
- `text-gray-500` -> `text-muted-foreground` 또는 `text-placeholder`
|
||||
- `border-gray-200`, `border-gray-300` -> `border-border`
|
||||
- `focus:ring-blue-500` -> `focus:ring-ring`
|
||||
- `text-blue-500`, `text-blue-600` -> `text-primary`
|
||||
- `bg-blue-500`, `bg-blue-600` -> `bg-primary`
|
||||
- `text-red-500`, `text-red-600` -> `text-destructive`
|
||||
- `bg-red-50` 등 경고 계열 -> `bg-accent` 또는 별도 위험 상태 규칙 검토
|
||||
|
||||
### 4.2 바로 토큰 치환하면 안 되는 경우
|
||||
|
||||
아래는 의미색일 가능성이 높아 바로 토큰화하지 않는다.
|
||||
|
||||
- 범례 색상
|
||||
- 데이터 구분용 heatmap/구간 색상
|
||||
- 수질/계절/유형별 비교 색상
|
||||
- 지도 오버레이에서 정보 카테고리 의미를 나타내는 색
|
||||
|
||||
이 경우는 다음 중 하나로 처리한다.
|
||||
|
||||
- 유지
|
||||
- 별도 의미 클래스 부여
|
||||
- 앱 전용 CSS로 상태 클래스를 만든 뒤 theme-safe surface만 토큰화
|
||||
|
||||
## 5. 우선순위
|
||||
|
||||
### 5.1 1순위: 일반 UI surface
|
||||
|
||||
가장 먼저 처리한다.
|
||||
|
||||
- 검색 입력창 배경/보더/텍스트
|
||||
- 검색 결과 dropdown 배경/hover
|
||||
- 팝업/모달 wrapper 배경/보더
|
||||
- 제목/라벨/안내문구 색
|
||||
- 버튼의 직접 blue/red/white 색상
|
||||
|
||||
이 영역은 theme 적용 체감이 가장 크고, 토큰 치환도 안전하다.
|
||||
|
||||
### 5.2 2순위: 테이블/리스트
|
||||
|
||||
- `selectedFcltList`
|
||||
- `facilityInfo`
|
||||
- modal 내부 표
|
||||
- overlay 내 표 구조
|
||||
|
||||
이 영역은 `bg-secondary`, `bg-accent`, `text-secondary-foreground`, `border-border` 계열로 정리한다.
|
||||
|
||||
### 5.3 3순위: 의미색 데이터 표현
|
||||
|
||||
- `facilityInfo`의 계절/항목별 `bg-green-*`, `bg-blue-*`
|
||||
- 지도 범례 색상
|
||||
- 지도 feature overlay 강조색
|
||||
- 대시보드 표 셀 상태색
|
||||
- `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx`
|
||||
이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다.
|
||||
|
||||
이 영역은 “없애는 것”보다 “역할을 분리하는 것”이 중요하다.
|
||||
|
||||
## 6. 실제 작업 순서
|
||||
|
||||
1. `apps/fo` 내부 하드코딩 색상 파일 목록 확정
|
||||
2. 공통 UI와 데이터 의미색을 분리
|
||||
3. 일반 UI 색상부터 `base.css` 토큰으로 치환
|
||||
4. 필요 시 `apps/fo/app/styles/*.css`에 앱 전용 의미 클래스 추가
|
||||
5. `dashboard`, `map`, `modal`, `search` 대표 화면 점검
|
||||
6. 남은 의미색 영역은 별도 상태색 정책으로 정리
|
||||
|
||||
## 7. 권장 치환 전략
|
||||
|
||||
### 7.1 JSX 안에서 바로 치환
|
||||
|
||||
아래처럼 className을 직접 토큰 기반으로 변경한다.
|
||||
|
||||
- `bg-white` -> `bg-background`
|
||||
- `text-gray-600` -> `text-label`
|
||||
- `border-gray-300` -> `border-border`
|
||||
|
||||
이 방식은 빠르지만 반복이 많으면 유지보수성이 떨어진다.
|
||||
|
||||
### 7.2 반복되는 UI는 앱 전용 클래스 도입
|
||||
|
||||
예:
|
||||
|
||||
- `fo-search-panel`
|
||||
- `fo-search-input`
|
||||
- `fo-popup-card`
|
||||
- `fo-empty-text`
|
||||
|
||||
반복되는 구조는 `apps/fo/app/styles/components*.css`에 올리고, JSX에서는 의미 클래스만 사용한다.
|
||||
|
||||
이 방식이 장기적으로 더 좋다.
|
||||
|
||||
## 8. 예상 주의사항
|
||||
|
||||
- `h-90` 같은 Tailwind v3 미지원 클래스가 섞여 있을 수 있다.
|
||||
- `bg-white`를 `bg-background`로 바꾸면 black theme에서 contrast가 크게 달라질 수 있으므로 텍스트 색도 같이 바꿔야 한다.
|
||||
- `bg-blue-*`, `bg-green-*`는 실제 status color인지 단순 강조색인지 구분하고 바꿔야 한다.
|
||||
- 지도 오버레이는 HTML string으로 만드는 부분이 있으면 className 치환만으로 끝나지 않을 수 있다.
|
||||
|
||||
## 9. 1차 적용 추천 대상
|
||||
|
||||
우선 아래 파일부터 적용하는 것이 효율적이다.
|
||||
|
||||
1. `apps/fo/app/components/common/SearchInput.tsx`
|
||||
2. `apps/fo/app/pages/map/searchBar/searchBar.tsx`
|
||||
3. `apps/fo/app/components/common/UnderComment.tsx`
|
||||
4. `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx`
|
||||
5. `apps/fo/app/components/common/FacilityTermModalHeader.tsx`
|
||||
6. `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx`
|
||||
7. `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx`
|
||||
|
||||
## 10. 결론
|
||||
|
||||
- `apps/fo`도 `apps/wr`와 동일하게 `base.css` 토큰 중심으로 정리 가능하다.
|
||||
- 1차는 일반 UI 하드코딩 제거에 집중하고, 데이터 의미색은 유지 또는 별도 정책으로 분리하는 것이 맞다.
|
||||
- 가장 안전한 방향은 “반복 UI는 앱 전용 의미 클래스”, “일회성은 JSX에서 직접 토큰 치환”의 혼합 방식이다.
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "./styles/components-black.css";
|
||||
import "./styles/components-blue.css";
|
||||
import "./styles/components-green.css";
|
||||
import "./styles/components-red.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 20%, var(--secondary));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 20%, var(--secondary));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 20%, var(--secondary));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--secondary));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--secondary));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--secondary));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 36%, var(--secondary));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 36%, var(--secondary));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 36%, var(--secondary));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 16%, var(--secondary));
|
||||
--fo-facility-info-header-bg: #5e7b9c;
|
||||
--fo-facility-info-header-fg: #ffffff;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 16%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 16%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 16%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 24%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 24%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 24%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 32%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 32%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 32%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 12%, var(--background));
|
||||
--fo-facility-info-header-bg: #eaf3fb;
|
||||
--fo-facility-info-header-fg: #647784;
|
||||
--fo-facility-info-header-border: #e5e5e5;
|
||||
|
||||
.card-border {
|
||||
@apply border-[#d8e3eb] bg-white shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#d8e3eb];
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply text-[#4b5563];
|
||||
}
|
||||
|
||||
.card.card-border {
|
||||
box-shadow: 0 1px 8px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.card.card-border .card.card-border {
|
||||
@apply border-[#dde6ed] bg-white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card.card-border input,
|
||||
.card.card-border button[role="combobox"] {
|
||||
@apply border-[#d8e3eb] bg-white text-[#737373];
|
||||
}
|
||||
|
||||
.card.card-border label {
|
||||
@apply text-[#666666];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
nav[aria-label="pagination"] li a {
|
||||
border-color: transparent !important;
|
||||
background-color: transparent !important;
|
||||
color: #0a0a0a !important;
|
||||
}
|
||||
|
||||
nav[aria-label="pagination"] li a:hover {
|
||||
background-color: #f6f6f6 !important;
|
||||
color: #0a0a0a !important;
|
||||
}
|
||||
|
||||
nav[aria-label="pagination"] li a[aria-current="page"] {
|
||||
border-color: #2094fa !important;
|
||||
background-color: #2094fa !important;
|
||||
color: #fafafa !important;
|
||||
}
|
||||
|
||||
nav[aria-label="pagination"] li a[aria-label="Go to previous page"],
|
||||
nav[aria-label="pagination"] li a[aria-label="Go to next page"] {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
color: #666666 !important;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 15%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 15%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 15%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 23%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 23%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 23%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 31%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 31%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 31%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-3) 12%, var(--background));
|
||||
--fo-facility-info-header-bg: #ebf7f7;
|
||||
--fo-facility-info-header-fg: #64908f;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 14%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 14%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 14%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 22%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 22%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 22%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 30%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 30%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 30%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--destructive) 12%, var(--background));
|
||||
--fo-facility-info-header-bg: #f6f6f9;
|
||||
--fo-facility-info-header-fg: #808080;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,22 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
:root {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 18%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 18%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 18%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 38%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 38%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 38%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 10%, var(--background));
|
||||
--fo-facility-info-header-bg: #eaf3fb;
|
||||
--fo-facility-info-header-fg: #647784;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
@@ -14,13 +30,63 @@
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
.fo-facility-info-flow-year,
|
||||
.fo-facility-info-water-year,
|
||||
.fo-facility-info-phosphorus-year,
|
||||
.fo-facility-info-flow-summer,
|
||||
.fo-facility-info-water-summer,
|
||||
.fo-facility-info-phosphorus-summer,
|
||||
.fo-facility-info-flow-winter,
|
||||
.fo-facility-info-water-winter,
|
||||
.fo-facility-info-phosphorus-winter {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.fo-facility-info-flow-year {
|
||||
background-color: var(--fo-facility-info-flow-year-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-water-year {
|
||||
background-color: var(--fo-facility-info-water-year-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-phosphorus-year {
|
||||
background-color: var(--fo-facility-info-phosphorus-year-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-flow-summer {
|
||||
background-color: var(--fo-facility-info-flow-summer-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-water-summer {
|
||||
background-color: var(--fo-facility-info-water-summer-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-phosphorus-summer {
|
||||
background-color: var(--fo-facility-info-phosphorus-summer-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-flow-winter {
|
||||
background-color: var(--fo-facility-info-flow-winter-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-water-winter {
|
||||
background-color: var(--fo-facility-info-water-winter-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-phosphorus-winter {
|
||||
background-color: var(--fo-facility-info-phosphorus-winter-bg);
|
||||
}
|
||||
|
||||
.fo-rain-impact-row td {
|
||||
background-color: var(--fo-rain-impact-row-bg);
|
||||
}
|
||||
|
||||
|
||||
.fo-facility-info-header-surface {
|
||||
border: 1px solid var(--fo-facility-info-header-border);
|
||||
background-color: var(--fo-facility-info-header-bg);
|
||||
color: var(--fo-facility-info-header-fg);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "./styles/components-black.css";
|
||||
import "./styles/components-blue.css";
|
||||
import "./styles/components-green.css";
|
||||
import "./styles/components-red.css";
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,6 @@
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ NEXT_PUBLIC_PROXY_HOST=http://localhost:3001
|
||||
SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_SITE_ID=ST
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_ST_DASHBOARD_BBS_ID=13
|
||||
@@ -1,54 +1,32 @@
|
||||
"use client";
|
||||
|
||||
|
||||
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
|
||||
getTabStoreState as getGridTabStoreState,
|
||||
|
||||
useTabStore as gridTabStore,
|
||||
|
||||
} from "@rsone/components";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
|
||||
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
|
||||
import {
|
||||
menuMatchesStatisticsTabId,
|
||||
resolveStatisticsTabId,
|
||||
} from "@/shared/lib/statistics-menu-url";
|
||||
import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-sync";
|
||||
|
||||
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
|
||||
|
||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||
|
||||
import { tokenManager } from "@/entities/auth";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
import { MenuApi } from "@/entities/menu";
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
|
||||
import { getConfig } from "@common/config";
|
||||
|
||||
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
|
||||
|
||||
import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport";
|
||||
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
const appConfig = getConfig();
|
||||
|
||||
@@ -58,42 +36,23 @@ const appSiteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const isLoggedIn = !!userId;
|
||||
|
||||
|
||||
|
||||
function WithBaseLayoutInner({
|
||||
|
||||
children,
|
||||
|
||||
}: {
|
||||
|
||||
children: React.ReactNode;
|
||||
|
||||
}) {
|
||||
|
||||
function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
|
||||
/** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */
|
||||
|
||||
const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] =
|
||||
|
||||
useState(false);
|
||||
|
||||
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore(
|
||||
|
||||
(state) => state.setCurrentMenuInfo,
|
||||
|
||||
);
|
||||
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
|
||||
|
||||
const addTab = useTabStore((state) => state.addTab);
|
||||
|
||||
const setActiveTab = useTabStore((state) => state.setActiveTab);
|
||||
@@ -108,16 +67,11 @@ function WithBaseLayoutInner({
|
||||
|
||||
const addTabForGrid = gridTabStore((state) => state.addTab);
|
||||
|
||||
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
|
||||
|
||||
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
|
||||
|
||||
const { syncTabToUrl } = useStatisticsTabUrlSync({
|
||||
|
||||
pathname,
|
||||
|
||||
menuList,
|
||||
@@ -129,41 +83,27 @@ function WithBaseLayoutInner({
|
||||
addTabForGrid,
|
||||
|
||||
setCurrentMenuInfo,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const { data: menuListData } = useQuery({
|
||||
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
|
||||
queryFn: () => MenuApi.getMenuRoles(appSiteId),
|
||||
|
||||
enabled: isLoggedIn,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (menuListData) {
|
||||
|
||||
setMenuList(menuListData);
|
||||
|
||||
setMenuListForGrid(menuListData);
|
||||
|
||||
}
|
||||
|
||||
}, [menuListData, setMenuList, setMenuListForGrid]);
|
||||
|
||||
|
||||
|
||||
// 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!menuList) return;
|
||||
|
||||
const appTabs = getTabStoreState().tabs;
|
||||
@@ -172,20 +112,13 @@ function WithBaseLayoutInner({
|
||||
|
||||
if (getGridTabStoreState().tabs.length > 0) return;
|
||||
|
||||
|
||||
|
||||
for (const tab of appTabs) {
|
||||
|
||||
getGridTabStoreState().addTab(tab.menuItem);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const activeId = getTabStoreState().activeTabId;
|
||||
|
||||
if (activeId) {
|
||||
|
||||
setActiveTab(activeId);
|
||||
|
||||
const activeTab = appTabs.find((t) => t.id === activeId);
|
||||
@@ -193,155 +126,104 @@ function WithBaseLayoutInner({
|
||||
const gridActiveId = activeTab?.menuItem.menuEngNm;
|
||||
|
||||
if (gridActiveId) {
|
||||
|
||||
getGridTabStoreState().setActiveTab(gridActiveId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}, [menuList, tabs, setActiveTab]);
|
||||
|
||||
|
||||
|
||||
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
|
||||
|
||||
const currentPath = useMemo(() => {
|
||||
|
||||
if (tabs.length === 0) return "";
|
||||
|
||||
|
||||
|
||||
if (activeTabId && menuList) {
|
||||
|
||||
// menuList에서 활성 탭의 메뉴 찾기
|
||||
|
||||
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
|
||||
|
||||
for (const item of items) {
|
||||
|
||||
if (item.urlPath === activeTabId) {
|
||||
|
||||
if (menuMatchesStatisticsTabId(item, activeTabId)) {
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
|
||||
const found = findMenuByEngNm(item.children);
|
||||
|
||||
if (found) return found;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const activeMenu = findMenuByEngNm(menuList);
|
||||
|
||||
|
||||
|
||||
if (activeMenu?.urlPath) {
|
||||
|
||||
return activeMenu.urlPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 탭에서 직접 찾기
|
||||
|
||||
if (activeTabId) {
|
||||
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
|
||||
const activeTab = currentTabs.find((t) => t.id === activeTabId);
|
||||
|
||||
if (activeTab?.menuItem?.urlPath) {
|
||||
|
||||
return activeTab.menuItem.urlPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// currentMenuInfo에서 가져오기
|
||||
|
||||
if (currentMenuInfo?.urlPath) {
|
||||
|
||||
return currentMenuInfo.urlPath;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 기본값으로 pathname 사용
|
||||
|
||||
return pathname ?? "";
|
||||
|
||||
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
|
||||
|
||||
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
|
||||
(item: MenuItem) => {
|
||||
if (item.urlPath.includes("/cms/")) return;
|
||||
|
||||
setCurrentMenuInfo(item);
|
||||
|
||||
|
||||
|
||||
if (isVerificationSelectionReportMenu(item)) {
|
||||
|
||||
setReportPanelOpen(true);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (isBbsNoticeMenu(item)) {
|
||||
if (item.urlPath) {
|
||||
router.push(item.urlPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
|
||||
if (addTabResult) {
|
||||
|
||||
addTabForGrid(item);
|
||||
|
||||
if (item.urlPath) {
|
||||
|
||||
syncTabToUrl(item.urlPath);
|
||||
|
||||
syncTabToUrl(resolveStatisticsTabId(item.urlPath));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!addTabResult) {
|
||||
|
||||
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
|
||||
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
[
|
||||
|
||||
setCurrentMenuInfo,
|
||||
|
||||
addTab,
|
||||
@@ -354,70 +236,38 @@ function WithBaseLayoutInner({
|
||||
|
||||
syncTabToUrl,
|
||||
|
||||
router,
|
||||
],
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<BaseLayout
|
||||
|
||||
queryClient={queryClient}
|
||||
|
||||
tokenManager={tokenManager}
|
||||
|
||||
currentPath={currentPath}
|
||||
|
||||
handleMenuClick={handleMenuClick}
|
||||
|
||||
>
|
||||
|
||||
<div className="flex flex-1 h-full">
|
||||
|
||||
<div className="flex h-full flex-1">
|
||||
{isVerificationSelectionReportPanelOpen && (
|
||||
|
||||
<VerificationSelectionReport
|
||||
|
||||
onClose={() => setReportPanelOpen(false)}
|
||||
|
||||
/>
|
||||
|
||||
)}
|
||||
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
|
||||
</div>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function WithBaseLayout({
|
||||
|
||||
children,
|
||||
|
||||
}: {
|
||||
|
||||
children: React.ReactNode;
|
||||
|
||||
}) {
|
||||
|
||||
return (
|
||||
|
||||
<Suspense fallback={null}>
|
||||
|
||||
<WithBaseLayoutInner>{children}</WithBaseLayoutInner>
|
||||
|
||||
</Suspense>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
/* 셀 클래스가 있으면 위 기본색보다 우선 (같은 specificity + 나중 규칙) */
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
|
||||
@apply !bg-[#f9f8f8] !text-[#666];
|
||||
@apply !bg-[#f0f0f0] !text-[#666];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
|
||||
@apply !bg-[#ffe6e6] !text-[#c00];
|
||||
|
||||
@@ -43,8 +43,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
|
||||
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0;
|
||||
const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0;
|
||||
const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0;
|
||||
const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0;
|
||||
const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0;
|
||||
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) {
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) {
|
||||
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
|
||||
import {
|
||||
getExcelImportColumnFieldNames,
|
||||
prepareGridColumnsForExcelExport,
|
||||
restoreGridColumnsAfterExcelExport,
|
||||
shouldPrepareColumnsForExcelExport,
|
||||
@@ -33,7 +34,7 @@ declare module "realgrid" {
|
||||
setOptions(options: {
|
||||
footer?: { visible?: boolean };
|
||||
summaryMode?: "aggregate" | "statistical" | "none";
|
||||
checkBar?: { visible?: boolean };
|
||||
checkBar?: { visible?: boolean; exclusive?: boolean };
|
||||
}): void;
|
||||
}
|
||||
}
|
||||
@@ -457,10 +458,31 @@ function sortRowsGroupPreserving(
|
||||
}
|
||||
|
||||
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
|
||||
function getDecimalPlacesFromDataFormat(dataFormat?: string): number | null {
|
||||
if (!dataFormat) return null;
|
||||
const match = dataFormat.match(/\.(0+|#+)/);
|
||||
const decimalToken = match?.[1];
|
||||
if (decimalToken) return decimalToken.length;
|
||||
// #,##0 등 정수 포맷 — 소수부 없음 → 0자리 절삭(내림)
|
||||
if (/[#0]/.test(dataFormat)) return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
function truncateByDecimalPlaces(value: number, decimalPlaces: number): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
if (decimalPlaces < 0) return value;
|
||||
const factor = 10 ** decimalPlaces;
|
||||
const scaled = value * factor;
|
||||
// 절삭(truncate): 반올림 없이 소수점 이하를 버린다.
|
||||
const truncated = scaled >= 0 ? Math.floor(scaled) : Math.ceil(scaled);
|
||||
return truncated / factor;
|
||||
}
|
||||
|
||||
function buildFieldsFromColumns(
|
||||
columns: {
|
||||
fieldName: string;
|
||||
valueType?: ValueType;
|
||||
dataFormat?: string;
|
||||
datetimeFormat?: string;
|
||||
editor?: { type?: string; datetimeFormat?: string };
|
||||
computed?: import("../type/DataGrid.type").ComputedFieldConfig;
|
||||
@@ -508,18 +530,52 @@ function buildFieldsFromColumns(
|
||||
"minuendFields" in col.computed &&
|
||||
"subtrahendFields" in col.computed &&
|
||||
col.computed.minuendFields.length > 0;
|
||||
const hasHalfSumOfSums =
|
||||
col.computed.type === "halfSumOfSums" &&
|
||||
"firstSumFields" in col.computed &&
|
||||
"secondSumFields" in col.computed &&
|
||||
col.computed.firstSumFields.length > 0 &&
|
||||
col.computed.secondSumFields.length > 0;
|
||||
const hasSumOfTwoHalfSumOfSums =
|
||||
col.computed.type === "sumOfTwoHalfSumOfSums" &&
|
||||
"firstHalfSum" in col.computed &&
|
||||
"secondHalfSum" in col.computed &&
|
||||
col.computed.firstHalfSum.firstSumFields.length > 0 &&
|
||||
col.computed.firstHalfSum.secondSumFields.length > 0 &&
|
||||
col.computed.secondHalfSum.firstSumFields.length > 0 &&
|
||||
col.computed.secondHalfSum.secondSumFields.length > 0;
|
||||
const hasSumOfMaxGroups =
|
||||
col.computed.type === "sumOfMaxOfFieldAndSumGroups" &&
|
||||
"groups" in col.computed &&
|
||||
Array.isArray(col.computed.groups) &&
|
||||
col.computed.groups.length > 0;
|
||||
const hasPumpGrossPpqty =
|
||||
col.computed.type === "pumpGrossPpqty" &&
|
||||
"pumpGroups" in col.computed &&
|
||||
col.computed.pumpGroups.length > 0;
|
||||
const hasBodPrcsRt =
|
||||
col.computed.type === "bodPrcsRt" &&
|
||||
"inflwField" in col.computed &&
|
||||
"dschgField" in col.computed;
|
||||
const hasQuotientRatioPercent =
|
||||
col.computed.type === "quotientRatioPercent" &&
|
||||
"numeratorQuotient" in col.computed &&
|
||||
"denominatorQuotient" in col.computed &&
|
||||
col.computed.numeratorQuotient.length === 2 &&
|
||||
col.computed.denominatorQuotient.length === 2;
|
||||
if (
|
||||
hasFields ||
|
||||
hasNumeratorDenominator ||
|
||||
hasMaxOfFieldAndSum ||
|
||||
hasDifferenceOfSums ||
|
||||
hasSumOfMaxGroups
|
||||
hasHalfSumOfSums ||
|
||||
hasSumOfTwoHalfSumOfSums ||
|
||||
hasSumOfMaxGroups ||
|
||||
hasPumpGrossPpqty ||
|
||||
hasBodPrcsRt ||
|
||||
hasQuotientRatioPercent
|
||||
) {
|
||||
field.valueCallback = getComputedValueCallback(
|
||||
const computedCallback = getComputedValueCallback(
|
||||
col.computed,
|
||||
) as (
|
||||
prod: unknown,
|
||||
@@ -528,6 +584,24 @@ function buildFieldsFromColumns(
|
||||
fieldNames: string[],
|
||||
values: unknown[],
|
||||
) => number;
|
||||
const decimalPlaces = getDecimalPlacesFromDataFormat(col.dataFormat);
|
||||
field.valueCallback = (
|
||||
prod: unknown,
|
||||
dataRow: number,
|
||||
fieldName: string,
|
||||
fieldNames: string[],
|
||||
values: unknown[],
|
||||
) => {
|
||||
const computedValue = computedCallback(
|
||||
prod,
|
||||
dataRow,
|
||||
fieldName,
|
||||
fieldNames,
|
||||
values,
|
||||
);
|
||||
if (decimalPlaces == null) return computedValue;
|
||||
return truncateByDecimalPlaces(computedValue, decimalPlaces);
|
||||
};
|
||||
}
|
||||
}
|
||||
return field;
|
||||
@@ -586,6 +660,7 @@ const DataGrid = ({
|
||||
const onItemAllCheckedRef =
|
||||
useRef<typeof onItemAllChecked>(onItemAllChecked);
|
||||
const isSyncingChecksFromRowDataRef = useRef(false);
|
||||
const columnsFieldsSigRef = useRef("");
|
||||
columnsRef.current = columns;
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
@@ -638,20 +713,6 @@ const DataGrid = ({
|
||||
.find((s) => s.menuEngNm === activeTabId)?.menuKornNm ||
|
||||
"export";
|
||||
|
||||
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const originalHeaders: Map<string, string> = new Map();
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header) {
|
||||
const originalText = gridCol.header.text || "";
|
||||
originalHeaders.set(col.name, originalText);
|
||||
const headerWithFieldName = `${originalText} (${col.fieldName})`;
|
||||
gridCol.header.text = headerWithFieldName;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
|
||||
const normalizeLayoutForExport = (
|
||||
layout: ColumnLayoutItem[],
|
||||
@@ -745,20 +806,6 @@ const DataGrid = ({
|
||||
excelExportVisibilitySnapshot,
|
||||
);
|
||||
}
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (
|
||||
gridCol &&
|
||||
gridCol.header &&
|
||||
originalHeaders.has(col.name)
|
||||
) {
|
||||
gridCol.header.text =
|
||||
originalHeaders.get(col.name) || "";
|
||||
}
|
||||
}
|
||||
});
|
||||
// columnLayout 원복 (footerUserSpans 유지)
|
||||
if (normalizedLayout && !layoutRestored) {
|
||||
layoutRestored = true;
|
||||
@@ -860,36 +907,11 @@ const DataGrid = ({
|
||||
: headerStartRow;
|
||||
const dataStartRow = maxHeaderRowIndex + 1;
|
||||
|
||||
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
|
||||
const headerToFieldNameMap = new Map<string, string>();
|
||||
headers.forEach((headerText, colIndex) => {
|
||||
let fieldName: string | null = null;
|
||||
|
||||
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const fieldNameMatch = headerText.match(
|
||||
/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/,
|
||||
);
|
||||
if (fieldNameMatch && fieldNameMatch[1]) {
|
||||
const extractedFieldName = fieldNameMatch[1];
|
||||
if (
|
||||
columns.find(
|
||||
(c) => c.fieldName === extractedFieldName,
|
||||
)
|
||||
) {
|
||||
fieldName = extractedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭 실패 시 원본 헤더 텍스트 사용
|
||||
if (!fieldName) {
|
||||
console.warn(
|
||||
`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`,
|
||||
);
|
||||
fieldName = headerText;
|
||||
}
|
||||
|
||||
headerToFieldNameMap.set(headerText, fieldName);
|
||||
});
|
||||
// 엑셀 N번째 열 → 그리드 N번째 fieldName (보내기 컬럼 순서 기준)
|
||||
const excelColumnFieldNames = getExcelImportColumnFieldNames(
|
||||
columns,
|
||||
columnLayout,
|
||||
);
|
||||
|
||||
// 데이터 행 읽기
|
||||
const dataRows: Record<string, string | number>[] = [];
|
||||
@@ -907,12 +929,8 @@ const DataGrid = ({
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
];
|
||||
const headerName =
|
||||
headers[colIndex] || `Column${colIndex + 1}`;
|
||||
// 헤더 텍스트를 fieldName으로 변환
|
||||
const fieldName =
|
||||
headerToFieldNameMap.get(headerName) ||
|
||||
headerName;
|
||||
const fieldName = excelColumnFieldNames[colIndex];
|
||||
if (!fieldName) continue;
|
||||
|
||||
let cellValue: string | number = "";
|
||||
if (
|
||||
@@ -996,6 +1014,21 @@ const DataGrid = ({
|
||||
const dataProvider = gridRef.current.dataProvider;
|
||||
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions);
|
||||
|
||||
const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001");
|
||||
if (columnsFieldsSig !== columnsFieldsSigRef.current) {
|
||||
columnsFieldsSigRef.current = columnsFieldsSig;
|
||||
const dpForFields = dataProvider as {
|
||||
setFields?: (f: unknown[]) => void;
|
||||
setRows?: (r: Record<string, string | number>[]) => void;
|
||||
};
|
||||
try {
|
||||
dpForFields.setFields?.(buildFieldsFromColumns(columns));
|
||||
dpForFields.setRows?.(rowDataRef.current);
|
||||
} catch (e) {
|
||||
console.warn("setFields sync:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리
|
||||
const hasMergeCells = columns.some((col) => col.mergeCells === true);
|
||||
|
||||
@@ -1046,6 +1079,14 @@ const DataGrid = ({
|
||||
}
|
||||
columns.forEach((column) => {
|
||||
const gv = gridView as GridViewWithSetColumnProperty;
|
||||
const exists =
|
||||
typeof (
|
||||
gridView as { columnByName?: (name: string) => unknown }
|
||||
).columnByName === "function" &&
|
||||
(gridView as { columnByName: (name: string) => unknown }).columnByName(
|
||||
column.name,
|
||||
) != null;
|
||||
if (!exists) return;
|
||||
gv.setColumnProperty?.(
|
||||
column.name,
|
||||
"header",
|
||||
@@ -1054,6 +1095,11 @@ const DataGrid = ({
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
}),
|
||||
);
|
||||
gv.setColumnProperty?.(
|
||||
column.name,
|
||||
"editable",
|
||||
column.editable !== false,
|
||||
);
|
||||
applyColumnListEditorProperties(gv, column);
|
||||
});
|
||||
if (columns.some((c) => isDropDownEditorColumn(c))) {
|
||||
@@ -1101,7 +1147,7 @@ const DataGrid = ({
|
||||
|
||||
// 헤더 체크박스 표시 여부 설정
|
||||
gridView.setOptions({
|
||||
checkBar: { visible: showHeaderCheckbox },
|
||||
checkBar: { visible: showHeaderCheckbox, exclusive: false },
|
||||
rowIndicator: { visible: false },
|
||||
});
|
||||
|
||||
@@ -1181,10 +1227,63 @@ const DataGrid = ({
|
||||
const hasFooterUserSpans = checkFooterUserSpans(columnLayout);
|
||||
|
||||
if (hasFooterUserSpans) {
|
||||
const validColumnNames = new Set(columns.map((c) => c.name));
|
||||
const sanitizeColumnLayout = (
|
||||
layout: unknown[],
|
||||
): unknown[] => {
|
||||
const result: unknown[] = [];
|
||||
for (const item of layout) {
|
||||
if (typeof item === "string") {
|
||||
if (validColumnNames.has(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!item || typeof item !== "object") continue;
|
||||
|
||||
if ("column" in item) {
|
||||
const columnName = (item as { column?: unknown }).column;
|
||||
if (
|
||||
typeof columnName === "string" &&
|
||||
validColumnNames.has(columnName)
|
||||
) {
|
||||
result.push(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("items" in item && Array.isArray((item as { items?: unknown[] }).items)) {
|
||||
const nextItems = sanitizeColumnLayout(
|
||||
(item as { items: unknown[] }).items,
|
||||
);
|
||||
if (nextItems.length > 0) {
|
||||
result.push({
|
||||
...(item as Record<string, unknown>),
|
||||
items: nextItems,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
|
||||
setTimeout(() => {
|
||||
if (gridRef.current?.gridView && columnLayout) {
|
||||
gridRef.current.gridView.setColumnLayout?.(columnLayout);
|
||||
const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]);
|
||||
if (safeLayout.length === 0) return;
|
||||
try {
|
||||
(
|
||||
gridRef.current.gridView.setColumnLayout as
|
||||
| ((layout: unknown) => void)
|
||||
| undefined
|
||||
)?.(safeLayout);
|
||||
} catch (e) {
|
||||
console.warn("setColumnLayout skipped due to invalid columns:", e);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
@@ -1654,6 +1753,22 @@ const DataGrid = ({
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onRGDataColumnsChanged={(grid) => {
|
||||
const dp = gridRef.current?.dataProvider as
|
||||
| {
|
||||
setFields?: (f: unknown[]) => void;
|
||||
setRows?: (
|
||||
r: Record<string, string | number>[],
|
||||
) => void;
|
||||
}
|
||||
| undefined;
|
||||
try {
|
||||
dp?.setFields?.(
|
||||
buildFieldsFromColumns(columnsRef.current),
|
||||
);
|
||||
dp?.setRows?.(rowDataRef.current);
|
||||
} catch (e) {
|
||||
console.warn("onRGDataColumnsChanged setFields:", e);
|
||||
}
|
||||
reapplyListEditorsOnGrid(
|
||||
grid as GridViewWithSetColumnProperty,
|
||||
columnsRef.current,
|
||||
@@ -1693,6 +1808,9 @@ const DataGrid = ({
|
||||
dataProvider.setFields(
|
||||
buildFieldsFromColumns(columns),
|
||||
);
|
||||
columnsFieldsSigRef.current = columns
|
||||
.map((c) => c.fieldName)
|
||||
.join("\u0001");
|
||||
}
|
||||
if (dataProvider?.setRows)
|
||||
dataProvider.setRows(rowData);
|
||||
@@ -1874,15 +1992,9 @@ const DataGrid = ({
|
||||
{columns.map((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
|
||||
const columnReactKey =
|
||||
column.values?.length &&
|
||||
isDropDownEditorColumn(column)
|
||||
? `${column.name}-dropdown-${column.values.length}`
|
||||
: column.name;
|
||||
|
||||
return (
|
||||
<RGDataColumn
|
||||
key={columnReactKey}
|
||||
key={column.name}
|
||||
name={column.name}
|
||||
fieldName={column.fieldName}
|
||||
valueType={
|
||||
|
||||
@@ -33,7 +33,7 @@ import DataGrid from "@/components/module/DataGrid";
|
||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
/** 통계 입력 현황 > 행 클릭 시 예외처리(prcsYn=3) 내역만 조회 */
|
||||
/** 통계 입력 현황 > 행 클릭 시 사용자예외처리(prcsYn=2) 내역만 조회 */
|
||||
export interface ExceptionProcessingHistoryQueryContext {
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
@@ -92,13 +92,15 @@ function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams)
|
||||
const exceptionHistoryColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
|
||||
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
|
||||
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
|
||||
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
{
|
||||
@@ -114,13 +116,15 @@ const exceptionHistoryColumnInfo: ColumnInfo = {
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"admdstNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"pbcpAprvYn",
|
||||
"stVrfRjtComNm",
|
||||
"prcsOpnnExpln",
|
||||
"rjctOpnnCn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"individualReject",
|
||||
@@ -190,9 +194,9 @@ export function ExceptionProcessingHistoryDialog({
|
||||
sggNm: queryContext.sggNm,
|
||||
statsYr: queryContext.statsYr,
|
||||
statsCd: queryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: PRCS_YN_LIST,
|
||||
rejectCodeList: "0,1,2",
|
||||
page: pageIndex + 1,
|
||||
size: 30,
|
||||
};
|
||||
@@ -308,14 +312,16 @@ export function ExceptionProcessingHistoryDialog({
|
||||
const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] =>
|
||||
entries.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYnNm: item.prcsYnNm ?? "-",
|
||||
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
|
||||
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
|
||||
rjctOpnnCn: item.rjctOpnnCn ?? "-",
|
||||
prcsYn: item.prcsYn ?? "",
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
individualReject: "개별 반려",
|
||||
|
||||
@@ -41,7 +41,7 @@ const getSggListFetcher = async (
|
||||
key: [string, string, string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
|
||||
return statisticsApi.getSggAllList(key[1], {
|
||||
return statisticsApi.getSggList(key[1], {
|
||||
instSeCd: key[2],
|
||||
gugunSkipYn,
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
const getSggAllListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsApi.getSggAllList(key[1]);
|
||||
return statisticsApi.getSggList(key[1]);
|
||||
};
|
||||
|
||||
type SidoSggFacilityNameSearchFormProps = {
|
||||
@@ -292,17 +292,12 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const ctpvNm = String(row.ctpvNm ?? sido);
|
||||
const sggNm = String(
|
||||
row.sggNm ?? (gugun !== "ALL" && gugun ? gugun : ""),
|
||||
);
|
||||
|
||||
const params: SeweragePipeFacilityAdminAreaListRequest = {
|
||||
statsYr: statsYr.trim(),
|
||||
swtrmtFcltCd,
|
||||
sewerBzmnCd: initialSearchValues?.bsnmCd || '',
|
||||
ctpvNm,
|
||||
...(doNameList.includes(ctpvNm) ? { sggNm } : {}),
|
||||
ctpvNm: initialSidoValue || '',
|
||||
...(doNameList.includes(initialSidoValue) ? { sggNm: initialGugunValue } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,7 @@ import SplitDataGrid from "./SplitDataGrid";
|
||||
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
||||
import {
|
||||
getExcelImportColumnFieldNames,
|
||||
prepareGridColumnsForExcelExport,
|
||||
restoreGridColumnsAfterExcelExport,
|
||||
shouldPrepareColumnsForExcelExport,
|
||||
@@ -222,8 +223,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
|
||||
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0;
|
||||
const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0;
|
||||
const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0;
|
||||
const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0;
|
||||
const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0;
|
||||
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) {
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) {
|
||||
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
|
||||
}
|
||||
}
|
||||
@@ -373,20 +376,6 @@ const TabDataGrid = ({
|
||||
const columns = columnInfoItem.columns || [];
|
||||
const columnLayout = columnInfoItem.columnLayout || [];
|
||||
|
||||
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const originalHeaders: Map<string, string> = new Map();
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header) {
|
||||
const originalText = gridCol.header.text || '';
|
||||
originalHeaders.set(col.name, originalText);
|
||||
const headerWithFieldName = `${originalText} (${col.fieldName})`;
|
||||
gridCol.header.text = headerWithFieldName;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
|
||||
const normalizeLayoutForExport = (layout: ColumnLayoutItem[]): ColumnLayoutItem[] => {
|
||||
return layout.map(item => {
|
||||
@@ -452,15 +441,6 @@ const TabDataGrid = ({
|
||||
if (excelExportVisibilitySnapshot) {
|
||||
restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot);
|
||||
}
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header && originalHeaders.has(col.name)) {
|
||||
gridCol.header.text = originalHeaders.get(col.name) || '';
|
||||
}
|
||||
}
|
||||
});
|
||||
// columnLayout 원복 (footerUserSpans 유지)
|
||||
if (normalizedLayout && !layoutRestored) {
|
||||
layoutRestored = true;
|
||||
@@ -546,29 +526,12 @@ const TabDataGrid = ({
|
||||
const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow;
|
||||
const dataStartRow = maxHeaderRowIndex + 1;
|
||||
|
||||
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
|
||||
const headerToFieldNameMap = new Map<string, string>();
|
||||
const columns = columnInfoItem.columns || [];
|
||||
headers.forEach((headerText, colIndex) => {
|
||||
let fieldName: string | null = null;
|
||||
|
||||
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/);
|
||||
if (fieldNameMatch && fieldNameMatch[1]) {
|
||||
const extractedFieldName = fieldNameMatch[1];
|
||||
if (columns.find(c => c.fieldName === extractedFieldName)) {
|
||||
fieldName = extractedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭 실패 시 원본 헤더 텍스트 사용
|
||||
if (!fieldName) {
|
||||
console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`);
|
||||
fieldName = headerText;
|
||||
}
|
||||
|
||||
headerToFieldNameMap.set(headerText, fieldName);
|
||||
});
|
||||
const columnLayout = columnInfoItem.columnLayout || [];
|
||||
const excelColumnFieldNames = getExcelImportColumnFieldNames(
|
||||
columns,
|
||||
columnLayout,
|
||||
);
|
||||
|
||||
// 데이터 행 읽기
|
||||
const dataRows: Record<string, string | number>[] = [];
|
||||
@@ -579,9 +542,8 @@ const TabDataGrid = ({
|
||||
for (let colIndex = 0; colIndex < headers.length; colIndex++) {
|
||||
const col = range.s.c + colIndex;
|
||||
const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })];
|
||||
const headerName = headers[colIndex] || `Column${colIndex + 1}`;
|
||||
// 헤더 텍스트를 fieldName으로 변환
|
||||
const fieldName = headerToFieldNameMap.get(headerName) || headerName;
|
||||
const fieldName = excelColumnFieldNames[colIndex];
|
||||
if (!fieldName) continue;
|
||||
|
||||
let cellValue: string | number = '';
|
||||
if (cell && cell.v !== undefined && cell.v !== null) {
|
||||
|
||||
@@ -92,13 +92,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
|
||||
{ name: "vrfcMthdNm", fieldName: "vrfcMthdNm", header: { text: "구분" }, editable: false },
|
||||
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
{
|
||||
@@ -111,13 +110,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
|
||||
columnLayout: [
|
||||
"vrfcMthdNm",
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"admdstNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYn",
|
||||
"pbcpAprvYn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"prcsOpnnExpln",
|
||||
@@ -202,13 +200,12 @@ export function VerificationResultDialog({
|
||||
vrfcMthdCd,
|
||||
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
prcsYn: item.prcsYnNm ?? PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
prcsOpnnExpln: item.prcsOpnnExpln ?? "",
|
||||
@@ -226,9 +223,9 @@ export function VerificationResultDialog({
|
||||
sggNm: stableQueryContext.sggNm,
|
||||
statsYr: stableQueryContext.statsYr,
|
||||
statsCd: stableQueryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0",
|
||||
rejectCodeList: "0,1,2",
|
||||
page: page + 1,
|
||||
size: 10000,
|
||||
};
|
||||
|
||||
@@ -137,12 +137,15 @@ function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackPara
|
||||
const verificationResultColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
|
||||
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
|
||||
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
|
||||
@@ -150,12 +153,15 @@ const verificationResultColumnInfo: ColumnInfo = {
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"admdstNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"stVrfRjtComNm",
|
||||
"prcsOpnnExpln",
|
||||
"rjctOpnnCn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"prcsYn",
|
||||
@@ -175,6 +181,8 @@ export interface VerificationResultHistoryDialogProps {
|
||||
} | null;
|
||||
title?: string;
|
||||
emptyMessage?: string;
|
||||
/** 예외 제출 취소 API 성공 후 부모(입력 화면)에서 메인 데이터를 다시 조회할 때 사용 */
|
||||
onExceptionResetSubmitted?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function VerificationResultHistoryDialog({
|
||||
@@ -183,6 +191,7 @@ export function VerificationResultHistoryDialog({
|
||||
queryContext,
|
||||
title = "검증 결과 내역",
|
||||
emptyMessage = "표시할 검증 결과가 없습니다.",
|
||||
onExceptionResetSubmitted,
|
||||
}: VerificationResultHistoryDialogProps) {
|
||||
const stableQueryContext = useMemo(() => {
|
||||
if (!queryContext) return null;
|
||||
@@ -245,9 +254,9 @@ export function VerificationResultHistoryDialog({
|
||||
sggNm: stableQueryContext.sggNm,
|
||||
statsYr: stableQueryContext.statsYr,
|
||||
statsCd: stableQueryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0,2,3",
|
||||
prcsYnList: "0,2",
|
||||
rejectCodeList: "0,1,2",
|
||||
page: pageIndex + 1,
|
||||
size: PAGE_SIZE,
|
||||
};
|
||||
@@ -376,14 +385,16 @@ export function VerificationResultHistoryDialog({
|
||||
vrfcSn: item.vrfcSn,
|
||||
__checked: isChecked ? 1 : 0,
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYnNm: item.prcsYnNm ?? "-",
|
||||
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
|
||||
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
|
||||
rjctOpnnCn: item.rjctOpnnCn ?? "-",
|
||||
prcsYn: item.prcsYn ?? "",
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
};
|
||||
@@ -451,12 +462,28 @@ export function VerificationResultHistoryDialog({
|
||||
alert("예외 제출 취소 처리되었습니다.");
|
||||
setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: new Map() }));
|
||||
await loadTabPage(tab, tabPages[tab].pageIndex);
|
||||
try {
|
||||
await onExceptionResetSubmitted?.();
|
||||
} catch {
|
||||
alert(
|
||||
"예외 제출 취소는 완료되었으나 화면 데이터를 다시 불러오지 못했습니다. 조회로 새로고침 해주세요.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert("예외 제출 취소 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setResetSubmitting(false);
|
||||
}
|
||||
}, [activeTab, alert, checkedVrfcByTab, entriesByTab, loadTabPage, stableQueryContext, tabPages]);
|
||||
}, [
|
||||
activeTab,
|
||||
alert,
|
||||
checkedVrfcByTab,
|
||||
entriesByTab,
|
||||
loadTabPage,
|
||||
onExceptionResetSubmitted,
|
||||
stableQueryContext,
|
||||
tabPages,
|
||||
]);
|
||||
|
||||
const renderActiveTabPanel = (tab: VerificationHistoryTabKey) => {
|
||||
const entries = entriesByTab[tab];
|
||||
|
||||
@@ -34,8 +34,8 @@ export async function hasAnyPostSubmitVerificationResult(
|
||||
sggNm: ctx.sggNm,
|
||||
statsYr: ctx.statsYr,
|
||||
statsCd: ctx.statsCd,
|
||||
pbcpAprvYn: "N" as const,
|
||||
prcsYnList: "0",
|
||||
rejectCodeList: "0,1,2",
|
||||
page: 1,
|
||||
size: 1,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
type InfiniteData,
|
||||
keepPreviousData,
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { useTabStore as useGridTabStore } from "@rsone/components";
|
||||
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service";
|
||||
import type {
|
||||
VerificationResultListRequest,
|
||||
VerificationResultListResponse,
|
||||
VerificationResultReportItem,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
VerificationInputReportItem,
|
||||
VerificationInputReportResponse,
|
||||
VerificationInputReportTypeCountingInfo,
|
||||
} from "@/entities/statistics/model/verification-input-report-interface";
|
||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import {
|
||||
findMenuItemByUrlPath,
|
||||
@@ -59,8 +59,8 @@ const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
const PAGE_SIZE = 100;
|
||||
const SCROLL_LOAD_THRESHOLD_PX = 120;
|
||||
|
||||
function getNextVerificationResultPage(
|
||||
lastPage: VerificationResultListResponse,
|
||||
function getNextVerificationInputReportPage(
|
||||
lastPage: VerificationInputReportResponse,
|
||||
): number | undefined {
|
||||
const pageInfo = lastPage?.data?.pageInfo;
|
||||
if (!pageInfo) return undefined;
|
||||
@@ -69,19 +69,29 @@ function getNextVerificationResultPage(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function flattenVerificationResults(
|
||||
data: InfiniteData<VerificationResultListResponse> | undefined,
|
||||
): VerificationResultReportItem[] {
|
||||
function flattenVerificationInputReportResults(
|
||||
data: InfiniteData<VerificationInputReportResponse> | undefined,
|
||||
): VerificationInputReportItem[] {
|
||||
return (
|
||||
data?.pages.flatMap((page) => page?.data?.verifyResultList ?? []) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function getVerificationResultTotalCount(
|
||||
data: InfiniteData<VerificationResultListResponse> | undefined,
|
||||
loadedCount: number,
|
||||
): number {
|
||||
return data?.pages[0]?.data?.pageInfo?.totalElements ?? loadedCount;
|
||||
function getVerifyTypeCountingInfo(
|
||||
data: InfiniteData<VerificationInputReportResponse> | undefined,
|
||||
): VerificationInputReportTypeCountingInfo | null {
|
||||
return data?.pages[0]?.data?.verifyTypeCountingInfo ?? null;
|
||||
}
|
||||
|
||||
function formatReportItemTitle(item: VerificationInputReportItem): string {
|
||||
return item.statsNm?.trim() || item.vrfcTrgtNm?.trim() || item.statsCd;
|
||||
}
|
||||
|
||||
function formatReportItemRegion(item: VerificationInputReportItem): string {
|
||||
const parts = [item.ctpvNm, item.sggNm].filter(
|
||||
(v): v is string => Boolean(v?.trim()),
|
||||
);
|
||||
return parts.length > 0 ? parts.join(" ") : "-";
|
||||
}
|
||||
|
||||
function useVerificationReportList(
|
||||
@@ -89,28 +99,31 @@ function useVerificationReportList(
|
||||
statsCdQueryKey: string,
|
||||
vrfcMthdCd: VerificationReportTabKey,
|
||||
statsCd: string,
|
||||
enabled: boolean,
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
"verificationInputReport",
|
||||
statsYr,
|
||||
vrfcMthdCd,
|
||||
statsCdQueryKey,
|
||||
],
|
||||
initialPageParam: 1,
|
||||
queryFn: ({ pageParam }) =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
verificationInputReportApi.getVerificationInputReport({
|
||||
statsYr,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
page: pageParam,
|
||||
size: PAGE_SIZE,
|
||||
prcsYnList: "0",
|
||||
vrfcMthdCd,
|
||||
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
|
||||
} satisfies VerificationResultListRequest),
|
||||
getNextPageParam: getNextVerificationResultPage,
|
||||
}),
|
||||
getNextPageParam: getNextVerificationInputReportPage,
|
||||
placeholderData: keepPreviousData,
|
||||
refetchOnWindowFocus: false,
|
||||
// 탭 전환만으로는 재조회하지 않음(명시적 새로고침/조건 변경 시만 조회)
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: false,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,28 +145,46 @@ const VerificationSelectionReport = ({
|
||||
const alert = useAlertStore((s) => s.setMessage);
|
||||
const [activeTab, setActiveTab] = useState<VerificationReportTabKey>("NV");
|
||||
const [statsCd, setStatsCd] = useState<string>("ALL");
|
||||
const [isFullReloading, setIsFullReloading] = useState(false);
|
||||
|
||||
const statsYr = String(dayjs().year() - 1);
|
||||
const statsCdQueryKey =
|
||||
statsCd === "" || statsCd === "ALL" ? "" : statsCd;
|
||||
|
||||
const handleStatsCdChange = useCallback(
|
||||
(nextStatsCd: string) => {
|
||||
setStatsCd(nextStatsCd);
|
||||
setActiveTab("NV");
|
||||
setIsFullReloading(true);
|
||||
|
||||
// 통계항목 변경 시 모든 탭 캐시 제거 후 NV부터 다시 조회
|
||||
void queryClient.removeQueries({
|
||||
queryKey: ["verificationInputReport", statsYr],
|
||||
});
|
||||
},
|
||||
[queryClient, statsYr],
|
||||
);
|
||||
|
||||
const nvQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"NV",
|
||||
statsCd,
|
||||
activeTab === "NV",
|
||||
);
|
||||
const imrQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"IMR",
|
||||
statsCd,
|
||||
activeTab === "IMR",
|
||||
);
|
||||
const hbQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"HB",
|
||||
statsCd,
|
||||
activeTab === "HB",
|
||||
);
|
||||
|
||||
const queryByTab = {
|
||||
@@ -162,44 +193,51 @@ const VerificationSelectionReport = ({
|
||||
HB: hbQuery,
|
||||
} as const;
|
||||
|
||||
const isInitialLoading =
|
||||
nvQuery.isPending || imrQuery.isPending || hbQuery.isPending;
|
||||
const isRefreshing =
|
||||
(nvQuery.isFetching && !nvQuery.isPending) ||
|
||||
(imrQuery.isFetching && !imrQuery.isPending) ||
|
||||
(hbQuery.isFetching && !hbQuery.isPending);
|
||||
const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error;
|
||||
const activeQuery = queryByTab[activeTab];
|
||||
const isRefreshing = activeQuery.isFetching && !activeQuery.isPending;
|
||||
const error = activeQuery.error;
|
||||
|
||||
/** 통계항목 변경·최초 진입: 탭 영역 숨기고 카드 전체 로딩만 표시 */
|
||||
const hideTabsSection =
|
||||
isFullReloading ||
|
||||
(activeTab === "NV" && !nvQuery.data && (nvQuery.isPending || nvQuery.isFetching));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFullReloading) return;
|
||||
if (nvQuery.isFetching) return;
|
||||
if (nvQuery.isSuccess || nvQuery.isError) {
|
||||
setIsFullReloading(false);
|
||||
}
|
||||
}, [isFullReloading, nvQuery.isFetching, nvQuery.isSuccess, nvQuery.isError]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
const tabCodes: VerificationReportTabKey[] = ["NV", "IMR", "HB"];
|
||||
void Promise.all(
|
||||
tabCodes.map((code) =>
|
||||
queryClient.resetQueries({
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
code,
|
||||
statsCdQueryKey,
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, [queryClient, statsYr, statsCdQueryKey]);
|
||||
setStatsCd("ALL");
|
||||
setActiveTab("NV");
|
||||
setIsFullReloading(true);
|
||||
void queryClient.removeQueries({
|
||||
queryKey: ["verificationInputReport", statsYr],
|
||||
});
|
||||
}, [queryClient, statsYr]);
|
||||
|
||||
const listsByTab: Record<VerificationReportTabKey, VerificationResultReportItem[]> = {
|
||||
NV: flattenVerificationResults(nvQuery.data),
|
||||
IMR: flattenVerificationResults(imrQuery.data),
|
||||
HB: flattenVerificationResults(hbQuery.data),
|
||||
const listsByTab: Record<VerificationReportTabKey, VerificationInputReportItem[]> = {
|
||||
NV: flattenVerificationInputReportResults(nvQuery.data),
|
||||
IMR: flattenVerificationInputReportResults(imrQuery.data),
|
||||
HB: flattenVerificationInputReportResults(hbQuery.data),
|
||||
};
|
||||
|
||||
const typeCountingInfo =
|
||||
getVerifyTypeCountingInfo(nvQuery.data) ??
|
||||
getVerifyTypeCountingInfo(imrQuery.data) ??
|
||||
getVerifyTypeCountingInfo(hbQuery.data);
|
||||
|
||||
const totalCountByTab: Record<VerificationReportTabKey, number> = {
|
||||
NV: getVerificationResultTotalCount(nvQuery.data, listsByTab.NV.length),
|
||||
IMR: getVerificationResultTotalCount(imrQuery.data, listsByTab.IMR.length),
|
||||
HB: getVerificationResultTotalCount(hbQuery.data, listsByTab.HB.length),
|
||||
NV: typeCountingInfo?.nvCount ?? listsByTab.NV.length,
|
||||
IMR: typeCountingInfo?.imrCount ?? listsByTab.IMR.length,
|
||||
HB: typeCountingInfo?.hbCount ?? listsByTab.HB.length,
|
||||
};
|
||||
|
||||
const openInputFromStatsCd = useCallback(
|
||||
(item: VerificationResultReportItem) => {
|
||||
(item: VerificationInputReportItem) => {
|
||||
const nav = resolveInputPageNavigationFromStatsCd(item.statsCd);
|
||||
if (!nav) {
|
||||
alert(
|
||||
@@ -263,9 +301,21 @@ const VerificationSelectionReport = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const isTabContentLoading = (tab: VerificationReportTabKey) => {
|
||||
const query = queryByTab[tab];
|
||||
return (
|
||||
activeTab === tab &&
|
||||
!hideTabsSection &&
|
||||
(query.isPending ||
|
||||
(query.isFetching && !query.isFetchingNextPage) ||
|
||||
(!query.data && !query.error))
|
||||
);
|
||||
};
|
||||
|
||||
const renderResultList = (
|
||||
typeCode: VerificationReportTabKey,
|
||||
items: VerificationResultReportItem[],
|
||||
items: VerificationInputReportItem[],
|
||||
isTabLoading: boolean,
|
||||
hasNextPage: boolean,
|
||||
isFetchingNextPage: boolean,
|
||||
fetchNextPage: () => void,
|
||||
@@ -281,7 +331,13 @@ const VerificationSelectionReport = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
{items.length === 0 && !isFetchingNextPage ? (
|
||||
{isTabLoading ? (
|
||||
<div className="flex h-full min-h-[8rem] items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : items.length === 0 && !isFetchingNextPage ? (
|
||||
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
|
||||
항목이 없습니다.
|
||||
</div>
|
||||
@@ -303,7 +359,7 @@ const VerificationSelectionReport = ({
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.statsNm}
|
||||
{formatReportItemTitle(item)}
|
||||
</span>
|
||||
<Badge
|
||||
color={
|
||||
@@ -320,7 +376,7 @@ const VerificationSelectionReport = ({
|
||||
{item.rsnExpln}
|
||||
</p>
|
||||
<div className="flex justify-between text-lg text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>{formatReportItemRegion(item)}</span>
|
||||
<span>
|
||||
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
|
||||
{item.compVlCn ?? "-"}
|
||||
@@ -383,25 +439,25 @@ const VerificationSelectionReport = ({
|
||||
<CodeSelectWithAll
|
||||
upCd="STITEM"
|
||||
value={statsCd}
|
||||
onValueChange={setStatsCd}
|
||||
onValueChange={handleStatsCdChange}
|
||||
placeholder="전체"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-body p-2.5 flex-1 flex flex-col overflow-hidden relative min-h-0">
|
||||
{isInitialLoading && (
|
||||
{hideTabsSection && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
{!hideTabsSection && error && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{!isInitialLoading && !error && (
|
||||
{!hideTabsSection && !error && (
|
||||
<section className="flex min-h-0 flex-1 flex-col">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
@@ -428,18 +484,14 @@ const VerificationSelectionReport = ({
|
||||
value={tab.value}
|
||||
className="mt-0 flex min-h-0 flex-1 flex-col p-0 data-[state=inactive]:hidden"
|
||||
>
|
||||
{activeTab === tab.value
|
||||
? renderResultList(
|
||||
tab.value,
|
||||
listsByTab[tab.value],
|
||||
queryByTab[tab.value]
|
||||
.hasNextPage ?? false,
|
||||
queryByTab[tab.value]
|
||||
.isFetchingNextPage,
|
||||
queryByTab[tab.value]
|
||||
.fetchNextPage,
|
||||
)
|
||||
: null}
|
||||
{renderResultList(
|
||||
tab.value,
|
||||
listsByTab[tab.value],
|
||||
isTabContentLoading(tab.value),
|
||||
queryByTab[tab.value].hasNextPage ?? false,
|
||||
queryByTab[tab.value].isFetchingNextPage,
|
||||
queryByTab[tab.value].fetchNextPage,
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
|
||||
@@ -157,7 +157,9 @@ const PublicSewageFacilityInp = () => {
|
||||
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [isNotApplicable, setIsNotApplicable] = useState(false);
|
||||
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [showIsNotApplicable, setShowIsNotApplicable] = useState(false);
|
||||
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
@@ -175,6 +177,19 @@ const PublicSewageFacilityInp = () => {
|
||||
const [mainFacilityMappingSaveDialogKey, setMainFacilityMappingSaveDialogKey] = useState(0);
|
||||
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const isNotApplicable = isNotApplicableByTab.get('ST00060001') ?? false;
|
||||
const setIsNotApplicable: Dispatch<SetStateAction<boolean>> = (next) => {
|
||||
setIsNotApplicableByTab((prev) => {
|
||||
const current = prev.get('ST00060001') ?? false;
|
||||
const resolved =
|
||||
typeof next === 'function'
|
||||
? (next as (prevState: boolean) => boolean)(current)
|
||||
: next;
|
||||
const nextMap = new Map(prev);
|
||||
nextMap.set('ST00060001', resolved);
|
||||
return nextMap;
|
||||
});
|
||||
};
|
||||
|
||||
const periodEnterableRef = useRef(true);
|
||||
const searchDoneRef = useRef(false);
|
||||
@@ -195,7 +210,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
@@ -276,15 +291,15 @@ const PublicSewageFacilityInp = () => {
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
if (!treatAsNoApplicable && submitList.length === 0) {
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
|
||||
const requestData = {
|
||||
@@ -515,19 +530,19 @@ const PublicSewageFacilityInp = () => {
|
||||
if (!(await confirm('저장 하시겠습니까?'))) return;
|
||||
|
||||
if (activeTab === 'ST00060001') {
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
if (!treatAsNoApplicable && saveList.length === 0) {
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('저장할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const param: PublicSewageFacilitySaveRequest = {
|
||||
statsYr: lastSearchedValues.year ?? '',
|
||||
ctpvNm: lastSearchedValues.sido ?? '',
|
||||
@@ -655,8 +670,6 @@ const PublicSewageFacilityInp = () => {
|
||||
param: PublicSewageFacilityRequestParams,
|
||||
searchContext: SearchFormValues | null,
|
||||
): Promise<boolean> => {
|
||||
// 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
|
||||
setIsNotApplicable(false);
|
||||
try {
|
||||
if (tabKey === 'ST00060001') {
|
||||
const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param);
|
||||
@@ -856,6 +869,11 @@ const PublicSewageFacilityInp = () => {
|
||||
setLastSearchedValues(values);
|
||||
searchDoneRef.current = true;
|
||||
setHasSearched(true);
|
||||
setIsNotApplicableByTab((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set('ST00060001', false);
|
||||
return next;
|
||||
});
|
||||
|
||||
setIsLoading(true);
|
||||
const param = buildPublicSewageFacilityParams(values);
|
||||
@@ -870,6 +888,19 @@ const PublicSewageFacilityInp = () => {
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
const prevTabKey = activeTabRef.current;
|
||||
if (prevTabKey === 'ST00060001') {
|
||||
const latestRowsInPrevTab =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (latestRowsInPrevTab.length > 0 || rowDataMap.has('ST00060001')) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set('ST00060001', latestRowsInPrevTab);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
|
||||
@@ -885,7 +916,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const handleReset = () => {
|
||||
setIsLoading(false);
|
||||
setHasSearched(false);
|
||||
setIsNotApplicable(false);
|
||||
setIsNotApplicableByTab(new Map());
|
||||
setShowIsNotApplicable(false);
|
||||
|
||||
setRowDataMap(new Map());
|
||||
@@ -1215,7 +1246,7 @@ const PublicSewageFacilityInp = () => {
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<DataGrid
|
||||
key={(tabEnterableForGrid || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
key={tab.tabKey}
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={tab.rowStartNum}
|
||||
excelExportFileSuffix={tab.tabLabel || tab.tabKey}
|
||||
@@ -1278,6 +1309,11 @@ const PublicSewageFacilityInp = () => {
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
if (!ctx || !searchParamRef.current) return;
|
||||
await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx);
|
||||
}}
|
||||
/>
|
||||
<SubmitDailog
|
||||
open={showDailog}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -396,6 +396,8 @@ const SewagePipeInp = () => {
|
||||
values: SearchFormValues;
|
||||
tabKey: string;
|
||||
} | null>(null);
|
||||
/** ST00020002 무데이터 안내/자동이동은 동일 조회조건에서 1회만 수행 */
|
||||
const factInstallNoDataHandledKeyRef = useRef<string | null>(null);
|
||||
|
||||
const [verificationSearchDefaults, setVerificationSearchDefaults] =
|
||||
useState<SearchFormValues | null>(null);
|
||||
@@ -549,6 +551,12 @@ const SewagePipeInp = () => {
|
||||
mergeIntoExistingMap: boolean,
|
||||
updateLastSearchedValues: boolean,
|
||||
): Promise<boolean> => {
|
||||
const searchConditionKey = JSON.stringify({
|
||||
statsYr: searchParam.statsYr ?? "",
|
||||
ctpvNm: searchParam.ctpvNm ?? "",
|
||||
sggNm: searchParam.sggNm ?? "",
|
||||
sewerBzmnCd: searchParam.sewerBzmnCd ?? "",
|
||||
});
|
||||
const applyRowMap = (apply: (m: Map<string, Record<string, string | number>[]>) => void) => {
|
||||
if (mergeIntoExistingMap) {
|
||||
setRowDataMap((prev) => {
|
||||
@@ -595,6 +603,23 @@ const SewagePipeInp = () => {
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
...d,
|
||||
}));
|
||||
if (convertedData.length === 0) {
|
||||
const alreadyHandledForSameSearch =
|
||||
factInstallNoDataHandledKeyRef.current === searchConditionKey;
|
||||
if (!alreadyHandledForSameSearch) {
|
||||
factInstallNoDataHandledKeyRef.current = searchConditionKey;
|
||||
alert("시설별 설치 현황에 조회된 데이터가 없습니다.\n시설별 현황 관리에서 설정해주세요");
|
||||
activeTabRef.current = "ST00020006";
|
||||
setActiveTab("ST00020006");
|
||||
return await loadSewagePipeActiveTabData(
|
||||
"ST00020006",
|
||||
searchParam,
|
||||
values,
|
||||
true,
|
||||
updateLastSearchedValues,
|
||||
);
|
||||
}
|
||||
}
|
||||
applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData)));
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
@@ -795,7 +820,39 @@ const SewagePipeInp = () => {
|
||||
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const getCurrentRowsForTab = (tabKey: string): Record<string, string | number>[] => {
|
||||
switch (tabKey) {
|
||||
case REGIONAL_FACT_TAB_KEY:
|
||||
return getGridDataRef.current?.() ?? [];
|
||||
case "ST00020002":
|
||||
return getFactInstallGridDataRef.current?.() ?? [];
|
||||
case "ST00020003":
|
||||
return getPipeTypeGridDataRef.current?.() ?? [];
|
||||
case "ST00020004":
|
||||
return getPipeDiameterGridDataRef.current?.() ?? [];
|
||||
case "ST00020005":
|
||||
return getManholeGridDataRef.current?.() ?? [];
|
||||
case "ST00020006":
|
||||
return [
|
||||
...(rowDataMap.get("treatmentPlantConnectionInp") ?? []),
|
||||
...(rowDataMap.get("adminAreaConnectionInp") ?? []),
|
||||
];
|
||||
default:
|
||||
return rowDataMap.get(tabKey) ?? [];
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
const prevTabKey = activeTabRef.current;
|
||||
const latestRowsInPrevTab = getCurrentRowsForTab(prevTabKey);
|
||||
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(prevTabKey, latestRowsInPrevTab);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
setStatsCd(tabStatsCdMap.get(tabKey) ?? "");
|
||||
@@ -1735,7 +1792,7 @@ const SewagePipeInp = () => {
|
||||
const maxYy = getManholeLastDataYear() % 100;
|
||||
|
||||
const saveList = currentData.map((d) => {
|
||||
const yearlyStats = [];
|
||||
const yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = [];
|
||||
for (let yy = minYy; yy <= maxYy; yy++) {
|
||||
const year = 2000 + yy;
|
||||
const wholKey = `yy${yy}WholMnhlCnt`;
|
||||
@@ -1978,7 +2035,7 @@ const SewagePipeInp = () => {
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
key={(tabEnterableForGrid || isAdminInfo) ? "enterable" : "readonly"}
|
||||
key={tabKey}
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
@@ -2062,6 +2119,12 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
: null
|
||||
}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.year || !values?.sido || !values?.bsnmCd) return;
|
||||
const refreshedParam = buildSewagePipeSearchParam(values);
|
||||
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
|
||||
}}
|
||||
/>
|
||||
<SubmitDailog
|
||||
open={showDailog}
|
||||
|
||||
@@ -80,6 +80,12 @@ const SewagePipeMaintainInpTabList = [
|
||||
|
||||
const firstTabKey = RepairStatusInpColumn.tabKey;
|
||||
|
||||
const SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(
|
||||
SewagePipeMaintainInpTabList.map((tab) => tab.tabKey).filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({
|
||||
columns: tab.columns ?? [],
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
@@ -302,6 +308,9 @@ const SewagePipeMaintainInp = () => {
|
||||
const [rowDataMap, setRowDataMap] = useState<
|
||||
Map<string, Record<string, string | number>[]>
|
||||
>(new Map());
|
||||
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
|
||||
Record<string, Record<string, string>>
|
||||
>({});
|
||||
|
||||
const searchDoneRef = useRef(false);
|
||||
const periodEnterableRef = useRef(true);
|
||||
@@ -328,6 +337,37 @@ const SewagePipeMaintainInp = () => {
|
||||
}
|
||||
}, [activeTabId, inputFocusNonce]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(tabKey);
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
const next: Record<string, string> = {};
|
||||
for (const item of res.data) {
|
||||
const key = item.engColNm?.trim();
|
||||
const guide = item.columnGuide?.trim();
|
||||
if (key && guide) next[key] = guide;
|
||||
}
|
||||
return [tabKey, next] as const;
|
||||
} catch {
|
||||
return [tabKey, {}] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTabId]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
statusInfo: SewerageStatusInfo | null,
|
||||
@@ -912,6 +952,16 @@ const SewagePipeMaintainInp = () => {
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
const prevTabKey = activeTabRef.current;
|
||||
const latestRowsInPrevTab = getTabGridRows(prevTabKey);
|
||||
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(prevTabKey, latestRowsInPrevTab);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return;
|
||||
@@ -941,22 +991,22 @@ const SewagePipeMaintainInp = () => {
|
||||
tabStatusInfoMap.get(tabKey) ?? null,
|
||||
inputPeriodEnterable,
|
||||
) || isAdminInfo;
|
||||
const columns = (tab.columns ?? []).map((col: ColumnConfig) =>
|
||||
canEdit
|
||||
? col
|
||||
: {
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? "rg-cell-readonly",
|
||||
},
|
||||
);
|
||||
const rawColumns = tab.columns ?? [];
|
||||
const base = canEdit
|
||||
? rawColumns
|
||||
: rawColumns.map((col) => ({ ...col, editable: false as const }));
|
||||
const columns = base
|
||||
.map((col) => {
|
||||
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
|
||||
return guide ? { ...col, tooltip: guide } : col;
|
||||
});
|
||||
map.set(tabKey, {
|
||||
columns,
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo]);
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
|
||||
|
||||
const searchContextForToolbar = lastSearchedValues ?? searchValues;
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
@@ -1110,11 +1160,7 @@ const SewagePipeMaintainInp = () => {
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
key={
|
||||
tabEnterableForGrid || isAdminInfo
|
||||
? `${tabKey}-enterable`
|
||||
: `${tabKey}-readonly`
|
||||
}
|
||||
key={`${tabKey}-${tabEnterableForGrid ? "enterable" : "readonly"}`}
|
||||
columnInfo={
|
||||
columnInfoByTab.get(tabKey) ??
|
||||
toGridColumnInfo(info)
|
||||
@@ -1157,6 +1203,17 @@ const SewagePipeMaintainInp = () => {
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values || !searchParamRef.current) return;
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
searchParamRef.current,
|
||||
values,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ConfirmDialog isOpen={false} />
|
||||
<SubmitDailog
|
||||
@@ -1170,6 +1227,17 @@ const SewagePipeMaintainInp = () => {
|
||||
open={verificationResultDialogOpen}
|
||||
onOpenChange={setVerificationResultDialogOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values || !searchParamRef.current) return;
|
||||
await loadActiveTabData(
|
||||
activeTabRef.current,
|
||||
searchParamRef.current,
|
||||
values,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -288,7 +288,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
setRowData([]);
|
||||
setStatusInfo(null);
|
||||
@@ -790,11 +791,13 @@ const SeweragePenetrationRateInp = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && !globalProgressVisible && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
{isLoading && !globalProgressVisible &&
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
}
|
||||
<DataGrid
|
||||
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
columnInfo={columnInfo}
|
||||
@@ -859,6 +862,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
: null
|
||||
}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values) return;
|
||||
await handleSearch(values);
|
||||
}}
|
||||
/>
|
||||
<VerificationResultDialog
|
||||
open={verificationResultDialogOpen}
|
||||
|
||||
@@ -196,6 +196,20 @@ const LogHistory = () => {
|
||||
|
||||
const handleSearch = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (startDate && endDate) {
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate);
|
||||
if (start.isAfter(end, "day")) {
|
||||
alert("시작일은 종료일보다 늦을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (end.isBefore(start, "day")) {
|
||||
alert("종료일은 시작일보다 빠를 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await loadLogs(1);
|
||||
};
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ function buildRowsFromStatusList(
|
||||
statKeys: string[],
|
||||
): Record<string, string | number>[] {
|
||||
const rowMap = new Map<string, Record<string, string | number>>();
|
||||
const rowOrder: string[] = [];
|
||||
|
||||
for (const item of statusList) {
|
||||
const { sido, sgg } = splitSidoSggNm(item.sidoSggNm);
|
||||
@@ -228,6 +229,7 @@ function buildRowsFromStatusList(
|
||||
row[key] = "";
|
||||
}
|
||||
rowMap.set(rowKey, row);
|
||||
rowOrder.push(rowKey);
|
||||
}
|
||||
|
||||
const row = rowMap.get(rowKey)!;
|
||||
@@ -236,11 +238,7 @@ function buildRowsFromStatusList(
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(rowMap.values()).sort((a, b) => {
|
||||
const sidoCmp = String(a[SIDO_FIELD]).localeCompare(String(b[SIDO_FIELD]), "ko");
|
||||
if (sidoCmp !== 0) return sidoCmp;
|
||||
return String(a[SGG_FIELD]).localeCompare(String(b[SGG_FIELD]), "ko");
|
||||
});
|
||||
return rowOrder.map((key) => rowMap.get(key)!);
|
||||
}
|
||||
|
||||
function buildUnSubmitDataRequest(
|
||||
|
||||
@@ -1,10 +1,123 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// 분리된 분뇨처리시설용 컬럼 구성 임포트
|
||||
import { ExcretaTreatmentStatusColumnConfig } from "@/components/util/column/total/SewerExcretaTreatmentStatusColumn";
|
||||
|
||||
// 집계표(TOTAL) API 및 인터페이스 명세 호출
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
ExcretaTreatmentMaintainTotalRequest
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
const ExcretaTreatmentFacilityTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
// null 타입 컴파일 에러를 방지하기 위해 엄격하게 정의한 데이터 상태
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const config = ExcretaTreatmentStatusColumnConfig;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadGridData = async (values: SearchFormValues): Promise<boolean> => {
|
||||
const param: ExcretaTreatmentMaintainTotalRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await statisticsTotalStatusApi.getExcretaTreatmentStatusTotalList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
// object[]로 변환하여 주입함으로써 상위 컴포넌트 간 null 에러 완벽 차단
|
||||
setRowData(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const ok = await loadGridData(values);
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: config.columns,
|
||||
columnLayout: config.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem] mt-[0.6rem]">
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={config.rowStartNum}
|
||||
excelExportFileSuffix={config.tabLabel}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData} // 필수 프로퍼티 에러 해결
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={config.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,206 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// 분리된 개별 3개 컬럼 스펙 파일 로드
|
||||
import { PrivateSewageTab1ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column";
|
||||
import { PrivateSewageTab2ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column";
|
||||
import { PrivateSewageTab3ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column";
|
||||
|
||||
// 통계 API 서비스 및 통합 DTO 명세 임포트
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
PrivateSewageFacilitySummaryRequest
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
const PrivateSewageColumnListTot = [
|
||||
PrivateSewageTab1ColumnConfig,
|
||||
PrivateSewageTab2ColumnConfig,
|
||||
PrivateSewageTab3ColumnConfig
|
||||
];
|
||||
|
||||
const firstTabKey = PrivateSewageColumnListTot[0]?.tabKey ?? "ST00040005_1";
|
||||
|
||||
const PrivateSewageFacilityTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
// null 컴파일 예러를 사전에 원천 차단한 안전 상태 맵 선언
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = PrivateSewageColumnListTot;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
/** 각 탭별 엔드포인트 비동기 디스패치 및 연동 */
|
||||
const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<boolean> => {
|
||||
const param: PrivateSewageFacilitySummaryRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
// [Tab1] 처리용량별 오수처리시설
|
||||
if (tabKey === "ST00080001") {
|
||||
const response = await statisticsTotalStatusApi.getPrivateSewageTab1List(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [Tab2] 정화조현황
|
||||
if (tabKey === "ST00080002") {
|
||||
const response = await statisticsTotalStatusApi.getPrivateSewageTab2List(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [Tab3] 개인하수도 관리지역 지정공고 현황
|
||||
if (tabKey === "ST00080003") {
|
||||
const response = await statisticsTotalStatusApi.getPrivatePrivateMgmtAreaList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const ok = await loadGridData(activeTab, values);
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
if (existing && existing.length > 0) return; // 캐싱 검증 스킵
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadGridData(tabKey, values);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{/* 하수관로 규칙과 100% 싱크 맞춘 중앙 정렬 소형 팝업 로딩 바 고정 */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
|
||||
// 단언 연산자(as) 배제 형태 완벽 적용
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,123 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// 분리된 중계펌프장용 컬럼 구성 임포트
|
||||
import { MediationPumpStatusColumnConfig } from "@/components/util/column/total/SewerMediationPumpStatusColumn";
|
||||
|
||||
// 집계표(TOTAL) API 및 인터페이스 명세 호출
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
MediationPumpMaintainTotalRequest
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
const PumpingStationTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
// null 타입 컴파일 에러가 발생하지 않도록 엄격하게 정의한 데이터 상태
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const config = MediationPumpStatusColumnConfig;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadGridData = async (values: SearchFormValues): Promise<boolean> => {
|
||||
const param: MediationPumpMaintainTotalRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await statisticsTotalStatusApi.getMediationPumpStationStatusTotalList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
// object[]로 변환하여 주입함으로써 null 에러 전면 방지
|
||||
setRowData(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const ok = await loadGridData(values);
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: config.columns,
|
||||
columnLayout: config.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem] mt-[0.6rem]">
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={config.rowStartNum}
|
||||
excelExportFileSuffix={config.tabLabel}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={config.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,162 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
// 12. 하수 및 분뇨 관리 산업 탭 설정 (경로 확인 필수)
|
||||
import { PublicSewageStaffTab1ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column";
|
||||
import { PublicHumanWasteStaffTab2ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column";
|
||||
import { HumanWasteCollectionTab3ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column";
|
||||
import { IndividualSewageTab4ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
SewerageIndustry,
|
||||
PublicSewageStaffTab1Response,
|
||||
PublicHumanWasteStaffTab2Response,
|
||||
HumanWasteCollectionTab3Response,
|
||||
IndividualSewageTab4Response
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface";
|
||||
|
||||
const SewageExcretaIndustryColumnListTot = [PublicSewageStaffTab1ColumnConfig,PublicHumanWasteStaffTab2ColumnConfig,HumanWasteCollectionTab3ColumnConfig,IndividualSewageTab4ColumnConfig].filter(Boolean);
|
||||
const firstTabKey = SewageExcretaIndustryColumnListTot[0]?.tabKey ?? "ST00120001";
|
||||
|
||||
const SewageExcretaRelatedTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<boolean> => {
|
||||
setIsLoading(true);
|
||||
const param: SewerageIndustry = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
if (tabKey === "ST00120001") {
|
||||
const response: PublicSewageStaffTab1Response = await statisticsTotalStatusApi.getPublicSewageStaffTab1List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (tabKey === "ST00120002") {
|
||||
const response: PublicHumanWasteStaffTab2Response = await statisticsTotalStatusApi.getPublicHumanWasteStaffTab2List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (tabKey === "ST00120003") {
|
||||
const response: HumanWasteCollectionTab3Response = await statisticsTotalStatusApi.getHumanWasteCollectionTab3List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// [TAB4] 개인하수처리관련업
|
||||
else if (tabKey === "ST00120004") {
|
||||
const response: IndividualSewageTab4Response = await statisticsTotalStatusApi.getIndividualSewageTab4List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
lastSearchedValuesRef.current = values;
|
||||
await loadGridData(activeTab, values);
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values || rowDataMap.has(tabKey)) return;
|
||||
await loadGridData(tabKey, values);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{SewageExcretaIndustryColumnListTot.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>{info.tabLabel}</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{SewageExcretaIndustryColumnListTot.map((info) => (
|
||||
<TabsContent key={info.tabKey} value={info.tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={{ columns: info.columns, columnLayout: info.columnLayout }}
|
||||
rowStartNum={info.rowStartNum}
|
||||
rowData={rowDataMap.get(info.tabKey) ?? []}
|
||||
excelExportFileSuffix={info.tabLabel}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(info.tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(info.tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,210 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// 4개 탭 컬럼 설정 임포트
|
||||
import { SludgeProcessingTab1ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column";
|
||||
import { DigestionTankTab2ColumnConfig } from "@/components/util/column/total/sludgeProcessing/DigestionTankTab2Column";
|
||||
import { SewageSludgeOperTab3ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column";
|
||||
import { HumanWasteSludgeTab4ColumnConfig } from "@/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column";
|
||||
|
||||
// API 인스턴스 및 인터페이스 임포트 (타입 안전성 확보)
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
SludgeSummaryRequest,
|
||||
SludgeProcessingTab1Response,
|
||||
DigestionTankOperTab2Response,
|
||||
SewageSludgeOperTab3Response,
|
||||
HumanWasteSludgeTab4Response
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-sludge-total-interface";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
// 탭 순회 설정
|
||||
const SludgeColumnListTot = [
|
||||
SludgeProcessingTab1ColumnConfig,
|
||||
DigestionTankTab2ColumnConfig,
|
||||
SewageSludgeOperTab3ColumnConfig,
|
||||
HumanWasteSludgeTab4ColumnConfig
|
||||
].filter(Boolean);
|
||||
|
||||
const firstTabKey = SludgeColumnListTot[0]?.tabKey ?? "ST00110001";
|
||||
|
||||
const SewageExcretaSlurgeTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = SludgeColumnListTot;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<boolean> => {
|
||||
setIsLoading(true);
|
||||
const param: SludgeSummaryRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
// [TAB1] 하수및분뇨찌꺼기 > 찌꺼기처리시설현황
|
||||
if (tabKey === "ST00110001") {
|
||||
const response: SludgeProcessingTab1Response = await statisticsTotalStatusApi.getSludgeProcessingTab1List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// [TAB2] 소화조 운영현황
|
||||
else if (tabKey === "ST00110002") {
|
||||
const response: DigestionTankOperTab2Response = await statisticsTotalStatusApi.getSludgeTab2List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// [TAB3] 하수찌꺼기 처리시설 운영현황
|
||||
else if (tabKey === "ST00110003") {
|
||||
const response: SewageSludgeOperTab3Response = await statisticsTotalStatusApi.getSludgeTab3List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// [TAB4] 분뇨찌꺼기 처리현황
|
||||
else if (tabKey === "ST00110004") {
|
||||
const response: HumanWasteSludgeTab4Response = await statisticsTotalStatusApi.getSludgeTab4List(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 검색 버튼 이벤트: SearchForm의 Values 타입을 기반으로 호출 */
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// 탭 변경 없이 현재 탭(activeTab)에 대해서만 명시적 타입 호출
|
||||
const ok = await loadGridData(activeTab, values);
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch (error) {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 탭 변경 이벤트: 현재 탭 상태와 SearchForm의 마지막 값을 동기화 */
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
// 마지막 검색 값이 있으면 해당 탭만 새로 로드
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values || rowDataMap.has(tabKey)) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadGridData(tabKey, values);
|
||||
} catch (error) {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>{info.tabLabel}</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={{ columns: info.columns, columnLayout: info.columnLayout }}
|
||||
rowStartNum={info.rowStartNum}
|
||||
rowData={rowData}
|
||||
excelExportFileSuffix={info.tabLabel}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,12 +15,17 @@ import type { SewagePipeMaintainTotalRequest } from "@/entities/statistics/model
|
||||
const firstTabKey = SewagePipeMaintainColumnListTot[0]?.tabKey ?? "ST00030001";
|
||||
|
||||
function buildSewagePipeMaintainTotSearchParam(values: SearchFormValues): SewagePipeMaintainTotalRequest {
|
||||
const isSummaryEnabled = values.isSummary !== 'ALL';
|
||||
const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined;
|
||||
|
||||
return {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: isSummaryEnabled,
|
||||
...(isSummaryEnabled ? { summaryYearCnt } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,7 +133,7 @@ const SewagePipeMaintainTot = () => {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
|
||||
@@ -1,10 +1,190 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// 분리된 개별 컬럼 구성 임포트 경로 유지
|
||||
import { ReservoirStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn";
|
||||
import { PumpStationStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn";
|
||||
|
||||
// 수정 적용한 API 및 인터페이스 명세 호출 유지
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
SewerReservoirMaintainTotalRequest
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
const SewerReservoirColumnListTot = [
|
||||
ReservoirStatusColumnConfig,
|
||||
PumpStationStatusColumnConfig
|
||||
];
|
||||
|
||||
const firstTabKey = SewerReservoirColumnListTot[0]?.tabKey ?? "ST00040001";
|
||||
|
||||
const SewageReservoirPumpTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
// 상태 정의에서 null을 제외하여 DataGrid와 타입을 일치시킵니다.
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = SewerReservoirColumnListTot;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<boolean> => {
|
||||
const param: SewerReservoirMaintainTotalRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
if (tabKey === "ST00040001") {
|
||||
const response = await statisticsTotalStatusApi.getSewerReservoirStatusTotalList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
if (tabKey === "ST00040002") {
|
||||
const response = await statisticsTotalStatusApi.getSewerPumpStationStatusTotalList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const ok = await loadGridData(activeTab, values);
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
if (existing && existing.length > 0) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadGridData(tabKey, values);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,217 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// 분리된 4개 개별 탭 컬럼 파일 바인딩
|
||||
import { SewerageFinancialTab1ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column";
|
||||
import { SewerageFeeTab2ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column";
|
||||
import { SewerageUsageFeeTab3ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column";
|
||||
import { SewerageOrgCauseTab4ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column";
|
||||
|
||||
// API 인스턴스 및 인터페이스 호출
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
SewerageFinanceSummaryRequest
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
const SewerageFinanceColumnListTot = [
|
||||
SewerageFinancialTab1ColumnConfig,
|
||||
SewerageFeeTab2ColumnConfig,
|
||||
SewerageUsageFeeTab3ColumnConfig,
|
||||
SewerageOrgCauseTab4ColumnConfig
|
||||
];
|
||||
|
||||
const firstTabKey = SewerageFinanceColumnListTot[0]?.tabKey ?? "ST00040006_1";
|
||||
|
||||
const SewerageFinanceTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
// 컴파일 에러가 발생하지 않도록 원천 가공 처리형 맵 적용
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = SewerageFinanceColumnListTot;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
/** 개별 조건별 데이터 동적 라우팅 디스패치 */
|
||||
const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<boolean> => {
|
||||
const param: SewerageFinanceSummaryRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
// [Tab1] 하수도재정 결산
|
||||
if (tabKey === "ST00090001") {
|
||||
const response = await statisticsTotalStatusApi.getSewerageFinancialList(param); // 엔드포인트 바인딩
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [Tab2] 하수도 요금
|
||||
if (tabKey === "ST00090002") {
|
||||
const response = await statisticsTotalStatusApi.getSewerageFeeList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [Tab3] 하수도 사용료
|
||||
if (tabKey === "ST00090003") {
|
||||
const response = await statisticsTotalStatusApi.getSewerageUsageList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [Tab4] 원인자 부담금
|
||||
if (tabKey === "ST00090004") {
|
||||
const response = await statisticsTotalStatusApi.getSewerageOrgCauseList(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const ok = await loadGridData(activeTab, values);
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
if (existing && existing.length > 0) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadGridData(tabKey, values);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
|
||||
// 요청하신 타입 단언 제거 형태 유지
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,131 +1,99 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { ComplaintInpColumn } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn";
|
||||
import { ResidentFriendlyFactTotColumn } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
const SewerageServiceLevelInpTabList = [
|
||||
ComplaintInpColumn,
|
||||
ResidentFriendlyFactTotColumn,
|
||||
];
|
||||
const SUPPORTED_TAB_KEYS = [ComplaintInpColumn.tabKey, ResidentFriendlyFactTotColumn.tabKey] as const;
|
||||
// 13. 하수도서비스수준 컬럼 설정
|
||||
import { SewageComplaintTab1ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn";
|
||||
import { ResidentFriendlyFacilityTab2ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn";
|
||||
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
SewerageServiceLevelRequest,
|
||||
SewerageServiceLevelComplaintResponse,
|
||||
SewerageServiceLevelResidentFriendlyFactResponse
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-service-level-total-interface";
|
||||
|
||||
const SewageServiceLevelColumnListTot = [
|
||||
SewageComplaintTab1ColumnConfig,
|
||||
ResidentFriendlyFacilityTab2ColumnConfig
|
||||
].filter(Boolean);
|
||||
|
||||
const firstTabKey = SewageServiceLevelColumnListTot[0]?.tabKey ?? "ST00130001";
|
||||
|
||||
const SewerageServiceLevelTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState(ComplaintInpColumn.tabKey);
|
||||
const lastSearchValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = SewerageServiceLevelInpTabList;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(ComplaintInpColumn.tabKey);
|
||||
lastSearchValuesRef.current = null;
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadSewerageServiceLevelTotTabData = async (
|
||||
tabKey: string,
|
||||
values: SearchFormValues,
|
||||
mergeIntoExistingMap: boolean,
|
||||
) => {
|
||||
const param = {
|
||||
const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<boolean> => {
|
||||
setIsLoading(true);
|
||||
const param: SewerageServiceLevelRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
const fetcherMap = {
|
||||
[ComplaintInpColumn.tabKey]: statisticsTotalStatusApi.getSewerageServiceLevelComplaint.bind(
|
||||
statisticsTotalStatusApi,
|
||||
),
|
||||
[ResidentFriendlyFactTotColumn.tabKey]:
|
||||
statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact.bind(
|
||||
statisticsTotalStatusApi,
|
||||
),
|
||||
};
|
||||
const fetcher = fetcherMap[tabKey];
|
||||
if (!fetcher) {
|
||||
return false;
|
||||
}
|
||||
const response = await fetcher(param);
|
||||
|
||||
if (response.status === "success" && response.data) {
|
||||
const rows = toGridRows(response.data as object[]);
|
||||
if (mergeIntoExistingMap) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, rows);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
const next = new Map<string, Record<string, string | number>[]>();
|
||||
next.set(tabKey, rows);
|
||||
setRowDataMap(next);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
let ok = false;
|
||||
if (SUPPORTED_TAB_KEYS.includes(activeTab)) {
|
||||
ok = await loadSewerageServiceLevelTotTabData(activeTab, values, true);
|
||||
}
|
||||
if (ok) {
|
||||
lastSearchValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchValuesRef.current = null;
|
||||
// [TAB1] 민원
|
||||
if (tabKey === "ST00130001") {
|
||||
const response: SewerageServiceLevelComplaintResponse = await statisticsTotalStatusApi.getSewerageServiceLevelComplaint(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// [TAB2] 주민친화시설
|
||||
else if (tabKey === "ST00130002") {
|
||||
const response: SewerageServiceLevelResidentFriendlyFactResponse = await statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact(param);
|
||||
if (response?.status === "success") {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchValuesRef.current = null;
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
lastSearchedValuesRef.current = values;
|
||||
await loadGridData(activeTab, values);
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
const values = lastSearchValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
const hasData = Array.isArray(existing) && existing.length > 0;
|
||||
if (hasData) return;
|
||||
|
||||
if (SUPPORTED_TAB_KEYS.includes(tabKey)) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadSewerageServiceLevelTotTabData(tabKey, values, true);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values || rowDataMap.has(tabKey)) return;
|
||||
await loadGridData(tabKey, values);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
@@ -133,58 +101,48 @@ const SewerageServiceLevelTot = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
{SewageServiceLevelColumnListTot.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>{info.tabLabel}</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? "");
|
||||
if (rowType === "DETAIL") return undefined;
|
||||
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
|
||||
return "rg-cell-rowtype-other";
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
|
||||
{SewageServiceLevelColumnListTot.map((info) => (
|
||||
<TabsContent key={info.tabKey} value={info.tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={{ columns: info.columns, columnLayout: info.columnLayout }}
|
||||
rowStartNum={info.rowStartNum}
|
||||
rowData={rowDataMap.get(info.tabKey) ?? []}
|
||||
excelExportFileSuffix={info.tabLabel}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(info.tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(info.tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,229 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import SearchForm from "../../module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
// [폴더 정밀 정비 완료] 프로젝트 트리 구조 5개 컬럼 스펙 패키지 호출
|
||||
import { SewerageReuseTab1ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageReuseTab1Column";
|
||||
import { SewerageGrayWaterTab2ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column";
|
||||
import { SewerageRainwaterTab3ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column";
|
||||
import { SewerageWwtRtlztnRegTab4ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column";
|
||||
import { SewerageWwtRtlztnSncnTab5ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column";
|
||||
|
||||
// 집계표(TOTAL) API 서비스 및 인터페이스 명세 임포트
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import type {
|
||||
WaterReuseSummaryRequest
|
||||
} from "@/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
|
||||
// 5개 멀티 탭 매핑을 위한 설정 취합 배열
|
||||
const WaterReuseColumnListTot = [
|
||||
SewerageReuseTab1ColumnConfig,
|
||||
SewerageGrayWaterTab2ColumnConfig,
|
||||
SewerageRainwaterTab3ColumnConfig,
|
||||
SewerageWwtRtlztnRegTab4ColumnConfig,
|
||||
SewerageWwtRtlztnSncnTab5ColumnConfig // 👈 5번째 탭 풀장착 완료
|
||||
];
|
||||
|
||||
const firstTabKey = WaterReuseColumnListTot[0]?.tabKey ?? "ST00040007_1";
|
||||
|
||||
const WaterReuseTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
// Map 데이터 구조를 사용하여 완벽하게 안전한 멀티 탭 데이터 상태 유지
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = WaterReuseColumnListTot;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<boolean> => {
|
||||
const param: WaterReuseSummaryRequest = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: values.isSummary !== 'ALL',
|
||||
summaryYearCnt: values.isSummary !== 'ALL' ? Number(values.isSummary) : undefined
|
||||
};
|
||||
|
||||
try {
|
||||
// [TAB1] 공공하수처리수 재이용 현황 조회
|
||||
if (tabKey === "ST00100001") {
|
||||
const response = await statisticsTotalStatusApi.getSewerageReuseTab1List(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [TAB2] 중수도 현황 조회
|
||||
if (tabKey === "ST00100002") {
|
||||
const response = await statisticsTotalStatusApi.getGrayWaterStatusTab2List(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [TAB3] 빗물이용시설현황 조회
|
||||
if (tabKey === "ST00100003") {
|
||||
const response = await statisticsTotalStatusApi.getRainwaterStatusTab3List(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [TAB4] 하·폐수처리수 재이용시설 시공업 등록현황 조회
|
||||
if (tabKey === "ST00100004") {
|
||||
const response = await statisticsTotalStatusApi.getWwtRtlztnRegStatusTab4List(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// [TAB5] 하·폐수처리수 재이용시설 시공업 인가현황 조회
|
||||
if (tabKey === "ST00100005") {
|
||||
const response = await statisticsTotalStatusApi.getWwtRtlztnSncnStatusTab5List(param);
|
||||
if (response && response.status === "success" && response.data) {
|
||||
setRowDataMap((prev) => new Map(prev).set(tabKey, toGridRows(response.data as object[])));
|
||||
return true;
|
||||
}
|
||||
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const ok = await loadGridData(activeTab, values);
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
// 중복 API 통신 방지 필터 캐싱 디스패치
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
if (existing && existing.length > 0) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadGridData(tabKey, values);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
|
||||
{/* 정중앙 5rem 크기 고정 격리형 로딩 애니메이션 */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CellColorInfo />
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={true}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -116,12 +116,22 @@ export type DataFieldValueCallback = (
|
||||
|
||||
/** 계산 필드 설정 (computedFieldCallbacks.ts 참고) */
|
||||
export type ComputedFieldConfig =
|
||||
| { fields: string[]; type: "sum" | "avg" | "max" | "difference" }
|
||||
| { fields: string[]; type: "sum" | "avg" | "max" | "difference" | "quotient" }
|
||||
| {
|
||||
minuendFields: string[];
|
||||
subtrahendFields: string[];
|
||||
type: "differenceOfSums";
|
||||
}
|
||||
| {
|
||||
firstSumFields: string[];
|
||||
secondSumFields: string[];
|
||||
type: "halfSumOfSums";
|
||||
}
|
||||
| {
|
||||
firstHalfSum: { firstSumFields: string[]; secondSumFields: string[] };
|
||||
secondHalfSum: { firstSumFields: string[]; secondSumFields: string[] };
|
||||
type: "sumOfTwoHalfSumOfSums";
|
||||
}
|
||||
| {
|
||||
baseField: string;
|
||||
sumFields: string[];
|
||||
@@ -149,6 +159,25 @@ export type ComputedFieldConfig =
|
||||
denominatorFields: string[];
|
||||
thirdFields: string[];
|
||||
type: "swrgInstlrtRate";
|
||||
}
|
||||
| {
|
||||
pumpGroups: Array<{
|
||||
pumpCntField: string;
|
||||
rsrvPumpCntField: string;
|
||||
perPpqtyField: string;
|
||||
}>;
|
||||
type: "pumpGrossPpqty";
|
||||
}
|
||||
| {
|
||||
inflwField: string;
|
||||
dschgField: string;
|
||||
type: "bodPrcsRt";
|
||||
}
|
||||
| {
|
||||
/** (quotient[0]/quotient[1]) / (denominatorQuotient[0]/denominatorQuotient[1]) × 100 */
|
||||
numeratorQuotient: [string, string];
|
||||
denominatorQuotient: [string, string];
|
||||
type: "quotientRatioPercent";
|
||||
};
|
||||
|
||||
export type ColumnConfig = {
|
||||
|
||||
@@ -8,58 +8,83 @@ import { ValueType } from "realgrid";
|
||||
export const ExcretaTreatFactInpColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false },
|
||||
{ name: 'gugun', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } },
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } },
|
||||
|
||||
// 기본 정보
|
||||
{ name: 'ecscs_nm', fieldName: '시설명', header: { text: '시설명' } },
|
||||
{ name: 'locplcs', fieldName: '소재지', header: { text: '소재지' } },
|
||||
{ name: 'telno', fieldName: '전화번호', header: { text: '전화번호' } },
|
||||
{ name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } },
|
||||
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' } },
|
||||
{ name: 'telNo', fieldName: 'telNo', header: { text: '전화번호' } },
|
||||
|
||||
// 날짜 정보
|
||||
{ name: 'comp_dt', fieldName: '준공일 yyyy-mm-dd', header: { text: '준공일 yyyy-mm-dd' } },
|
||||
{ name: 'rn_dt', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일 yyyy-mm-dd' } },
|
||||
{
|
||||
name: 'cmptnStrtYmd',
|
||||
fieldName: 'cmptnStrtYmd',
|
||||
header: { text: '준공일 yyyy-mm-dd' },
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
editor: {
|
||||
type: 'date' as const,
|
||||
dropDownWhenClick: true,
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'oprtngStrtYmd',
|
||||
fieldName: 'oprtngStrtYmd',
|
||||
header: { text: '가동개시일 yyyy-mm-dd' },
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
editor: {
|
||||
type: 'date' as const,
|
||||
dropDownWhenClick: true,
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
},
|
||||
},
|
||||
|
||||
// 사업비/용량 정보
|
||||
{ name: 'wct', fieldName: '사업비(백만원)', header: { text: '사업비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ecscs_cpcty', fieldName: '시설용량(m³/일)', header: { text: '시설용량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'process_cpcty', fieldName: '처리량(m³/일)', header: { text: '처리량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'opr_daycnt', fieldName: '가동일수(일/년)', header: { text: '가동일수(일/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wholPjtcoAmt', fieldName: 'wholPjtcoAmt', header: { text: '사업비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'cpct', fieldName: 'cpct', header: { text: '처리량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수(일/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 처리 공법 및 연계
|
||||
{ name: 'trt_msrc', fieldName: '처리공법', header: { text: '처리공법' } },
|
||||
{ name: 'cntc_prcplt', fieldName: '연계처리장명', header: { text: '연계처리장명' } },
|
||||
{ name: 'assct_trt_cpc', fieldName: '연계처리량(m³/일)', header: { text: '연계처리량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리공법' } },
|
||||
{ name: 'linkTmplntCn', fieldName: 'linkTmplntCn', header: { text: '연계처리장명' } },
|
||||
{ name: 'linkPrcsQnt', fieldName: 'linkPrcsQnt', header: { text: '연계처리량(m³/일)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 유입수질
|
||||
{ name: 'infl_wqlt_bod', fieldName: 'BOD', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'infl_wqlt_toc', fieldName: 'TOC', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'infl_wqlt_ss', fieldName: 'SS', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'infl_wqlt_tn', fieldName: 'T-N', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'infl_wqlt_tp', fieldName: 'T-P', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'infl_wqlt_clfgrpcnt', fieldName: '대장균군수', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'inflwBodWqVl', fieldName: 'inflwBodWqVl', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflwTocWqVl', fieldName: 'inflwTocWqVl', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflwSsWqVl', fieldName: 'inflwSsWqVl', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflwTnWqVl', fieldName: 'inflwTnWqVl', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'inflwTpWqVl', fieldName: 'inflwTpWqVl', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'inflwClfgCnt', fieldName: 'inflwClfgCnt', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 방류수질
|
||||
{ name: 'effl_wqlt_bod', fieldName: 'BOD', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'effl_wqlt_toc', fieldName: 'TOC', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'effl_wqlt_ss', fieldName: 'SS', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'effl_wqlt_tn', fieldName: 'T-N', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'effl_wqlt_tp', fieldName: 'T-P', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'effl_wqlt_clfgrpcnt', fieldName: '대장균군수', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'dschgBodWqVl', fieldName: 'dschgBodWqVl', header: { text: 'BOD' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'dschgTocWqVl', fieldName: 'dschgTocWqVl', header: { text: 'TOC' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'dschgSsWqVl', fieldName: 'dschgSsWqVl', header: { text: 'SS' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'dschgTnWqVl', fieldName: 'dschgTnWqVl', header: { text: 'T-N' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'dschgTpWqVl', fieldName: 'dschgTpWqVl', header: { text: 'T-P' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'dschgClfgCnt', fieldName: 'dschgClfgCnt', header: { text: '대장균군수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 방류수역
|
||||
{ name: 'rvbsn', fieldName: '수계', header: { text: '수계' } },
|
||||
{ name: 'bstrm', fieldName: '지류', header: { text: '지류' } },
|
||||
{ name: 'lgbsn', fieldName: '중권역', header: { text: '중권역' } },
|
||||
{ name: 'mdmbsn', fieldName: '소권역', header: { text: '소권역' } },
|
||||
{ name: 'wtsysNm', fieldName: 'wtsysNm', header: { text: '수계' } },
|
||||
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } },
|
||||
{ name: 'msirbCn', fieldName: 'msirbCn', header: { text: '중권역' } },
|
||||
{ name: 'ssirbCn', fieldName: 'ssirbCn', header: { text: '소권역' } },
|
||||
|
||||
// 운영 정보
|
||||
{ name: 'oper_mth', fieldName: '운영주체(자체/공기업/민간위탁)', header: { text: '운영주체(자체/공기업/민간위탁)' } },
|
||||
{ name: 'cntc_prcplt_cpc', fieldName: '직원총수(명)', header: { text: '직원총수(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'safe_acdnt_co', fieldName: '안전사고건수(건/인/년)', header: { text: '안전사고건수(건/인/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'operMthdCdCn', fieldName: 'operMthdCdCn', header: { text: '운영주체(자체/공기업/민간위탁)' } },
|
||||
{ name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수(건/인/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 비고
|
||||
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
|
||||
// Hidden Columns : 시스템 관리를 위한 값
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, },
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설일련번호' }, visible: false, },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
@@ -67,41 +92,51 @@ export const ExcretaTreatFactInpColumn = {
|
||||
name: 'adminGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: ['sido', 'gugun'],
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] },
|
||||
'sggNm',
|
||||
],
|
||||
},
|
||||
'ecscs_nm',
|
||||
'locplcs',
|
||||
'telno',
|
||||
'comp_dt',
|
||||
'rn_dt',
|
||||
'wct',
|
||||
'ecscs_cpcty',
|
||||
'process_cpcty',
|
||||
'opr_daycnt',
|
||||
'trt_msrc',
|
||||
'cntc_prcplt',
|
||||
'assct_trt_cpc',
|
||||
'fcltNm',
|
||||
'fcltPstnCn',
|
||||
'telNo',
|
||||
'cmptnStrtYmd',
|
||||
'oprtngStrtYmd',
|
||||
'wholPjtcoAmt',
|
||||
'prcsFcltCpct',
|
||||
'cpct',
|
||||
'oprtngDayCnt',
|
||||
'prcsMthdExpln',
|
||||
'linkTmplntCn',
|
||||
'linkPrcsQnt',
|
||||
{
|
||||
name: 'influenceGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '유입수질(mg/L, 개/mL)' },
|
||||
items: ['infl_wqlt_bod', 'infl_wqlt_toc', 'infl_wqlt_ss', 'infl_wqlt_tn', 'infl_wqlt_tp', 'infl_wqlt_clfgrpcnt'],
|
||||
items: ['inflwBodWqVl', 'inflwTocWqVl', 'inflwSsWqVl', 'inflwTnWqVl', 'inflwTpWqVl', 'inflwClfgCnt'],
|
||||
},
|
||||
{
|
||||
name: 'effluentGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '방류수질(mg/L, 개/mL)' },
|
||||
items: ['effl_wqlt_bod', 'effl_wqlt_toc', 'effl_wqlt_ss', 'effl_wqlt_tn', 'effl_wqlt_tp', 'effl_wqlt_clfgrpcnt'],
|
||||
items: ['dschgBodWqVl', 'dschgTocWqVl', 'dschgSsWqVl', 'dschgTnWqVl', 'dschgTpWqVl', 'dschgClfgCnt'],
|
||||
},
|
||||
{
|
||||
name: 'dischargeGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '방류수역' },
|
||||
items: ['rvbsn', 'bstrm', 'lgbsn', 'mdmbsn'],
|
||||
items: ['wtsysNm', 'trbtrNm', 'msirbCn', 'ssirbCn'],
|
||||
},
|
||||
'oper_mth',
|
||||
'cntc_prcplt_cpc',
|
||||
'safe_acdnt_co',
|
||||
'rmk',
|
||||
]
|
||||
'operMthdCdCn',
|
||||
'wholNope',
|
||||
'sftyAcdntEvntcsCnt',
|
||||
'rmrk',
|
||||
'admdstCd',
|
||||
'fcltSn',
|
||||
'admdstNm'
|
||||
],
|
||||
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 2,
|
||||
tabKey: 'ST00070001',
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user