diff --git a/apps/cm/next-env.d.ts b/apps/cm/next-env.d.ts deleted file mode 100644 index 3cd7048ed..000000000 --- a/apps/cm/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/cs/src/pages/admin/ui/admin-page.tsx b/apps/cs/src/pages/admin/ui/admin-page.tsx index 6a621b865..4874f6525 100644 --- a/apps/cs/src/pages/admin/ui/admin-page.tsx +++ b/apps/cs/src/pages/admin/ui/admin-page.tsx @@ -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() { diff --git a/apps/cs/src/pages/banner/ui/banner-page.tsx b/apps/cs/src/pages/banner/ui/banner-page.tsx index e09f5ab58..4e1e4733c 100644 --- a/apps/cs/src/pages/banner/ui/banner-page.tsx +++ b/apps/cs/src/pages/banner/ui/banner-page.tsx @@ -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>(new Map()); - + const { params, setParams, resetParams } = + useSearchParamsState(DEFAULT_PARAMS); const [rowSelection, setRowSelection] = useState({}); const [postingTargets, setPostingTargets] = useState([]); - const [bnrTtl, setBnrTtl] = useState(""); - const [searchParams, setSearchParams] = useState({ - bnrTtl: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - 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) => { + const pagination: PaginationState = { + pageIndex: params.page, + pageSize: params.size, + }; + + const handlePaginationChange: OnChangeFn = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { 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: { diff --git a/apps/cs/src/pages/bbs/ui/bbs-page.tsx b/apps/cs/src/pages/bbs/ui/bbs-page.tsx index abb1ec9db..27d0fb879 100644 --- a/apps/cs/src/pages/bbs/ui/bbs-page.tsx +++ b/apps/cs/src/pages/bbs/ui/bbs-page.tsx @@ -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({}); - const [bbsTypeCd, setBbsTypeCd] = useState(""); - const [bbsNm, setBbsNm] = useState(""); - const [searchParams, setSearchParams] = useState({ - bbsTypeCd: "", - bbsNm: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - 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) => { + const pagination: PaginationState = { + pageIndex: params.page, + pageSize: params.size, + }; + + const handlePaginationChange: OnChangeFn = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { 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: { diff --git a/apps/cs/src/pages/code/model/upcode-table-columns.tsx b/apps/cs/src/pages/code/model/upcode-table-columns.tsx index c35ea4d70..5c9108543 100644 --- a/apps/cs/src/pages/code/model/upcode-table-columns.tsx +++ b/apps/cs/src/pages/code/model/upcode-table-columns.tsx @@ -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[] = [ @@ -24,7 +27,7 @@ export const columns: ColumnDef[] = [ header: "상위코드명", }, { - accessorKey: "cdSe", + accessorKey: "cdSeNm", header: "코드구분", }, { diff --git a/apps/cs/src/pages/contents/ui/contents-page.tsx b/apps/cs/src/pages/contents/ui/contents-page.tsx index 105aecdba..bda7c9ed6 100644 --- a/apps/cs/src/pages/contents/ui/contents-page.tsx +++ b/apps/cs/src/pages/contents/ui/contents-page.tsx @@ -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({ - contsTtl: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - 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) => { + const pagination: PaginationState = { + pageIndex: params.page, + pageSize: params.size, + }; + + const handlePaginationChange: OnChangeFn = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { 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, }, diff --git a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx index d787a6ab7..466148ed4 100644 --- a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx @@ -354,7 +354,7 @@ function DashboardPage() {

실시간 알림

-
+
{noticeUserList?.map((item) => (

일정 알림

-
+
{scheduleList && scheduleList.length > 0 ? ( scheduleList.map((schedule, index) => { diff --git a/apps/cs/src/pages/file/ui/file-page.tsx b/apps/cs/src/pages/file/ui/file-page.tsx index f4ff3f97a..8669d6717 100644 --- a/apps/cs/src/pages/file/ui/file-page.tsx +++ b/apps/cs/src/pages/file/ui/file-page.tsx @@ -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() { diff --git a/apps/cs/src/pages/groups/ui/add-user-dialog.tsx b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx index f2a1b68a2..1b94048fc 100644 --- a/apps/cs/src/pages/groups/ui/add-user-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx @@ -23,6 +23,11 @@ import { Input, Checkbox, Label, + Select, + SelectValue, + SelectContent, + SelectItem, + SelectTrigger, } from "@common/components"; import { UserApi, @@ -42,9 +47,11 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) { const alert = useAlertStore((state) => state.setMessage); const [open, setOpen] = useState(false); - const [userName, setUserName] = useState(""); + const [keywordType, setKeywordType] = useState("all"); + const [keyword, setKeyword] = useState(""); const [searchParam, setSearchParam] = useState({ - userName: "", + keywordType: "all", + keyword: "", }); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -156,7 +163,7 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) { ...pagination, pageIndex: 0, }); - setSearchParam({ userName }); + setSearchParam({ keywordType, keyword }); }; const resetSearch = () => { @@ -164,8 +171,8 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) { pageIndex: 0, pageSize: 10, }); - setUserName(""); - setSearchParam({ userName: "" }); + setKeyword(""); + setSearchParam({ keywordType: "all", keyword: "" }); setUserRowSelection({}); }; @@ -184,15 +191,26 @@ function AddUserDialog({ groupId }: AdditionalUserDialogProps) {
- + setUserName(e.target.value)} - placeholder="사용자 이름으로 검색" + value={keyword} + onChange={(e) => setKeyword(e.target.value)} + className="!text-[1.3rem]" + placeholder="검색어를 입력하세요" />
diff --git a/apps/cs/src/pages/role/ui/role-page.tsx b/apps/cs/src/pages/role/ui/role-page.tsx index b370ed338..a6c7ec2d1 100644 --- a/apps/cs/src/pages/role/ui/role-page.tsx +++ b/apps/cs/src/pages/role/ui/role-page.tsx @@ -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({ - keywordType: "roleName", - keyword: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - 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 = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { + 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) => { - e.preventDefault(); - setPagination({ - ...pagination, - pageIndex: 0, - }); - setSearchParams({ - ...searchParams, - keyword, - }); - }; - - const handleReset = () => { - setKeyword(""); - setSearchParams({ - ...searchParams, - keyword: "", - }); - }; - return (
diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index fbdd9d36c..20a38e998 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -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({ - siteNm: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - 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) => { + const pagination: PaginationState = { + pageIndex: params.page, + pageSize: params.size, + }; + + const handlePaginationChange: OnChangeFn = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { 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, }, diff --git a/apps/cs/src/pages/survey/ui/survey-page.tsx b/apps/cs/src/pages/survey/ui/survey-page.tsx index 27f080343..f3e0481d5 100644 --- a/apps/cs/src/pages/survey/ui/survey-page.tsx +++ b/apps/cs/src/pages/survey/ui/survey-page.tsx @@ -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({ - srvyTtl: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, - }); + const { params, setParams, resetParams } = + useSearchParamsState(DEFAULT_PARAMS); + const [srvyTtl, setSrvyTtl] = useState(params.srvyTtl); const [rowSelection, setRowSelection] = useState({}); const [postingTargets, setPostingTargets] = useState([]); 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 = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { + 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) => { - e.preventDefault(); - setPagination({ - ...pagination, - pageIndex: 0, - }); - setSearchParams({ - ...searchParams, - srvyTtl, - }); - }; - - const handleReset = () => { - setSrvyTtl(""); - setSearchParams({ - ...searchParams, - srvyTtl: "", - }); - }; - useEffect(() => { const postingTargets = table .getFilteredSelectedRowModel() diff --git a/apps/cs/src/pages/template/ui/template-page.tsx b/apps/cs/src/pages/template/ui/template-page.tsx index d4ceb3bca..5db3f1572 100644 --- a/apps/cs/src/pages/template/ui/template-page.tsx +++ b/apps/cs/src/pages/template/ui/template-page.tsx @@ -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({ - keyword: "", - tmpltSeCd: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - 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) => { + const pagination: PaginationState = { + pageIndex: params.page, + pageSize: params.size, + }; + + const handlePaginationChange: OnChangeFn = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { 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, }, diff --git a/apps/cs/src/pages/terms/ui/terms-page.tsx b/apps/cs/src/pages/terms/ui/terms-page.tsx index ea00276df..73b6b0d16 100644 --- a/apps/cs/src/pages/terms/ui/terms-page.tsx +++ b/apps/cs/src/pages/terms/ui/terms-page.tsx @@ -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({ - keyword: "", - trmsSeCd: "", - sort: "regDt,desc", - }); - const [pagination, setPagination] = useState({ - 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) => { + const pagination: PaginationState = { + pageIndex: params.page, + pageSize: params.size, + }; + + const handlePaginationChange: OnChangeFn = (updater) => { + const next = typeof updater === "function" ? updater(pagination) : updater; + setParams({ page: next.pageIndex, size: next.pageSize }); + }; + + const handleSubmit = (e: React.FormEvent) => { 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, }, diff --git a/apps/cs/src/pages/users/ui/users-page.tsx b/apps/cs/src/pages/users/ui/users-page.tsx index 17b44707c..cabf8eb42 100644 --- a/apps/cs/src/pages/users/ui/users-page.tsx +++ b/apps/cs/src/pages/users/ui/users-page.tsx @@ -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({ - pageIndex: 0, - pageSize: 10, - }); + const { params, setParams, resetParams } = + useSearchParamsState(DEFAULT_PARAMS); const [presetDateRange, setPresetDateRange] = useState(""); - const [dateRange, setDateRange] = useState({ - 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({ - keywordType: "all", - keyword: "", - userStateCode: "", - aprvYn: "", - grpId: "", - frDateYmd: "", - toDateYmd: "", - sort: "regDt,desc", - }); + const [dateRange, setDateRange] = useState(() => + 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 = (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) => { + const handleSubmit = (e: React.FormEvent) => { 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() {

회원 목록

{isOpen && processFacilityList.length > 0 && ( -
+
{processFacilityList.map((item : facilityListProps, index : number) => (
handleSuggestionClick(item.bscFctCd, item.bscFctNm, Number(item.fctCpc), item.trtFrfNm, item.efflWqltStndrdNm)} role="button" tabIndex={0} diff --git a/apps/fo/app/components/common/UnderComment.tsx b/apps/fo/app/components/common/UnderComment.tsx index 898eb861e..eda31e81a 100644 --- a/apps/fo/app/components/common/UnderComment.tsx +++ b/apps/fo/app/components/common/UnderComment.tsx @@ -17,7 +17,7 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
{commentArray.map((item, index) => ( - + {commentStartPoint} {item}
@@ -28,4 +28,4 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"} } } -export default UnderComment; \ No newline at end of file +export default UnderComment; diff --git a/apps/fo/app/pages/(map-layout)/map/page.tsx b/apps/fo/app/pages/(map-layout)/map/page.tsx index 4fa553c97..09e8f969f 100644 --- a/apps/fo/app/pages/(map-layout)/map/page.tsx +++ b/apps/fo/app/pages/(map-layout)/map/page.tsx @@ -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 = ` -
${title}
- +
${title}
+
${Object.entries(props) .map( ([k, v]) => ` - + ` ) diff --git a/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx index fb59cfc13..3b670f651 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/drainageEquipment/page.tsx @@ -54,23 +54,23 @@ export default function DrainageEquipment({searchYear = '', fcltyList = []}: Dra const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { return ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'part1' ? 구분 : head.id === 'part2' ? 구분 : head.id == 'nowSum' ? {'현재\n(기준년도)'} : head.id == 'stepOneSum' ? {'변경(금회계획)'} : head.id == 'etc' ? 비고 : null))} - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'stepOneSum' ? 1단계 : head.id === 'stepTwoSum' ? 2단계 : head.id === 'stepThreeSum' ? 3단계 : head.id === 'stepFourSum' ? 4단계 : null))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { - return {head.title}; + return {head.title}; } })} diff --git a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx index 850d52528..d08a5f78b 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/modal/page.tsx @@ -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; diff --git a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx index e65fe3cc8..ac33ffe28 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx @@ -32,12 +32,12 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s return ( <> { upHeadList.map((head: TableUpperProps) => - (!head.upSequnce?{head.title}: - head.upName === 'upChangeRe'&& head.upSequnce=== 1?{'시설계획\n(금회)'}:null))} + (!head.upSequnce?{head.title}: + head.upName === 'upChangeRe'&& head.upSequnce=== 1?{'시설계획\n(금회)'}:null))} {upHeadList.map((head: TableUpperProps, index: number) => - (head.upName === 'upChangeRe'?{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}:null))} + (head.upName === 'upChangeRe'?{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}:null))} ) } @@ -52,20 +52,20 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s const headMakeColSpanOperationStatus = (upHeadList: TableUpperProps[], searchYear: string) => { return ( <> - {searchYear+'년도 운영실태'} + {searchYear+'년도 운영실태'} { upHeadList.map((head: TableUpperProps) => - (head.upName === 'upChangeRe1'&&head.upSequnce===1?{head.title}: - !head.upSequnce?{head.title}: - head.upName === 'upChangeRe2'&&head.upSequnce===1?{'연간 운영실태'}: - head.upName === 'upChangeRe3'&&head.upSequnce===1?{'하절기 운영실태\n(6~8월)'}: - head.upName === 'upChangeRe4'&&head.upSequnce===1?{'동절기 운영실태\n(1~2월)'}: - head.upName === 'upChangeRe5'&&head.upSequnce===1?{'지하수 유입량 추정'}:null))} + (head.upName === 'upChangeRe1'&&head.upSequnce===1?{head.title}: + !head.upSequnce?{head.title}: + head.upName === 'upChangeRe2'&&head.upSequnce===1?{'연간 운영실태'}: + head.upName === 'upChangeRe3'&&head.upSequnce===1?{'하절기 운영실태\n(6~8월)'}: + head.upName === 'upChangeRe4'&&head.upSequnce===1?{'동절기 운영실태\n(1~2월)'}: + head.upName === 'upChangeRe5'&&head.upSequnce===1?{'지하수 유입량 추정'}:null))} {upHeadList.map((head: TableUpperProps) => - (head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?{head.title}:null))} + (head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?{head.title}:null))} ) } @@ -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'))? - {el[head.id]} + {el[head.id]} :((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') ?{el[head.id]} :(head.id === "provisionalApproval" && index === 4) - ?{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)} + ?{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)} :((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') ) ?null :(head.id === 'item' && index != 5)? - {el[head.id]} - :{el[head.id]} + {el[head.id]} + :{el[head.id]} ))} ))} @@ -278,15 +278,15 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}: (head.id === 'rdiRain'&&index === 0)?{el['rdiRain']}: (head.id === 'rdiRain'&&index === 7)?: (head.id === 'waterQlty')?{el[head.id]}: - ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: - ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: + ((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}: null ))} diff --git a/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx index 8eddbd098..595fe89e1 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/maingraph/page.tsx @@ -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 = { diff --git a/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx b/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx index 55e6275c8..33f1d298b 100644 --- a/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx @@ -50,7 +50,7 @@ export default function RainManageTab({ searchYear = '', fcltyList = [] }: facil widthFixed={true} > - ☞ 일수에 강우영향일 포함됨 + ☞ 일수에 강우영향일 포함됨 ) } diff --git a/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx b/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx index 67e704570..05d906500 100644 --- a/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx +++ b/apps/fo/app/pages/(with-layout)/dashboardMain/page.tsx @@ -1,5 +1,5 @@ /** - * 대시보드 - 표로보기 상단구역 페에지 + * 대시보드 - 표로보기 상단구역 페이지 * * - 대시보드 - 표로보기의 3개의 탭 상단에 조회년도, 행정정보, 시설명의 구역을 컴포넌트 */ diff --git a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx index 811f1c0ff..392deb858 100644 --- a/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx @@ -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) { ); -} \ No newline at end of file +} diff --git a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx index aa1e36abd..472c13e6a 100644 --- a/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx +++ b/apps/fo/app/pages/(with-layout)/facilityWaterQlty/HeadMakeColSpan.tsx @@ -17,27 +17,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp return ( layoutNum === 0 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 행정구역 + return 행정구역 }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { - return 유출 + return 유출 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -45,27 +45,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 1 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 권역 + return 권역 }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { - return 유출 + return 유출 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -73,29 +73,29 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 2 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 권역 + return 권역 }else if(head.upName==="upChangeRe2"&&head.upSequnce===1) { - return 행정구역 + return 행정구역 }else if(head.upName==="upChangeRe3"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe4"&&head.upSequnce===1) { - return 유출 + return 유출 } })} - + {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 {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -103,25 +103,25 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 3 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1"&&head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe2"&&head.upSequnce===1) { - return 방류 + return 방류 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -129,23 +129,23 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 4 ? ( <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe"&&head.upSequnce===1) { - return 방류 + return 방류 } })} - + {upHeadList.map((head: TableUpperProps, idx: number) => { if (head.upName === 'upChangeRe') { if (idx == 1 || idx == 2 || idx == 3) { - return {head.title}; + return {head.title}; } else { - return {head.title}; + return {head.title}; } } })} @@ -153,21 +153,21 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ) : layoutNum === 5 ? ( // 총인설비 수질현황 헤더 <> - + {upHeadList.map((head: TableUpperProps) => { if(!head.upName) { - return {head.title} + return {head.title} }else if(head.upName==="upChangeRe1" && head.upSequnce===1) { - return 유입 + return 유입 }else if(head.upName==="upChangeRe2" && head.upSequnce===1) { - return 방류 + return 방류 } })} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') { - return {head.title}; + return {head.title}; } })} diff --git a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx index 0cffa775e..ecf09d7da 100644 --- a/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/flowRate/modal/page.tsx @@ -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], ); diff --git a/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx b/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx index 4045ed7b2..89829445e 100644 --- a/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx +++ b/apps/fo/app/pages/(with-layout)/processArea/HeadMakeColSpan.tsx @@ -5,77 +5,77 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp return ( layoutNum === 0 ? ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'행정구역'} : - head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : - head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'시설계획'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'행정구역'} : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'시설계획'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => ( - head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : - head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : - head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : null ))} - + {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 {head.title}; + return {head.title}; } })} ) : layoutNum === 1 ? ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'유역'} : - head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe' && head.upSequnce === 1 ? {'유역'} : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => ( - head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : - head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : - head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : + head.upName === 'upChangeRe4' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : null ))} - + {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 {head.title}; + return {head.title}; } })} ) : ( <> - {upHeadList.map((head: TableUpperProps) => + {upHeadList.map((head: TableUpperProps) => (head.id === 'trtFrfNm' ? {head.title} : head.id === 'laeNm' ? {head.title} : head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? {head.title} : head.id === 'trstOprEtpNm' ? {head.title} : - head.upName === 'upChangeRe1' && head.upSequnce === 1 ? {'권역'} : - head.upName === 'upChangeRe2' && head.upSequnce === 1 ? {'환경청'} : - head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : - head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : - head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : - !head.upName ? {head.title} : null))} + head.upName === 'upChangeRe1' && head.upSequnce === 1 ? {'권역'} : + head.upName === 'upChangeRe2' && head.upSequnce === 1 ? {'환경청'} : + head.upName === 'upChangeRe3' && head.upSequnce === 1 ? {'증설계획(m³/일)'} : + head.upName === 'upChangeRe5' && head.upSequnce === 1 ? {'처리인구(인)'} : + head.upName === 'upChangeRe6' && head.upSequnce === 1 ? {'처리구역(k㎡)'} : + !head.upName ? {head.title} : null))} - + {upHeadList.map((head: TableUpperProps) => { if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' ||head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') { - return {head.title}; + return {head.title}; } })} @@ -84,4 +84,4 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp ); } -export { HeadMakeColSpan }; \ No newline at end of file +export { HeadMakeColSpan }; diff --git a/apps/fo/app/pages/map/searchBar/searchBar.tsx b/apps/fo/app/pages/map/searchBar/searchBar.tsx index 0b1315465..b4981bfbd 100644 --- a/apps/fo/app/pages/map/searchBar/searchBar.tsx +++ b/apps/fo/app/pages/map/searchBar/searchBar.tsx @@ -116,7 +116,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) { onSelectValue(val.toString(), "ctpvCd")} @@ -125,7 +125,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) { onSelectValue(val.toString(), "sggCd")} @@ -134,7 +134,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) { onSelectValue(val.toString(), "fcltCd")} diff --git a/apps/fo/docs/작업계획/테마적용 작업계획 fo_20260625.md b/apps/fo/docs/작업계획/테마적용 작업계획 fo_20260625.md new file mode 100644 index 000000000..f1486a67d --- /dev/null +++ b/apps/fo/docs/작업계획/테마적용 작업계획 fo_20260625.md @@ -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에서 직접 토큰 치환”의 혼합 방식이다. diff --git a/apps/fo/next-env.d.ts b/apps/fo/next-env.d.ts index 1b3be0840..830fb594c 100644 --- a/apps/fo/next-env.d.ts +++ b/apps/fo/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/fo/public/images/help.png b/apps/fo/public/images/help.png index e872802e4..977ef9d38 100644 Binary files a/apps/fo/public/images/help.png and b/apps/fo/public/images/help.png differ diff --git a/apps/fo/src/app/index.tsx b/apps/fo/src/app/index.tsx index 692b55ef0..4cf2ad5fa 100644 --- a/apps/fo/src/app/index.tsx +++ b/apps/fo/src/app/index.tsx @@ -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"; diff --git a/apps/fo/src/app/styles/components-black.css b/apps/fo/src/app/styles/components-black.css new file mode 100644 index 000000000..9d2916269 --- /dev/null +++ b/apps/fo/src/app/styles/components-black.css @@ -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; + } +} diff --git a/apps/fo/src/app/styles/components-blue.css b/apps/fo/src/app/styles/components-blue.css new file mode 100644 index 000000000..30200d31b --- /dev/null +++ b/apps/fo/src/app/styles/components-blue.css @@ -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; + } +} diff --git a/apps/fo/src/app/styles/components-green.css b/apps/fo/src/app/styles/components-green.css new file mode 100644 index 000000000..470e47f98 --- /dev/null +++ b/apps/fo/src/app/styles/components-green.css @@ -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; + } +} diff --git a/apps/fo/src/app/styles/components-red.css b/apps/fo/src/app/styles/components-red.css new file mode 100644 index 000000000..802f628d0 --- /dev/null +++ b/apps/fo/src/app/styles/components-red.css @@ -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; + } +} diff --git a/apps/fo/src/app/styles/components.css b/apps/fo/src/app/styles/components.css index 4dc30bafd..521ae07f0 100644 --- a/apps/fo/src/app/styles/components.css +++ b/apps/fo/src/app/styles/components.css @@ -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"] { diff --git a/apps/sn/next-env.d.ts b/apps/sn/next-env.d.ts index 1b3be0840..830fb594c 100644 --- a/apps/sn/next-env.d.ts +++ b/apps/sn/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/sn/public/images/help.png b/apps/sn/public/images/help.png index e872802e4..977ef9d38 100644 Binary files a/apps/sn/public/images/help.png and b/apps/sn/public/images/help.png differ diff --git a/apps/sn/src/app/index.tsx b/apps/sn/src/app/index.tsx index 29d73b03f..d70dc3d73 100644 --- a/apps/sn/src/app/index.tsx +++ b/apps/sn/src/app/index.tsx @@ -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"; diff --git a/apps/sn/src/app/styles/components-black.css b/apps/sn/src/app/styles/components-black.css new file mode 100644 index 000000000..4dc30bafd --- /dev/null +++ b/apps/sn/src/app/styles/components-black.css @@ -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; + } +} diff --git a/apps/sn/src/app/styles/components-blue.css b/apps/sn/src/app/styles/components-blue.css new file mode 100644 index 000000000..7c1e9db27 --- /dev/null +++ b/apps/sn/src/app/styles/components-blue.css @@ -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; + } +} diff --git a/apps/sn/src/app/styles/components-green.css b/apps/sn/src/app/styles/components-green.css new file mode 100644 index 000000000..4b4cc770e --- /dev/null +++ b/apps/sn/src/app/styles/components-green.css @@ -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; + } +} diff --git a/apps/sn/src/app/styles/components-red.css b/apps/sn/src/app/styles/components-red.css new file mode 100644 index 000000000..cadffac52 --- /dev/null +++ b/apps/sn/src/app/styles/components-red.css @@ -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; + } +} diff --git a/apps/sn/src/app/styles/components.css b/apps/sn/src/app/styles/components.css index 4dc30bafd..3aa3c770d 100644 --- a/apps/sn/src/app/styles/components.css +++ b/apps/sn/src/app/styles/components.css @@ -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; } diff --git a/apps/st/.env.example b/apps/st/.env.example index e9667be0f..c2715e0cd 100644 --- a/apps/st/.env.example +++ b/apps/st/.env.example @@ -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 \ No newline at end of file +NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 +NEXT_PUBLIC_ST_DASHBOARD_BBS_ID=13 \ No newline at end of file diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index e849c3a1d..287cc4c38 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -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 ( - - -
- +
{isVerificationSelectionReportPanelOpen && ( - setReportPanelOpen(false)} - /> - )} {children} -
- - ); - } - - export default function WithBaseLayout({ - children, - }: { - children: React.ReactNode; - }) { - return ( - - {children} - - ); - } - - diff --git a/apps/st/app/api/st/summary-excel/download/route.ts b/apps/st/app/api/st/summary-excel/download/route.ts new file mode 100644 index 000000000..b1fda0d6b --- /dev/null +++ b/apps/st/app/api/st/summary-excel/download/route.ts @@ -0,0 +1,63 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +import { + AUTH_USER_ID, + CLAIM_NAME, + SITE_ID, + ST_SERVICE_URL, +} from "@/shared/config"; +import { SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME } from "@/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface"; + +const BACKEND_DOWNLOAD_URL = `${ST_SERVICE_URL}/st-management-service/api/v1/summary-excel/download`; + +/** middleware `/server` rewrite는 장시간 blob 응답에서 연결이 끊길 수 있어 전용 프록시 사용 */ +export const dynamic = "force-dynamic"; +export const maxDuration = 3600; + +export async function POST(request: Request) { + const cookieStore = await cookies(); + const token = cookieStore.get(CLAIM_NAME)?.value; + const userId = cookieStore.get(AUTH_USER_ID)?.value; + + const headers: Record = { + "Content-Type": "application/json", + "X-Site-Id": SITE_ID, + Accept: SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME, + }; + if (token) headers[CLAIM_NAME] = token; + if (userId) headers[AUTH_USER_ID] = userId; + + let body: string; + try { + body = await request.text(); + } catch { + return NextResponse.json({ message: "요청 본문을 읽을 수 없습니다." }, { status: 400 }); + } + + let backendRes: Response; + try { + backendRes = await fetch(BACKEND_DOWNLOAD_URL, { + method: "POST", + headers, + body, + cache: "no-store", + }); + } catch { + return NextResponse.json( + { message: "엑셀 서버에 연결할 수 없습니다." }, + { status: 502 }, + ); + } + + const responseHeaders = new Headers(); + const contentType = backendRes.headers.get("content-type"); + const contentDisposition = backendRes.headers.get("content-disposition"); + if (contentType) responseHeaders.set("Content-Type", contentType); + if (contentDisposition) responseHeaders.set("Content-Disposition", contentDisposition); + + return new NextResponse(backendRes.body, { + status: backendRes.status, + headers: responseHeaders, + }); +} diff --git a/apps/st/src/components/button/VerificationResultButton.css b/apps/st/src/components/button/VerificationResultButton.css new file mode 100644 index 000000000..c08a1f2fb --- /dev/null +++ b/apps/st/src/components/button/VerificationResultButton.css @@ -0,0 +1,24 @@ +.verification-result-btn-wrap { + position: relative; + display: inline-flex; +} + +.verification-result-btn-failed-dot { + position: absolute; + left: 0.35rem; + top: 0.35rem; + width: 0.55rem; + height: 0.55rem; + border-radius: 9999px; + background: #ff6c64; + box-shadow: 0 0 0 0 rgba(255, 108, 100, 0.75); + animation: verification-result-btn-dot-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; + pointer-events: none; +} + +@keyframes verification-result-btn-dot-ping { + 75%, + 100% { + box-shadow: 0 0 0 0.5rem rgba(255, 108, 100, 0); + } +} diff --git a/apps/st/src/components/button/VerificationResultButton.tsx b/apps/st/src/components/button/VerificationResultButton.tsx index 580f6d0d0..73eab7f98 100644 --- a/apps/st/src/components/button/VerificationResultButton.tsx +++ b/apps/st/src/components/button/VerificationResultButton.tsx @@ -1,16 +1,34 @@ import { Button } from "@common/components"; +import "./VerificationResultButton.css"; + interface VerificationResultButtonProps { VerificationResultButtonFunc: () => void; + isVerificationFailed?: boolean; } -const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => { +const VerificationResultButton = ({ + VerificationResultButtonFunc, + isVerificationFailed = false, +}: VerificationResultButtonProps) => { return ( - + + + {isVerificationFailed && ( + + )} + ); }; -export default VerificationResultButton; \ No newline at end of file +export default VerificationResultButton; diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index 843289179..2f3519372 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -6,7 +6,10 @@ } /* 셀 클래스가 있으면 위 기본색보다 우선 (같은 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-readonly.rg-cell-rowtype-curr { + @apply !bg-white !text-[#333]; } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error { @apply !bg-[#ffe6e6] !text-[#c00]; @@ -153,6 +156,39 @@ .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-other { @apply !bg-[#fff7bf]; } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr, + .rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr { + @apply !bg-white !text-[#333]; + } + .rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-curr, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr, + .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr { + @apply !bg-white !text-[#333]; + } + + /* 하수관로 검토: CURR 구분 이후 미입력 — OLD 있음(노랑) / OLD 없음(하늘) */ + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn, + .rg-grid .rg-body .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn, + .rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly.rg-cell-review-curr-empty-warn, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn, + .rg-grid .rg-body .rg-data-cell.rg-cell-review-curr-empty-warn.rg-cell-review-curr-empty-warn, + .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-review-curr-empty-warn.rg-cell-review-curr-empty-warn, + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn, + .rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn { + @apply !bg-[#feee85] !text-[#333]; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new, + .rg-grid .rg-body .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new, + .rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly.rg-cell-review-curr-empty-new, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new, + .rg-grid .rg-body .rg-data-cell.rg-cell-review-curr-empty-new.rg-cell-review-curr-empty-new, + .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-review-curr-empty-new.rg-cell-review-curr-empty-new, + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new, + .rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new, + .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new { + @apply !bg-[#bae6fd] !text-[#333]; + } /* * 검증 오류 셀(rg-cell-verify-error / -imr) 최우선 적용. diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index c1fdbd226..a8605c52e 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -3,9 +3,27 @@ import { ValueType } from "realgrid" import RealGrid, { GridFitStyle } from "realgrid" import { RealGridReact, RGDataColumn } from "realgrid-react" -import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback } from "../type/DataGrid.type" +import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback, CellStyleCallbackResult, CellStyleCallbackParams } from "../type/DataGrid.type" import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" +function mergeCellStyleCallbackResult( + baseStyleName: string | undefined, + extra: CellStyleCallbackResult | undefined, +): CellStyleCallbackResult { + if (!extra) { + return baseStyleName ? { styleName: baseStyleName } : {}; + } + if (typeof extra === "string") { + const styleName = [extra, baseStyleName].filter(Boolean).join(" "); + return styleName ? { styleName } : {}; + } + const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" "); + return { + ...extra, + ...(styleName ? { styleName } : {}), + }; +} + // RealGrid 타입 확장 declare module "realgrid" { interface GridView { @@ -40,11 +58,14 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT }; if (col.computed) { const hasFields = "fields" in col.computed && col.computed.fields.length > 0; - const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; + const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorFields" in col.computed && 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) { + const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField); + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } @@ -408,18 +429,33 @@ const CompactDataGrid = ({ columns.forEach((column) => { const baseStyleName = column.styleName; const gridCol = currentGridView.columnByName(column.name) as { - styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string; + styleCallback?: ( + g: unknown, + dataCell: { + itemIndex?: number; + index?: { itemIndex?: number }; + dataField?: string; + fieldName?: string; + }, + ) => CellStyleCallbackResult; } | null; if (gridCol) { - gridCol.styleCallback = (_g, dataCell) => { + gridCol.styleCallback = (grid, dataCell) => { const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1; - const fieldName = (dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? ''; + const fieldName = + (dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? ""; const data = rowDataRef.current; - if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? ''; + if (itemIndex < 0 || itemIndex >= data.length) { + return mergeCellStyleCallbackResult(baseStyleName, undefined); + } const row = data[itemIndex] as Record; - const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row }); - if (!extra) return baseStyleName ?? ''; - return baseStyleName ? `${extra} ${baseStyleName}` : extra; + const extra = cellStyleCallbackRef.current?.({ + rowIndex: itemIndex, + fieldName, + row, + grid: grid as CellStyleCallbackParams["grid"], + }); + return mergeCellStyleCallbackResult(baseStyleName, extra); }; } }); diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 4e8a9c86d..d99127482 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef } from "react"; +import { useEffect, useLayoutEffect, useRef, type MutableRefObject } from "react"; import JSZip from "jszip"; import { ValueType } from "realgrid"; @@ -11,7 +11,10 @@ import { Button } from "@common/components"; import type { CellStyleCallback, + CellStyleCallbackParams, + CellStyleCallbackResult, CellTooltipCallback, + ColumnConfig, ColumnLayoutItem, DataGridProps, FooterItemWithCallback, @@ -21,19 +24,114 @@ import type { import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import { + getExcelImportColumnFieldNames, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, shouldPrepareColumnsForExcelExport, } from "@/components/util/gridExcelExport"; import { useTabStore } from "@/store/tab-store"; +function mergeCellStyleCallbackResult( + baseStyleName: string | undefined, + extra: CellStyleCallbackResult | undefined, +): CellStyleCallbackResult { + if (!extra) { + return baseStyleName ? { styleName: baseStyleName } : {}; + } + if (typeof extra === "string") { + const styleName = [baseStyleName, extra].filter(Boolean).join(" "); + return styleName ? { styleName } : {}; + } + const styleName = [baseStyleName, extra.styleName].filter(Boolean).join(" "); + return { + ...extra, + ...(styleName ? { styleName } : {}), + }; +} + +type GridColumnWithStyleCallback = { + styleCallback?: ( + grid: unknown, + dataCell: { + itemIndex?: number; + index?: { itemIndex?: number }; + dataField?: string; + fieldName?: string; + }, + ) => CellStyleCallbackResult; +}; + +function mergeGridRowsWithSourceMetadata( + rows: Record[], + sourceRows: Record[], + columnFieldNames: string[], +): Record[] { + const columnFieldSet = new Set(columnFieldNames); + return rows.map((row, index) => { + const source = sourceRows[index]; + if (!source) return row; + + const merged: Record = { ...row }; + for (const [key, value] of Object.entries(source)) { + if (columnFieldSet.has(key) || value === undefined) continue; + merged[key] = value as string | number; + } + return merged; + }); +} + +function applyCellStyleCallbacksToColumns( + gridView: unknown, + columns: ColumnConfig[], + rowDataRef: MutableRefObject[]>, + cellStyleCallbackRef: MutableRefObject, +) { + if (!cellStyleCallbackRef.current || !gridView) return; + + const gv = gridView as { + columnByName?: (name: string) => GridColumnWithStyleCallback | null | undefined; + }; + if (typeof gv.columnByName !== "function") return; + + columns.forEach((column) => { + const baseStyleName = column.styleName; + let gridCol: GridColumnWithStyleCallback | null | undefined; + try { + // columnByName은 GridView 인스턴스 메서드 — this 바인딩을 위해 gv에서 직접 호출 + gridCol = gv.columnByName!(column.name); + } catch { + return; + } + if (!gridCol) return; + + gridCol.styleCallback = (grid, dataCell) => { + const itemIndex = + dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1; + const fieldName = + dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName; + const data = rowDataRef.current; + if (itemIndex < 0 || itemIndex >= data.length) { + return mergeCellStyleCallbackResult(baseStyleName, undefined); + } + const row = data[itemIndex] as Record; + const extra = cellStyleCallbackRef.current?.({ + rowIndex: itemIndex, + fieldName, + row, + grid: grid as CellStyleCallbackParams["grid"], + }); + return mergeCellStyleCallbackResult(baseStyleName, extra); + }; + }); +} + // RealGrid 타입 확장 declare module "realgrid" { interface GridView { setOptions(options: { footer?: { visible?: boolean }; summaryMode?: "aggregate" | "statistical" | "none"; - checkBar?: { visible?: boolean }; + checkBar?: { visible?: boolean; exclusive?: boolean }; }): void; } } @@ -457,10 +555,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; @@ -499,6 +618,7 @@ function buildFieldsFromColumns( const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && + "denominatorFields" in col.computed && col.computed.denominatorFields.length > 0; const hasMaxOfFieldAndSum = "baseField" in col.computed && @@ -508,18 +628,68 @@ 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 hasSumPerTenThousand = + col.computed.type === "sumPerTenThousand" && + "numeratorFields" in col.computed && + col.computed.numeratorFields.length > 0 && + "denominatorField" in col.computed && + Boolean(col.computed.denominatorField); + const hasQuotientRatioPercent = + col.computed.type === "quotientRatioPercent" && + "numeratorQuotient" in col.computed && + "denominatorQuotient" in col.computed && + col.computed.numeratorQuotient.length === 2 && + col.computed.denominatorQuotient.length === 2; + const hasQuotientTimesMultiplierPercent = + col.computed.type === "quotientTimesMultiplierPercent" && + "numeratorField" in col.computed && + "denominatorField" in col.computed && + "multiplierField" in col.computed && + Boolean(col.computed.numeratorField) && + Boolean(col.computed.denominatorField) && + Boolean(col.computed.multiplierField); if ( hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || - hasSumOfMaxGroups + hasHalfSumOfSums || + hasSumOfTwoHalfSumOfSums || + hasSumOfMaxGroups || + hasPumpGrossPpqty || + hasBodPrcsRt || + hasSumPerTenThousand || + hasQuotientRatioPercent || + hasQuotientTimesMultiplierPercent ) { - field.valueCallback = getComputedValueCallback( + const computedCallback = getComputedValueCallback( col.computed, ) as ( prod: unknown, @@ -528,6 +698,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; @@ -551,6 +739,7 @@ const DataGrid = ({ onCellEdited, onRowClick, onDataCellClick, + onCellDblClick, onItemChecked, onItemAllChecked, onScrollToBottom, @@ -586,6 +775,7 @@ const DataGrid = ({ const onItemAllCheckedRef = useRef(onItemAllChecked); const isSyncingChecksFromRowDataRef = useRef(false); + const columnsFieldsSigRef = useRef(""); columnsRef.current = columns; rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; @@ -596,6 +786,7 @@ const DataGrid = ({ onCellEditedRef.current = onCellEdited; onDataCellClickRef.current = onDataCellClick; + // highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택 useEffect(() => { if (!gridRef.current?.gridView || highlightedRowId == null) return; @@ -611,11 +802,54 @@ const DataGrid = ({ gridView.setFocus(); } }, [highlightedRowId, rowData, idFieldName]); +// 행 더블클릭 이벤트 +useEffect(() => { + const gridView = gridRef.current?.gridView; + if (!gridView || !onCellDblClick) return; + gridView.onCellDblClicked = (_grid, clickData) => { + if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return; + const row = rowDataRef.current[clickData.itemIndex]; + const cellIndex = gridView.getCurrent(); + const column = cellIndex.column; + const fieldName = + cellIndex.fieldName ?? + (typeof column === "string" ? column : ""); + if (row) { + onCellDblClick({ + row: row as Record, + fieldName, + itemIndex: clickData.itemIndex, + }); + } + }; + + return () => { + gridView.onCellDblClicked = undefined; + }; +}, [onCellDblClick]); useLayoutEffect(() => { - if (!cellStyleCallback || !gridRef.current?.gridView) return; - gridRef.current.gridView.refresh(); - }, [cellStyleCallback, rowData]); + const gv = gridRef.current?.gridView; + if (!gv || !cellStyleCallback) return; + + const apply = () => { + applyCellStyleCallbacksToColumns( + gv, + columns, + rowDataRef, + cellStyleCallbackRef, + ); + (gv as { refresh?: () => void }).refresh?.(); + }; + + apply(); + // footer setColumns / setColumnLayout 비동기 처리 이후 styleCallback 재적용 + const timerId = window.setTimeout(apply, 300); + + return () => { + window.clearTimeout(timerId); + }; + }, [cellStyleCallback, columns, rowData]); const { setMessage: alert } = useAlertStore((state) => state); const activeTabId = useTabStore((state) => state.activeTabId); @@ -638,20 +872,6 @@ const DataGrid = ({ .find((s) => s.menuEngNm === activeTabId)?.menuKornNm || "export"; - // 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)") - const originalHeaders: Map = 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 +965,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 +1066,11 @@ const DataGrid = ({ : headerStartRow; const dataStartRow = maxHeaderRowIndex + 1; - // 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑 - const headerToFieldNameMap = new Map(); - 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[] = []; @@ -907,12 +1088,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 ( @@ -994,8 +1171,27 @@ const DataGrid = ({ const gridView = gridRef.current.gridView; const dataProvider = gridRef.current.dataProvider; + const footerTimerIds: ReturnType[] = []; + const scheduleFooterTask = (task: () => void, delayMs: number) => { + footerTimerIds.push(setTimeout(task, delayMs)); + }; 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[]) => 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 +1242,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 +1258,11 @@ const DataGrid = ({ tooltip: resolveColumnTooltip(column), }), ); + gv.setColumnProperty?.( + column.name, + "editable", + column.editable !== false, + ); applyColumnListEditorProperties(gv, column); }); if (columns.some((c) => isDropDownEditorColumn(c))) { @@ -1101,13 +1310,15 @@ const DataGrid = ({ // 헤더 체크박스 표시 여부 설정 gridView.setOptions({ - checkBar: { visible: showHeaderCheckbox }, + checkBar: { visible: showHeaderCheckbox, exclusive: false }, rowIndicator: { visible: false }, }); const applyFooterSettings = ( - targetGridView: typeof gridView, + targetGridView: typeof gridView | null | undefined, ) => { + if (!targetGridView?.setOptions) return; + const hasFooter = columns.some((col) => col.footer); if (!hasFooter) return; @@ -1181,20 +1392,79 @@ 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), + items: nextItems, + }); + } + continue; + } + + result.push(item); + } + return result; + }; + // setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함 - setTimeout(() => { + scheduleFooterTask(() => { 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); + applyCellStyleCallbacksToColumns( + gridRef.current.gridView, + columns, + rowDataRef, + cellStyleCallbackRef, + ); + gridRef.current.gridView.refresh?.(); + } catch (e) { + console.warn("setColumnLayout skipped due to invalid columns:", e); + } } }, 100); } // footer 설정 (배열 또는 단일 객체 모두 처리) // setFooters 호출 후 footer 배열 설정 - setTimeout(() => { + scheduleFooterTask(() => { const currentGridView = gridRef.current?.gridView; - if (!currentGridView || !currentGridView.columnByName) { - console.warn("GridView not available for footer setup"); + if (!currentGridView?.columnByName) { return; } @@ -1253,15 +1523,24 @@ const DataGrid = ({ if (hasFooterUserSpans) { currentGridView.setColumnLayout?.(columnLayout); } + applyCellStyleCallbacksToColumns( + currentGridView, + columns, + rowDataRef, + cellStyleCallbackRef, + ); // setColumns 호출 후 footer 배열의 valueCallback을 다시 설정 - setTimeout(() => { + scheduleFooterTask(() => { + const liveGridView = gridRef.current?.gridView; + if (!liveGridView?.columnByName) return; + columns.forEach((col) => { if (!col.footer || !Array.isArray(col.footer)) return; try { - const column = currentGridView.columnByName( + const column = liveGridView.columnByName( col.name, ); if (!column) return; @@ -1293,11 +1572,17 @@ const DataGrid = ({ ); } }); - applyFooterSettings(currentGridView); + applyFooterSettings(liveGridView); if (hasFooterUserSpans) { - currentGridView.setColumnLayout?.(columnLayout); + liveGridView.setColumnLayout?.(columnLayout); } - currentGridView.refresh?.(); + applyCellStyleCallbacksToColumns( + liveGridView, + columns, + rowDataRef, + cellStyleCallbackRef, + ); + liveGridView.refresh?.(); }, 50); } catch (e) { console.warn("Failed to set columns with footer array:", e); @@ -1354,11 +1639,16 @@ const DataGrid = ({ r[f] = ds.getValue(i, f) as string | number; rows.push(r); } - return rows; + return mergeGridRowsWithSourceMetadata( + rows, + rowDataRef.current, + fieldNames, + ); }; } return () => { + footerTimerIds.forEach(clearTimeout); if (getGridDataRef) getGridDataRef.current = null; }; }, [ @@ -1654,6 +1944,22 @@ const DataGrid = ({ layout={columnLayout} rows={rowData} onRGDataColumnsChanged={(grid) => { + const dp = gridRef.current?.dataProvider as + | { + setFields?: (f: unknown[]) => void; + setRows?: ( + r: Record[], + ) => 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 +1999,9 @@ const DataGrid = ({ dataProvider.setFields( buildFieldsFromColumns(columns), ); + columnsFieldsSigRef.current = columns + .map((c) => c.fieldName) + .join("\u0001"); } if (dataProvider?.setRows) dataProvider.setRows(rowData); @@ -1784,19 +2093,9 @@ const DataGrid = ({ }; } columns.forEach((column) => { - const baseStyleName = column.styleName; const gridCol = gv.columnByName( column.name, ) as { - styleCallback?: ( - g: unknown, - dataCell: { - itemIndex?: number; - index?: { itemIndex?: number }; - dataField?: string; - fieldName?: string; - }, - ) => string; renderer?: { showTooltip?: boolean }; } | null; if (gridCol) { @@ -1835,38 +2134,14 @@ const DataGrid = ({ { showTooltip: true }, ); } - gridCol.styleCallback = (_g, dataCell) => { - const itemIndex = - dataCell?.index?.itemIndex ?? - dataCell?.itemIndex ?? - -1; - const fieldName = - dataCell?.dataField ?? - dataCell?.fieldName ?? - column.fieldName; - const data = rowDataRef.current; - if ( - itemIndex < 0 || - itemIndex >= data.length - ) - return baseStyleName ?? ""; - const row = data[itemIndex] as Record< - string, - string | number - >; - const extra = - cellStyleCallbackRef.current?.({ - rowIndex: itemIndex, - fieldName, - row, - }); - if (!extra) return baseStyleName ?? ""; - return baseStyleName - ? `${extra} ${baseStyleName}` - : extra; - }; } }); + applyCellStyleCallbacksToColumns( + gv, + columns, + rowDataRef, + cellStyleCallbackRef, + ); gv.refresh(); }, 0); }} @@ -1874,15 +2149,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 ( 행 클릭 시 예외처리(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: "개별 반려", diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index a82822756..ccc76bc3d 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -41,7 +41,7 @@ const getSggListFetcher = async ( key: [string, string, string, string], ): Promise> => { const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y"; - return statisticsApi.getSggAllList(key[1], { + return statisticsApi.getSggList(key[1], { instSeCd: key[2], gugunSkipYn, }); @@ -137,11 +137,11 @@ const SearchForm = (props: SearchFormProps = {}) => { prev.sido ? prev : { - ...prev, - sido: first.sido, - gugun: "", - bsnmCd: first.bsnmCd, - }, + ...prev, + sido: first.sido, + gugun: "", + bsnmCd: first.bsnmCd, + }, ); }, [sidoList, props.defaultValues?.sido, mergedProps.sido]); @@ -172,6 +172,30 @@ const SearchForm = (props: SearchFormProps = {}) => { ); const sggOptions = sggList?.data ?? []; + // 시도 설정 후 시군구 목록이 오면 첫 번째 항목 자동 선택 + useEffect(() => { + if (!mergedProps.gugun) return; + if (props.defaultValues?.gugun) return; + if (!values.sido || values.sido === "ALL") return; + + const firstSgg = sggList?.data?.[0]; + if (!firstSgg) return; + + setValues((prev) => { + if (prev.gugun) return prev; + return { + ...prev, + gugun: firstSgg.gugun, + bsnmCd: firstSgg.bsnmCd, + }; + }); + }, [ + sggList, + values.sido, + mergedProps.gugun, + props.defaultValues?.gugun, + ]); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); props.onSubmit?.(values); @@ -184,7 +208,7 @@ const SearchForm = (props: SearchFormProps = {}) => { const handleSidoChange = (value: string) => { const selectedBsnm = - sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || ""; + sidoList?.data?.sidoList?.find((v) => v.sido === value)?.bsnmCd || ""; setValues((prev) => ({ ...prev, sido: value, @@ -197,7 +221,7 @@ const SearchForm = (props: SearchFormProps = {}) => { const listForBsnm = sggList?.data ?? []; const selectedBsnm = value === "ALL" - ? sidoList?.data.sidoList.find((v) => v.sido === values.sido) + ? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido) ?.bsnmCd || "" : listForBsnm.find((v) => v.gugun === value)?.bsnmCd || ""; @@ -398,16 +422,18 @@ const SearchForm = (props: SearchFormProps = {}) => {
)}
- + {mergedProps.isReset && ( + + )}
diff --git a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx index f32bd5300..338faa475 100644 --- a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx +++ b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx @@ -116,7 +116,7 @@ const getSidoListFetcher = async (): Promise> => { const getSggAllListFetcher = async ( key: [string, string], ): Promise> => { - 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 { diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index e4c23eb83..6b7eacc87 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -10,13 +10,32 @@ import { useAlertStore } from "@/shared/lib"; import { Tabs, TabsContent,TabsList, TabsTrigger } from "@common/components"; import { Button } from "@common/components"; -import type { CellStyleCallback,ColumnConfig, ColumnInfo, ColumnLayoutItem, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo, TabDataGridProps } from "../type/DataGrid.type" +import type { CellStyleCallback, CellStyleCallbackParams, CellStyleCallbackResult, ColumnConfig, ColumnInfo, ColumnLayoutItem, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo, TabDataGridProps } from "../type/DataGrid.type" import GridTopActionButton from "./GridTopActionButton"; + +function mergeCellStyleCallbackResult( + baseStyleName: string | undefined, + extra: CellStyleCallbackResult | undefined, +): CellStyleCallbackResult { + if (!extra) { + return baseStyleName ? { styleName: baseStyleName } : {}; + } + if (typeof extra === "string") { + const styleName = [extra, baseStyleName].filter(Boolean).join(" "); + return styleName ? { styleName } : {}; + } + const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" "); + return { + ...extra, + ...(styleName ? { styleName } : {}), + }; +} import SplitDataGrid from "./SplitDataGrid"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" import { + getExcelImportColumnFieldNames, prepareGridColumnsForExcelExport, restoreGridColumnsAfterExcelExport, shouldPrepareColumnsForExcelExport, @@ -72,10 +91,12 @@ function safeGridViewRefresh(gv: { refresh?: () => void } | null | undefined) { function safeColumnByName( gv: { columnByName?: (name: string) => unknown } | null | undefined, name: string, -): { styleCallback?: (g: unknown, dataCell: Record) => string } | null { +): { styleCallback?: (g: unknown, dataCell: Record) => CellStyleCallbackResult } | null { if (!gv?.columnByName) return null; try { - const col = gv.columnByName(name) as { styleCallback?: (g: unknown, dataCell: Record) => string } | null; + const col = gv.columnByName(name) as { + styleCallback?: (g: unknown, dataCell: Record) => CellStyleCallbackResult; + } | null; return col ?? null; } catch { return null; @@ -219,11 +240,14 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT }; if (col.computed) { const hasFields = "fields" in col.computed && col.computed.fields.length > 0; - const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; + const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorFields" in col.computed && 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) { + const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField); + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } @@ -292,7 +316,7 @@ const TabDataGrid = ({ const gridCol = safeColumnByName(gv, column.name); if (!gridCol) continue; const baseStyleName = column.styleName; - gridCol.styleCallback = (_g, dataCell) => { + gridCol.styleCallback = (grid, dataCell) => { const dc = dataCell as { itemIndex?: number; index?: { itemIndex?: number }; @@ -303,16 +327,16 @@ const TabDataGrid = ({ const fieldName = dc?.dataField ?? dc?.fieldName ?? column.fieldName; const data = rowDataMapRef.current.get(tabKey) || []; if (itemIndex < 0 || itemIndex >= data.length) { - return baseStyleName ?? ""; + return mergeCellStyleCallbackResult(baseStyleName, undefined); } const row = data[itemIndex] as Record; const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row, + grid: grid as CellStyleCallbackParams["grid"], }); - if (!extra) return baseStyleName ?? ""; - return baseStyleName ? `${extra} ${baseStyleName}` : extra; + return mergeCellStyleCallbackResult(baseStyleName, extra); }; attachedCount += 1; } @@ -373,20 +397,6 @@ const TabDataGrid = ({ const columns = columnInfoItem.columns || []; const columnLayout = columnInfoItem.columnLayout || []; - // 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)") - const originalHeaders: Map = 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 +462,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 +547,12 @@ const TabDataGrid = ({ const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow; const dataStartRow = maxHeaderRowIndex + 1; - // 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑 - const headerToFieldNameMap = new Map(); 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[] = []; @@ -579,9 +563,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) { diff --git a/apps/st/src/components/module/VerificationResultDialog.tsx b/apps/st/src/components/module/VerificationResultDialog.tsx index 1141348aa..0f2de8007 100644 --- a/apps/st/src/components/module/VerificationResultDialog.tsx +++ b/apps/st/src/components/module/VerificationResultDialog.tsx @@ -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, }; diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx index 6fe0606d7..863a82226 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -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; } export function VerificationResultHistoryDialog({ @@ -183,6 +191,7 @@ export function VerificationResultHistoryDialog({ queryContext, title = "검증 결과 내역", emptyMessage = "표시할 검증 결과가 없습니다.", + onExceptionResetSubmitted, }: VerificationResultHistoryDialogProps) { const stableQueryContext = useMemo(() => { if (!queryContext) return null; @@ -225,6 +234,7 @@ export function VerificationResultHistoryDialog({ }; const checkedVrfcByTabRef = useRef(checkedVrfcByTab); checkedVrfcByTabRef.current = checkedVrfcByTab; + const loadedTabKeysRef = useRef>(new Set()); const resetState = useCallback(() => { setActiveTab("NV"); @@ -234,6 +244,7 @@ export function VerificationResultHistoryDialog({ setErrorMessage(null); setResetSubmitting(false); setCheckedVrfcByTab(createEmptyCheckedVrfcByTab()); + loadedTabKeysRef.current.clear(); }, []); const fetchVerificationResultPage = useCallback( @@ -245,9 +256,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, }; @@ -270,10 +281,10 @@ export function VerificationResultHistoryDialog({ ); const loadTabPage = useCallback( - async (tab: VerificationHistoryTabKey, pageIndex: number) => { + async (tab: VerificationHistoryTabKey, pageIndex: number): Promise => { if (!stableQueryContext) { setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다."); - return; + return false; } setErrorMessage(null); setLoadingByTab((prev) => ({ ...prev, [tab]: true })); @@ -287,10 +298,12 @@ export function VerificationResultHistoryDialog({ ? Math.max(0, Number(apiCurrentPage) - 1) : pageIndex; applyPageResponse(tab, resolvedPageIndex, list, totalPages); + return true; } catch { const tabLabel = verificationHistoryTabs.find((t) => t.value === tab)?.label ?? tab; setErrorMessage(`${tabLabel} 조회 중 오류가 발생했습니다.`); + return false; } finally { setLoadingByTab((prev) => ({ ...prev, [tab]: false })); } @@ -305,40 +318,27 @@ export function VerificationResultHistoryDialog({ } setErrorMessage(null); setTabPages(initialTabPageState()); - setLoadingByTab({ NV: true, IMR: true, HB: true }); - try { - const results = await Promise.all( - verificationHistoryTabs.map((tab) => - fetchVerificationResultPage(tab.value, 0), - ), - ); - setEntriesByTab({ - NV: results[0]?.data?.verifyResultList ?? [], - IMR: results[1]?.data?.verifyResultList ?? [], - HB: results[2]?.data?.verifyResultList ?? [], - }); - setTabPages({ - NV: { - pageIndex: 0, - pageCount: Math.max(results[0]?.data?.pageInfo?.totalPages ?? 1, 1), - }, - IMR: { - pageIndex: 0, - pageCount: Math.max(results[1]?.data?.pageInfo?.totalPages ?? 1, 1), - }, - HB: { - pageIndex: 0, - pageCount: Math.max(results[2]?.data?.pageInfo?.totalPages ?? 1, 1), - }, - }); - } catch { - setEntriesByTab({ NV: [], IMR: [], HB: [] }); - setTabPages(initialTabPageState()); - setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다."); - } finally { - setLoadingByTab({ NV: false, IMR: false, HB: false }); + setEntriesByTab({ NV: [], IMR: [], HB: [] }); + loadedTabKeysRef.current.clear(); + const loaded = await loadTabPage("NV", 0); + if (loaded) { + loadedTabKeysRef.current.add("NV"); } - }, [fetchVerificationResultPage, stableQueryContext]); + }, [loadTabPage, stableQueryContext]); + + const handleActiveTabChange = useCallback( + (tab: VerificationHistoryTabKey) => { + setActiveTab(tab); + if (loadedTabKeysRef.current.has(tab)) return; + void (async () => { + const loaded = await loadTabPage(tab, 0); + if (loaded) { + loadedTabKeysRef.current.add(tab); + } + })(); + }, + [loadTabPage], + ); const handleTabPageChange = useCallback( (tab: VerificationHistoryTabKey, pageIndex: number) => { @@ -376,14 +376,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 +453,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]; @@ -550,7 +568,7 @@ export function VerificationResultHistoryDialog({ )} setActiveTab(value as VerificationHistoryTabKey)} + onValueChange={(value) => handleActiveTabChange(value as VerificationHistoryTabKey)} className="w-full" variant="default" > diff --git a/apps/st/src/components/module/post-submit-verification-results.ts b/apps/st/src/components/module/post-submit-verification-results.ts index a46ad79cb..4efe25993 100644 --- a/apps/st/src/components/module/post-submit-verification-results.ts +++ b/apps/st/src/components/module/post-submit-verification-results.ts @@ -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, }; diff --git a/apps/st/src/components/statistics/VerificationSelectionReport.tsx b/apps/st/src/components/statistics/VerificationSelectionReport.tsx index 18edbf4cc..f9e0fc599 100644 --- a/apps/st/src/components/statistics/VerificationSelectionReport.tsx +++ b/apps/st/src/components/statistics/VerificationSelectionReport.tsx @@ -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 = { 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 | undefined, -): VerificationResultReportItem[] { +function flattenVerificationInputReportResults( + data: InfiniteData | undefined, +): VerificationInputReportItem[] { return ( data?.pages.flatMap((page) => page?.data?.verifyResultList ?? []) ?? [] ); } -function getVerificationResultTotalCount( - data: InfiniteData | undefined, - loadedCount: number, -): number { - return data?.pages[0]?.data?.pageInfo?.totalElements ?? loadedCount; +function getVerifyTypeCountingInfo( + data: InfiniteData | 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("NV"); const [statsCd, setStatsCd] = useState("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 = { - NV: flattenVerificationResults(nvQuery.data), - IMR: flattenVerificationResults(imrQuery.data), - HB: flattenVerificationResults(hbQuery.data), + const listsByTab: Record = { + 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 = { - 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 ? ( +
+
+ +
+
+ ) : items.length === 0 && !isFetchingNextPage ? (
항목이 없습니다.
@@ -303,7 +359,7 @@ const VerificationSelectionReport = ({ >
- {item.statsNm} + {formatReportItemTitle(item)}
- {item.admdstNm} + {formatReportItemRegion(item)} 현재: {item.nowVlCn ?? "-"} / 비교:{" "} {item.compVlCn ?? "-"} @@ -383,25 +439,25 @@ const VerificationSelectionReport = ({
- {isInitialLoading && ( + {hideTabsSection && (
)} - {error && ( + {!hideTabsSection && error && (
데이터를 불러올 수 없습니다.
)} - {!isInitialLoading && !error && ( + {!hideTabsSection && !error && (
- {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, + )} ))} diff --git a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx index 626682536..ca9872612 100644 --- a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx @@ -1,80 +1,1098 @@ -import { useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type RefObject, +} from "react"; + +import { flushSync } from "react-dom"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { ValueType } from "realgrid"; +import type { RealGridReact } from "realgrid-react"; +import { LoadingWithAnimation } from "@common/components"; -import SearchForm from "@/components/module/SearchForm"; -import DataGrid from "@/components/module/DataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; import AddArticleButton from "@/components/button/AddArticleButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton"; +import LastYearDataButton from "@/components/button/LastYearDataButton"; import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton"; +import SaveButton from "@/components/button/SaveButton"; +import SubmitButton from "@/components/button/SumitButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import SubmitDailog from "./SubmitDailog"; +import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn"; +import type { + CellStyleCallbackParams, + CellTooltipCallbackParams, + ColumnConfig, +} from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag"; +import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat"; +import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import { + EXCRETA_TREATMENT_FACILITY_STATS_CD, + type ExcretaTreatmentFacilityItem, + type ExcretaTreatmentFacilityRequestParams, + type ExcretaTreatmentFacilitySaveItem, + type ExcretaTreatmentFacilityVerificationResult, +} from "@/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import type { + ApiResponse, + cancelSubmitRequest, + ImrStatusResponse, + Sgg, + SewerageStatusInfo, +} from "@/entities/statistics/model/statistics-interface"; +import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; +import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAdminStore } from "@/store/admin-store"; -import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn" +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +type GridRowWithVerification = Record & { + verificationResult?: ExcretaTreatmentFacilityVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = + isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function buildExcretaTreatmentFacilitySearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): ExcretaTreatmentFacilityRequestParams { + const fcltNm = values.fctName?.trim(); + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(fcltNm ? { fcltNm } : {}), + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function parseSggListFromResponse(res: ApiResponse): Sgg[] { + if (res.status !== "success" || res.data == null) return []; + return Array.isArray(res.data) ? res.data : []; +} + +function buildSggNmColumn( + col: ColumnConfig, + canEdit: boolean, + sggOptions: Sgg[], +): ColumnConfig { + if (!canEdit) { + return { ...col, editable: false as const }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +function mapExcretaTreatmentFacilityListToGridRows( + list: ExcretaTreatmentFacilityItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const row = toGridRows([ + { + ...item, + ctpvNm, + cmptnStrtYmd: formatCompactYmdForGrid(item.cmptnStrtYmd), + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + }, + ])[0] as GridRowWithVerification; + if (item.verificationResult) { + row.verificationResult = item.verificationResult; + } + return row; + }); +} + +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function toApiYmd(value: string | number | Date | null | undefined): string | null { + return toCompactYmdOrNull(value); +} + +function resolveExcretaTreatmentFacilityAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + if (existingCd && existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm }; + } + const sggNm = String(row.sggNm ?? "").trim(); + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; +} + +/** 그리드 행 → ST00070001 저장/삭제 API 항목 */ +function buildExcretaTreatmentFacilitySaveItem( + row: Record, + sggOptions: Sgg[] = [], +): ExcretaTreatmentFacilitySaveItem { + const { admdstCd, admdstNm } = resolveExcretaTreatmentFacilityAdmdst( + row, + sggOptions, + ); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? ""), + fcltPstnCn: String(row.fcltPstnCn ?? ""), + telNo: String(row.telNo ?? ""), + cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd), + oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd), + wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt), + prcsFcltCpct: toGridNumber(row.prcsFcltCpct), + cpct: toGridNumber(row.cpct), + oprtngDayCnt: toGridNumber(row.oprtngDayCnt), + prcsMthdExpln: String(row.prcsMthdExpln ?? ""), + linkTmplntCn: String(row.linkTmplntCn ?? ""), + linkPrcsQnt: toGridNumber(row.linkPrcsQnt), + inflwBodWqVl: toGridNumber(row.inflwBodWqVl), + inflwTocWqVl: toGridNumber(row.inflwTocWqVl), + inflwSsWqVl: toGridNumber(row.inflwSsWqVl), + inflwTnWqVl: toGridNumber(row.inflwTnWqVl), + inflwTpWqVl: toGridNumber(row.inflwTpWqVl), + inflwClfgCnt: toGridNumber(row.inflwClfgCnt), + dschgBodWqVl: toGridNumber(row.dschgBodWqVl), + dschgTocWqVl: toGridNumber(row.dschgTocWqVl), + dschgSsWqVl: toGridNumber(row.dschgSsWqVl), + dschgTnWqVl: toGridNumber(row.dschgTnWqVl), + dschgTpWqVl: toGridNumber(row.dschgTpWqVl), + dschgClfgCnt: toGridNumber(row.dschgClfgCnt), + wtsysNm: String(row.wtsysNm ?? ""), + trbtrNm: String(row.trbtrNm ?? ""), + msirbCn: String(row.msirbCn ?? ""), + ssirbCn: String(row.ssirbCn ?? ""), + operMthdCdCn: String(row.operMthdCdCn ?? ""), + wholNope: toGridNumber(row.wholNope), + sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + +function createEmptyExcretaTreatmentFacilityRow( + ctpvNm: string, + sggNm = "", +): GridRowWithVerification { + const row: GridRowWithVerification = { ctpvNm, sggNm }; + for (const col of ExcretaTreatFactInpColumn.columns) { + const fieldName = col.fieldName; + if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} // 하수도 통계 관리 > 07. 분뇨처리시설 const ExcretaTreatmentFacilityInp = () => { - - const submitExceptionFunc = () => { - console.log('submit exception'); - // 예외제출 로직 + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + + const [rowData, setRowData] = useState[]>([]); + const [isNotApplicable, setIsNotApplicable] = useState(false); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = + useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [statusInfo, setStatusInfo] = useState(null); + const [statsCd, setStatsCd] = useState(""); + const [isVerificationFailed, setIsVerificationFailed] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [sggOptions, setSggOptions] = useState([]); + const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState< + Record + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = + useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [deleteList, setDeleteList] = useState[]>([]); + + const gridRef = useRef(null); + const gridGetRowsRef = + useRef<(() => Record[]) | null>(null); + const sggOptionsSidoRef = useRef(null); + const [searchLoaded, setSearchLoaded] = useState(false); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + const res = await statisticsApi.getColumnGuides( + EXCRETA_TREATMENT_FACILITY_STATS_CD, + ); + if (cancelled) return; + if (res.status !== "success" || !Array.isArray(res.data)) { + setColumnGuidesByEngCol({}); + return; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + setColumnGuidesByEngCol(next); + } catch { + if (!cancelled) setColumnGuidesByEngCol({}); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + if (!sido || sido === "ALL") { + setSggOptions([]); + return []; + } + try { + const res = await statisticsApi.getSggAllList(sido); + const list = parseSggListFromResponse(res); + setSggOptions(list); + return list; + } catch { + setSggOptions([]); + return []; + } + }, []); + + const columnInfo = useMemo(() => { + const enterable = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const rawColumns = ExcretaTreatFactInpColumn.columns ?? []; + const base = enterable + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + + const columns = base + .map((col: ColumnConfig) => { + if (col.fieldName === "ctpvNm") { + return { ...col, editable: false as const }; + } + if (col.fieldName === "sggNm" && enterable) { + return buildSggNmColumn(col, true, sggOptions); + } + return col; + }) + .map((col) => { + const guide = columnGuidesByEngCol[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); + + return { + columns, + columnLayout: ExcretaTreatFactInpColumn.columnLayout ?? [], + }; + }, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]); + + const canEditGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + + const getCurrentRows = (): Record[] => + gridGetRowsRef.current?.() ?? rowData; + + const commitGrid = () => { + const gridView = gridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } }; - const getLastYearDataFunc = () => { - console.log('get last year data'); - // 지난연도 데이터 가져오기 로직 + const fetchExcretaTreatmentFacilityList = async ( + values: SearchFormValues, + overrides?: { mode?: string }, + ): Promise => { + const response = + await statisticsInputExcretaTreatmentFacilityApi.getExcretaTreatmentFacilityList( + buildExcretaTreatmentFacilitySearchParam(values, overrides), + ); + + if (response.status !== "success" || !response.data) { + alert(response.message ?? "데이터 조회 중 오류가 발생했습니다."); + setSearchLoaded(false); + return false; + } + + const { + commonInfo, + statusInfo: nextStatusInfo, + excretaTreatmentFacilityList, + } = response.data; + const list = excretaTreatmentFacilityList ?? []; + const gridRows = mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm); + + setRowData(gridRows); + setStatusInfo(nextStatusInfo); + setStatsCd(commonInfo.statsCd); + setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo)); + if (overrides?.mode !== "PRE") { + setIsNotApplicable(gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicable(false); + } + setSearchLoaded(true); + return true; }; - const submitFunc = () => { - console.log('submit'); - // 제출 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) + ? periodResponse.data + : []; + setInputPeriodEnterable(isInputPeriodCurrent(periods)); + + setIsLoading(true); + setHasSearched(true); + setLastSearchedValues(values); + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + setSearchLoaded(false); + return; + } + + await fetchExcretaTreatmentFacilityList(values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + setRowData([]); + setStatusInfo(null); + setSearchLoaded(false); + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + setDeleteList([]); + setRowData([]); + setLastSearchedValues(null); + setHasSearched(false); + setStatusInfo(null); + setStatsCd(""); + setIsVerificationFailed(false); + setIsNotApplicable(false); + setInputPeriodEnterable(true); + setSggOptions([]); + sggOptionsSidoRef.current = null; + setSearchLoaded(false); + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched || !searchLoaded) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + await fetchExcretaTreatmentFacilityList(values, { mode: "PRE" }); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await fetchExcretaTreatmentFacilityList(lastSearchedValues); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const saveFunc = () => { - console.log('save'); - // 저장 로직 + void (async () => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildExcretaTreatmentFacilitySaveItem(row, sggOptions), + ); + const deleteListForSave = deleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputExcretaTreatmentFacilityApi.saveExcretaTreatmentFacilityList( + { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + saveList, + deleteList: deleteListForSave, + }, + ); + + if (response.status === "success") { + setDeleteList([]); + await fetchExcretaTreatmentFacilityList(lastSearchedValues); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const addArticleFunc = () => { - console.log('save'); - // 항목 추가 로직 + const submitFunc = () => { + void (async () => { + try { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + } catch { + alert("제출 중 오류가 발생했습니다."); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildExcretaTreatmentFacilitySaveItem(row, sggOptions), + ); + const deleteListForSubmit = deleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const statsCdForSubmit = statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForSubmit, + }; + + const reloadAfterSubmit = async () => { + setDeleteList([]); + await fetchExcretaTreatmentFacilityList(lastSearchedValues); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = + await statisticsInputExcretaTreatmentFacilityApi.submitExcretaTreatmentFacilityList( + { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + submitList, + deleteList: deleteListForSubmit, + }, + ); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForSubmit, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + }; + + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + let list = sggOptions; + if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { + list = await loadSggOptions(ctx.sido); + sggOptionsSidoRef.current = ctx.sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + commitGrid(); + const currentRows = getCurrentRows(); + const newRow = createEmptyExcretaTreatmentFacilityRow(ctx.sido, sggNm); + + flushSync(() => { + setSggOptions(list); + setRowData([...currentRows, newRow]); + }); }; const deleteArticleFunc = () => { - console.log('save'); - // 항목 제외 로직 + commitGrid(); + const currentRows = getCurrentRows(); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const gridView = gridRef.current?.gridView as + | { + getCheckedRows?: (refresh?: boolean) => number[]; + getCheckedItems?: (refresh?: boolean) => number[]; + } + | undefined; + + const checkedIndexes = + gridView?.getCheckedRows?.(false) ?? + gridView?.getCheckedItems?.(false) ?? + []; + + if (checkedIndexes.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + const indexSet = new Set( + checkedIndexes.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < currentRows.length, + ), + ); + const removedRows = currentRows.filter((_, idx) => indexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + setDeleteList((prev) => [...prev, ...removedRows]); + setRowData(currentRows.filter((_, idx) => !indexSet.has(idx))); }; - - const [rowData, setRowData] = useState[]>([]); - const [isNotApplicable, setIsNotApplicable] = useState(false); + + const verificationCellStyle = ({ + fieldName, + row, + }: CellStyleCallbackParams): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: CellTooltipCallbackParams): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 -"; + return `${prefix}${e.rsnExpln} +(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const toolbarSnapshot = computeTabToolbarSnapshot( + statusInfo, + rowData.length > 0, + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + toolbarSnapshot.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + toolbarSnapshot.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + toolbarSnapshot.showActionToolbarCard && toolbarGroupAllowed; + const showArticleToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const showNotApplicable = + searchLoaded && + hasSearched && + rowData.length === 0 && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const tabEnterableForGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.EXCRETA_TREATMENT_FACILITY, + onAutoSearch: (values) => handleSearch(values), + }); return ( <> - -
-
- - - - -
+
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> + + + )} + {statusInfo?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showArticleToolbarCard && ( +
+
+ {showNotApplicable && ( + + )} + + +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} + } + columnInfo={columnInfo} + rowStartNum={ExcretaTreatFactInpColumn.rowStartNum} + fixedColumns={ExcretaTreatFactInpColumn.fixedColumns} + rowData={rowData} + setRowData={setRowData} + getGridDataRef={gridGetRowsRef} + showHeaderCheckbox + statusInfo={statusInfo} + isExcelImport={ + searchLoaded && tabEnterableForGrid && gugunOk + } + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + />
-
-
- - - -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchExcretaTreatmentFacilityList(values); + }} /> + { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchExcretaTreatmentFacilityList(values); + }} + /> + + ); }; -export default ExcretaTreatmentFacilityInp; \ No newline at end of file +export default ExcretaTreatmentFacilityInp; diff --git a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx index b430d6584..b2edb8f2c 100644 --- a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx @@ -1,62 +1,1324 @@ -import { useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; -import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; +import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; +import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { statisticsInputPrivateSewageFacilityApi } from "@/entities/statistics/api/input/statistics-input-private-sewage-facility-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + PrivateSewageDesigStatusSaveItem, + PrivateSewageFacilityVerificationResult, + PrivateSewageDesigStatusItem, + PrivateSewageDesigStatusSaveRequest, + PrivateSewageDesigStatusSubmitRequest, + SepticTankStatusSaveItem, + SepticTankStatusSaveRequest, + SepticTankStatusSubmitRequest, + SewageTreatFactStatusItem, + SewageTreatFactStatusSaveItem, + SewageTreatFactStatusSaveRequest, + SewageTreatFactStatusSubmitRequest, + SepticTankStatusItem, + SewageTreatFactStatusRequestParams, +} from "@/entities/statistics/model/input/statistics-input-private-sewage-facility-interface"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import type { + cancelSubmitRequest, + ImrStatusResponse, + SewerageStatusInfo, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +import SubmitDailog from "./SubmitDailog"; + +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import SubmitButton from "@/components/button/SumitButton"; import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; +import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn"; +import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn"; +import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; +import { useAdminStore } from "@/store/admin-store"; -import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn" -import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn" -import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn" +const PrivateSewageFacilityInpTabList = [ + SewageTreatFactStatusInpColumn, + SepticTankStatusInpColumn, + PrivateSewageDesigStatusInpColumn, +] as const; + +const firstTabKey = SewageTreatFactStatusInpColumn.tabKey; + +/** getColumnGuides API는 statsCd 기준 — 3번째 탭 tabKey와 statsCd가 다름 */ +const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record = { + [SewageTreatFactStatusInpColumn.tabKey]: SewageTreatFactStatusInpColumn.tabKey, + [SepticTankStatusInpColumn.tabKey]: SepticTankStatusInpColumn.tabKey, + [PrivateSewageDesigStatusInpColumn.tabKey]: "ST00080003", +}; + +const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS = Array.from( + new Set(Object.values(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY)), +); + +type GridRowWithVerification = Record & { + verificationResult?: PrivateSewageFacilityVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof PrivateSewageFacilityInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function buildPrivateSewageFacilitySearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewageTreatFactStatusRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function tabRowDataExists( + m: Map[]>, + tabKey: string, +): boolean { + const rows = m.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +function mapRegionalListToGridRows< + T extends { verificationResult?: PrivateSewageFacilityVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function normalizeGridRmrk(value: unknown): string | null { + if (value === null || value === undefined) return null; + const s = typeof value === "string" ? value : String(value); + return s.trim() === "" ? null : s; +} + +function buildColumnFormatNumber( + columns: { fieldName?: string; dataFormat?: string }[], +) { + const columnFormatMap = new Map(); + columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + return (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = + typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; +} + +function buildSewageTreatFactStatusSaveList( + currentData: Record[], +): SewageTreatFactStatusSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewageTreatFactStatusInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + step1StaaPrcsQnt: formatNumber(d.step1StaaPrcsQnt, "step1StaaPrcsQnt"), + step2StaaPrcsQnt: formatNumber(d.step2StaaPrcsQnt, "step2StaaPrcsQnt"), + step3StaaPrcsQnt: formatNumber(d.step3StaaPrcsQnt, "step3StaaPrcsQnt"), + step4StaaPrcsQnt: formatNumber(d.step4StaaPrcsQnt, "step4StaaPrcsQnt"), + step5StaaPrcsQnt: formatNumber(d.step5StaaPrcsQnt, "step5StaaPrcsQnt"), + step6StaaPrcsQnt: formatNumber(d.step6StaaPrcsQnt, "step6StaaPrcsQnt"), + step7StaaPrcsQnt: formatNumber(d.step7StaaPrcsQnt, "step7StaaPrcsQnt"), + step1StaaExcpPrcsQnt: formatNumber(d.step1StaaExcpPrcsQnt, "step1StaaExcpPrcsQnt"), + step2StaaExcpPrcsQnt: formatNumber(d.step2StaaExcpPrcsQnt, "step2StaaExcpPrcsQnt"), + step3StaaExcpPrcsQnt: formatNumber(d.step3StaaExcpPrcsQnt, "step3StaaExcpPrcsQnt"), + step4StaaExcpPrcsQnt: formatNumber(d.step4StaaExcpPrcsQnt, "step4StaaExcpPrcsQnt"), + step5StaaExcpPrcsQnt: formatNumber(d.step5StaaExcpPrcsQnt, "step5StaaExcpPrcsQnt"), + step6StaaExcpPrcsQnt: formatNumber(d.step6StaaExcpPrcsQnt, "step6StaaExcpPrcsQnt"), + step7StaaExcpPrcsQnt: formatNumber(d.step7StaaExcpPrcsQnt, "step7StaaExcpPrcsQnt"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +function buildSepticTankStatusSaveList( + currentData: Record[], +): SepticTankStatusSaveItem[] { + const formatNumber = buildColumnFormatNumber(SepticTankStatusInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + staaInsdSttkNprsflCnt: formatNumber(d.staaInsdSttkNprsflCnt, "staaInsdSttkNprsflCnt"), + step1StaaInsdSttkFrcCnt: formatNumber(d.step1StaaInsdSttkFrcCnt, "step1StaaInsdSttkFrcCnt"), + staaInsdSprktpFltrtbCnt: formatNumber(d.staaInsdSprktpFltrtbCnt, "staaInsdSprktpFltrtbCnt"), + staaInsdAertMthdCnt: formatNumber(d.staaInsdAertMthdCnt, "staaInsdAertMthdCnt"), + staaInsdCntcAertCnt: formatNumber(d.staaInsdCntcAertCnt, "staaInsdCntcAertCnt"), + staaInsdEtcCnt: formatNumber(d.staaInsdEtcCnt, "staaInsdEtcCnt"), + step1StaaInsdNope: formatNumber(d.step1StaaInsdNope, "step1StaaInsdNope"), + step2StaaInsdNope: formatNumber(d.step2StaaInsdNope, "step2StaaInsdNope"), + step3StaaInsdNope: formatNumber(d.step3StaaInsdNope, "step3StaaInsdNope"), + step4StaaInsdNope: formatNumber(d.step4StaaInsdNope, "step4StaaInsdNope"), + step5StaaInsdNope: formatNumber(d.step5StaaInsdNope, "step5StaaInsdNope"), + step6StaaInsdNope: formatNumber(d.step6StaaInsdNope, "step6StaaInsdNope"), + step7StaaInsdNope: formatNumber(d.step7StaaInsdNope, "step7StaaInsdNope"), + step8StaaInsdNope: formatNumber(d.step8StaaInsdNope, "step8StaaInsdNope"), + staaExcpSttkNprsflCnt: formatNumber(d.staaExcpSttkNprsflCnt, "staaExcpSttkNprsflCnt"), + step1StaaExcpSttkFrcCnt: formatNumber(d.step1StaaExcpSttkFrcCnt, "step1StaaExcpSttkFrcCnt"), + staaExcpSprktpFltrtbCnt: formatNumber(d.staaExcpSprktpFltrtbCnt, "staaExcpSprktpFltrtbCnt"), + staaExcpAertMthdCnt: formatNumber(d.staaExcpAertMthdCnt, "staaExcpAertMthdCnt"), + staaExcpCntcAertCnt: formatNumber(d.staaExcpCntcAertCnt, "staaExcpCntcAertCnt"), + staaExcpEtcCnt: formatNumber(d.staaExcpEtcCnt, "staaExcpEtcCnt"), + step1StaaExcpNope: formatNumber(d.step1StaaExcpNope, "step1StaaExcpNope"), + step2StaaExcpNope: formatNumber(d.step2StaaExcpNope, "step2StaaExcpNope"), + step3StaaExcpNope: formatNumber(d.step3StaaExcpNope, "step3StaaExcpNope"), + step4StaaExcpNope: formatNumber(d.step4StaaExcpNope, "step4StaaExcpNope"), + step5StaaExcpNope: formatNumber(d.step5StaaExcpNope, "step5StaaExcpNope"), + step6StaaExcpNope: formatNumber(d.step6StaaExcpNope, "step6StaaExcpNope"), + step7StaaExcpNope: formatNumber(d.step7StaaExcpNope, "step7StaaExcpNope"), + step8StaaExcpNope: formatNumber(d.step8StaaExcpNope, "step8StaaExcpNope"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +function buildPrivateSewageDesigStatusSaveList( + currentData: Record[], +): PrivateSewageDesigStatusSaveItem[] { + const formatNumber = buildColumnFormatNumber(PrivateSewageDesigStatusInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + indvSwagePbancRgnCnt: formatNumber(d.indvSwagePbancRgnCnt, "indvSwagePbancRgnCnt"), + indvSwageDsgnScpLen: formatNumber(d.indvSwageDsgnScpLen, "indvSwageDsgnScpLen"), + spctnkGrssFcltCnt: formatNumber(d.spctnkGrssFcltCnt, "spctnkGrssFcltCnt"), + spctnkQnt: formatNumber(d.spctnkQnt, "spctnkQnt"), + drtwPrcsFcltGrssFcltCnt: formatNumber( + d.drtwPrcsFcltGrssFcltCnt, + "drtwPrcsFcltGrssFcltCnt", + ), + drtwPrcsFcltQnt: formatNumber(d.drtwPrcsFcltQnt, "drtwPrcsFcltQnt"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + +function resolvePrivateSewageFacilityTab(tabKey: string) { + return { + isSewageTreatTab: tabKey === SewageTreatFactStatusInpColumn.tabKey, + isSepticTankTab: tabKey === SepticTankStatusInpColumn.tabKey, + isDesigStatusTab: tabKey === PrivateSewageDesigStatusInpColumn.tabKey, + }; +} // 하수도 통계 관리 > 08. 개인하수처리시설 const PrivateSewageFacilityInp = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const globalProgressVisible = useGlobalProgressStore((state) => state.visible); - // tab list - const PrivateSewageFacilityInpTabList = [ - SewageTreatFactStatusInpColumn, - SepticTankStatusInpColumn, - PrivateSewageDesigStatusInpColumn - ]; + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => { + try { + const res = await statisticsApi.getColumnGuides(statsCd); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [statsCd, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [statsCd, next] as const; + } catch { + return [statsCd, {}] as const; + } + }), + ); + if (cancelled) return; + const guidesByStatsCd = Object.fromEntries(entries); + setColumnGuidesByTab( + Object.fromEntries( + Object.entries(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( + ([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}], + ), + ), + ); + })(); + return () => { + cancelled = true; + }; + }, []); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); }; - const lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewageTreatFactStatusRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + + if (tabKey === SewageTreatFactStatusInpColumn.tabKey) { + const response = + await statisticsInputPrivateSewageFacilityApi.getSewageTreatFactStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewageTreatFactStatusList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewageTreatFactStatusList ?? []) as SewageTreatFactStatusItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewageTreatFactStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === SepticTankStatusInpColumn.tabKey) { + const response = + await statisticsInputPrivateSewageFacilityApi.getSepticTankStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, septicTankStatusList } = response.data; + const gridRows = mapRegionalListToGridRows( + (septicTankStatusList ?? []) as SepticTankStatusItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SepticTankStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === PrivateSewageDesigStatusInpColumn.tabKey) { + const response = + await statisticsInputPrivateSewageFacilityApi.getPrivateSewageDesigStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, privateSewageDesigStatusList } = response.data; + const gridRows = mapRegionalListToGridRows( + (privateSewageDesigStatusList ?? []) as PrivateSewageDesigStatusItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(PrivateSewageDesigStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + }; + + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildPrivateSewageFacilitySearchParam(values); + searchParamRef.current = searchParam; + + await loadActiveTabData( + opts?.forcedTabKey ?? activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + 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; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const lastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildPrivateSewageFacilitySearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const submitFunc = () => { - // 제출 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } = + resolvePrivateSewageFacilityTab(tabKey); + if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } = + resolvePrivateSewageFacilityTab(tabKey); + if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {}; + + const submitInfoBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + }; + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + true, + false, + ); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + let response: { status: string; message?: string; data?: { finalStatus?: string; nvpassed?: boolean } }; + + if (isSewageTreatTab) { + const param: SewageTreatFactStatusSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewageTreatFactStatusSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputPrivateSewageFacilityApi.submitSewageTreatFactStatus( + param, + ); + } else if (isSepticTankTab) { + const param: SepticTankStatusSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSepticTankStatusSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputPrivateSewageFacilityApi.submitSepticTankStatus(param); + } else { + const param: PrivateSewageDesigStatusSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildPrivateSewageDesigStatusSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputPrivateSewageFacilityApi.submitPrivateSewageDesigStatus( + param, + ); + } + + if (response.status !== "success") { + alert(response.message || "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + setShowSubmitDialog(false); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSewageTreatTab, isSepticTankTab, isDesigStatusTab } = + resolvePrivateSewageFacilityTab(tabKey); + if (!isSewageTreatTab && !isSepticTankTab && !isDesigStatusTab) { + alert("저장할 수 없는 탭입니다."); + return; + } + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + if (!(await confirm("저장 하시겠습니까?"))) return; + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("저장할 데이터가 없습니다."); + return; + } + + const baseRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + + if (isSewageTreatTab) { + const param: SewageTreatFactStatusSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewageTreatFactStatusSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputPrivateSewageFacilityApi.saveSewageTreatFactStatus( + param, + ); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + if (isSepticTankTab) { + const param: SepticTankStatusSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSepticTankStatusSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputPrivateSewageFacilityApi.saveSepticTankStatus(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + const param: PrivateSewageDesigStatusSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildPrivateSewageDesigStatusSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputPrivateSewageFacilityApi.savePrivateSewageDesigStatus( + param, + ); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + } catch { + alert("데이터 저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of PrivateSewageFacilityInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + 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, columnGuidesByTab]); + + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = + activeTabToolbar; + + const verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.PRIVATE_SEWAGE_FACILITY, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); return ( <> - +
+ + {showActionToolbarCard && (
- - + + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> - + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {isLoading && !globalProgressVisible && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {PrivateSewageFacilityInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {PrivateSewageFacilityInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + 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; + }); + }} + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + /> + + ); + })} +
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> ); }; -export default PrivateSewageFacilityInp; \ No newline at end of file +export default PrivateSewageFacilityInp; diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 1aaa36585..8b24a86eb 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -1,8 +1,10 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react'; import { useConfirm } from '@frontend-opensource/use-react-hooks'; import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from '@/shared/lib'; +import { STATISTICS_INPUT_URL_PATH } from '@/shared/lib/stats-cd-input-navigation'; +import { useStatisticsInputPendingNavigation } from '@/shared/lib/use-statistics-input-pending-navigation'; import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service'; import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service'; import { statisticsApi } from '@/entities/statistics/api/statistics-service'; @@ -46,6 +48,7 @@ import type { SearchFormValues } from '@/components/type/SearchForm.type'; import { doList } from '@/components/util/column/input/DoList'; import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn'; import { toGridRows } from '@/components/util/grid/gridFunction'; +import { patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag'; import { useAdminStore } from '@/store/admin-store'; type GridRowWithVerification = Record & { @@ -157,12 +160,17 @@ const PublicSewageFacilityInp = () => { const globalProgressVisible = useGlobalProgressStore((state) => state.visible); const [isLoading, setIsLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); const [showIsNotApplicable, setShowIsNotApplicable] = useState(false); const [searchValues, setSearchValues] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState(null); const [tabStatusInfoMap, setTabStatusInfoMap] = useState>(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); /** 탭키별 `engColNm -> columnGuide` 캐시 */ const [columnGuidesByTab, setColumnGuidesByTab] = useState>>({}); const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState>(() => new Map()); @@ -175,6 +183,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> = (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 +216,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 +297,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 +536,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 ?? '', @@ -567,11 +588,6 @@ const PublicSewageFacilityInp = () => { })(); }; - const updateOperateDataButtonFunc = () => { - console.log('updatedOperateData'); - // 운영 데이터 업데이트 로직 - }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [activeTab, setActiveTab] = useState( PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001', @@ -655,8 +671,6 @@ const PublicSewageFacilityInp = () => { param: PublicSewageFacilityRequestParams, searchContext: SearchFormValues | null, ): Promise => { - // 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화 - setIsNotApplicable(false); try { if (tabKey === 'ST00060001') { const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param); @@ -677,8 +691,10 @@ const PublicSewageFacilityInp = () => { if (list.length === 0) { setShowIsNotApplicable(true); + setIsNotApplicable(true); } else { setShowIsNotApplicable(false); + setIsNotApplicable(false); } const gridRows = toGridRows(convertedData); @@ -692,6 +708,9 @@ const PublicSewageFacilityInp = () => { next.set(tabKey, statsCd); return next; }); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); setRowDataMap((prev) => { const next = new Map(prev); next.set('ST00060001', gridRows); @@ -736,6 +755,9 @@ const PublicSewageFacilityInp = () => { next.set(tabKey, statsCd); return next; }); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, response.data.commonInfo), + ); setRowDataMap((prev) => { const next = new Map(prev); next.set('MainFacility', leftGridRows); @@ -753,6 +775,52 @@ const PublicSewageFacilityInp = () => { } }; + const updateOperateDataButtonFunc = () => { + void (async () => { + if (!lastSearchedValues) { + alert('먼저 조회를 진행해주세요.'); + return; + } + if (!(await confirm('운영 데이터를 업데이트 하시겠습니까?'))) return; + + try { + showGlobalProgress({ label: '운영 데이터 업데이트 중...', progress: 0 }); + const userInfo = await getUserInfo(); + const request = { + statsYr: lastSearchedValues.year ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + userId: userInfo.userUnqId, + }; + + const response = + await statisticsInputPublicSewageFacilityApiService.updatePublicSewageFacilityOperationData( + request, + ); + if (response.status !== 'success') { + alert(response.message || '운영 데이터 업데이트 중 오류가 발생했습니다.'); + return; + } + + setGlobalProgress(50); + const param: PublicSewageFacilityRequestParams = { + statsYr: request.statsYr, + ctpvNm: request.ctpvNm, + sggNm: request.sggNm, + sewerBzmnCd: request.sewerBzmnCd, + }; + await loadPublicSewageFacilityActiveTabData('ST00060001', param, lastSearchedValues); + setGlobalProgress(100); + alert('운영 데이터가 업데이트되었습니다.'); + } catch { + alert('운영 데이터 업데이트 중 오류가 발생했습니다.'); + } finally { + hideGlobalProgress(); + } + })(); + }; + const runMainFacilityMappingSave = async ( draft: MainFacilityMappingSaveDraft, dialogInput: MainFacilityMappingSaveDialogValues | null, @@ -820,7 +888,10 @@ const PublicSewageFacilityInp = () => { } }; - const handleSearch = async (values: SearchFormValues) => { + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { if (!values.sido || values.sido === 'ALL') { alert('시도를 선택해주세요.'); return; @@ -848,6 +919,7 @@ const PublicSewageFacilityInp = () => { setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setToolbarSnapshotByTab(new Map()); activeTabRef.current = firstTabKey; setActiveTab(firstTabKey); @@ -860,7 +932,13 @@ const PublicSewageFacilityInp = () => { setIsLoading(true); const param = buildPublicSewageFacilityParams(values); searchParamRef.current = param; - const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current; + const tabKeyToLoad = + opts?.forcedTabKey ?? + (yearChangedFromLastSearch ? firstTabKey : activeTabRef.current); + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param, values); } catch { alert('데이터 조회 중 오류가 발생했습니다.'); @@ -870,6 +948,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,12 +976,13 @@ const PublicSewageFacilityInp = () => { const handleReset = () => { setIsLoading(false); setHasSearched(false); - setIsNotApplicable(false); + setIsNotApplicableByTab(new Map()); setShowIsNotApplicable(false); setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setToolbarSnapshotByTab(new Map()); setInputPeriodEnterable(true); @@ -1066,10 +1158,23 @@ const PublicSewageFacilityInp = () => { }; })(); + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); + return ( <>
{ {!isMainFacilityTab && ( )} {showEnterableToolbarButtons && !isMainFacilityTab && ( @@ -1215,7 +1323,7 @@ const PublicSewageFacilityInp = () => { return ( { open={verificationHistoryOpen} onOpenChange={setVerificationHistoryOpen} queryContext={verificationQueryContext} + onExceptionResetSubmitted={async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx || !searchParamRef.current) return; + await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx); + }} /> 05. 펌프장 +type GridRowWithVerification = Record & { + verificationResult?: PumpingStationVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = + isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function buildPumpingStationSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): PumpingStationRequestParams { + const fcltNm = values.fctName?.trim(); + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(fcltNm ? { fcltNm } : {}), + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function parseSggListFromResponse(res: ApiResponse): Sgg[] { + if (res.status !== "success" || res.data == null) return []; + return Array.isArray(res.data) ? res.data : []; +} + +function buildSggNmColumn( + col: ColumnConfig, + canEdit: boolean, + sggOptions: Sgg[], +): ColumnConfig { + if (!canEdit) { + return { ...col, editable: false as const }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +function mapPumpingStationListToGridRows( + list: PumpingStationItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const row = toGridRows([ + { + ...item, + ctpvNm, + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + }, + ])[0] as GridRowWithVerification; + if (item.verificationResult) { + row.verificationResult = item.verificationResult; + } + return row; + }); +} + +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function resolvePumpingStationAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + if (existingCd && existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm }; + } + const sggNm = String(row.sggNm ?? "").trim(); + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; +} + +/** 그리드 행 → ST00050001 저장/삭제 API 항목 */ +function buildPumpingStationSaveItem( + row: Record, + sggOptions: Sgg[] = [], +): PumpingStationSaveItem { + const { admdstCd, admdstNm } = resolvePumpingStationAdmdst(row, sggOptions); + const fcltSn = String(row.fcltSn ?? "").trim(); + return { + admdstCd, + admdstNm, + fcltSn: fcltSn, + swtrmtFcltNm: String(row.swtrmtFcltNm ?? ""), + fcltNm: String(row.fcltNm ?? ""), + pumpSeNm: String(row.pumpSeNm ?? ""), + fcltPstnCn: String(row.fcltPstnCn ?? ""), + oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), + planMltvArea: toGridNumber(row.planMltvArea), + sndyArea: toGridNumber(row.sndyArea), + rainArea: toGridNumber(row.rainArea), + drtwPumpN1DmtrArea: toGridNumber(row.drtwPumpN1DmtrArea), + drtwPumpN1Ppqty: toGridNumber(row.drtwPumpN1Ppqty), + drtwPumpN1Cnt: toGridNumber(row.drtwPumpN1Cnt), + drtwPumpN1RsrvCnt: toGridNumber(row.drtwPumpN1RsrvCnt), + drtwPumpN2DmtrArea: toGridNumber(row.drtwPumpN2DmtrArea), + drtwPumpN2Ppqty: toGridNumber(row.drtwPumpN2Ppqty), + drtwPumpN2Cnt: toGridNumber(row.drtwPumpN2Cnt), + drtwPumpN2RsrvCnt: toGridNumber(row.drtwPumpN2RsrvCnt), + drtwPumpN3DmtrArea: toGridNumber(row.drtwPumpN3DmtrArea), + drtwPumpN3Ppqty: toGridNumber(row.drtwPumpN3Ppqty), + drtwPumpN3Cnt: toGridNumber(row.drtwPumpN3Cnt), + drtwPumpN3RsrvCnt: toGridNumber(row.drtwPumpN3RsrvCnt), + exlnPumpN1DmtrArea: toGridNumber(row.exlnPumpN1DmtrArea), + exlnPumpN1Ppqty: toGridNumber(row.exlnPumpN1Ppqty), + exlnPumpN1Cnt: toGridNumber(row.exlnPumpN1Cnt), + exlnPumpN1RsrvCnt: toGridNumber(row.exlnPumpN1RsrvCnt), + exlnPumpN2DmtrArea: toGridNumber(row.exlnPumpN2DmtrArea), + exlnPumpN2Ppqty: toGridNumber(row.exlnPumpN2Ppqty), + exlnPumpN2Cnt: toGridNumber(row.exlnPumpN2Cnt), + exlnPumpN2RsrvCnt: toGridNumber(row.exlnPumpN2RsrvCnt), + exlnPumpN3DmtrArea: toGridNumber(row.exlnPumpN3DmtrArea), + exlnPumpN3Ppqty: toGridNumber(row.exlnPumpN3Ppqty), + exlnPumpN3Cnt: toGridNumber(row.exlnPumpN3Cnt), + exlnPumpN3RsrvCnt: toGridNumber(row.exlnPumpN3RsrvCnt), + anlElpwrUseQnt: toGridNumber(row.anlElpwrUseQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + +function createEmptyPumpingStationRow( + ctpvNm: string, + sggNm = "", +): GridRowWithVerification { + const row: GridRowWithVerification = { ctpvNm, sggNm }; + for (const col of PumpingStationInpColumn.columns) { + const fieldName = col.fieldName; + if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} + +// 하수도 통계 관리 > 05. 펌프장 (SewageReservoirPumpInp 단일 탭 패턴) const PumpingStationInp = () => { - - const SubmitExceptionFunc = () => { - console.log('submit exception'); - // 예외 제출 로직 - }; - - const getLastYearDataFunc = () => { - console.log('get last year data'); - // 지난연도 데이터 가져오기 로직 - }; - - const submitFunc = () => { - console.log('submit'); - // 제출 로직 - }; - - const saveFunc = () => { - console.log('save'); - // 저장 로직 - }; - - const addArticleFunc = () => { - console.log('save'); - // 항목 추가 로직 - }; - - const deleteArticleFunc = () => { - console.log('save'); - // 항목 제외 로직 - }; + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const [rowData, setRowData] = useState[]>([]); const [isNotApplicable, setIsNotApplicable] = useState(false); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = + useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [statusInfo, setStatusInfo] = useState(null); + const [statsCd, setStatsCd] = useState(""); + const [isVerificationFailed, setIsVerificationFailed] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [sggOptions, setSggOptions] = useState([]); + const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState< + Record + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = + useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [pumpingStationDeleteList, setPumpingStationDeleteList] = useState< + Record[] + >([]); + + const gridRef = useRef(null); + const gridGetRowsRef = + useRef<(() => Record[]) | null>(null); + const sggOptionsSidoRef = useRef(null); + const [searchLoaded, setSearchLoaded] = useState(false); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + const res = await statisticsApi.getColumnGuides(PUMPING_STATION_STATS_CD); + if (cancelled) return; + if (res.status !== "success" || !Array.isArray(res.data)) { + setColumnGuidesByEngCol({}); + return; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + setColumnGuidesByEngCol(next); + } catch { + if (!cancelled) setColumnGuidesByEngCol({}); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + if (!sido || sido === "ALL") { + setSggOptions([]); + return []; + } + try { + const res = await statisticsApi.getSggAllList(sido); + const list = parseSggListFromResponse(res); + setSggOptions(list); + return list; + } catch { + setSggOptions([]); + return []; + } + }, []); + + const columnInfo = useMemo(() => { + const enterable = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const rawColumns = PumpingStationInpColumn.columns ?? []; + const base = enterable + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + + const columns = base + .map((col: ColumnConfig) => { + if (col.fieldName === "ctpvNm") { + return { ...col, editable: false as const }; + } + if (col.fieldName === "sggNm" && enterable) { + return buildSggNmColumn(col, true, sggOptions); + } + return col; + }) + .map((col) => { + const guide = columnGuidesByEngCol[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); + + return { + columns, + columnLayout: PumpingStationInpColumn.columnLayout ?? [], + }; + }, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]); + + const canEditGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + + const getCurrentRows = (): Record[] => + gridGetRowsRef.current?.() ?? rowData; + + const commitGrid = () => { + const gridView = gridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } + }; + + const fetchPumpingStationList = async ( + values: SearchFormValues, + overrides?: { mode?: string }, + ): Promise => { + const response = await statisticsInputPumpingStationApi.getPumpingStationList( + buildPumpingStationSearchParam(values, overrides), + ); + + if (response.status !== "success" || !response.data) { + alert(response.message ?? "데이터 조회 중 오류가 발생했습니다."); + setSearchLoaded(false); + return false; + } + + const { commonInfo, statusInfo: nextStatusInfo, pumpingStationList } = + response.data; + const list = pumpingStationList ?? []; + const gridRows = mapPumpingStationListToGridRows(list, commonInfo.ctpvNm); + + setPumpingStationDeleteList([]); + setRowData(gridRows); + setStatusInfo(nextStatusInfo); + setStatsCd(commonInfo.statsCd); + setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo)); + if (overrides?.mode !== "PRE") { + setIsNotApplicable(gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicable(false); + } + setSearchLoaded(true); + return true; + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) + ? periodResponse.data + : []; + setInputPeriodEnterable(isInputPeriodCurrent(periods)); + + setIsLoading(true); + setHasSearched(true); + setLastSearchedValues(values); + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + setSearchLoaded(false); + return; + } + + await fetchPumpingStationList(values); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + setRowData([]); + setStatusInfo(null); + setSearchLoaded(false); + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + setPumpingStationDeleteList([]); + setRowData([]); + setLastSearchedValues(null); + setHasSearched(false); + setStatusInfo(null); + setStatsCd(""); + setIsVerificationFailed(false); + setIsNotApplicable(false); + setInputPeriodEnterable(true); + setSggOptions([]); + sggOptionsSidoRef.current = null; + setSearchLoaded(false); + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched || !searchLoaded) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + await fetchPumpingStationList(values, { mode: "PRE" }); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCd || PUMPING_STATION_STATS_CD, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await fetchPumpingStationList(lastSearchedValues); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const submitFunc = () => { + void (async () => { + try { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + } catch { + alert("제출 중 오류가 발생했습니다."); + } + })(); + }; + + const saveFunc = () => { + void (async () => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildPumpingStationSaveItem(row, sggOptions), + ); + const deleteList = pumpingStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildPumpingStationSaveItem(row, sggOptions)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = await statisticsInputPumpingStationApi.savePumpingStationList({ + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + saveList, + deleteList, + }); + + if (response.status === "success") { + setPumpingStationDeleteList([]); + await fetchPumpingStationList(lastSearchedValues); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitGrid(); + const noApplicable = isNotApplicable; + const currentData = getCurrentRows(); + const submitList = noApplicable + ? [] + : currentData.map((row) => buildPumpingStationSaveItem(row, sggOptions)); + const deleteList = pumpingStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildPumpingStationSaveItem(row, sggOptions)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const statsCdForSubmit = statsCd || PUMPING_STATION_STATS_CD; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForSubmit, + }; + + const reloadAfterSubmit = async () => { + setPumpingStationDeleteList([]); + await fetchPumpingStationList(lastSearchedValues); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = await statisticsInputPumpingStationApi.submitPumpingStationList({ + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + submitList, + deleteList, + }); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForSubmit, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + }; + + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + let list = sggOptions; + if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { + list = await loadSggOptions(ctx.sido); + sggOptionsSidoRef.current = ctx.sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + commitGrid(); + const currentRows = getCurrentRows(); + const newRow = createEmptyPumpingStationRow(ctx.sido, sggNm); + + flushSync(() => { + setSggOptions(list); + setRowData([...currentRows, newRow]); + }); + }; + + const deleteArticleFunc = () => { + commitGrid(); + const currentRows = getCurrentRows(); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const gridView = gridRef.current?.gridView as + | { + getCheckedRows?: (refresh?: boolean) => number[]; + getCheckedItems?: (refresh?: boolean) => number[]; + } + | undefined; + + const checkedIndexes = + gridView?.getCheckedRows?.(false) ?? + gridView?.getCheckedItems?.(false) ?? + []; + + if (checkedIndexes.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + const indexSet = new Set( + checkedIndexes.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < currentRows.length, + ), + ); + const removedRows = currentRows.filter((_, idx) => indexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + setPumpingStationDeleteList((prev) => [...prev, ...removedRows]); + setRowData(currentRows.filter((_, idx) => !indexSet.has(idx))); + }; + + const verificationCellStyle = ({ + fieldName, + row, + }: CellStyleCallbackParams): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: CellTooltipCallbackParams): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 -"; + return `${prefix}${e.rsnExpln} +(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const toolbarSnapshot = computeTabToolbarSnapshot( + statusInfo, + rowData.length > 0, + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + toolbarSnapshot.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + toolbarSnapshot.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + toolbarSnapshot.showActionToolbarCard && toolbarGroupAllowed; + const showArticleToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const showNotApplicable = + searchLoaded && + hasSearched && + rowData.length === 0 && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const tabEnterableForGrid = + computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.PUMPING_STATION, + onAutoSearch: (values) => handleSearch(values), + }); return ( <> - +
+ + {showActionToolbarCard && (
-
- - +
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> - + + )} + {statusInfo?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && }
+ )} + {showArticleToolbarCard && (
-
- +
+ {showNotApplicable && ( + + )}
+ )} + {isLoading && ( +
+
+ +
+
+ )} } + columnInfo={columnInfo} + rowStartNum={PumpingStationInpColumn.rowStartNum} + fixedColumns={PumpingStationInpColumn.fixedColumns} rowData={rowData} setRowData={setRowData} + getGridDataRef={gridGetRowsRef} + showHeaderCheckbox + statusInfo={statusInfo} + isExcelImport={ + searchLoaded && tabEnterableForGrid && gugunOk + } + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + /> +
+ { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchPumpingStationList(values); + }} /> + { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await fetchPumpingStationList(values); + }} + /> + + ); }; -export default PumpingStationInp; \ No newline at end of file +export default PumpingStationInp; diff --git a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx index b4470e438..addeda5c0 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx @@ -1,81 +1,2117 @@ -import { useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react"; + +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; +import { ValueType } from "realgrid"; +import type { RealGridReact } from "realgrid-react"; +import { flushSync } from "react-dom"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; import AddArticleButton from "@/components/button/AddArticleButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton"; - -import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn" -import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn" +import LastYearDataButton from "@/components/button/LastYearDataButton"; +import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton"; +import SaveButton from "@/components/button/SaveButton"; +import SubmitButton from "@/components/button/SumitButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { + CellStyleCallback, + CellStyleCallbackParams, + CellStyleCallbackResult, + CellTooltipCallbackParams, + ColumnConfig, + ColumnInfo, +} from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; import { ExcretaRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn"; import { PrivateSewageTreatRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn"; +import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn"; +import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; +import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + ExcretaRelatedIndustrySaveItem, + PrivateSewageTreatRelatedIndustrySaveItem, + PublicInstitutionSewerIndustrySaveItem, + SewageExcretaRelatedRequestParams, + SewageExcretaRelatedVerificationResult, +} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface"; +import { + SEWAGE_EXCRETA_EXCRETA_RELATED_INDUSTRY_STATS_CD, + SEWAGE_EXCRETA_PRIVATE_SEWAGE_TREAT_RELATED_INDUSTRY_STATS_CD, + SEWAGE_EXCRETA_PUBLIC_INSTITUTION_EXCRETA_STATS_CD, + SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD, +} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { + ApiResponse, + cancelSubmitRequest, + ImrStatusResponse, + SewerageStatusInfo, + Sgg, +} from "@/entities/statistics/model/statistics-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; +import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAdminStore } from "@/store/admin-store"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +import SubmitDailog from "./SubmitDailog"; + +const SewageExcretaRelatedInpTabList = [ + PublicInstitutionSewerIndustryInpColumn, + PublicInstitutionExcretaIndustryInpColumn, + ExcretaRelatedIndustryInpColumn, + PrivateSewageTreatRelatedIndustryInpColumn, +] as const; + +const firstTabKey = PublicInstitutionSewerIndustryInpColumn.tabKey; + +/** 직제(ognzSn) 행 추가·제외 — ST00120001·ST00120002 */ +const ARTICLE_EDIT_TAB_KEYS = new Set([ + PublicInstitutionSewerIndustryInpColumn.tabKey, + PublicInstitutionExcretaIndustryInpColumn.tabKey, +]); + +/** 해당사항 없음 — ST00120001·ST00120002 */ +const NOT_APPLICABLE_TAB_KEYS = new Set([ + PublicInstitutionSewerIndustryInpColumn.tabKey, + PublicInstitutionExcretaIndustryInpColumn.tabKey, +]); + +/** 항목 추가 행 — 시군구(sggNm) 드롭다운 선택 */ +const SGG_NM_DROPDOWN_TAB_KEYS = new Set([ + PublicInstitutionSewerIndustryInpColumn.tabKey, + PublicInstitutionExcretaIndustryInpColumn.tabKey, +]); + +/** 저장·제출 API 연동 — ST00120001~ST00120004 */ +const SAVE_SUPPORTED_TAB_KEYS = new Set([ + PublicInstitutionSewerIndustryInpColumn.tabKey, + PublicInstitutionExcretaIndustryInpColumn.tabKey, + ExcretaRelatedIndustryInpColumn.tabKey, + PrivateSewageTreatRelatedIndustryInpColumn.tabKey, +]); + +const SUBMIT_SUPPORTED_TAB_KEYS = SAVE_SUPPORTED_TAB_KEYS; + +/** 행정구역 고정 행 탭 — saveList/submitList만 (ST00120003·ST00120004) */ +const DISTRICT_ROW_TAB_KEYS = new Set([ + ExcretaRelatedIndustryInpColumn.tabKey, + PrivateSewageTreatRelatedIndustryInpColumn.tabKey, +]); + +const ARTICLE_TAB_BY_KEY = new Map( + SewageExcretaRelatedInpTabList.map((tab) => [tab.tabKey, tab]), +); + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof SewageExcretaRelatedInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function tabRowDataExists( + rowDataMap: Map[]>, + tabKey: string, +): boolean { + const rows = rowDataMap.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function createEmptyArticleRow( + tabKey: string, + ctpvNm: string, + sggNm = "", +): Record { + const row: Record = { ctpvNm, sggNm, __checked: 0 }; + const columns = (ARTICLE_TAB_BY_KEY.get(tabKey)?.columns ?? []) as ColumnConfig[]; + for (const col of columns) { + const fieldName = col.fieldName; + if ( + !fieldName || + fieldName === "ctpvNm" || + fieldName === "sggNm" || + col.editable === false + ) { + continue; + } + if (row[fieldName] === undefined) { + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + } + return row; +} + +function isCheckedRow(row: Record): boolean { + const v = row.__checked; + return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true"; +} + +type ArticleOgnzSnSet = Set; + +function extractOgnzSnSet(rows: Record[]): ArticleOgnzSnSet { + const set = new Set(); + for (const row of rows) { + const ognzSn = String(row.ognzSn ?? "").trim(); + if (ognzSn) set.add(ognzSn); + } + return set; +} + +function isNewArticleRow(row: Record): boolean { + const v = row.__isNewArticle; + return v === 1 || v === "1"; +} + +function setArticleOgnzSnSetForTab( + storeRef: { current: Map }, + tabKey: string, + rows: Record[], +) { + storeRef.current.set(tabKey, extractOgnzSnSet(rows)); +} + +function clearArticleOgnzSnTracking( + persistedRef: { current: Map }, + preYearRef: { current: Map }, +) { + persistedRef.current = new Map(); + preYearRef.current = new Map(); +} + +/** ST00120001·ST00120002 — deleteList 대상 판별 (`ognzSn` + 전년도·신규 행 구분) */ +function isPersistedPublicInstitutionArticleRow( + tabKey: string, + row: Record, + persistedRef: { current: Map }, + preYearRef: { current: Map }, +): boolean { + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) return false; + if (isNewArticleRow(row)) return false; + const ognzSn = String(row.ognzSn ?? "").trim(); + if (!ognzSn) return false; + if ((preYearRef.current.get(tabKey) ?? new Set()).has(ognzSn)) return false; + return (persistedRef.current.get(tabKey) ?? new Set()).has(ognzSn); +} + +function syncPublicInstitutionArticleTracking( + tabKey: string, + gridRows: Record[], + fromPreYear: boolean, + persistedRef: { current: Map }, + preYearRef: { current: Map }, + clearDeleteList: (tabKey: string) => void, +) { + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) return; + if (fromPreYear) { + setArticleOgnzSnSetForTab(preYearRef, tabKey, gridRows); + return; + } + clearDeleteList(tabKey); + setArticleOgnzSnSetForTab(persistedRef, tabKey, gridRows); +} + +function parseSggListFromResponse(res: ApiResponse): Sgg[] { + if (res.status !== "success" || res.data == null) return []; + return Array.isArray(res.data) ? res.data : []; +} + +function buildSggNmColumn( + col: ColumnConfig, + canEdit: boolean, + sggOptions: Sgg[], +): ColumnConfig { + if (!canEdit) { + return { ...col, editable: false as const, styleName: "rg-cell-readonly" }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true, styleName: undefined }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + styleName: undefined, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +/** 시군구명(`sggNm`) 기준 `admdstCd`·`admdstNm` — 저장 시 그리드 행에서 해석 */ +function resolvePublicInstitutionAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const sggNm = String(row.sggNm ?? "").trim(); + if (sggNm) { + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + } + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; +} + +function mergeGridRowsWithMapRows( + gridRows: Record[], + mapRows: Record[], + hiddenFields: string[] = [], +): Record[] { + return gridRows.map((gridRow, index) => { + const mapRow = mapRows[index] ?? {}; + const merged: Record = { ...mapRow, ...gridRow }; + if (hiddenFields.length === 0) return merged; + const next = { ...merged }; + for (const field of hiddenFields) { + const fromGrid = String(gridRow[field] ?? "").trim(); + const fromMap = String(mapRow[field] ?? "").trim(); + next[field] = fromGrid || fromMap; + } + return next; + }); +} + +/** 그리드 조회 시 `__isNewArticle` 등 메타 필드 유실 방지 */ +function mergeGridRowsWithArticleMeta( + gridRows: Record[], + mapRows: Record[], +): Record[] { + return gridRows.map((gridRow, index) => { + const mapRow = mapRows[index]; + if (!mapRow) return gridRow; + if (!isNewArticleRow(gridRow) && isNewArticleRow(mapRow)) { + return { + ...gridRow, + __isNewArticle: mapRow.__isNewArticle as string | number, + }; + } + return gridRow; + }); +} + +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +/** ST00120001·ST00120002 — 그리드 행 → 저장/삭제 API 항목 */ +function buildPublicInstitutionIndustrySaveItem( + row: Record, + sggOptions: Sgg[], +): PublicInstitutionSewerIndustrySaveItem { + const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + ognzSn: String(row.ognzSn ?? "").trim(), + ognzNm: String(row.ognzNm ?? "").trim(), + pbadmsNope: toGridNumber(row.pbadmsNope), + engrkCnt: toGridNumber(row.engrkCnt), + mchnCnt: toGridNumber(row.mchnCnt), + prvtmCnt: toGridNumber(row.prvtmCnt), + chmengCnt: toGridNumber(row.chmengCnt), + envCnt: toGridNumber(row.envCnt), + enjbEtcCnt: toGridNumber(row.enjbEtcCnt), + etcCnt: toGridNumber(row.etcCnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** ST00120003 — 그리드 행 → 저장/제출 API 항목 */ +function buildExcretaRelatedIndustrySaveItem( + row: Record, +): ExcretaRelatedIndustrySaveItem { + return { + admdstCd: String(row.admdstCd ?? "").trim(), + admdstNm: String(row.admdstNm ?? "").trim(), + bzentyCnt: toGridNumber(row.bzentyCnt), + empNope: toGridNumber(row.empNope), + n3TonUdrCnt: toGridNumber(row.n3TonUdrCnt), + n4TonUdrCnt: toGridNumber(row.n4TonUdrCnt), + n8TonUdrCnt: toGridNumber(row.n8TonUdrCnt), + etcCnt: toGridNumber(row.etcCnt), + clctCnvncPrfmncQnt: toGridNumber(row.clctCnvncPrfmncQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** ST00120004 — 그리드 행 → 저장/제출 API 항목 */ +function buildPrivateSewageTreatRelatedIndustrySaveItem( + row: Record, +): PrivateSewageTreatRelatedIndustrySaveItem { + return { + admdstCd: String(row.admdstCd ?? "").trim(), + admdstNm: String(row.admdstNm ?? "").trim(), + designMdsrCnt: toGridNumber(row.designMdsrCnt), + designMdsrNope: toGridNumber(row.designMdsrNope), + mfbizCnt: toGridNumber(row.mfbizCnt), + mfbizMdsrNope: toGridNumber(row.mfbizMdsrNope), + mngCnt: toGridNumber(row.mngCnt), + mngMdsrNope: toGridNumber(row.mngMdsrNope), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildDistrictRowSaveItem( + tabKey: string, + row: Record, +): ExcretaRelatedIndustrySaveItem | PrivateSewageTreatRelatedIndustrySaveItem { + if (tabKey === ExcretaRelatedIndustryInpColumn.tabKey) { + return buildExcretaRelatedIndustrySaveItem(row); + } + return buildPrivateSewageTreatRelatedIndustrySaveItem(row); +} + +function resolveStatsCdForTab( + tabKey: string, + tabStatsCdMap: Map, +): string { + const fromMap = tabStatsCdMap.get(tabKey); + if (fromMap) return fromMap; + if (tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey) { + return SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD; + } + if (tabKey === PublicInstitutionExcretaIndustryInpColumn.tabKey) { + return SEWAGE_EXCRETA_PUBLIC_INSTITUTION_EXCRETA_STATS_CD; + } + if (tabKey === ExcretaRelatedIndustryInpColumn.tabKey) { + return SEWAGE_EXCRETA_EXCRETA_RELATED_INDUSTRY_STATS_CD; + } + if (tabKey === PrivateSewageTreatRelatedIndustryInpColumn.tabKey) { + return SEWAGE_EXCRETA_PRIVATE_SEWAGE_TREAT_RELATED_INDUSTRY_STATS_CD; + } + return tabKey; +} + +type GridRowWithVerification = Record & { + verificationResult?: SewageExcretaRelatedVerificationResult; +}; + +function mergeCellStyleResults( + ...results: (CellStyleCallbackResult | undefined)[] +): CellStyleCallbackResult | undefined { + const styleNames: string[] = []; + let editable: boolean | undefined; + let readOnly: boolean | undefined; + + for (const result of results) { + if (!result) continue; + if (typeof result === "string") { + styleNames.push(result); + continue; + } + if (result.styleName) styleNames.push(result.styleName); + if (result.editable === false) editable = false; + if (result.readOnly === true) readOnly = true; + } + + const styleName = styleNames.join(" ").trim(); + if (!styleName && editable === undefined && readOnly === undefined) { + return undefined; + } + return { + ...(styleName ? { styleName } : {}), + ...(editable !== undefined ? { editable } : {}), + ...(readOnly !== undefined ? { readOnly } : {}), + }; +} + +function verificationCellStyle({ + fieldName, + row, +}: CellStyleCallbackParams): CellStyleCallbackResult | undefined { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } +} + +function verificationCellTooltip({ + fieldName, + row, +}: CellTooltipCallbackParams): string | undefined { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln} +(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } +} + +function buildSewageExcretaRelatedSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewageExcretaRelatedRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function mapListToGridRows( + list: T[] | undefined, + ctpvNm: string, +): GridRowWithVerification[] { + return (list ?? []).map((item) => { + const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification; + if (item.verificationResult) { + row.verificationResult = item.verificationResult; + } + return row; + }); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 12. 하수 및 분뇨관련산업 const SewageExcretaRelatedInp = () => { - - // tab list - const SewageExcretaRelatedInpTabList = [ - PublicInstitutionSewerIndustryInpColumn, - PublicInstitutionExcretaIndustryInpColumn, - ExcretaRelatedIndustryInpColumn, - PrivateSewageTreatRelatedIndustryInpColumn - ]; + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const isAdminInfo = useAdminStore((state) => state.isAdminInfo); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState< + Map[]> + >(new Map()); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState( + null, + ); + const [hasSearched, setHasSearched] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [loadedTabKeys, setLoadedTabKeys] = useState>(new Set()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); + const [deleteListByTab, setDeleteListByTab] = useState< + Map[]> + >(() => new Map()); + const [sggOptions, setSggOptions] = useState([]); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const searchParamRef = useRef(null); + const activeTabRef = useRef(activeTab); + const loadedTabKeysRef = useRef>(new Set()); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + /** ST00120001·ST00120002 — 당해 조회 API로 로드된 ognzSn (deleteList 대상) */ + const persistedArticleOgnzSnByTabRef = useRef>(new Map()); + /** ST00120001·ST00120002 — 전년도(PRE) 조회로 로드된 ognzSn */ + const preYearArticleOgnzSnByTabRef = useRef>(new Map()); + const sggOptionsSidoRef = useRef(null); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SewageExcretaRelatedInpTabList.map(async (tab) => { + const tabKey = tab.tabKey; + try { + const res = await statisticsApi.getColumnGuides(tabKey); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [tabKey, {}] as const; + } + const next: Record = {}; + 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; + }; + }, []); + + const shouldShowNotApplicable = ( + tabKey: string, + hasRows: boolean, + statusInfo: SewerageStatusInfo | null | undefined, + ): boolean => + NOT_APPLICABLE_TAB_KEYS.has(tabKey) && + hasSearched && + !hasRows && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const setIsNotApplicableForTab = useCallback((tabKey: string, nextValue: boolean) => { + setIsNotApplicableByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, nextValue); + return next; + }); + }, []); + + const clearDeleteListForTab = useCallback((tabKey: string) => { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.delete(tabKey); + return next; + }); + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + if (!sido || sido === "ALL") { + setSggOptions([]); + return []; + } + try { + const res = await statisticsApi.getSggAllList(sido); + const list = parseSggListFromResponse(res); + setSggOptions(list); + return list; + } catch { + setSggOptions([]); + return []; + } + }, []); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); }; - const lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const markTabLoaded = (tabKey: string) => { + loadedTabKeysRef.current.add(tabKey); + setLoadedTabKeys(new Set(loadedTabKeysRef.current)); + }; + + const getTabGridRows = (tabKey: string): Record[] => { + const mapRows = rowDataMap.get(tabKey) ?? []; + if (tabKey !== activeTabRef.current) return mapRows; + const gridRows = getGridDataRef.current?.() ?? mapRows; + if (gridRows.length === 0) return gridRows; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey) || mapRows.length === 0) return gridRows; + return mergeGridRowsWithArticleMeta(gridRows, mapRows); + }; + + const commitActiveGrid = () => { + const gridView = getGridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + const prevTabKey = activeTabRef.current; + commitActiveGrid(); + const latestRowsInPrevTab = getTabGridRows(prevTabKey); + if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) { + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(prevTabKey, latestRowsInPrevTab); + return next; + }); + } + + flushSync(() => { + activeTabRef.current = tabKey; + setActiveTab(tabKey); + }); + + if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; + if (loadedTabKeysRef.current.has(tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewageExcretaRelatedRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + const fromPreYear = overrides?.mode === "PRE"; + + const applyNotApplicableForLoadedTab = ( + gridRows: readonly Record[], + ) => { + if (!NOT_APPLICABLE_TAB_KEYS.has(tabKey)) return; + if (!fromPreYear) { + setIsNotApplicableForTab(tabKey, gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicableForTab(tabKey, false); + } + }; + + if (tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaRelatedApi.getPublicInstitutionSewerIndustryList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, publicInstitutionSewerIndustryList } = response.data; + const gridRows = mapListToGridRows( + publicInstitutionSewerIndustryList, + commonInfo.ctpvNm, + ); + syncPublicInstitutionArticleTracking( + tabKey, + gridRows, + fromPreYear, + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + clearDeleteListForTab, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(PublicInstitutionSewerIndustryInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + applyNotApplicableForLoadedTab(gridRows); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === PublicInstitutionExcretaIndustryInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaRelatedApi.getPublicInstitutionExcretaIndustryList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, publicInstitutionExcretaIndustryList } = + response.data; + const gridRows = mapListToGridRows( + publicInstitutionExcretaIndustryList, + commonInfo.ctpvNm, + ); + syncPublicInstitutionArticleTracking( + tabKey, + gridRows, + fromPreYear, + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + clearDeleteListForTab, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(PublicInstitutionExcretaIndustryInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + applyNotApplicableForLoadedTab(gridRows); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === ExcretaRelatedIndustryInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaRelatedApi.getExcretaRelatedIndustryList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, excretaRelatedIndustryList } = response.data; + const gridRows = mapListToGridRows(excretaRelatedIndustryList, commonInfo.ctpvNm); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(ExcretaRelatedIndustryInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === PrivateSewageTreatRelatedIndustryInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaRelatedApi.getPrivateSewageTreatRelatedIndustryList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, privateSewageTreatRelatedIndustryList } = + response.data; + const gridRows = mapListToGridRows( + privateSewageTreatRelatedIndustryList, + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(PrivateSewageTreatRelatedIndustryInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + markTabLoaded(tabKey); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setInputPeriodEnterable(true); + searchDoneRef.current = false; + periodEnterableRef.current = true; + searchParamRef.current = null; + loadedTabKeysRef.current.clear(); + setLoadedTabKeys(new Set()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setIsNotApplicableByTab(new Map()); + setDeleteListByTab(new Map()); + clearArticleOgnzSnTracking( + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + ); + setSggOptions([]); + sggOptionsSidoRef.current = null; + setVerificationHistoryOpen(false); + }; + + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!values.bsnmCd) { + alert("사업자를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodEnterable = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodEnterable; + setInputPeriodEnterable(periodEnterable); + } catch { + alert("입력 기간 확인 중 오류가 발생했습니다."); + return; + } + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setIsNotApplicableByTab(new Map()); + setDeleteListByTab(new Map()); + clearArticleOgnzSnTracking( + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + ); + loadedTabKeysRef.current.clear(); + setLoadedTabKeys(new Set()); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + try { + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + setSearchValues(values); + + const searchParam = buildSewageExcretaRelatedSearchParam(values); + searchParamRef.current = searchParam; + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } + + const ok = await loadActiveTabData( + opts?.forcedTabKey ?? activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + if (!ok) return; + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + const ctx = lastSearchedValues ?? searchValues; + if (!ctx) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: ctx.bsnmCd ?? "", + ctpvNm: ctx.sido ?? "", + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + if (searchParamRef.current) { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + ctx, + true, + false, + ); + } + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? activeTab; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !activeTabStatsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const lastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!values.bsnmCd) { + alert("사업자를 선택해주세요."); + return; + } + if (!hasSearched || !searchParamRef.current) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildSewageExcretaRelatedSearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; const submitFunc = () => { - // 제출 로직 + void (async () => { + const tabKey = activeTabRef.current; + if (!SUBMIT_SUPPORTED_TAB_KEYS.has(tabKey)) { + alert("해당 탭은 제출 기능이 준비되지 않았습니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitActiveGrid(); + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + if (NOT_APPLICABLE_TAB_KEYS.has(tabKey)) { + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + } else if (currentData.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + if (!SUBMIT_SUPPORTED_TAB_KEYS.has(tabKey)) { + alert("해당 탭은 제출 기능이 준비되지 않았습니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitActiveGrid(); + + const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey); + const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + const mapRows = rowDataMap.get(tabKey) ?? []; + + const searchContextBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + }; + + let submitList: + | PublicInstitutionSewerIndustrySaveItem[] + | ExcretaRelatedIndustrySaveItem[] + | PrivateSewageTreatRelatedIndustrySaveItem[]; + let deleteList: PublicInstitutionSewerIndustrySaveItem[] = []; + let sggList: Sgg[] = []; + + if (isDistrictRowTab) { + const rowsForSubmit = mergeGridRowsWithMapRows(currentData, mapRows, [ + "admdstCd", + "admdstNm", + ]); + if (rowsForSubmit.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + submitList = + tabKey === ExcretaRelatedIndustryInpColumn.tabKey + ? rowsForSubmit.map((row) => buildExcretaRelatedIndustrySaveItem(row)) + : rowsForSubmit.map((row) => + buildPrivateSewageTreatRelatedIndustrySaveItem(row), + ); + } else if (isPublicInstitutionTab) { + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + sggList = list; + + const rowsForSubmit = mergeGridRowsWithMapRows(currentData, mapRows, [ + "ognzSn", + "admdstCd", + "admdstNm", + "__isNewArticle", + ]); + + submitList = noApplicable + ? [] + : rowsForSubmit.map((row) => + buildPublicInstitutionIndustrySaveItem(row, list), + ); + deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => + isPersistedPublicInstitutionArticleRow( + tabKey, + row, + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + ), + ) + .map((row) => buildPublicInstitutionIndustrySaveItem(row, list)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + } else { + alert("해당 탭은 제출 기능이 준비되지 않았습니다."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = resolveStatsCdForTab(tabKey, tabStatsCdMap); + + const submitRequestBase = { + ...searchContextBase, + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isPublicInstitutionTab ? { noApplicable } : {}), + ...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}), + }; + + const verificationQuery = { + sewerBzmnCd: searchContextBase.sewerBzmnCd, + ctpvNm: searchContextBase.ctpvNm, + sggNm: searchContextBase.sggNm, + statsYr: searchContextBase.statsYr, + statsCd: statsCdForTab, + }; + + const reloadAfterSubmit = async () => { + if (isPublicInstitutionTab) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + preYearArticleOgnzSnByTabRef.current.delete(tabKey); + if (sggList.length > 0) { + setSggOptions(sggList); + } + } + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + true, + false, + ); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + const response = + tabKey === ExcretaRelatedIndustryInpColumn.tabKey + ? await statisticsInputSewageExcretaRelatedApi.submitExcretaRelatedIndustry({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: submitList as ExcretaRelatedIndustrySaveItem[], + }) + : tabKey === PrivateSewageTreatRelatedIndustryInpColumn.tabKey + ? await statisticsInputSewageExcretaRelatedApi.submitPrivateSewageTreatRelatedIndustry( + { + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: + submitList as PrivateSewageTreatRelatedIndustrySaveItem[], + }, + ) + : tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey + ? await statisticsInputSewageExcretaRelatedApi.submitPublicInstitutionSewerIndustry( + { + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: submitList as PublicInstitutionSewerIndustrySaveItem[], + deleteList, + }, + ) + : await statisticsInputSewageExcretaRelatedApi.submitPublicInstitutionExcretaIndustry( + { + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: submitList as PublicInstitutionSewerIndustrySaveItem[], + deleteList, + }, + ); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + setShowSubmitDialog(false); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!SAVE_SUPPORTED_TAB_KEYS.has(tabKey)) { + alert("해당 탭은 저장 기능이 준비되지 않았습니다."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitActiveGrid(); + + const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey); + const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + const mapRows = rowDataMap.get(tabKey) ?? []; + + const baseSaveRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(isPublicInstitutionTab && noApplicable + ? { noApplicable: true as const } + : {}), + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const userFields = { + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}), + }; + + let response: { status: string; message?: string }; + let sggList: Sgg[] = []; + + if (isDistrictRowTab) { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "admdstCd", + "admdstNm", + ]); + const saveList = rowsForSave.map((row) => + buildDistrictRowSaveItem(tabKey, row), + ); + if (saveList.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + response = + tabKey === ExcretaRelatedIndustryInpColumn.tabKey + ? await statisticsInputSewageExcretaRelatedApi.saveExcretaRelatedIndustry( + { + ...baseSaveRequest, + ...userFields, + saveList: saveList as ExcretaRelatedIndustrySaveItem[], + }, + ) + : await statisticsInputSewageExcretaRelatedApi.savePrivateSewageTreatRelatedIndustry( + { + ...baseSaveRequest, + ...userFields, + saveList: + saveList as PrivateSewageTreatRelatedIndustrySaveItem[], + }, + ); + } else if (isPublicInstitutionTab) { + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + sggList = list; + + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "ognzSn", + "admdstCd", + "admdstNm", + "__isNewArticle", + ]); + + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildPublicInstitutionIndustrySaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => + isPersistedPublicInstitutionArticleRow( + tabKey, + row, + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + ), + ) + .map((row) => buildPublicInstitutionIndustrySaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey + ? await statisticsInputSewageExcretaRelatedApi.savePublicInstitutionSewerIndustry( + { + ...baseSaveRequest, + ...userFields, + saveList, + deleteList, + }, + ) + : await statisticsInputSewageExcretaRelatedApi.savePublicInstitutionExcretaIndustry( + { + ...baseSaveRequest, + ...userFields, + saveList, + deleteList, + }, + ); + } else { + alert("해당 탭은 저장 기능이 준비되지 않았습니다."); + return; + } + + if (response.status === "success") { + if (isPublicInstitutionTab) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + preYearArticleOgnzSnByTabRef.current.delete(tabKey); + if (sggList.length > 0) { + setSggOptions(sggList); + } + } + + const param = + searchParamRef.current ?? + buildSewageExcretaRelatedSearchParam(lastSearchedValues); + await loadActiveTabData(tabKey, param, lastSearchedValues, true, false); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const addArticleFunc = () => { - // 항목 추가 로직 + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(ctx)) { + alert("시군구를 선택해주세요."); + return; + } + + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 추가할 수 없는 탭입니다."); + return; + } + + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + if (noApplicable) { + alert("해당사항 없음이 체크된 상태에서는 항목을 추가할 수 없습니다."); + return; + } + + let list = sggOptions; + if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { + list = await loadSggOptions(ctx.sido); + sggOptionsSidoRef.current = ctx.sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + const newRow = createEmptyArticleRow(tabKey, ctx.sido, sggNm); + newRow.__isNewArticle = 1; + if (sggNm) { + const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(newRow, list); + newRow.admdstCd = admdstCd; + newRow.admdstNm = admdstNm; + } + + flushSync(() => { + setSggOptions(list); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, [...currentRows, newRow]); + return next; + }); + }); + + commitToolbarSnapshotForLoadedTab( + tabKey, + tabStatusInfoMap.get(tabKey) ?? null, + true, + ctx, + ); + }; + + const getCheckedItemIndexesFromGrid = (rowCount: number): number[] => { + const gridView = getGridRef.current?.gridView as + | { + getCheckedItems?: () => number[]; + getCheckedRows?: () => number[]; + } + | undefined; + + const checked = + (typeof gridView?.getCheckedItems === "function" + ? gridView.getCheckedItems() + : typeof gridView?.getCheckedRows === "function" + ? gridView.getCheckedRows() + : []) ?? []; + + return checked.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount, + ); }; const deleteArticleFunc = () => { - console.log('save'); - // 항목 제외 로직 + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 제외할 수 없는 탭입니다."); + return; + } + + const ctx = lastSearchedValues ?? searchValues; + if (!ctx) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(ctx)) { + alert("시군구를 선택해주세요."); + return; + } + + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const checkedIndexes = getCheckedItemIndexesFromGrid(currentRows.length); + const checkedIndexSet = + checkedIndexes.length > 0 + ? new Set(checkedIndexes) + : new Set( + currentRows + .map((row, idx) => (isCheckedRow(row) ? idx : -1)) + .filter((idx) => idx >= 0), + ); + + const removedRows = currentRows.filter((_, idx) => checkedIndexSet.has(idx)); + const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + const persistedRemovedRows = removedRows.filter((row) => + isPersistedPublicInstitutionArticleRow( + tabKey, + row, + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + ), + ); + if (persistedRemovedRows.length > 0) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, [...(prev.get(tabKey) ?? []), ...persistedRemovedRows]); + return next; + }); + } + + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, nextRows); + return next; + }); + + commitToolbarSnapshotForLoadedTab( + tabKey, + tabStatusInfoMap.get(tabKey) ?? null, + nextRows.length > 0, + ctx, + ); }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of SewageExcretaRelatedInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base.map((col) => { + let next: ColumnConfig = col; + if (col.fieldName === "ctpvNm") { + next = { ...col, editable: false as const, styleName: "rg-cell-readonly" }; + } else if ( + SGG_NM_DROPDOWN_TAB_KEYS.has(tabKey) && + col.fieldName === "sggNm" + ) { + next = buildSggNmColumn(col, canEdit, sggOptions); + } else if (col.fieldName === "sggNm") { + next = { ...col, editable: false as const, styleName: "rg-cell-readonly" }; + } + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...next, tooltip: guide } : next; + }); + map.set(tabKey, { + columns, + columnLayout: tab.columnLayout ?? [], + }); + } + return map; + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab, sggOptions]); + + const publicInstitutionArticleCellStyleCallback = useCallback( + ({ fieldName, row }) => { + if (fieldName !== "sggNm") return undefined; + if (isNewArticleRow(row)) return undefined; + return { styleName: "rg-cell-readonly", editable: false }; + }, + [], + ); + + const resolveGridCellStyleCallback = useCallback( + (tabKey: string): CellStyleCallback => + (params) => { + const verification = verificationCellStyle(params); + if (!SGG_NM_DROPDOWN_TAB_KEYS.has(tabKey)) { + return verification; + } + return mergeCellStyleResults( + verification, + publicInstitutionArticleCellStyleCallback(params), + ); + }, + [publicInstitutionArticleCellStyleCallback], + ); + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab); + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + activeTabHasRowData, + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = activeTabToolbar.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + toolbarGroupAllowed && + (activeTabToolbar.showActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab))); + const showArticleEditButtons = + ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons; + const showNotApplicableForActiveTab = + NOT_APPLICABLE_TAB_KEYS.has(activeTab) && + loadedTabKeys.has(activeTab) && + shouldShowNotApplicable(activeTab, activeTabHasRowData, activeTabStatus); + const showSecondaryToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false; + const setIsNotApplicableForActiveTab: Dispatch> = useCallback( + (next) => { + const tabKey = activeTabRef.current; + const current = isNotApplicableByTab.get(tabKey) ?? false; + const resolved = + typeof next === "function" + ? (next as (prevState: boolean) => boolean)(current) + : next; + setIsNotApplicableForTab(tabKey, resolved); + }, + [isNotApplicableByTab, setIsNotApplicableForTab], + ); + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_RELATED, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); return ( <> - -
-
- - - - -
-
-
-
- - -
-
- + + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForActiveTab && ( + + )} + {showArticleEditButtons && ( + <> + void addArticleFunc()} /> + + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewageExcretaRelatedInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {SewageExcretaRelatedInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + const tabEnterableForGrid = + computeTabEnterable(tabStatusInfo, inputPeriodEnterable) || + isAdminInfo; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const nextRows = + typeof updater === "function" + ? updater(cur) + : updater; + next.set(tabKey, nextRows); + return next; + }); + }} + showHeaderCheckbox={ARTICLE_EDIT_TAB_KEYS.has(tabKey)} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + getGridRef={activeTab === tabKey ? getGridRef : undefined} + cellStyleCallback={resolveGridCellStyleCallback(tabKey)} + cellTooltipCallback={verificationCellTooltip} + /> + + ); + })} + +
+
+ { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> ); }; -export default SewageExcretaRelatedInp; \ No newline at end of file +export default SewageExcretaRelatedInp; diff --git a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx index 6df8ce665..885d00f8b 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx @@ -1,82 +1,2213 @@ -import { useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type Dispatch, + type SetStateAction, +} from "react"; + +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; +import { ValueType } from "realgrid"; +import type { RealGridReact } from "realgrid-react"; +import { flushSync } from "react-dom"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; import AddArticleButton from "@/components/button/AddArticleButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton"; +import LastYearDataButton from "@/components/button/LastYearDataButton"; import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton"; - -import { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn"; -import { DigestorOperatingStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn"; -import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn"; +import SaveButton from "@/components/button/SaveButton"; +import SubmitButton from "@/components/button/SumitButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import SubmitDailog from "./SubmitDailog"; +import type { + CellEditedCallback, + CellStyleCallback, + CellStyleCallbackParams, + CellStyleCallbackResult, + CellTooltipCallbackParams, + ColumnConfig, + ColumnInfo, +} from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { + DigestorOperatingStatusInpColumn, + DIGESTOR_OPERATING_STATUS_EN_YN_FIELD, + isDigestorOperatingRowDisabled, +} from "@/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn"; import { ExcretaSlurgeProcessingStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn"; +import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn"; +import { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn"; +import { doList } from "@/components/util/column/input/DoList"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; +import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD, + SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD, + SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD, + SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD, +} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface"; +import type { + SewageExcretaSlurgeRequestParams, + DigestorOperatingStatusSaveItem, + ExcretaSlurgeProcessingStatusSaveItem, + SewageExcretaSlurgeVerificationResult, + SewageSlurgeProcessingFacStatusSaveItem, + SlurgeProcessingFacStatusSaveItem, +} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { ApiResponse, cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo, Sgg } from "@/entities/statistics/model/statistics-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; +import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAdminStore } from "@/store/admin-store"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +type GridRowWithVerification = Record & { + verificationResult?: SewageExcretaSlurgeVerificationResult; +}; + +function mergeCellStyleResults( + ...results: (CellStyleCallbackResult | undefined)[] +): CellStyleCallbackResult | undefined { + const styleNames: string[] = []; + let editable: boolean | undefined; + let readOnly: boolean | undefined; + + for (const result of results) { + if (!result) continue; + if (typeof result === "string") { + styleNames.push(result); + continue; + } + if (result.styleName) styleNames.push(result.styleName); + if (result.editable === false) editable = false; + if (result.readOnly === true) readOnly = true; + } + + const styleName = styleNames.join(" ").trim(); + if (!styleName && editable === undefined && readOnly === undefined) { + return undefined; + } + return { + ...(styleName ? { styleName } : {}), + ...(editable !== undefined ? { editable } : {}), + ...(readOnly !== undefined ? { readOnly } : {}), + }; +} + +function verificationCellStyle({ + fieldName, + row, +}: CellStyleCallbackParams): CellStyleCallbackResult | undefined { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } +} + +function verificationCellTooltip({ + fieldName, + row, +}: CellTooltipCallbackParams): string | undefined { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln} +(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } +} + +function buildSewageExcretaSlurgeSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewageExcretaSlurgeRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + fcltNm: values.fctName?.trim() || undefined, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function mapListToGridRows( + list: T[] | undefined, + ctpvNm: string, +): GridRowWithVerification[] { + return (list ?? []).map((item) => { + const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification; + if (item.verificationResult) { + row.verificationResult = item.verificationResult; + } + return row; + }); +} + +const SewageExcretaSlurgeInpTabList = [ + SlurgeProcessingFacStatusInpColumn, + DigestorOperatingStatusInpColumn, + SewageSlurgeProcessingFacStatusInpColumn, + ExcretaSlurgeProcessingStatusInpColumn, +] as const; + +const firstTabKey = SlurgeProcessingFacStatusInpColumn.tabKey; + +/** 항목 추가·제외 대상 탭 — ST00110003 (ST00110001·ST00110002·ST00110004 제외) */ +const ARTICLE_EDIT_TAB_KEYS = new Set([ + SewageSlurgeProcessingFacStatusInpColumn.tabKey, +]); + +/** ST00110003 — 시군구(sggNm) 드롭다운 선택 */ +const SGG_NM_DROPDOWN_TAB_KEYS = new Set([ + SewageSlurgeProcessingFacStatusInpColumn.tabKey, +]); + +const EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG = + "06. 공공하수처리시설 데이터가 없으면 전년도 데이터 조회 및 항목 추가가 불가능 합니다."; + +const EMPTY_EXCRETA_TREATMENT_FACILITIES_MSG = + "07. 분뇨처리시설 데이터가 없으면 전년도 데이터 조회가 불가능 합니다."; + +/** ST00110001~ST00110004 — 저장 API 연동 탭 */ +const SAVE_SUPPORTED_TAB_KEYS = new Set([ + SlurgeProcessingFacStatusInpColumn.tabKey, + DigestorOperatingStatusInpColumn.tabKey, + SewageSlurgeProcessingFacStatusInpColumn.tabKey, + ExcretaSlurgeProcessingStatusInpColumn.tabKey, +]); + +function mergeHiddenFieldsForSubmitTab(tabKey: string): string[] { + if ( + tabKey === SlurgeProcessingFacStatusInpColumn.tabKey || + tabKey === DigestorOperatingStatusInpColumn.tabKey + ) { + return ["swtrmtFcltCd", "fcltSn", "admdstCd", "admdstNm"]; + } + if (tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey) { + return ["fcltSn", "admdstCd", "admdstNm"]; + } + if (tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey) { + return ["fcltSn", "admdstCd", "admdstNm", "fcltPstnCn"]; + } + return []; +} + +/** ST00110001~ST00110004 — 제출 API 연동 탭 */ +const SUBMIT_SUPPORTED_TAB_KEYS = new Set([ + SlurgeProcessingFacStatusInpColumn.tabKey, + DigestorOperatingStatusInpColumn.tabKey, + SewageSlurgeProcessingFacStatusInpColumn.tabKey, + ExcretaSlurgeProcessingStatusInpColumn.tabKey, +]); + +const ARTICLE_TAB_BY_KEY = new Map( + SewageExcretaSlurgeInpTabList.map((tab) => [tab.tabKey, tab]), +); + +function createEmptyArticleRow( + tabKey: string, + ctpvNm: string, + sggNm = "", +): Record { + const row: Record = { ctpvNm, sggNm }; + const columns = (ARTICLE_TAB_BY_KEY.get(tabKey)?.columns ?? []) as ColumnConfig[]; + for (const col of columns) { + const fieldName = col.fieldName; + if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} + +type ArticleFcltSnSet = Set; + +function extractFcltSnSet(rows: Record[]): ArticleFcltSnSet { + const set = new Set(); + for (const row of rows) { + const fcltSn = String(row.fcltSn ?? "").trim(); + if (fcltSn) set.add(fcltSn); + } + return set; +} + +function setArticleFcltSnSet( + storeRef: { current: ArticleFcltSnSet }, + rows: Record[], +) { + storeRef.current = extractFcltSnSet(rows); +} + +function isNewArticleRow(row: Record): boolean { + const v = row.__isNewArticle; + return v === 1 || v === "1"; +} + +function clearSlurgeFacStatusArticleFcltSnTracking( + persistedRef: { current: ArticleFcltSnSet }, + preYearRef: { current: ArticleFcltSnSet }, +) { + persistedRef.current = new Set(); + preYearRef.current = new Set(); +} + +/** + * ST00110001 — deleteList 대상 판별. + * fcltSn만으로는 전년도(PRE)·항목추가 행과 구분 불가 → __isNewArticle + preYear ref 사용. + */ +function isPersistedSlurgeFacStatusArticleRow( + row: Record, + persistedRef: { current: ArticleFcltSnSet }, + preYearRef: { current: ArticleFcltSnSet }, +): boolean { + if (isNewArticleRow(row)) return false; + const fcltSn = String(row.fcltSn ?? "").trim(); + if (!fcltSn) return false; + if (preYearRef.current.has(fcltSn)) return false; + return persistedRef.current.has(fcltSn); +} + +/** ST00110003 등 — 전년도 조회 시 fcltSn 미제공, fcltSn 유무로 판단 */ +function isPersistedArticleRowByFcltSn(row: Record): boolean { + if (isNewArticleRow(row)) return false; + return String(row.fcltSn ?? "").trim() !== ""; +} + +const toGridColumnInfo = (tab: (typeof SewageExcretaSlurgeInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function tabRowDataExists( + rowDataMap: Map[]>, + tabKey: string, +): boolean { + const rows = rowDataMap.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = + isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function parseSggListFromResponse(res: ApiResponse): Sgg[] { + if (res.status !== "success" || res.data == null) return []; + return Array.isArray(res.data) ? res.data : []; +} + +function buildSggNmColumn( + col: ColumnConfig, + canEdit: boolean, + sggOptions: Sgg[], +): ColumnConfig { + if (!canEdit) { + return { ...col, editable: false as const, styleName: "rg-cell-readonly" }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true, styleName: undefined }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + styleName: undefined, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +function buildEmptyDataAlertOverrides(tabKey: string): + | { alertIfDigestorEmpty?: boolean; alertIfExcretaSlurgeEmpty?: boolean } + | undefined { + if (tabKey === DigestorOperatingStatusInpColumn.tabKey) { + return { alertIfDigestorEmpty: true }; + } + if (tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey) { + return { alertIfExcretaSlurgeEmpty: true }; + } + return undefined; +} + +function mergeGridRowsWithMapRows( + gridRows: Record[], + mapRows: Record[], + hiddenFields: string[] = [], +): Record[] { + return gridRows.map((gridRow, index) => { + const mapRow = mapRows[index] ?? {}; + const merged: Record = { ...mapRow, ...gridRow }; + if (hiddenFields.length === 0) return merged; + const next = { ...merged }; + for (const field of hiddenFields) { + const fromGrid = String(gridRow[field] ?? "").trim(); + const fromMap = String(mapRow[field] ?? "").trim(); + next[field] = fromGrid || fromMap; + } + return next; + }); +} + +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function resolveSlurgeFacStatusAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const sggNm = String(row.sggNm ?? "").trim(); + if (sggNm) { + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + } + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + if (existingCd || existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; + } + return { admdstCd: "", admdstNm: sggNm }; +} + +/** 그리드 행 → ST00110001 저장/삭제 API 항목 */ +function buildSlurgeProcessingFacStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): SlurgeProcessingFacStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + fcltSn: String(row.fcltSn ?? "").trim(), + itslfQnt: toGridNumber(row.itslfQnt), + otsdInflwQnt: toGridNumber(row.otsdInflwQnt), + transfTmplntNm: String(row.transfTmplntNm ?? "").trim(), + transfPrcsQnt: toGridNumber(row.transfPrcsQnt), + srpFncRt: toGridNumber(row.srpFncRt), + itslfTs1RclmQnt: toGridNumber(row.itslfTs1RclmQnt), + itslfTs1IncnrQnt: toGridNumber(row.itslfTs1IncnrQnt), + itslfTs1AltdQnt: toGridNumber(row.itslfTs1AltdQnt), + itslfTs1DryQnt: toGridNumber(row.itslfTs1DryQnt), + itslfTs1FuelQnt: toGridNumber(row.itslfTs1FuelQnt), + itslfTs2CmpstgQnt: toGridNumber(row.itslfTs2CmpstgQnt), + cmpsPrcsFcltBzentyNm: String(row.cmpsPrcsFcltBzentyNm ?? "").trim(), + incnrPrcsLfvlm: toGridNumber(row.incnrPrcsLfvlm), + incnrPrcsPrdctRmtrlQnt: toGridNumber(row.incnrPrcsPrdctRmtrlQnt), + incnrPrcsFmlyrQnt: toGridNumber(row.incnrPrcsFmlyrQnt), + incnrPrcsFcltNm: String(row.incnrPrcsFcltNm ?? "").trim(), + stbznPrcsLfvlm: toGridNumber(row.stbznPrcsLfvlm), + stbznPrcsFmlyrQnt: toGridNumber(row.stbznPrcsFmlyrQnt), + stbznPrcsFcltNm: String(row.stbznPrcsFcltNm ?? "").trim(), + dryPrcsLfvlm: toGridNumber(row.dryPrcsLfvlm), + dryPrcsIncnrQnt: toGridNumber(row.dryPrcsIncnrQnt), + dryPrcsFuelQnt: toGridNumber(row.dryPrcsFuelQnt), + dryPrcsPrdctRmtrlQnt: toGridNumber(row.dryPrcsPrdctRmtrlQnt), + dryPrcsFmlyrQnt: toGridNumber(row.dryPrcsFmlyrQnt), + dryPrcsCmpstgQnt: toGridNumber(row.dryPrcsCmpstgQnt), + dryPrcsFcltNm: String(row.dryPrcsFcltNm ?? "").trim(), + fuelPrcsIncnrQnt: toGridNumber(row.fuelPrcsIncnrQnt), + fuelPrcsFuelQnt: toGridNumber(row.fuelPrcsFuelQnt), + fuelPrcsFcltNm: String(row.fuelPrcsFcltNm ?? "").trim(), + otsdCnsgnRclmQnt: toGridNumber(row.otsdCnsgnRclmQnt), + otsdCnsgnIncnrQnt: toGridNumber(row.otsdCnsgnIncnrQnt), + otsdCnsgnFuelPwplQnt: toGridNumber(row.otsdCnsgnFuelPwplQnt), + otsdCnsgnPrdctRmtrlQnt: toGridNumber(row.otsdCnsgnPrdctRmtrlQnt), + otsdCnsgnFmlyrQnt: toGridNumber(row.otsdCnsgnFmlyrQnt), + otsdCnsgnCmpstgQnt: toGridNumber(row.otsdCnsgnCmpstgQnt), + otsdCnsgnFcltNm: String(row.otsdCnsgnFcltNm ?? "").trim(), + cparTs1Whol: toGridNumber(row.cparTs1Whol), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** 그리드 행 → ST00110002 저장 API 항목 */ +function buildDigestorOperatingStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): DigestorOperatingStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + const base = { + admdstCd, + admdstNm, + swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + fcltSn: String(row.fcltSn ?? "").trim(), + digstnEnYn: String(row.digstnEnYn ?? "").trim(), + }; + + if (isDigestorOperatingRowDisabled(row)) { + return { + ...base, + digstnEnYn: "N", + digstnCpct: 0, + actlDrvnCpct: 0, + digstnGasInputQnt: 0, + digstnGasInputSldDnst: 0, + digstnTnkOprtngDayCnt: 0, + ocrnGasQnt: 0, + digstnGasUtztnNtslQnt: 0, + digstnGasUtztnEtgtQnt: 0, + digstnGasUtztnItslfQnt: 0, + digstnGasUtztnEtcQnt: 0, + surplsGasQnt: 0, + rmrk: null, + }; + } + + return { + ...base, + digstnCpct: toGridNumber(row.digstnCpct), + actlDrvnCpct: toGridNumber(row.actlDrvnCpct), + digstnGasInputQnt: toGridNumber(row.digstnGasInputQnt), + digstnGasInputSldDnst: toGridNumber(row.digstnGasInputSldDnst), + digstnTnkOprtngDayCnt: toGridNumber(row.digstnTnkOprtngDayCnt), + ocrnGasQnt: toGridNumber(row.ocrnGasQnt), + digstnGasUtztnNtslQnt: toGridNumber(row.digstnGasUtztnNtslQnt), + digstnGasUtztnEtgtQnt: toGridNumber(row.digstnGasUtztnEtgtQnt), + digstnGasUtztnItslfQnt: toGridNumber(row.digstnGasUtztnItslfQnt), + digstnGasUtztnEtcQnt: toGridNumber(row.digstnGasUtztnEtcQnt), + surplsGasQnt: toGridNumber(row.surplsGasQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** 그리드 행 → ST00110003 저장/삭제 API 항목 */ +function buildSewageSlurgeProcessingFacStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): SewageSlurgeProcessingFacStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + prcsMthExpln: String(row.prcsMthExpln ?? "").trim(), + fcltCpct: toGridNumber(row.fcltCpct), + anlPrcsQnt: toGridNumber(row.anlPrcsQnt), + dayAvgOprtngHr: toGridNumber(row.dayAvgOprtngHr), + anlOprtngDayCnt: toGridNumber(row.anlOprtngDayCnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** 그리드 행 → ST00110004 저장 API 항목 */ +function buildExcretaSlurgeProcessingStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): ExcretaSlurgeProcessingStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), + rcycQnt: toGridNumber(row.rcycQnt), + olandLfvlm: toGridNumber(row.olandLfvlm), + incnrQnt: toGridNumber(row.incnrQnt), + oceanInputQnt: toGridNumber(row.oceanInputQnt), + fuelQnt: toGridNumber(row.fuelQnt), + etcQnt: toGridNumber(row.etcQnt), + nondspsQnt: toGridNumber(row.nondspsQnt), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리 const SewageExcretaSlurgeInp = () => { - - // tab list - const SewageExcretaSlurgeInpTabList = [ - SlurgeProcessingFacStatusInpColumn, - DigestorOperatingStatusInpColumn, - SewageSlurgeProcessingFacStatusInpColumn, - ExcretaSlurgeProcessingStatusInpColumn - ]; + const { isAdminInfo } = useAdminStore(); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState< + Map[]> + >(new Map()); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); + const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [sggOptions, setSggOptions] = useState([]); + const [loadedTabKeys, setLoadedTabKeys] = useState>(() => new Set()); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [deleteListByTab, setDeleteListByTab] = useState< + Map[]> + >(() => new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const loadedTabKeysRef = useRef>(new Set()); + const sggOptionsSidoRef = useRef(null); + /** ST00110002 당해 조회 결과 유무(공공하수처리시설 연동) — 전년도 조회 가능 여부 */ + const publicSewageFacilitiesAvailableRef = useRef(false); + /** ST00110004 당해 조회 결과 유무(분뇨처리시설 연동 데이터) — 전년도 조회 가능 여부 */ + const excretaTreatmentFacilitiesAvailableRef = useRef(false); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + /** ST00110001 — 당해 연도 조회 API로 로드된 fcltSn (deleteList 대상) */ + const persistedArticleFcltSnRef = useRef(new Set()); + /** ST00110001 — 전년도(PRE) 조회로 로드된 fcltSn */ + const preYearArticleFcltSnRef = useRef(new Set()); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SewageExcretaSlurgeInpTabList.map(async (tab) => { + const tabKey = tab.tabKey; + try { + const res = await statisticsApi.getColumnGuides(tabKey); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [tabKey, {}] as const; + } + const next: Record = {}; + 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; + }; + }, []); + + const shouldShowNotApplicable = ( + hasRows: boolean, + statusInfo: SewerageStatusInfo | null | undefined, + ): boolean => + hasSearched && + !hasRows && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const setIsNotApplicableForTab = useCallback((tabKey: string, nextValue: boolean) => { + setIsNotApplicableByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, nextValue); + return next; + }); + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + if (!sido || sido === "ALL") { + setSggOptions([]); + return []; + } + try { + const res = await statisticsApi.getSggAllList(sido); + const list = parseSggListFromResponse(res); + setSggOptions(list); + return list; + } catch { + setSggOptions([]); + return []; + } + }, []); + + const markTabLoaded = (tabKey: string) => { + loadedTabKeysRef.current.add(tabKey); + setLoadedTabKeys(new Set(loadedTabKeysRef.current)); }; - const lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); + }; + + const getTabGridRows = (tabKey: string): Record[] => + tabKey === activeTabRef.current + ? (getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []) + : (rowDataMap.get(tabKey) ?? []); + + const commitActiveGrid = () => { + const gridView = getGridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + const prevTabKey = activeTabRef.current; + commitActiveGrid(); + const latestRowsInPrevTab = getTabGridRows(prevTabKey); + if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) { + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(prevTabKey, latestRowsInPrevTab); + return next; + }); + } + + flushSync(() => { + activeTabRef.current = tabKey; + setActiveTab(tabKey); + }); + + if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; + + if (loadedTabKeysRef.current.has(tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + buildEmptyDataAlertOverrides(tabKey), + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const syncSlurgeFacStatusArticleFcltSnTracking = ( + gridRows: Record[], + fromPreYear: boolean, + ) => { + if (fromPreYear) { + setArticleFcltSnSet(preYearArticleFcltSnRef, gridRows); + return; + } + setArticleFcltSnSet(persistedArticleFcltSnRef, gridRows); + }; + + const isPersistedArticleRowForTab = useCallback( + (tabKey: string, row: Record) => { + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + return isPersistedSlurgeFacStatusArticleRow( + row, + persistedArticleFcltSnRef, + preYearArticleFcltSnRef, + ); + } + return isPersistedArticleRowByFcltSn(row); + }, + [], + ); + + const loadActiveTabData = async ( + tabKey: string, + param: SewageExcretaSlurgeRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { + mode?: string; + alertIfDigestorEmpty?: boolean; + alertIfExcretaSlurgeEmpty?: boolean; + }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + const fromPreYear = overrides?.mode === "PRE"; + + const applyNotApplicableForLoadedTab = ( + gridRows: readonly Record[], + ) => { + if (!fromPreYear) { + setIsNotApplicableForTab(tabKey, gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicableForTab(tabKey, false); + } + }; + + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getSlurgeProcessingFacStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, slurgeProcessingFacStatusList } = response.data; + const gridRows = mapListToGridRows(slurgeProcessingFacStatusList, commonInfo.ctpvNm); + syncSlurgeFacStatusArticleFcltSnTracking(gridRows, fromPreYear); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SlurgeProcessingFacStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + applyNotApplicableForLoadedTab(gridRows); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === DigestorOperatingStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getDigestorOperatingStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, digestorOperatingStatusList } = response.data; + const gridRows = mapListToGridRows(digestorOperatingStatusList, commonInfo.ctpvNm); + if (!fromPreYear) { + publicSewageFacilitiesAvailableRef.current = gridRows.length > 0; + if (gridRows.length === 0 && overrides?.alertIfDigestorEmpty) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + } + } + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(DigestorOperatingStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + applyNotApplicableForLoadedTab(gridRows); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getSewageSlurgeProcessingFacStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewageSlurgeProcessingFacStatusList } = + response.data; + const gridRows = mapListToGridRows( + sewageSlurgeProcessingFacStatusList, + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewageSlurgeProcessingFacStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + applyNotApplicableForLoadedTab(gridRows); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getExcretaSlurgeProcessingStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, excretaSlurgeProcessingStatusList } = response.data; + const gridRows = mapListToGridRows( + excretaSlurgeProcessingStatusList, + commonInfo.ctpvNm, + ); + if (!fromPreYear) { + excretaTreatmentFacilitiesAvailableRef.current = gridRows.length > 0; + if (gridRows.length === 0 && overrides?.alertIfExcretaSlurgeEmpty) { + alert(EMPTY_EXCRETA_TREATMENT_FACILITIES_MSG); + } + } + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(ExcretaSlurgeProcessingStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + applyNotApplicableForLoadedTab(gridRows); + markTabLoaded(tabKey); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + setIsNotApplicableByTab(new Map()); + searchDoneRef.current = false; + periodEnterableRef.current = true; + loadedTabKeysRef.current.clear(); + setLoadedTabKeys(new Set()); + setSggOptions([]); + publicSewageFacilitiesAvailableRef.current = false; + excretaTreatmentFacilitiesAvailableRef.current = false; + setDeleteListByTab(new Map()); + clearSlurgeFacStatusArticleFcltSnTracking( + persistedArticleFcltSnRef, + preYearArticleFcltSnRef, + ); + sggOptionsSidoRef.current = null; + }; + + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + } catch { + alert("입력 기간 확인 중 오류가 발생했습니다."); + return; + } + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + loadedTabKeysRef.current.clear(); + setLoadedTabKeys(new Set()); + setDeleteListByTab(new Map()); + clearSlurgeFacStatusArticleFcltSnTracking( + persistedArticleFcltSnRef, + preYearArticleFcltSnRef, + ); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + try { + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildSewageExcretaSlurgeSearchParam(values); + searchParamRef.current = searchParam; + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } + + const ok = await loadActiveTabData( + opts?.forcedTabKey ?? activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + buildEmptyDataAlertOverrides( + opts?.forcedTabKey ?? activeTabRef.current, + ), + ); + if (!ok) return; + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const lastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + if ( + activeTabRef.current === DigestorOperatingStatusInpColumn.tabKey && + !publicSewageFacilitiesAvailableRef.current + ) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + return; + } + if ( + activeTabRef.current === ExcretaSlurgeProcessingStatusInpColumn.tabKey && + !excretaTreatmentFacilitiesAvailableRef.current + ) { + alert(EMPTY_EXCRETA_TREATMENT_FACILITIES_MSG); + return; + } + + try { + setIsLoading(true); + const param = buildSewageExcretaSlurgeSearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; const submitFunc = () => { - // 제출 로직 + void (async () => { + const tabKey = activeTabRef.current; + if (!SUBMIT_SUPPORTED_TAB_KEYS.has(tabKey)) { + alert("해당 탭은 제출 기능이 준비되지 않았습니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitActiveGrid(); + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + if (!SUBMIT_SUPPORTED_TAB_KEYS.has(tabKey)) { + alert("해당 탭은 제출 기능이 준비되지 않았습니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitActiveGrid(); + + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + const mapRows = rowDataMap.get(tabKey) ?? []; + + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const isSlurgeFacTab = tabKey === SlurgeProcessingFacStatusInpColumn.tabKey; + const isDigestorTab = tabKey === DigestorOperatingStatusInpColumn.tabKey; + const isSewageSlurgeFacTab = tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey; + const isExcretaSlurgeTab = tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey; + const rowsForSubmit = mergeGridRowsWithMapRows( + currentData, + mapRows, + mergeHiddenFieldsForSubmitTab(tabKey), + ); + + const slurgeSubmitList = + noApplicable || !isSlurgeFacTab + ? [] + : rowsForSubmit.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list)); + const digestorSubmitList = + noApplicable || !isDigestorTab + ? [] + : rowsForSubmit.map((row) => buildDigestorOperatingStatusSaveItem(row, list)); + const sewageSlurgeFacSubmitList = + noApplicable || !isSewageSlurgeFacTab + ? [] + : rowsForSubmit.map((row) => + buildSewageSlurgeProcessingFacStatusSaveItem(row, list), + ); + const excretaSlurgeSubmitList = + noApplicable || !isExcretaSlurgeTab + ? [] + : rowsForSubmit.map((row) => + buildExcretaSlurgeProcessingStatusSaveItem(row, list), + ); + const slurgeDeleteList = isSlurgeFacTab + ? (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list)) + : []; + const sewageSlurgeFacDeleteList = isSewageSlurgeFacTab + ? (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSewageSlurgeProcessingFacStatusSaveItem(row, list)) + : []; + + if ( + !noApplicable && + ((isSlurgeFacTab && slurgeSubmitList.length === 0) || + (isDigestorTab && digestorSubmitList.length === 0) || + (isSewageSlurgeFacTab && sewageSlurgeFacSubmitList.length === 0) || + (isExcretaSlurgeTab && excretaSlurgeSubmitList.length === 0)) + ) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForSubmit = + tabStatsCdMap.get(tabKey) ?? + (isDigestorTab + ? SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD + : isSewageSlurgeFacTab + ? SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD + : isExcretaSlurgeTab + ? SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD + : SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD); + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForSubmit, + }; + + const reloadAfterSubmit = async () => { + if (isSlurgeFacTab || isSewageSlurgeFacTab) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + if (isSlurgeFacTab) { + preYearArticleFcltSnRef.current = new Set(); + } + } + setSggOptions(list); + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + true, + false, + ); + }; + + const submitRequestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}), + noApplicable, + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = isSlurgeFacTab + ? await statisticsInputSewageExcretaSlurgeApi.submitSlurgeProcessingFacStatus({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: slurgeSubmitList, + deleteList: slurgeDeleteList, + }) + : isDigestorTab + ? await statisticsInputSewageExcretaSlurgeApi.submitDigestorOperatingStatus({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: digestorSubmitList, + }) + : isSewageSlurgeFacTab + ? await statisticsInputSewageExcretaSlurgeApi.submitSewageSlurgeProcessingFacStatus( + { + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: sewageSlurgeFacSubmitList, + deleteList: sewageSlurgeFacDeleteList, + }, + ) + : await statisticsInputSewageExcretaSlurgeApi.submitExcretaSlurgeProcessingStatus( + { + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: excretaSlurgeSubmitList, + }, + ); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForSubmit, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + setShowSubmitDialog(false); + } + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!SAVE_SUPPORTED_TAB_KEYS.has(tabKey)) { + alert("해당 탭은 저장 기능이 준비되지 않았습니다."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitActiveGrid(); + + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + const mapRows = rowDataMap.get(tabKey) ?? []; + + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const baseSaveRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(noApplicable ? { noApplicable: true } : {}), + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const userFields = { + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}), + }; + + let response: { status: string; message?: string }; + + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "swtrmtFcltCd", + "fcltSn", + "admdstCd", + "admdstNm", + ]); + + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildSlurgeProcessingFacStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveSlurgeProcessingFacStatus( + { + ...baseSaveRequest, + ...userFields, + saveList, + deleteList, + }, + ); + } else if (tabKey === DigestorOperatingStatusInpColumn.tabKey) { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "swtrmtFcltCd", + "fcltSn", + "admdstCd", + "admdstNm", + ]); + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildDigestorOperatingStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveDigestorOperatingStatus({ + ...baseSaveRequest, + ...userFields, + saveList, + }); + } else if (tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey) { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "fcltSn", + "admdstCd", + "admdstNm", + ]); + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildSewageSlurgeProcessingFacStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSewageSlurgeProcessingFacStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveSewageSlurgeProcessingFacStatus( + { + ...baseSaveRequest, + ...userFields, + saveList, + deleteList, + }, + ); + } else { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "fcltSn", + "admdstCd", + "admdstNm", + "fcltPstnCn", + ]); + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildExcretaSlurgeProcessingStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveExcretaSlurgeProcessingStatus( + { + ...baseSaveRequest, + ...userFields, + saveList, + }, + ); + } + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + preYearArticleFcltSnRef.current = new Set(); + } + setSggOptions(list); + + const param = + searchParamRef.current ?? + buildSewageExcretaSlurgeSearchParam(lastSearchedValues); + await loadActiveTabData(tabKey, param, lastSearchedValues, true, false); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const addArticleFunc = () => { - // 항목 추가 로직 + const isCheckedRow = (row: Record): boolean => { + const v = row.__checked; + return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true"; + }; + + const getCheckedItemIndexesFromGrid = (rowCount: number): number[] => { + const gridView = getGridRef.current?.gridView as + | { + getCheckedItems?: () => number[]; + getCheckedRows?: () => number[]; + } + | undefined; + + const checked = + (typeof gridView?.getCheckedItems === "function" + ? gridView.getCheckedItems() + : typeof gridView?.getCheckedRows === "function" + ? gridView.getCheckedRows() + : []) ?? []; + + return checked.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount, + ); + }; + + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 추가할 수 없는 탭입니다."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + let list = sggOptions; + if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { + list = await loadSggOptions(ctx.sido); + sggOptionsSidoRef.current = ctx.sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + const newRow = createEmptyArticleRow(tabKey, ctx.sido, sggNm); + if (ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + newRow.__isNewArticle = 1; + } + + flushSync(() => { + setSggOptions(list); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, [...currentRows, newRow]); + return next; + }); + }); + + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx); }; const deleteArticleFunc = () => { - // 항목 제외 로직 + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 제외할 수 없는 탭입니다."); + return; + } + + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const checkedIndexes = getCheckedItemIndexesFromGrid(currentRows.length); + const checkedIndexSet = + checkedIndexes.length > 0 + ? new Set(checkedIndexes) + : new Set( + currentRows + .map((row, idx) => (isCheckedRow(row) ? idx : -1)) + .filter((idx) => idx >= 0), + ); + + const removedRows = currentRows.filter((_, idx) => checkedIndexSet.has(idx)); + const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + const persistedRemovedRows = removedRows.filter((row) => + isPersistedArticleRowForTab(tabKey, row), + ); + if (persistedRemovedRows.length > 0) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, [...(prev.get(tabKey) ?? []), ...persistedRemovedRows]); + return next; + }); + } + + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, nextRows); + return next; + }); + + const ctx = lastSearchedValues ?? searchValues; + if (ctx) { + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + nextRows.length > 0, + ctx, + ); + } }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of SewageExcretaSlurgeInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base.map((col) => { + let next: ColumnConfig = col; + if (col.fieldName === "ctpvNm") { + next = { ...col, editable: false as const, styleName: "rg-cell-readonly" }; + } else if ( + SGG_NM_DROPDOWN_TAB_KEYS.has(tabKey) && + col.fieldName === "sggNm" + ) { + next = buildSggNmColumn(col, canEdit, sggOptions); + } else if (col.fieldName === "sggNm" || col.fieldName === "swtrmtFcltNm") { + next = { ...col, editable: false as const, styleName: "rg-cell-readonly" }; + } + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...next, tooltip: guide } : next; + }); + map.set(tabKey, { + columns, + columnLayout: tab.columnLayout ?? [], + }); + } + return map; + }, [ + tabStatusInfoMap, + inputPeriodEnterable, + isAdminInfo, + sggOptions, + hasSearched, + lastSearchedValues, + searchValues, + columnGuidesByTab, + ]); + + const digestorOperatingCellStyleCallback = useCallback( + ({ row, fieldName, rowIndex, grid }) => { + if (fieldName === DIGESTOR_OPERATING_STATUS_EN_YN_FIELD) { + return undefined; + } + if (!isDigestorOperatingRowDisabled(row, grid, rowIndex)) { + return undefined; + } + return { styleName: "rg-cell-readonly", editable: false }; + }, + [], + ); + + const handleDigestorCellEdited = useCallback( + ({ fieldName }) => { + if (fieldName !== DIGESTOR_OPERATING_STATUS_EN_YN_FIELD) { + return; + } + // 편집 중 setRowDataMap → setRows/refresh와 충돌하지 않도록 편집 완료 후 갱신 + window.setTimeout(() => { + getGridRef.current?.gridView?.refresh?.(); + }, 0); + }, + [], + ); + + const resolveGridCellStyleCallback = useCallback( + (tabKey: string): CellStyleCallback => + (params) => { + const verification = verificationCellStyle(params); + if (tabKey !== DigestorOperatingStatusInpColumn.tabKey) { + return verification; + } + return mergeCellStyleResults( + verification, + digestorOperatingCellStyleCallback(params), + ); + }, + [digestorOperatingCellStyleCallback], + ); + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab); + const activeTabToolbar = computeTabToolbarSnapshot( + activeTabStatus, + activeTabHasRowData, + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + + const { + showSaveInToolbar: baseShowSaveInToolbar, + showEnterableToolbarButtons: baseShowEnterableToolbarButtons, + showActionToolbarCard: baseShowActionToolbarCard, + } = activeTabToolbar; + + const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + baseShowEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + toolbarGroupAllowed && + (baseShowActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab))); + const showArticleEditButtons = + ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons; + const showNotApplicableForActiveTab = + loadedTabKeys.has(activeTab) && + shouldShowNotApplicable(activeTabHasRowData, activeTabStatus); + const showSecondaryToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false; + const setIsNotApplicableForActiveTab: Dispatch> = useCallback( + (next) => { + const tabKey = activeTabRef.current; + const current = isNotApplicableByTab.get(tabKey) ?? false; + const resolved = + typeof next === "function" + ? (next as (prevState: boolean) => boolean)(current) + : next; + setIsNotApplicableForTab(tabKey, resolved); + }, + [isNotApplicableByTab, setIsNotApplicableForTab], + ); + + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const activeTabStatsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !activeTabStatsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_SLURGE, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForActiveTab && ( + + )} + {showArticleEditButtons && ( + <> + + + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewageExcretaSlurgeInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {SewageExcretaSlurgeInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const nextRows = + typeof updater === "function" + ? updater(cur) + : updater; + next.set(tabKey, nextRows); + return next; + }); + }} + showHeaderCheckbox={ARTICLE_EDIT_TAB_KEYS.has(tabKey)} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={tabRowDataExists(rowDataMap, tabKey)} + isExcelExport + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + getGridRef={ + activeTab === tabKey ? getGridRef : undefined + } + cellStyleCallback={resolveGridCellStyleCallback(tabKey)} + cellTooltipCallback={verificationCellTooltip} + onCellEdited={ + tabKey === DigestorOperatingStatusInpColumn.tabKey + ? handleDigestorCellEdited + : undefined + } + /> + + ); + })} +
-
-
- - - -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + + ); }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index d8ffdde0d..a042a51f8 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -69,6 +69,9 @@ import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePi import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn"; import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import { + patchVerificationFailedByTab, +} from "@/components/util/input/verification-failed-flag"; import { useAdminStore } from "@/store/admin-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useTabStore } from "@/store/tab-store"; @@ -360,6 +363,9 @@ const SewagePipeInp = () => { const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); const [statsCd, setStatsCd] = useState(''); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [periodCheckList, setPeriodCheckList] = useState([]); /** 탭키별 `engColNm -> columnGuide` 캐시 */ @@ -396,6 +402,8 @@ const SewagePipeInp = () => { values: SearchFormValues; tabKey: string; } | null>(null); + /** ST00020002 무데이터 안내/자동이동은 동일 조회조건에서 1회만 수행 */ + const factInstallNoDataHandledKeyRef = useRef(null); const [verificationSearchDefaults, setVerificationSearchDefaults] = useState(null); @@ -468,6 +476,7 @@ const SewagePipeInp = () => { setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setLastSearchedYear(null); setLastSearchedValues(null); setHasSearched(false); @@ -549,6 +558,12 @@ const SewagePipeInp = () => { mergeIntoExistingMap: boolean, updateLastSearchedValues: boolean, ): Promise => { + const searchConditionKey = JSON.stringify({ + statsYr: searchParam.statsYr ?? "", + ctpvNm: searchParam.ctpvNm ?? "", + sggNm: searchParam.sggNm ?? "", + sewerBzmnCd: searchParam.sewerBzmnCd ?? "", + }); const applyRowMap = (apply: (m: Map[]>) => void) => { if (mergeIntoExistingMap) { setRowDataMap((prev) => { @@ -574,6 +589,9 @@ const SewagePipeInp = () => { applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData))); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); commitToolbarSnapshotForLoadedTab( @@ -595,9 +613,29 @@ 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)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); commitToolbarSnapshotForLoadedTab( @@ -621,6 +659,9 @@ const SewagePipeInp = () => { applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData))); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); commitToolbarSnapshotForLoadedTab( @@ -645,6 +686,9 @@ const SewagePipeInp = () => { applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData))); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); commitToolbarSnapshotForLoadedTab( @@ -668,6 +712,9 @@ const SewagePipeInp = () => { applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData))); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); commitToolbarSnapshotForLoadedTab( @@ -698,6 +745,9 @@ const SewagePipeInp = () => { }); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); if (updateLastSearchedValues) setLastSearchedValues(values); commitToolbarSnapshotForLoadedTab( @@ -749,6 +799,7 @@ const SewagePipeInp = () => { setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setStatsCd(""); setToolbarSnapshotByTab(new Map()); } @@ -795,7 +846,39 @@ const SewagePipeInp = () => { }, [searchFormRemountKey]); + const getCurrentRowsForTab = (tabKey: string): Record[] => { + 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 +1818,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`; @@ -1839,6 +1922,9 @@ const SewagePipeInp = () => {
{showEnterableToolbarButtons && ( <> @@ -1978,7 +2064,7 @@ const SewagePipeInp = () => { return ( { } : 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); + }} /> tab.tabKey).filter(Boolean), + ), +); + const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({ columns: tab.columns ?? [], columnLayout: tab.columnLayout ?? [], @@ -285,6 +292,9 @@ const SewagePipeMaintainInp = () => { Map >(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< Map >(new Map()); @@ -302,6 +312,9 @@ const SewagePipeMaintainInp = () => { const [rowDataMap, setRowDataMap] = useState< Map[]> >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); const searchDoneRef = useRef(false); const periodEnterableRef = useRef(true); @@ -328,6 +341,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 = {}; + 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, @@ -401,6 +445,9 @@ const SewagePipeMaintainInp = () => { }); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); if (updateLastSearchedValues) { setLastSearchedValues(searchContext); } @@ -434,6 +481,9 @@ const SewagePipeMaintainInp = () => { }); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); if (updateLastSearchedValues) { setLastSearchedValues(searchContext); } @@ -572,7 +622,7 @@ const SewagePipeMaintainInp = () => { tabKey, searchParamRef.current!, lastSearchedValues, - false, + true, false, ); }; @@ -725,7 +775,7 @@ const SewagePipeMaintainInp = () => { RepairStatusInpColumn.tabKey, searchParamRef.current, lastSearchedValues, - false, + true, false, ); setGlobalProgress(100); @@ -762,7 +812,7 @@ const SewagePipeMaintainInp = () => { PipeDredgingStatusInpColumn.tabKey, searchParamRef.current, lastSearchedValues, - false, + true, false, ); setGlobalProgress(100); @@ -811,7 +861,7 @@ const SewagePipeMaintainInp = () => { tabKey, searchParamRef.current, lastSearchedValues, - false, + true, false, ); alert("제출 취소되었습니다."); @@ -828,6 +878,7 @@ const SewagePipeMaintainInp = () => { setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setToolbarSnapshotByTab(new Map()); setActiveTab(firstTabKey); setLastSearchedValues(null); @@ -872,6 +923,7 @@ const SewagePipeMaintainInp = () => { setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setToolbarSnapshotByTab(new Map()); activeTabRef.current = firstTabKey; setActiveTab(firstTabKey); @@ -912,6 +964,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 +1003,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; @@ -1059,6 +1121,9 @@ const SewagePipeMaintainInp = () => {
{showEnterableToolbarButtons && ( <> @@ -1110,11 +1175,7 @@ const SewagePipeMaintainInp = () => { return ( { 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, + ); + }} /> { 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, + ); + }} /> ); diff --git a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx index d95cb428f..69e9d946b 100644 --- a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx +++ b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx @@ -1,13 +1,17 @@ import { useCallback, + useEffect, useMemo, useRef, useState, - type RefObject, + type Dispatch, + type SetStateAction, } from "react"; import { flushSync } from "react-dom"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + import { ValueType } from "realgrid"; import type { RealGridReact } from "realgrid-react"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; @@ -20,12 +24,14 @@ import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButt import SaveButton from "@/components/button/SaveButton"; import SubmitButton from "@/components/button/SumitButton"; import VerificationResultButton from "@/components/button/VerificationResultButton"; -import DataGrid, { applyDropDownColumnToGrid } from "@/components/module/DataGrid"; +import DataGrid from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import SubmitDailog from "./SubmitDailog"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { - CellEditedCallbackParams, CellStyleCallbackParams, CellTooltipCallbackParams, ColumnConfig, @@ -36,24 +42,41 @@ import { doList } from "@/components/util/column/input/DoList"; import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn"; import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; -import { formatCompactYmdForGrid, normalizeOxYnValue } from "@/components/util/grid/ymdFormat"; +import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; +import { + formatCompactYmdForGrid, + normalizeOxYnValue, + toCompactYmdOrNull, +} from "@/components/util/grid/ymdFormat"; import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import { SEWAGE_RESERVOIR_STATS_CD, type SewageReservoirItem, type SewageReservoirRequestParams, + type SewageReservoirSaveItem, type RainfallPumpStationItem, + type RainfallPumpStationSaveItem, type SewageReservoirVerificationResult, } from "@/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import type { ApiResponse, + cancelSubmitRequest, + ImrStatusResponse, Sgg, SewerageStatusInfo, } from "@/entities/statistics/model/statistics-interface"; -import { useAlertStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; +import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { useAdminStore } from "@/store/admin-store"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + const SewageReservoirPumpInpTabList = [ SewageReservoirInpColumn, RainfallPumpStationInpColumn, @@ -68,6 +91,12 @@ const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? SEWAGE_RESERVOIR const rainfallPumpTabKey = RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp"; +const SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS = Array.from( + new Set( + SewageReservoirPumpInpTabList.map((tab) => tab.tabKey).filter(Boolean), + ), +); + type GridRowWithVerification = Record & { verificationResult?: SewageReservoirVerificationResult; }; @@ -144,7 +173,7 @@ function buildSewageReservoirSearchParam( }; } -/** 항목 추가 시 빈 행 (시도는 조회 시 선택한 시도명) */ +/** 항목 추가 시 빈 행 (`ctpvNm` = 조회 시도명) */ function createEmptySewageReservoirRow( ctpvNm: string, sggNm = "", @@ -173,20 +202,6 @@ function createEmptyRainfallPumpStationRow( return row; } -function applySggNmDropDownToReservoirGrid( - gridRef: RefObject, - labels: string[], -) { - if (labels.length === 0) return; - applyDropDownColumnToGrid( - gridRef.current?.gridView as { - setColumnProperty?: (col: string, key: string, value: unknown) => void; - }, - "sggNm", - labels, - ); -} - function parseSggListFromResponse(res: ApiResponse): Sgg[] { if (res.status !== "success" || res.data == null) return []; return Array.isArray(res.data) ? res.data : []; @@ -198,11 +213,7 @@ function buildSggNmColumn( sggOptions: Sgg[], ): ColumnConfig { if (!canEdit) { - return { - ...col, - editable: false as const, - styleName: col.styleName ?? "rg-cell-readonly", - }; + return { ...col, editable: false as const }; } if (sggOptions.length === 0) { return { ...col, editable: true }; @@ -248,6 +259,146 @@ function mapSewageReservoirListToGridRows( }); } +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +/** 펌프 구경(mm) — 콤마 제거 후 숫자 변환, 소수점 버림 */ +function toPumpDmtrExpln(value: string | number | null | undefined): number { + const raw = String(value ?? "").trim(); + if (raw === "") return 0; + const normalized = raw.replace(/,/g, ""); + const num = Number(normalized); + if (!Number.isFinite(num)) return 0; + return Math.floor(num); +} + +/** 시군구명(`sggNm`) 기준 `admdstCd`(bsnmCd)·`admdstNm`(시군구명) */ +function resolveSewageReservoirAdmdst( + sggNm: string, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const trimmed = sggNm.trim(); + const matched = sggOptions.find((s) => s.gugun === trimmed); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + return { admdstCd: "", admdstNm: trimmed }; +} + +/** 그리드 행 → ST00040001 저장/삭제 API 항목 */ +function buildSewageReservoirSaveItem( + row: Record, + sggOptions: Sgg[] = [], +): SewageReservoirSaveItem { + const { admdstCd, admdstNm } = resolveSewageReservoirAdmdst( + String(row.sggNm ?? ""), + sggOptions, + ); + const fcltSn = String(row.fcltSn ?? "").trim(); + return { + admdstCd, + admdstNm, + // 항목 추가 직후 저장(신규) — 그리드 fcltSn 없음 → API에는 "1" + fcltSn: fcltSn, + fcltNm: String(row.fcltNm ?? ""), + fcltPstnCn: String(row.fcltPstnCn ?? ""), + oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), + planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt), + fldngPrvtnYn: normalizeOxYnValue(row.fldngPrvtnYn), + wqPlltnMtgtYn: normalizeOxYnValue(row.wqPlltnMtgtYn), + rtlztnUsgYn: normalizeOxYnValue(row.rtlztnUsgYn), + fcltStrctCn: nullableTrimmedString(row.fcltStrctCn), + wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), + impdFcltCpct: toGridNumber(row.impdFcltCpct), + grssPpqty: toGridNumber(row.grssPpqty), + pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln), + pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty), + pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt), + pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt), + pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln), + pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty), + pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt), + pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt), + pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln), + pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), + pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), + pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), + prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), + ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), + oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), + mltvMthCn: nullableTrimmedString(row.mltvMthCn), + impdMthCn: nullableTrimmedString(row.impdMthCn), + impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn), + dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""), + prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt), + ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt), + prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""), + ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""), + oprtngDayCnt: toGridNumber(row.oprtngDayCnt), + grssMltvQnt: toGridNumber(row.grssMltvQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** 그리드 행 → ST00040002 저장/삭제 API 항목 */ +function buildRainfallPumpStationSaveItem( + row: Record, + sggOptions: Sgg[] = [], +): RainfallPumpStationSaveItem { + const { admdstCd, admdstNm } = resolveSewageReservoirAdmdst( + String(row.sggNm ?? ""), + sggOptions, + ); + const fcltSn = String(row.fcltSn ?? "").trim(); + return { + admdstCd, + admdstNm, + fcltSn: fcltSn, + fcltNm: String(row.fcltNm ?? ""), + fcltPstnCn: String(row.fcltPstnCn ?? ""), + fcltyKndCn: String(row.fcltyKndCn ?? ""), + oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), + planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt), + wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), + rsvrYn: String(row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : ""), + rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt), + abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt), + grssPpqty: toGridNumber(row.grssPpqty), + pumpFcltN1DmtrExpln: toPumpDmtrExpln(row.pumpFcltN1DmtrExpln), + pumpFcltN1PerPpqty: toGridNumber(row.pumpFcltN1PerPpqty), + pumpFcltN1PumpCnt: toGridNumber(row.pumpFcltN1PumpCnt), + pumpFcltN1RsrvPumpCnt: toGridNumber(row.pumpFcltN1RsrvPumpCnt), + pumpFcltN2DmtrExpln: toPumpDmtrExpln(row.pumpFcltN2DmtrExpln), + pumpFcltN2PerPpqty: toGridNumber(row.pumpFcltN2PerPpqty), + pumpFcltN2PumpCnt: toGridNumber(row.pumpFcltN2PumpCnt), + pumpFcltN2RsrvPumpCnt: toGridNumber(row.pumpFcltN2RsrvPumpCnt), + pumpFcltN3DmtrExpln: toPumpDmtrExpln(row.pumpFcltN3DmtrExpln), + pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), + pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), + pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), + prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), + ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), + oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), + dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""), + prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt), + ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt), + prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""), + ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""), + oprtngDayCnt: toGridNumber(row.oprtngDayCnt), + grssMltvQnt: toGridNumber(row.grssMltvQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + function mapRainfallPumpStationListToGridRows( list: RainfallPumpStationItem[], ctpvNm: string, @@ -272,34 +423,498 @@ function mapRainfallPumpStationListToGridRows( }); } +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + // 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장 const SewageReservoirPumpInp = () => { const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); const { isAdminInfo } = useAdminStore(); const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); const submitFunc = () => { - console.log("submit"); - }; - - const saveFunc = () => { - console.log("save"); + void (async () => { + const tabKey = activeTabRef.current; + if ( + tabKey !== SEWAGE_RESERVOIR_STATS_CD && + tabKey !== rainfallPumpTabKey + ) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getCurrentRowsForTab(tabKey); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); }; const cnacelSubmitFunc = () => { - console.log("cancel submit"); + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + const userInfo = await getUserInfo(); + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData(tabKey, lastSearchedValues, true, false); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const isCheckedRow = (row: Record): boolean => { + const v = row.__checked; + return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true"; + }; + + const getCheckedItemIndexesFromGrid = ( + tabKey: string, + rowCount: number, + ): number[] => { + const gridRef = + tabKey === SEWAGE_RESERVOIR_STATS_CD + ? reservoirGridRef + : tabKey === rainfallPumpTabKey + ? rainfallPumpGridRef + : null; + if (!gridRef?.current?.gridView) return []; + + const gridView = gridRef.current.gridView as { + getCheckedItems?: () => number[]; + getCheckedRows?: () => number[]; + }; + + const checked = + (typeof gridView.getCheckedItems === "function" + ? gridView.getCheckedItems() + : typeof gridView.getCheckedRows === "function" + ? gridView.getCheckedRows() + : []) ?? []; + + return checked.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount, + ); + }; + + const getCurrentRowsForTab = ( + tabKey: string, + ): Record[] => { + if (tabKey === SEWAGE_RESERVOIR_STATS_CD) { + return reservoirGridGetRowsRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + } + if (tabKey === rainfallPumpTabKey) { + return rainfallPumpGridGetRowsRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + } + return rowDataMap.get(tabKey) ?? []; + }; + + const commitGridForTab = (tabKey: string) => { + const gridRef = + tabKey === SEWAGE_RESERVOIR_STATS_CD + ? reservoirGridRef + : tabKey === rainfallPumpTabKey + ? rainfallPumpGridRef + : null; + const gridView = gridRef?.current?.gridView as + | { + commit?: (force?: boolean) => void; + cancel?: () => void; + } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } }; const deleteArticleFunc = () => { - console.log("delete article"); + const tabKey = activeTabRef.current; + const currentRows = getCurrentRowsForTab(tabKey); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const checkedIndexes = getCheckedItemIndexesFromGrid( + tabKey, + currentRows.length, + ); + const checkedIndexSet = + checkedIndexes.length > 0 + ? new Set(checkedIndexes) + : new Set( + currentRows + .map((row, idx) => (isCheckedRow(row) ? idx : -1)) + .filter((idx) => idx >= 0), + ); + + const removedRows = currentRows.filter((_, idx) => + checkedIndexSet.has(idx), + ); + const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + if (tabKey === SEWAGE_RESERVOIR_STATS_CD) { + setSewageReservoirDeleteList((prev) => [...prev, ...removedRows]); + } else if (tabKey === rainfallPumpTabKey) { + setRainfallPumpStationDeleteList((prev) => [...prev, ...removedRows]); + } + + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, nextRows); + return next; + }); }; + const saveFunc = () => { + void (async () => { + const tabKey = activeTabRef.current; + if ( + tabKey !== SEWAGE_RESERVOIR_STATS_CD && + tabKey !== rainfallPumpTabKey + ) { + alert("저장할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitGridForTab(tabKey); + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getCurrentRowsForTab(tabKey); + const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; + const saveList = noApplicable + ? [] + : isReservoirTab + ? currentData.map((row) => + buildSewageReservoirSaveItem(row, sggOptions), + ) + : currentData.map((row) => + buildRainfallPumpStationSaveItem(row, sggOptions), + ); + const deleteList = isReservoirTab + ? sewageReservoirDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildSewageReservoirSaveItem(row, sggOptions)) + : rainfallPumpStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildRainfallPumpStationSaveItem(row, sggOptions)); + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const requestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + ...(noApplicable ? { noApplicable: true } : {}), + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = isReservoirTab + ? await statisticsInputSewageReservoirPumpApi.saveSewageReservoir({ + ...requestBase, + userId: userInfo.userUnqId, + saveList: saveList as SewageReservoirSaveItem[], + deleteList: deleteList as SewageReservoirSaveItem[], + }) + : await statisticsInputSewageReservoirPumpApi.saveRainfallPumpStation({ + ...requestBase, + userId: userInfo.userUnqId, + saveList: saveList as RainfallPumpStationSaveItem[], + deleteList: deleteList as RainfallPumpStationSaveItem[], + }); + + if (response.status === "success") { + if (isReservoirTab) { + setSewageReservoirDeleteList([]); + } else { + setRainfallPumpStationDeleteList([]); + } + await loadActiveTabData(tabKey, lastSearchedValues, true, false); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + if ( + tabKey !== SEWAGE_RESERVOIR_STATS_CD && + tabKey !== rainfallPumpTabKey + ) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitGridForTab(tabKey); + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getCurrentRowsForTab(tabKey); + const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; + const submitList = noApplicable + ? [] + : isReservoirTab + ? currentData.map((row) => buildSewageReservoirSaveItem(row, sggOptions)) + : currentData.map((row) => buildRainfallPumpStationSaveItem(row, sggOptions)); + const deleteList = isReservoirTab + ? sewageReservoirDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildSewageReservoirSaveItem(row, sggOptions)) + : rainfallPumpStationDeleteList + .filter((row) => String(row.fcltSn ?? "").trim() !== "") + .map((row) => buildRainfallPumpStationSaveItem(row, sggOptions)); + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + if (isReservoirTab) { + setSewageReservoirDeleteList([]); + } else { + setRainfallPumpStationDeleteList([]); + } + await loadActiveTabData(tabKey, lastSearchedValues, true, false); + }; + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const submitRequestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(noApplicable ? { noApplicable: true } : {}), + laePicNm: submitInfo.laePicNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laePicTelNo: submitInfo.laePicTelNo, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = isReservoirTab + ? await statisticsInputSewageReservoirPumpApi.submitSewageReservoir({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: submitList as SewageReservoirSaveItem[], + deleteList: deleteList as SewageReservoirSaveItem[], + }) + : await statisticsInputSewageReservoirPumpApi.submitRainfallPumpStation({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: submitList as RainfallPumpStationSaveItem[], + deleteList: deleteList as RainfallPumpStationSaveItem[], + }); + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + }; + + const [sewageReservoirDeleteList, setSewageReservoirDeleteList] = useState< + Record[] + >([]); + const [rainfallPumpStationDeleteList, setRainfallPumpStationDeleteList] = + useState[]>([]); const [activeTab, setActiveTab] = useState(firstTabKey); const [rowDataMap, setRowDataMap] = useState< Map[]> >(new Map()); - const [isNotApplicable, setIsNotApplicable] = useState(false); - const [showIsNotApplicable, setShowIsNotApplicable] = useState(false); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); const [searchValues, setSearchValues] = useState(null); const [lastSearchedValues, setLastSearchedValues] = useState( null, @@ -314,16 +929,82 @@ const SewageReservoirPumpInp = () => { Map >(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = + useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); const [sggOptions, setSggOptions] = useState([]); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); const reservoirGridRef = useRef(null); + const rainfallPumpGridRef = useRef(null); + const reservoirGridGetRowsRef = + useRef<(() => Record[]) | null>(null); + const rainfallPumpGridGetRowsRef = + useRef<(() => Record[]) | null>(null); const sggOptionsSidoRef = useRef(null); + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWAGE_RESERVOIR_PUMP_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 = {}; + 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; + }; + }, []); + const searchDoneRef = useRef(false); const periodEnterableRef = useRef(true); const activeTabRef = useRef(activeTab); + const loadedTabKeysRef = useRef>(new Set()); activeTabRef.current = activeTab; + const shouldShowNotApplicable = ( + tabKey: string, + hasRows: boolean, + statusInfo: SewerageStatusInfo | null | undefined, + ): boolean => + (tabKey === SEWAGE_RESERVOIR_STATS_CD || tabKey === rainfallPumpTabKey) && + hasSearched && + !hasRows && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const setIsNotApplicableForTab = useCallback( + (tabKey: string, nextValue: boolean) => { + setIsNotApplicableByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, nextValue); + return next; + }); + }, + [], + ); + const commitToolbarSnapshotForLoadedTab = ( tabKey: string, statusInfo: SewerageStatusInfo | null, @@ -371,8 +1052,7 @@ const SewageReservoirPumpInp = () => { updateLastSearchedValues: boolean, overrides?: { mode?: string }, ): Promise => { - // 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화 - setIsNotApplicable(false); + const fromPreYear = overrides?.mode === "PRE"; if (tabKey === SEWAGE_RESERVOIR_STATS_CD) { const response = await statisticsInputSewageReservoirPumpApi.getSewageReservoirList( @@ -386,20 +1066,20 @@ const SewageReservoirPumpInp = () => { const { commonInfo, statusInfo, sewageReservoirList } = response.data; const list = sewageReservoirList ?? []; + setSewageReservoirDeleteList([]); const gridRows = mapSewageReservoirListToGridRows( list, commonInfo.ctpvNm, ); - if (activeTabRef.current === tabKey) { - setShowIsNotApplicable(list.length === 0); - } - applyRowMap(mergeIntoExistingMap, (m) => { m.set(tabKey, gridRows); }); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); if (updateLastSearchedValues) { setLastSearchedValues(values); } @@ -409,6 +1089,12 @@ const SewageReservoirPumpInp = () => { gridRows.length > 0, values, ); + if (!fromPreYear) { + setIsNotApplicableForTab(tabKey, gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicableForTab(tabKey, false); + } + loadedTabKeysRef.current.add(tabKey); return true; } @@ -425,6 +1111,7 @@ const SewageReservoirPumpInp = () => { const { commonInfo, statusInfo, rainfallPumpStationList } = response.data; const list = rainfallPumpStationList ?? []; + setRainfallPumpStationDeleteList([]); const gridRows = mapRainfallPumpStationListToGridRows( list, commonInfo.ctpvNm, @@ -435,6 +1122,9 @@ const SewageReservoirPumpInp = () => { }); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); if (updateLastSearchedValues) { setLastSearchedValues(values); } @@ -444,6 +1134,12 @@ const SewageReservoirPumpInp = () => { gridRows.length > 0, values, ); + if (!fromPreYear) { + setIsNotApplicableForTab(tabKey, gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicableForTab(tabKey, false); + } + loadedTabKeysRef.current.add(tabKey); return true; } @@ -490,28 +1186,53 @@ const SewageReservoirPumpInp = () => { }, []); const handleReset = () => { + setSewageReservoirDeleteList([]); + setRainfallPumpStationDeleteList([]); setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setToolbarSnapshotByTab(new Map()); setLastSearchedValues(null); setSggOptions([]); sggOptionsSidoRef.current = null; setHasSearched(false); setInputPeriodEnterable(true); - setIsNotApplicable(false); - setShowIsNotApplicable(false); + setIsNotApplicableByTab(new Map()); searchDoneRef.current = false; periodEnterableRef.current = true; + loadedTabKeysRef.current.clear(); setActiveTab(firstTabKey); }; - const handleSearch = async (values: SearchFormValues) => { + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { if (!values.sido || values.sido === "ALL") { alert("시도를 선택해주세요."); return; } + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) + ? periodResponse.data + : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + } catch { + alert("입력 기간 확인 중 오류가 발생했습니다."); + return; + } + const yearChangedFromLastSearch = lastSearchedValues != null && (lastSearchedValues.year ?? "") !== (values.year ?? ""); @@ -520,7 +1241,9 @@ const SewageReservoirPumpInp = () => { setRowDataMap(new Map()); setTabStatusInfoMap(new Map()); setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); setToolbarSnapshotByTab(new Map()); + loadedTabKeysRef.current.clear(); setActiveTab(firstTabKey); } @@ -529,9 +1252,20 @@ const SewageReservoirPumpInp = () => { searchDoneRef.current = true; setHasSearched(true); setLastSearchedValues(values); + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } - const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current; - if (yearChangedFromLastSearch) { + const tabKeyToLoad = + opts?.forcedTabKey ?? + (yearChangedFromLastSearch ? firstTabKey : activeTabRef.current); + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } else if (yearChangedFromLastSearch) { activeTabRef.current = firstTabKey; setActiveTab(firstTabKey); } @@ -551,15 +1285,21 @@ const SewageReservoirPumpInp = () => { }; const handleActiveTabChange = async (tabKey: string) => { - setActiveTab(tabKey); - if (tabKey === SEWAGE_RESERVOIR_STATS_CD) { - setShowIsNotApplicable( - hasSearched && !tabRowDataExists(rowDataMap, tabKey), - ); - } else { - setShowIsNotApplicable(false); + const prevTabKey = activeTabRef.current; + commitGridForTab(prevTabKey); + 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); if (!hasSearched || !lastSearchedValues) return; + if (loadedTabKeysRef.current.has(tabKey)) return; if (tabRowDataExists(rowDataMap, tabKey)) return; try { @@ -596,12 +1336,33 @@ const SewageReservoirPumpInp = () => { baseShowEnterableToolbarButtons && toolbarGroupAllowed; const showActionToolbarCard = baseShowActionToolbarCard && toolbarGroupAllowed; - // 항목추가/제거 버튼은 "데이터가 있든 없든", "과거 데이터(입력 불가 상태)여도" - // 조회를 하고나면 항상 보여야 합니다. const isReservoirOrRainfallTab = activeTab === SEWAGE_RESERVOIR_STATS_CD || activeTab === rainfallPumpTabKey; const showArticleToolbarCard = - hasSearched && isReservoirOrRainfallTab && toolbarGroupAllowed; + hasSearched && + isReservoirOrRainfallTab && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false; + const showNotApplicableForActiveTab = + loadedTabKeysRef.current.has(activeTab) && + shouldShowNotApplicable( + activeTab, + tabRowDataExists(rowDataMap, activeTab), + tabStatusInfoMap.get(activeTab), + ); + const setIsNotApplicableForActiveTab: Dispatch> = useCallback( + (next) => { + const tabKey = activeTabRef.current; + const current = isNotApplicableByTab.get(tabKey) ?? false; + const resolved = + typeof next === "function" + ? (next as (prevState: boolean) => boolean)(current) + : next; + setIsNotApplicableForTab(tabKey, resolved); + }, + [isNotApplicableByTab, setIsNotApplicableForTab], + ); const openVerificationHistoryDialog = () => { const ctx = lastSearchedValues ?? searchValues; @@ -631,20 +1392,6 @@ const SewageReservoirPumpInp = () => { const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; - if (isRainfallPumpTab) { - const newRow = createEmptyRainfallPumpStationRow(ctx.sido, sggNm); - setRowDataMap((prev) => { - const next = new Map(prev); - const current = next.get(rainfallPumpTabKey) ?? []; - next.set(rainfallPumpTabKey, [...current, newRow]); - return next; - }); - - const statusInfo = tabStatusInfoMap.get(rainfallPumpTabKey) ?? null; - commitToolbarSnapshotForLoadedTab(rainfallPumpTabKey, statusInfo, true, ctx); - return; - } - let list = sggOptions; if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { list = await loadSggOptions(ctx.sido); @@ -655,26 +1402,23 @@ const SewageReservoirPumpInp = () => { return; } - const tabKey = SEWAGE_RESERVOIR_STATS_CD; - const labels = list.map((s) => s.gugun).filter(Boolean); - const newRow = createEmptySewageReservoirRow(ctx.sido, sggNm); + const tabKey = activeTabKey; + const newRow = isReservoirTab + ? createEmptySewageReservoirRow(ctx.sido, sggNm) + : createEmptyRainfallPumpStationRow(ctx.sido, sggNm); + + commitGridForTab(tabKey); + const currentRows = getCurrentRowsForTab(tabKey); flushSync(() => { setSggOptions(list); setRowDataMap((prev) => { const next = new Map(prev); - const current = next.get(tabKey) ?? []; - next.set(tabKey, [...current, newRow]); + next.set(tabKey, [...currentRows, newRow]); return next; }); }); - // 드롭다운 컬럼은 columnInfoByTab(buildSggNmColumn) 렌더링으로 반영한다. - // 행 추가 직후 setColumnProperty를 강제 재적용하면 타이밍 이슈로 - // `fieldIndex is out of bounds`가 발생할 수 있어 제거한다. - - setShowIsNotApplicable(false); - const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx); }; @@ -689,17 +1433,29 @@ const SewageReservoirPumpInp = () => { inputPeriodEnterable, ) || isAdminInfo; const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; - const columns = (tab.columns ?? []).map((col: ColumnConfig) => { - if (isReservoirTab && col.fieldName === "sggNm") { - return buildSggNmColumn(col, canEdit, sggOptions); + const isRainfallPumpTab = tabKey === rainfallPumpTabKey; + const rawColumns = tab.columns ?? []; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base.map((col: ColumnConfig) => { + if ( + (isReservoirTab || isRainfallPumpTab) && + col.fieldName === "ctpvNm" + ) { + return { ...col, editable: false as const }; } - return canEdit - ? col - : { - ...col, - editable: false as const, - styleName: col.styleName ?? "rg-cell-readonly", - }; + if ( + (isReservoirTab || isRainfallPumpTab) && + col.fieldName === "sggNm" && + canEdit + ) { + return buildSggNmColumn(col, true, sggOptions); + } + return col; + }).map((col) => { + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; }); map.set(tabKey, { columns, @@ -707,32 +1463,7 @@ const SewageReservoirPumpInp = () => { }); } return map; - }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions]); - - const handleSewageReservoirCellEdited = useCallback( - (params: CellEditedCallbackParams) => { - if (params.fieldName !== "sggNm") return; - const selected = sggOptions.find((s) => s.gugun === params.newValue); - if (!selected) return; - - const tabKey = SEWAGE_RESERVOIR_STATS_CD; - setRowDataMap((prev) => { - const next = new Map(prev); - const rows = [...(next.get(tabKey) ?? [])]; - const row = rows[params.itemIndex]; - if (!row) return prev; - rows[params.itemIndex] = { - ...row, - sggNm: selected.gugun, - admdstCd: selected.adstDstrCd, - admdstNm: selected.gugun, - }; - next.set(tabKey, rows); - return next; - }); - }, - [sggOptions], - ); + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByTab]); const verificationCellStyle = ({ fieldName, @@ -784,10 +1515,23 @@ const SewageReservoirPumpInp = () => { }; })(); + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_RESERVOIR_PUMP, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); + return ( <>
{
{showEnterableToolbarButtons && ( <> @@ -817,10 +1564,10 @@ const SewageReservoirPumpInp = () => { {showArticleToolbarCard && (
- {showIsNotApplicable && ( + {showNotApplicableForActiveTab && ( )} @@ -859,13 +1606,13 @@ const SewageReservoirPumpInp = () => { return ( { return next; }); }} + getGridDataRef={ + tabKey === SEWAGE_RESERVOIR_STATS_CD + ? reservoirGridGetRowsRef + : tabKey === rainfallPumpTabKey + ? rainfallPumpGridGetRowsRef + : undefined + } showHeaderCheckbox statusInfo={tabStatusInfo} isExcelImport={ - tabRowDataExists(rowDataMap, tabKey) && + loadedTabKeysRef.current.has(tabKey) && tabEnterableForGrid && gugunOk } cellStyleCallback={verificationCellStyle} cellTooltipCallback={verificationCellTooltip} - onCellEdited={ - isReservoirTab && - tabEnterableForGrid && - sggOptions.length > 0 - ? handleSewageReservoirCellEdited - : undefined - } /> ); @@ -920,7 +1667,30 @@ const SewageReservoirPumpInp = () => { open={verificationHistoryOpen} onOpenChange={setVerificationHistoryOpen} queryContext={verificationQueryContext} + onExceptionResetSubmitted={async () => { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await loadActiveTabData(activeTabRef.current, values, true, false); + }} /> + + { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await loadActiveTabData(activeTabRef.current, values, true, false); + }} + /> + ); }; diff --git a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx index 3f3d57f4e..058eaf784 100644 --- a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx @@ -1,64 +1,1389 @@ -import { useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; -import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; +import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; +import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/input/statistics-input-sewerage-finance-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + CauserBurdenItem, + CauserBurdenSaveItem, + CauserBurdenSaveRequest, + CauserBurdenSubmitRequest, + SewerageChargeItem, + SewerageChargeSaveItem, + SewerageChargeSaveRequest, + SewerageChargeSubmitRequest, + SewerageFinanceRequestParams, + SewerageFinanceSettleItem, + SewerageFinanceSettleSaveItem, + SewerageFinanceSettleSaveRequest, + SewerageFinanceSettleSubmitRequest, + SewerageFinanceSubmitResponse, + SewerageFinanceVerificationResult, + SewerageUsageFeeItem, + SewerageUsageFeeSaveItem, + SewerageUsageFeeSaveRequest, + SewerageUsageFeeSubmitRequest, +} from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import type { + cancelSubmitRequest, + ImrStatusResponse, + SewerageStatusInfo, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +import SubmitDailog from "./SubmitDailog"; + +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; +import SaveButton from "@/components/button/SaveButton"; +import SubmitButton from "@/components/button/SumitButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; +import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn"; +import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn"; +import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn"; +import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; +import { useAdminStore } from "@/store/admin-store"; -import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn" -import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn" -import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn" -import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn" +const SewerageFinanceInpTabList = [ + SewerageFinanceSettleInpColumn, + SewerageChargeInpColumn, + SewerageUsageFeeInpColumn, + CauserBurdenInpColumn, +] as const; + +const firstTabKey = SewerageFinanceSettleInpColumn.tabKey; + +const SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS = Array.from( + new Set(SewerageFinanceInpTabList.map((tab) => tab.tabKey).filter(Boolean)), +); + +type GridRowWithVerification = Record & { + verificationResult?: SewerageFinanceVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof SewerageFinanceInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function buildSewerageFinanceSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewerageFinanceRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function tabRowDataExists( + m: Map[]>, + tabKey: string, +): boolean { + const rows = m.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +function mapRegionalListToGridRows< + T extends { verificationResult?: SewerageFinanceVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function normalizeGridRmrk(value: unknown): string | null { + if (value === null || value === undefined) return null; + const s = typeof value === "string" ? value : String(value); + return s.trim() === "" ? null : s; +} + +function buildColumnFormatNumber(columns: { fieldName?: string; dataFormat?: string }[]) { + const columnFormatMap = new Map(); + columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + return (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = + typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; +} + +function resolveSewerageFinanceTab(tabKey: string) { + return { + isSettleTab: tabKey === SewerageFinanceSettleInpColumn.tabKey, + isChargeTab: tabKey === SewerageChargeInpColumn.tabKey, + isUsageFeeTab: tabKey === SewerageUsageFeeInpColumn.tabKey, + isCauserBurdenTab: tabKey === CauserBurdenInpColumn.tabKey, + }; +} + +function buildSewerageFinanceSettleSaveList( + currentData: Record[], +): SewerageFinanceSettleSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewerageFinanceSettleInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + cnterGovSprtAmt: formatNumber(d.cnterGovSprtAmt, "cnterGovSprtAmt"), + mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"), + wareaSggLclAmt: formatNumber(d.wareaSggLclAmt, "wareaSggLclAmt"), + sewerUseAmt: formatNumber(d.sewerUseAmt, "sewerUseAmt"), + orgprBrdnAmt: formatNumber(d.orgprBrdnAmt, "orgprBrdnAmt"), + etcIncmAmt: formatNumber(d.etcIncmAmt, "etcIncmAmt"), + crfwAmt: formatNumber(d.crfwAmt, "crfwAmt"), + lbltAmt: formatNumber(d.lbltAmt, "lbltAmt"), + tmplntFcltAmt: formatNumber(d.tmplntFcltAmt, "tmplntFcltAmt"), + tmplntMendngAmt: formatNumber(d.tmplntMendngAmt, "tmplntMendngAmt"), + tmplntOperKeepAmt: formatNumber(d.tmplntOperKeepAmt, "tmplntOperKeepAmt"), + tmplntEtcAmt: formatNumber(d.tmplntEtcAmt, "tmplntEtcAmt"), + swagePplFcltAmt: formatNumber(d.swagePplFcltAmt, "swagePplFcltAmt"), + swagePplMendngAmt: formatNumber(d.swagePplMendngAmt, "swagePplMendngAmt"), + swagePplOperKeepAmt: formatNumber(d.swagePplOperKeepAmt, "swagePplOperKeepAmt"), + swagePplEtcAmt: formatNumber(d.swagePplEtcAmt, "swagePplEtcAmt"), + swageImpdFcltAmt: formatNumber(d.swageImpdFcltAmt, "swageImpdFcltAmt"), + swageImpdMendngAmt: formatNumber(d.swageImpdMendngAmt, "swageImpdMendngAmt"), + swageImpdOperKeepAmt: formatNumber(d.swageImpdOperKeepAmt, "swageImpdOperKeepAmt"), + swageImpdFcltEtcAmt: formatNumber(d.swageImpdFcltEtcAmt, "swageImpdFcltEtcAmt"), + etcAcdntCrfwAmt: formatNumber(d.etcAcdntCrfwAmt, "etcAcdntCrfwAmt"), + etcNrcpAmt: formatNumber(d.etcNrcpAmt, "etcNrcpAmt"), + etcAmt: formatNumber(d.etcAmt, "etcAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + fnnByWholPjtcoAmt: formatNumber(d.fnnByWholPjtcoAmt, "fnnByWholPjtcoAmt"), + usgByWholPjtcoAmt: formatNumber(d.usgByWholPjtcoAmt, "usgByWholPjtcoAmt"), + tmplntWholAmt: formatNumber(d.tmplntWholAmt, "tmplntWholAmt"), + wholSwagePplAmt: formatNumber(d.wholSwagePplAmt, "wholSwagePplAmt"), + wholSwageImpdFcltAmt: formatNumber(d.wholSwageImpdFcltAmt, "wholSwageImpdFcltAmt"), + etcWholAmt: formatNumber(d.etcWholAmt, "etcWholAmt"), + })); +} + +function buildSewerageChargeSaveList( + currentData: Record[], +): SewerageChargeSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewerageChargeInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"), + levyAmt: formatNumber(d.levyAmt, "levyAmt"), + grssPrmcoAmt: formatNumber(d.grssPrmcoAmt, "grssPrmcoAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + avgUntprcAmt: formatNumber(d.avgUntprcAmt, "avgUntprcAmt"), + grssUnitPrmcoAmt: formatNumber(d.grssUnitPrmcoAmt, "grssUnitPrmcoAmt"), + rlztnRt: formatNumber(d.rlztnRt, "rlztnRt"), + })); +} + +function buildSewerageUsageFeeSaveList( + currentData: Record[], +): SewerageUsageFeeSaveItem[] { + const formatNumber = buildColumnFormatNumber(SewerageUsageFeeInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"), + hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"), + hshusUntprcAmt: formatNumber(d.hshusUntprcAmt, "hshusUntprcAmt"), + taskFrLevyAmt: formatNumber(d.taskFrLevyAmt, "taskFrLevyAmt"), + taskFrLevyQnt: formatNumber(d.taskFrLevyQnt, "taskFrLevyQnt"), + taskFrUntprcAmt: formatNumber(d.taskFrUntprcAmt, "taskFrUntprcAmt"), + fbuLevyAmt: formatNumber(d.fbuLevyAmt, "fbuLevyAmt"), + fbuLevyQnt: formatNumber(d.fbuLevyQnt, "fbuLevyQnt"), + fbuUntprcAmt: formatNumber(d.fbuUntprcAmt, "fbuUntprcAmt"), + bthhsFrSpcs1LevyAmt: formatNumber(d.bthhsFrSpcs1LevyAmt, "bthhsFrSpcs1LevyAmt"), + bthhsFrSpcs1LevyQnt: formatNumber(d.bthhsFrSpcs1LevyQnt, "bthhsFrSpcs1LevyQnt"), + bthhsFrSpcs1UntprcAmt: formatNumber(d.bthhsFrSpcs1UntprcAmt, "bthhsFrSpcs1UntprcAmt"), + bthhsFrSpcs2LevyAmt: formatNumber(d.bthhsFrSpcs2LevyAmt, "bthhsFrSpcs2LevyAmt"), + bthhsFrSpcs2LevyQnt: formatNumber(d.bthhsFrSpcs2LevyQnt, "bthhsFrSpcs2LevyQnt"), + bthhsFrSpcs2UntprcAmt: formatNumber(d.bthhsFrSpcs2UntprcAmt, "bthhsFrSpcs2UntprcAmt"), + induLevyAmt: formatNumber(d.induLevyAmt, "induLevyAmt"), + induLevyQnt: formatNumber(d.induLevyQnt, "induLevyQnt"), + induUntprcAmt: formatNumber(d.induUntprcAmt, "induUntprcAmt"), + etcLevyAmt: formatNumber(d.etcLevyAmt, "etcLevyAmt"), + etcLevyQnt: formatNumber(d.etcLevyQnt, "etcLevyQnt"), + etcLevyUntprcAmt: formatNumber(d.etcLevyUntprcAmt, "etcLevyUntprcAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + levyAmt: formatNumber(d.levyAmt, "levyAmt"), + levyQnt: formatNumber(d.levyQnt, "levyQnt"), + })); +} + +function buildCauserBurdenSaveList( + currentData: Record[], +): CauserBurdenSaveItem[] { + const formatNumber = buildColumnFormatNumber(CauserBurdenInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + sggNm: normalizeGridString(d.sggNm), + admdstNm: normalizeGridString(d.admdstNm), + levyEvntcsCnt: formatNumber(d.levyEvntcsCnt, "levyEvntcsCnt"), + levyAmt: formatNumber(d.levyAmt, "levyAmt"), + mltvEqpmEvntcsCnt: formatNumber(d.mltvEqpmEvntcsCnt, "mltvEqpmEvntcsCnt"), + mltvEqpmAmt: formatNumber(d.mltvEqpmAmt, "mltvEqpmAmt"), + othrCstrnOthrActrCnt: formatNumber(d.othrCstrnOthrActrCnt, "othrCstrnOthrActrCnt"), + othrCstrnOthrActrAmt: formatNumber(d.othrCstrnOthrActrAmt, "othrCstrnOthrActrAmt"), + rmrk: normalizeGridRmrk(d.rmrk), + trgtByWholClctnAmt: formatNumber(d.trgtByWholClctnAmt, "trgtByWholClctnAmt"), + })); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 09. 하수도재정 const SewerageFinanceInp = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); - // tab list - const SewerageFinanceInpTabList = [ - SewerageFinanceSettleInpColumn, - SewerageChargeInpColumn, - SewerageUsageFeeInpColumn, - CauserBurdenInpColumn - ]; + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>( + new Map(), + ); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWERAGE_FINANCE_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 = {}; + 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; + }; + }, []); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); }; - const lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewerageFinanceRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + + if (tabKey === SewerageFinanceSettleInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getSewerageFinanceSettleList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewerageFinanceSettleList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewerageFinanceSettleList ?? []) as SewerageFinanceSettleItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewerageFinanceSettleInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === SewerageChargeInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getSewerageChargeList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewerageChargeList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewerageChargeList ?? []) as SewerageChargeItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewerageChargeInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === SewerageUsageFeeInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getSewerageUsageFeeList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewerageUsageFeeList } = response.data; + const gridRows = mapRegionalListToGridRows( + (sewerageUsageFeeList ?? []) as SewerageUsageFeeItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewerageUsageFeeInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === CauserBurdenInpColumn.tabKey) { + const response = + await statisticsInputSewerageFinanceApi.getCauserBurdenList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, causerBurdenList } = response.data; + const gridRows = mapRegionalListToGridRows( + (causerBurdenList ?? []) as CauserBurdenItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(CauserBurdenInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + }; + + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildSewerageFinanceSearchParam(values); + searchParamRef.current = searchParam; + + await loadActiveTabData( + opts?.forcedTabKey ?? activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + 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; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const lastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildSewerageFinanceSearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; const submitFunc = () => { - // 제출 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSettleTab, isChargeTab, isUsageFeeTab, isCauserBurdenTab } = + resolveSewerageFinanceTab(tabKey); + if (!isSettleTab && !isChargeTab && !isUsageFeeTab && !isCauserBurdenTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const { isSettleTab, isChargeTab, isUsageFeeTab, isCauserBurdenTab } = + resolveSewerageFinanceTab(tabKey); + if (!isSettleTab && !isChargeTab && !isUsageFeeTab && !isCauserBurdenTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {}; + + const submitInfoBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + }; + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + true, + false, + ); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + let response: SewerageFinanceSubmitResponse; + + if (isSettleTab) { + const param: SewerageFinanceSettleSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewerageFinanceSettleSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputSewerageFinanceApi.submitSewerageFinanceSettle( + param, + ); + } else if (isChargeTab) { + const param: SewerageChargeSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewerageChargeSaveList(currentData), + ...adminOpt, + }; + response = await statisticsInputSewerageFinanceApi.submitSewerageCharge(param); + } else if (isUsageFeeTab) { + const param: SewerageUsageFeeSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewerageUsageFeeSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputSewerageFinanceApi.submitSewerageUsageFee(param); + } else { + const param: CauserBurdenSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildCauserBurdenSaveList(currentData), + ...adminOpt, + }; + response = await statisticsInputSewerageFinanceApi.submitCauserBurden(param); + } + + if (response.status !== "success") { + alert(response.message || "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + setShowSubmitDialog(false); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSettleTab, isChargeTab, isUsageFeeTab, isCauserBurdenTab } = + resolveSewerageFinanceTab(tabKey); + if (!isSettleTab && !isChargeTab && !isUsageFeeTab && !isCauserBurdenTab) { + alert("저장할 수 없는 탭입니다."); + return; + } + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + if (!(await confirm("저장 하시겠습니까?"))) return; + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("저장할 데이터가 없습니다."); + return; + } + + const baseRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + + if (isSettleTab) { + const param: SewerageFinanceSettleSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewerageFinanceSettleSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputSewerageFinanceApi.saveSewerageFinanceSettle(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + if (isChargeTab) { + const param: SewerageChargeSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewerageChargeSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputSewerageFinanceApi.saveSewerageCharge(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + if (isUsageFeeTab) { + const param: SewerageUsageFeeSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildSewerageUsageFeeSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = + await statisticsInputSewerageFinanceApi.saveSewerageUsageFee(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + return; + } + + const param: CauserBurdenSaveRequest = { + ...baseRequest, + userId: userInfo.userUnqId, + saveList: buildCauserBurdenSaveList(currentData), + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + const response = await statisticsInputSewerageFinanceApi.saveCauserBurden(param); + if (response.status === "success") { + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } else { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + } + } catch { + alert("데이터 저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of SewerageFinanceInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + 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, columnGuidesByTab]); + + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = + activeTabToolbar; + + const verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_FINANCE, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewerageFinanceInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {SewerageFinanceInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + 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; + }); + }} + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + /> + + ); + })} +
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> ); }; -export default SewerageFinanceInp; \ No newline at end of file +export default SewerageFinanceInp; diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 6a31e84da..fa4b09ee7 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -49,6 +49,7 @@ import { doList } from "@/components/util/column/input/DoList"; import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; import { getFooterSummary } from "@/components/util/grid/footerSummary"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag"; import { useAdminStore } from "@/store/admin-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useTabStore } from "@/store/tab-store"; @@ -131,6 +132,7 @@ const SeweragePenetrationRateInp = () => { /** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */ const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false); const [statsCd, setStatsCd] = useState(''); + const [isVerificationFailed, setIsVerificationFailed] = useState(false); /** `engColNm` → `columnGuide` (헤더 hover) */ const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState>({}); const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null); @@ -240,6 +242,7 @@ const SeweragePenetrationRateInp = () => { setLastSearchedValues(null); setFooterFromApi(null); setVerificationSearchDefaults(null); + setIsVerificationFailed(false); setSearchFormRemountKey((k) => k + 1); }; @@ -275,6 +278,7 @@ const SeweragePenetrationRateInp = () => { : null ); setStatsCd(commonInfo.statsCd); + setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo)); if (statusInfo && statusInfo.laeSbmsnSttsCd) { if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) { setIsEnterable(false); @@ -288,7 +292,8 @@ const SeweragePenetrationRateInp = () => { } else { alert(response.message); } - } catch { + } catch (error) { + console.log(error); alert('데이터 조회 중 오류가 발생했습니다.'); setRowData([]); setStatusInfo(null); @@ -778,6 +783,7 @@ const SeweragePenetrationRateInp = () => {
{showEnterableToolbarButtons && ( <> @@ -790,11 +796,13 @@ const SeweragePenetrationRateInp = () => {
)} - {isLoading && !globalProgressVisible &&
-
- + {isLoading && !globalProgressVisible && +
+
+ +
-
} + } { } : null } + onExceptionResetSubmitted={async () => { + const values = lastSearchedValues ?? searchValues; + if (!values) return; + await handleSearch(values); + }} /> = { + [ComplaintInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD, + [ResidentFriendlyFactInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD, +}; + +const EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG = + "06. 공공하수처리시설 데이터가 없으면 전년도 데이터 조회 및 항목 추가가 불가능 합니다."; + +function buildEmptyDataAlertOverrides( + tabKey: string, +): { alertIfResidentFriendlyEmpty?: boolean } | undefined { + if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { + return { alertIfResidentFriendlyEmpty: true }; + } + return undefined; +} + +type GridRowWithVerification = Record & { + verificationResult?: SewerageServiceLevelVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof SewerageServiceLevelInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function tabRowDataExists( + rowDataMap: Map[]>, + tabKey: string, +): boolean { + const rows = rowDataMap.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function buildSewerageServiceLevelSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewerageServiceLevelComplaintRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function mapServiceLevelListToGridRows< + T extends { verificationResult?: SewerageServiceLevelVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function normalizeGridRmrk(value: unknown): string | null { + if (value === null || value === undefined) return null; + const s = typeof value === "string" ? value : String(value); + return s.trim() === "" ? null : s; +} + +function buildColumnFormatNumber( + columns: { fieldName?: string; dataFormat?: string }[], +) { + const columnFormatMap = new Map(); + columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + return (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = + typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; +} + +function buildComplaintSaveList( + currentData: Record[], +): SewerageServiceLevelComplaintSaveItem[] { + const formatNumber = buildColumnFormatNumber(ComplaintInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + swtrmtPpltnCnt: formatNumber(d.swtrmtPpltnCnt, "swtrmtPpltnCnt"), + dschgWqCnt: formatNumber(d.dschgWqCnt, "dschgWqCnt"), + odorCnt: formatNumber(d.odorCnt, "odorCnt"), + ldsCnt: formatNumber(d.ldsCnt, "ldsCnt"), + plltnAcdntCnt: formatNumber(d.plltnAcdntCnt, "plltnAcdntCnt"), + swagePplCnt: formatNumber(d.swagePplCnt, "swagePplCnt"), + sewerCrgAmt: formatNumber(d.sewerCrgAmt, "sewerCrgAmt"), + etcCnt: formatNumber(d.etcCnt, "etcCnt"), + rmrk: normalizeGridRmrk(d.rmrk), + whol: formatNumber(d.whol, "whol"), + crtrPpltnCvlcptEvntcsCnt: formatNumber( + d.crtrPpltnCvlcptEvntcsCnt, + "crtrPpltnCvlcptEvntcsCnt", + ), + })); +} + +function buildResidentFriendlyFactSaveList( + currentData: Record[], +): SewerageServiceLevelResidentFriendlyFactSaveItem[] { + const formatNumber = buildColumnFormatNumber(ResidentFriendlyFactInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + swtrmtFcltCd: normalizeGridString(d.swtrmtFcltCd), + swtrmtFcltNm: normalizeGridString(d.swtrmtFcltNm), + fcltSn: normalizeGridString(d.fcltSn), + tmplntArea: formatNumber(d.tmplntArea, "tmplntArea"), + brkFcltArea: formatNumber(d.brkFcltArea, "brkFcltArea"), + childFrshpSpacArea: formatNumber(d.childFrshpSpacArea, "childFrshpSpacArea"), + bdySportsFcltArea: formatNumber(d.bdySportsFcltArea, "bdySportsFcltArea"), + sportsFcltArea: formatNumber(d.sportsFcltArea, "sportsFcltArea"), + pkltArea: formatNumber(d.pkltArea, "pkltArea"), + eduMultiFcltArea: formatNumber(d.eduMultiFcltArea, "eduMultiFcltArea"), + wtlndMakeArea: formatNumber(d.wtlndMakeArea, "wtlndMakeArea"), + etcCnt: formatNumber(d.etcCnt, "etcCnt"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 13. 하수도 서비스 수준 const SewerageServiceLevelInp = () => { - - // tab List - const SewerageServiceLevelInpTabList = [ - ComplaintInpColumn, - ResidentFriendlyFactInpColumn - ]; + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>( + new Map(), + ); + const [isNotApplicable, setIsNotApplicable] = useState(false); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [searchLoaded, setSearchLoaded] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + /** ST00130002 당해 조회 결과 유무(공공하수처리시설 연동) — 전년도 조회 가능 여부 */ + const publicSewageFacilitiesAvailableRef = useRef(false); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => { + try { + const res = await statisticsApi.getColumnGuides(statsCd); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [statsCd, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [statsCd, next] as const; + } catch { + return [statsCd, {}] as const; + } + }), + ); + if (cancelled) return; + const guidesByStatsCd = Object.fromEntries(entries); + setColumnGuidesByTab( + Object.fromEntries( + Object.entries(SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( + ([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}], + ), + ), + ); + })(); + return () => { + cancelled = true; + }; + }, []); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); }; - const getLastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewerageServiceLevelComplaintRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string; alertIfResidentFriendlyEmpty?: boolean }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + const fromPreYear = overrides?.mode === "PRE"; + + if (tabKey === ComplaintInpColumn.tabKey) { + const response = + await statisticsInputSewerageServiceLevelApi.getComplaintList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, complaintList } = response.data; + const gridRows = mapServiceLevelListToGridRows( + (complaintList ?? []) as SewerageServiceLevelComplaintItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(ComplaintInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + setSearchLoaded(true); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { + const response = + await statisticsInputSewerageServiceLevelApi.getResidentFriendlyFactList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, residentFriendlyFactList } = response.data; + const gridRows = mapServiceLevelListToGridRows( + (residentFriendlyFactList ?? []) as SewerageServiceLevelResidentFriendlyFactItem[], + commonInfo.ctpvNm, + ); + if (!fromPreYear) { + publicSewageFacilitiesAvailableRef.current = gridRows.length > 0; + if (gridRows.length === 0 && overrides?.alertIfResidentFriendlyEmpty) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + } + } + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(ResidentFriendlyFactInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + setSearchLoaded(true); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + if (!fromPreYear) { + setIsNotApplicable(gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicable(false); + } + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setSearchLoaded(false); + setIsNotApplicable(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + publicSewageFacilitiesAvailableRef.current = false; + }; + + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setIsNotApplicable(false); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } + + const tabForLoad = opts?.forcedTabKey ?? activeTabRef.current; + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildSewerageServiceLevelSearchParam(values); + searchParamRef.current = searchParam; + + await loadActiveTabData( + tabForLoad, + searchParam, + values, + !yearChangedFromLastSearch, + true, + buildEmptyDataAlertOverrides(tabForLoad), + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + 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); + setIsNotApplicable(false); + + if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + buildEmptyDataAlertOverrides(tabKey), + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + if ( + activeTabRef.current === ResidentFriendlyFactInpColumn.tabKey && + !publicSewageFacilitiesAvailableRef.current + ) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + return; + } + + try { + setIsLoading(true); + const param = buildSewerageServiceLevelSearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; const submitFunc = () => { - // 제출 로직 + void (async () => { + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const tabKey = activeTabRef.current; + const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; + const latestRows = getTabGridRows(tabKey); + const noApplicable = isResidentTab && isNotApplicable; + if (latestRows.length === 0) { + if (isResidentTab && !noApplicable) { + alert("해당사항 없음을 체크해주세요."); + return; + } + if (!isResidentTab) { + alert("제출할 데이터가 없습니다."); + return; + } + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const isComplaintTab = tabKey === ComplaintInpColumn.tabKey; + const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; + + if (!isComplaintTab && !isResidentTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const latestRows = getTabGridRows(tabKey); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, latestRows); + return next; + }); + + const noApplicable = isResidentTab && isNotApplicable; + const complaintSubmitList = buildComplaintSaveList(latestRows); + const residentSubmitList = noApplicable + ? [] + : buildResidentFriendlyFactSaveList(latestRows); + + if (isComplaintTab && complaintSubmitList.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + if (isResidentTab && !noApplicable && residentSubmitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = + tabStatsCdMap.get(tabKey) ?? + (isComplaintTab + ? SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD + : SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD); + + const submitInfoBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + }; + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + true, + false, + ); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + const response = isComplaintTab + ? await statisticsInputSewerageServiceLevelApi.submitComplaintList({ + ...submitInfoBase, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + submitList: complaintSubmitList, + }) + : await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({ + ...submitInfoBase, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + submitList: residentSubmitList, + }); + + if (response.status !== "success") { + alert(response.message || "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { + notify: alert, + notifySubmitComplete: false, + }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; + const latestRows = getTabGridRows(tabKey); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, latestRows); + return next; + }); + + const noApplicable = isResidentTab && isNotApplicable; + const baseRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + + if (tabKey === ComplaintInpColumn.tabKey) { + const saveList = buildComplaintSaveList(latestRows); + if (saveList.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + const response = + await statisticsInputSewerageServiceLevelApi.saveComplaintList({ + ...baseRequest, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + saveList, + }); + + if (response.status !== "success") { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + return; + } + } else if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { + const saveList = noApplicable + ? [] + : buildResidentFriendlyFactSaveList(latestRows); + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const response = + await statisticsInputSewerageServiceLevelApi.saveResidentFriendlyFactList( + { + ...baseRequest, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + saveList, + }, + ); + + if (response.status !== "success") { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + return; + } + } else { + alert("저장할 수 없는 탭입니다."); + return; + } + + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } catch { + alert("데이터 저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const openVerificationHistoryDialog = () => { + setVerificationHistoryOpen(true); + }; + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of SewerageServiceLevelInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + 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, columnGuidesByTab]); + + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabRowData = rowDataMap.get(activeTab) ?? []; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + activeTabToolbar.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed; + const showNotApplicableForResidentTab = + activeTab === ResidentFriendlyFactInpColumn.tabKey && + searchLoaded && + hasSearched && + activeTabRowData.length === 0 && + computeTabEnterable(activeTabStatus, inputPeriodEnterable); + const showSecondaryToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons || showNotApplicableForResidentTab); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_SERVICE_LEVEL, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForResidentTab && ( + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewerageServiceLevelInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {SewerageServiceLevelInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const nextRows = + typeof updater === "function" + ? updater(cur) + : updater; + next.set(tabKey, nextRows); + return next; + }); + }} + showHeaderCheckbox={false} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + /> + + ); + })} +
-
-
- -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> ); }; -export default SewerageServiceLevelInp; \ No newline at end of file +export default SewerageServiceLevelInp; diff --git a/apps/st/src/components/statistics/input/StatisticsReviewHistory.tsx b/apps/st/src/components/statistics/input/StatisticsReviewHistory.tsx index 669ae0c76..ddd8bc8dc 100644 --- a/apps/st/src/components/statistics/input/StatisticsReviewHistory.tsx +++ b/apps/st/src/components/statistics/input/StatisticsReviewHistory.tsx @@ -18,8 +18,6 @@ const StatisticsReviewHistory = () => { { name: "statsYr", fieldName: "statsYr", header: { text: "연도" }, editable: false }, { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, - { name: "sewerBzmnCd", fieldName: "sewerBzmnCd", header: { text: "하수사업자코드" }, editable: false }, - { name: "statsCd", fieldName: "statsCd", header: { text: "통계코드" }, editable: false }, { name: "menuNm", fieldName: "menuNm", header: { text: "통계항목" }, editable: false }, { name: "chckerOpinionSm", fieldName: "chckerOpinionSm", header: { text: "검토자의견" }, editable: false }, { name: "wrterOpinionSm", fieldName: "wrterOpinionSm", header: { text: "처리내역" }, editable: false }, @@ -29,8 +27,6 @@ const StatisticsReviewHistory = () => { "statsYr", "ctpvNm", "sggNm", - "sewerBzmnCd", - "statsCd", "menuNm", "chckerOpinionSm", "wrterOpinionSm", diff --git a/apps/st/src/components/statistics/input/WaterReuseInp.tsx b/apps/st/src/components/statistics/input/WaterReuseInp.tsx index 26798d169..a1735f454 100644 --- a/apps/st/src/components/statistics/input/WaterReuseInp.tsx +++ b/apps/st/src/components/statistics/input/WaterReuseInp.tsx @@ -1,92 +1,2258 @@ -import { useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type Dispatch, + type SetStateAction, +} from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; +import { ValueType } from "realgrid"; +import type { RealGridReact } from "realgrid-react"; +import { flushSync } from "react-dom"; -import SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; -import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton" import AddArticleButton from "@/components/button/AddArticleButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton"; +import LastYearDataButton from "@/components/button/LastYearDataButton"; import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton"; - +import SaveButton from "@/components/button/SaveButton"; +import SubmitButton from "@/components/button/SumitButton"; +import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; import { PublicSewageTreatWaterReuseInpColumn } from "@/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn"; -import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn"; import { RainFallUtilFactStatusInpColumn } from "@/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn"; -import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn"; +import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn"; import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn"; +import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { + patchVerificationFailedByTab, +} from "@/components/util/input/verification-failed-flag"; +import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat"; +import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service"; +import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import { + WATER_REUSE_BUSINESS_APPR_STATS_CD, + WATER_REUSE_DESIGN_CONST_STATS_CD, + WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD, + WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD, + WATER_REUSE_RECYCLED_WATER_STATS_CD, + type PublicSewageTreatWaterReuseItem, + type PublicSewageTreatWaterReuseSaveItem, + type RainFallUtilFactStatusItem, + type RainFallUtilFactStatusSaveItem, + type RecycledWaterStatusItem, + type RecycledWaterStatusSaveItem, + type TreatWaterReuseBusinessApprStatusItem, + type TreatWaterReuseBusinessApprStatusSaveItem, + type TreatWaterReuseDesignConstStatusItem, + type TreatWaterReuseDesignConstStatusSaveItem, + type WaterReuseRequestParams, + type WaterReuseVerificationResult, +} from "@/entities/statistics/model/input/statistics-input-water-reuse-interface"; +import type { + ApiResponse, + ImrStatusResponse, + Sgg, + SewerageStatusInfo, + cancelSubmitRequest, +} from "@/entities/statistics/model/statistics-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; +import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; +import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAdminStore } from "@/store/admin-store"; + +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +import SubmitDailog from "./SubmitDailog"; + +const WaterReuseInpTabList = [ + PublicSewageTreatWaterReuseInpColumn, + RecycledWaterStatusInpColumn, + RainFallUtilFactStatusInpColumn, + TreatWaterReuseDesignConstStatusInpColumn, + TreatWaterReuseBusinessApprStatusInpColumn, +] as const; + +const firstTabKey = PublicSewageTreatWaterReuseInpColumn.tabKey; + +const WATER_REUSE_COLUMN_GUIDE_STATS_CDS = [ + WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD, + WATER_REUSE_RECYCLED_WATER_STATS_CD, + WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD, + WATER_REUSE_DESIGN_CONST_STATS_CD, + WATER_REUSE_BUSINESS_APPR_STATS_CD, +] as const; + +const WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record = { + [PublicSewageTreatWaterReuseInpColumn.tabKey]: WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD, + [RecycledWaterStatusInpColumn.tabKey]: WATER_REUSE_RECYCLED_WATER_STATS_CD, + [RainFallUtilFactStatusInpColumn.tabKey]: WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD, + [TreatWaterReuseDesignConstStatusInpColumn.tabKey]: WATER_REUSE_DESIGN_CONST_STATS_CD, + [TreatWaterReuseBusinessApprStatusInpColumn.tabKey]: WATER_REUSE_BUSINESS_APPR_STATS_CD, +}; + +const ARTICLE_EDIT_TAB_KEYS = new Set([ + RecycledWaterStatusInpColumn.tabKey, + RainFallUtilFactStatusInpColumn.tabKey, + TreatWaterReuseDesignConstStatusInpColumn.tabKey, + TreatWaterReuseBusinessApprStatusInpColumn.tabKey, +]); + +const NO_APPLICABLE_TAB_KEYS = ARTICLE_EDIT_TAB_KEYS; + +const WATER_REUSE_TAB_BY_KEY = new Map( + WaterReuseInpTabList.map((tab) => [tab.tabKey, tab]), +); + +type GridRowWithVerification = Record & { + verificationResult?: WaterReuseVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof WaterReuseInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function tabRowDataExists( + rowDataMap: Map[]>, + tabKey: string, +): boolean { + const rows = rowDataMap.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +const computeTabEnterable = ( + statusInfo: SewerageStatusInfo | null | undefined, + periodEnterable: boolean, +): boolean => { + if (statusInfo?.laeSbmsnSttsCd) { + if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) { + return false; + } + return periodEnterable; + } + return periodEnterable; +}; + +function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { + return doList.includes(values?.bsnmCd || "") + ? Boolean(values?.gugun && values.gugun !== "ALL") + : true; +} + +function computeTabToolbarSnapshot( + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + treatAsSearched: boolean, + searchContext: SearchFormValues | null, + periodEnterable: boolean, + isAdminInfo: boolean, +): TabToolbarSnapshot { + const gugunOk = buildGugunOkForToolbar(searchContext); + const enterable = computeTabEnterable(statusInfo, periodEnterable); + const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; + const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; + const showEnterableToolbarButtons = enterable && gugunOk; + const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; + const showActionToolbarCard = + (hasRowData || treatAsSearched) && + (showSaveInToolbar || + showEnterableToolbarButtons || + showSubmittedInInputPeriodToolbar || + (isSubmitted && isAdminInfo)); + return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard }; +} + +function buildWaterReuseSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): WaterReuseRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function parseSggListFromResponse(res: ApiResponse): Sgg[] { + if (res.status !== "success" || res.data == null) return []; + return Array.isArray(res.data) ? res.data : []; +} + +function buildSggNmColumn( + col: ColumnConfig, + canEdit: boolean, + sggOptions: Sgg[], +): ColumnConfig { + if (!canEdit) { + return { ...col, editable: false as const }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +function nullableTrimmedString( + value: string | number | null | undefined, +): string | null { + const s = String(value ?? "").trim(); + return s === "" ? null : s; +} + +function toGridNumber(value: string | number | null | undefined): number { + const n = Number(value); + return Number.isFinite(n) ? n : 0; +} + +function resolveArticleAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + if (existingCd && existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm }; + } + const sggNm = String(row.sggNm ?? "").trim(); + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; +} + +function getArticleRowSnField(tabKey: string): "fcltSn" | "bplcSn" | null { + if ( + tabKey === RecycledWaterStatusInpColumn.tabKey || + tabKey === RainFallUtilFactStatusInpColumn.tabKey + ) { + return "fcltSn"; + } + if ( + tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey || + tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey + ) { + return "bplcSn"; + } + return null; +} + +function isPersistedArticleRow( + tabKey: string, + row: Record, +): boolean { + const snField = getArticleRowSnField(tabKey); + if (!snField) return false; + return String(row[snField] ?? "").trim() !== ""; +} + +function buildRecycledWaterStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): RecycledWaterStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + bldgSeCn: String(row.bldgSeCn ?? "").trim(), + fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), + lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), + cmptnStrtYmd: toCompactYmdOrNull(row.cmptnStrtYmd), + oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), + archWholArea: toGridNumber(row.archWholArea), + prcsCpct: toGridNumber(row.prcsCpct), + wtrUseQnt: toGridNumber(row.wtrUseQnt), + ruswtrUtztnQnt: toGridNumber(row.ruswtrUtztnQnt), + sbstCntUtztnQnt: toGridNumber(row.sbstCntUtztnQnt), + mnUsgCn: String(row.mnUsgCn ?? "").trim(), + ruswtrOperAmt: toGridNumber(row.ruswtrOperAmt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildRainFallUtilFactStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): RainFallUtilFactStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + bdstTypCn: String(row.bdstTypCn ?? "").trim(), + fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), + lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), + cmptYmd: toCompactYmdOrNull(row.cmptYmd), + instlAmt: toGridNumber(row.instlAmt), + rcptMynCn: String(row.rcptMynCn ?? "").trim(), + filtrPrcsFcltYnExpln: String(row.filtrPrcsFcltYnExpln ?? "").trim(), + impdCpct: toGridNumber(row.impdCpct), + anlUseQnt: toGridNumber(row.anlUseQnt), + exlnUtlzUsgExpln: String(row.exlnUtlzUsgExpln ?? "").trim(), + anlOperAmt: toGridNumber(row.anlOperAmt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildTreatWaterReuseDesignConstStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): TreatWaterReuseDesignConstStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + bplcSn: String(row.bplcSn ?? "").trim(), + bplcNm: String(row.bplcNm ?? "").trim(), + rprsvNm: String(row.rprsvNm ?? "").trim(), + bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), + coRegYmd: toCompactYmdOrNull(row.coRegYmd), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildTreatWaterReuseBusinessApprStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): TreatWaterReuseBusinessApprStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + bplcSn: String(row.bplcSn ?? "").trim(), + bplcNm: String(row.bplcNm ?? "").trim(), + rprsvNm: String(row.rprsvNm ?? "").trim(), + bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), + coRegYmd: toCompactYmdOrNull(row.coRegYmd), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildPublicSewageTreatWaterReuseSaveItem( + row: Record, +): PublicSewageTreatWaterReuseSaveItem { + return { + admdstCd: String(row.admdstCd ?? "").trim(), + admdstNm: String(row.admdstNm ?? row.sggNm ?? "").trim(), + fcltSn: String(row.fcltSn ?? "").trim(), + swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + wholPrcsQnt: toGridNumber(row.wholPrcsQnt), + lglYn: String(row.lglYn ?? "").trim(), + instlYn: String(row.instlYn ?? "").trim(), + oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), + instlAprvYmd: toCompactYmdOrNull(row.instlAprvYmd), + rprcsFcltCpct: toGridNumber(row.rprcsFcltCpct), + mtcnsNm: String(row.mtcnsNm ?? "").trim(), + ruswtPplLen: toGridNumber(row.ruswtPplLen), + ruswtPumpLen: toGridNumber(row.ruswtPumpLen), + rmrk: nullableTrimmedString(row.rmrk), + rtlztnRt: toGridNumber(row.rtlztnRt), + }; +} + +function createEmptyArticleRow( + tabKey: string, + ctpvNm: string, + sggNm = "", +): GridRowWithVerification { + const row: GridRowWithVerification = { ctpvNm, sggNm }; + const columns = (WATER_REUSE_TAB_BY_KEY.get(tabKey)?.columns ?? []) as ColumnConfig[]; + for (const col of columns) { + const fieldName = col.fieldName; + if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} + +function mapPublicSewageTreatWaterReuseListToGridRows( + list: PublicSewageTreatWaterReuseItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + sncnYmd: formatCompactYmdForGrid(item.sncnYmd), + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + instlAprvYmd: formatCompactYmdForGrid(item.instlAprvYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function mapRecycledWaterStatusListToGridRows( + list: RecycledWaterStatusItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + cmptnStrtYmd: formatCompactYmdForGrid(item.cmptnStrtYmd), + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function mapRainFallUtilFactStatusListToGridRows( + list: RainFallUtilFactStatusItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + cmptYmd: formatCompactYmdForGrid(item.cmptYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function mapTreatWaterReuseStatusListToGridRows< + T extends { coRegYmd: string; verificationResult?: WaterReuseVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + coRegYmd: formatCompactYmdForGrid(item.coRegYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 10. 물 재이용 const WaterReuseInp = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); - // tab list - const WaterReuseInpTabList = [ - PublicSewageTreatWaterReuseInpColumn, - RecycledWaterStatusInpColumn, - RainFallUtilFactStatusInpColumn, - TreatWaterReuseDesignConstStatusInpColumn, - TreatWaterReuseBusinessApprStatusInpColumn - ]; - const SubmitExceptionFunc = () => { - // 예외 제출 로직 + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>( + new Map(), + ); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< + Map + >(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [sggOptions, setSggOptions] = useState([]); + const [deleteListByTab, setDeleteListByTab] = useState< + Map[]> + >(() => new Map()); + + const searchParamRef = useRef(null); + const sggOptionsSidoRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + const loadedTabKeysRef = useRef>(new Set()); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + WATER_REUSE_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => { + try { + const res = await statisticsApi.getColumnGuides(statsCd); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [statsCd, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [statsCd, next] as const; + } catch { + return [statsCd, {}] as const; + } + }), + ); + if (cancelled) return; + const guidesByStatsCd = Object.fromEntries(entries); + setColumnGuidesByTab( + Object.fromEntries( + Object.entries(WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( + ([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}], + ), + ), + ); + })(); + return () => { + cancelled = true; + }; + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + if (!sido || sido === "ALL") { + setSggOptions([]); + return []; + } + try { + const res = await statisticsApi.getSggAllList(sido); + const list = parseSggListFromResponse(res); + setSggOptions(list); + return list; + } catch { + setSggOptions([]); + return []; + } + }, []); + + const shouldShowNotApplicable = ( + tabKey: string, + hasRows: boolean, + statusInfo: SewerageStatusInfo | null | undefined, + ): boolean => + NO_APPLICABLE_TAB_KEYS.has(tabKey) && + hasSearched && + !hasRows && + computeTabEnterable(statusInfo, inputPeriodEnterable); + + const setIsNotApplicableForTab = useCallback((tabKey: string, nextValue: boolean) => { + setIsNotApplicableByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, nextValue); + return next; + }); + }, []); + + const commitToolbarSnapshotForLoadedTab = ( + tabKey: string, + statusInfo: SewerageStatusInfo | null, + hasRowData: boolean, + searchContext: SearchFormValues, + ) => { + setToolbarSnapshotByTab((prev) => { + const next = new Map(prev); + next.set( + tabKey, + computeTabToolbarSnapshot( + statusInfo, + hasRowData, + searchDoneRef.current, + searchContext, + periodEnterableRef.current, + isAdminInfo, + ), + ); + return next; + }); }; - const getLastYearDataFunc = () => { - // 전연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: WaterReuseRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + const fromPreYear = overrides?.mode === "PRE"; + + const applyNotApplicableForLoadedTab = ( + gridRows: readonly Record[], + ) => { + if (!NO_APPLICABLE_TAB_KEYS.has(tabKey)) return; + if (!fromPreYear) { + setIsNotApplicableForTab(tabKey, gridRows.length === 0); + } else if (gridRows.length > 0) { + setIsNotApplicableForTab(tabKey, false); + } + }; + + if (ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + } + + if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) { + const response = + await statisticsInputWaterReuseApi.getPublicSewageTreatWaterReuseList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, publicSewageTreatWaterReuseList } = response.data; + const gridRows = mapPublicSewageTreatWaterReuseListToGridRows( + (publicSewageTreatWaterReuseList ?? []) as PublicSewageTreatWaterReuseItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(PublicSewageTreatWaterReuseInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + applyNotApplicableForLoadedTab(gridRows); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === RecycledWaterStatusInpColumn.tabKey) { + const response = await statisticsInputWaterReuseApi.getRecycledWaterStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, recycledWaterList } = response.data; + const gridRows = mapRecycledWaterStatusListToGridRows( + (recycledWaterList ?? []) as RecycledWaterStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(RecycledWaterStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + applyNotApplicableForLoadedTab(gridRows); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) { + const response = await statisticsInputWaterReuseApi.getRainFallUtilFactStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, rainFallUtilFactStatusList } = response.data; + const gridRows = mapRainFallUtilFactStatusListToGridRows( + (rainFallUtilFactStatusList ?? []) as RainFallUtilFactStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(RainFallUtilFactStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + applyNotApplicableForLoadedTab(gridRows); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) { + const response = + await statisticsInputWaterReuseApi.getTreatWaterReuseDesignConstStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, treatWaterReuseDesignConstStatusList } = response.data; + const gridRows = mapTreatWaterReuseStatusListToGridRows( + (treatWaterReuseDesignConstStatusList ?? + []) as TreatWaterReuseDesignConstStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(TreatWaterReuseDesignConstStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + applyNotApplicableForLoadedTab(gridRows); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) { + const response = + await statisticsInputWaterReuseApi.getTreatWaterReuseBusinessApprStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, treatWaterReuseBusinessApprStatusList } = + response.data; + const gridRows = mapTreatWaterReuseStatusListToGridRows( + (treatWaterReuseBusinessApprStatusList ?? + []) as TreatWaterReuseBusinessApprStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(TreatWaterReuseBusinessApprStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + setIsVerificationFailedByTab((prev) => + patchVerificationFailedByTab(prev, tabKey, commonInfo), + ); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + applyNotApplicableForLoadedTab(gridRows); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + tabKey === activeTabRef.current + ? (getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []) + : (rowDataMap.get(tabKey) ?? []); + + const commitActiveGrid = () => { + const gridView = getGridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } + }; + + const isCheckedRow = (row: Record): boolean => { + const v = row.__checked; + return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true"; + }; + + const getCheckedItemIndexesFromGrid = (rowCount: number): number[] => { + if (!getGridRef.current?.gridView) return []; + + const gridView = getGridRef.current.gridView as { + getCheckedItems?: () => number[]; + getCheckedRows?: () => number[]; + }; + + const checked = + (typeof gridView.getCheckedItems === "function" + ? gridView.getCheckedItems() + : typeof gridView.getCheckedRows === "function" + ? gridView.getCheckedRows() + : []) ?? []; + + return checked.filter( + (idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount, + ); + }; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setIsNotApplicableByTab(new Map()); + setDeleteListByTab(new Map()); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + loadedTabKeysRef.current.clear(); + setSggOptions([]); + sggOptionsSidoRef.current = null; + }; + + const handleSearch = async ( + values: SearchFormValues, + opts?: { forcedTabKey?: string }, + ) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod( + values.year || "", + ); + if (periodResponse.status !== "success") { + alert(periodResponse.message); + return; + } + const periods = Array.isArray(periodResponse.data) ? periodResponse.data : []; + const periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setIsVerificationFailedByTab(new Map()); + setToolbarSnapshotByTab(new Map()); + setDeleteListByTab(new Map()); + loadedTabKeysRef.current.clear(); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + if (opts?.forcedTabKey) { + activeTabRef.current = opts.forcedTabKey; + setActiveTab(opts.forcedTabKey); + } + + const tabForLoad = opts?.forcedTabKey ?? activeTabRef.current; + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildWaterReuseSearchParam(values); + searchParamRef.current = searchParam; + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (ARTICLE_EDIT_TAB_KEYS.has(tabForLoad) && loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + await loadActiveTabData( + tabForLoad, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + const prevTabKey = activeTabRef.current; + commitActiveGrid(); + 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; + if (loadedTabKeysRef.current.has(tabKey)) return; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildWaterReuseSearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; const submitFunc = () => { - // 제출 로직 + void (async () => { + const tabKey = activeTabRef.current; + const isPublicSewageTab = tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey; + const isArticleTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); + if (!isPublicSewageTab && !isArticleTab) { + alert("제출 API 연동 준비 중입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + if (isPublicSewageTab) { + const currentData = getTabGridRows(tabKey); + if (currentData.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + } else { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const isPublicSewageTab = tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey; + const isArticleTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); + if (!isPublicSewageTab && !isArticleTab) { + alert("제출 API 연동 준비 중입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitActiveGrid(); + + const submitRequestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + + let articleSggOptions: Sgg[] | null = null; + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = + tabStatsCdMap.get(tabKey) ?? + WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? + tabKey; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadAfterSubmit = async () => { + if (isArticleTab) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + if (articleSggOptions) { + setSggOptions(articleSggOptions); + } + } + const param = + searchParamRef.current ?? + buildWaterReuseSearchParam(lastSearchedValues); + await loadActiveTabData(tabKey, param, lastSearchedValues, true, false); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + let response: { + status: string; + message?: string; + data?: { finalStatus: string; nvpassed: boolean }; + }; + + if (isPublicSewageTab) { + const currentData = getTabGridRows(tabKey); + const submitList = currentData.map(buildPublicSewageTreatWaterReuseSaveItem); + if (submitList.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + + response = await statisticsInputWaterReuseApi.submitPublicSewageTreatWaterReuse({ + ...submitRequestBase, + userId: userInfo.userUnqId, + noApplicable: false, + submitList, + }); + } else { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + articleSggOptions = list; + + const articleSubmitRequest = { + ...submitRequestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + }; + + if (tabKey === RecycledWaterStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildRecycledWaterStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRecycledWaterStatusSaveItem(row, list)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = await statisticsInputWaterReuseApi.submitRecycledWaterStatus({ + ...articleSubmitRequest, + submitList, + deleteList, + }); + } else if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildRainFallUtilFactStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = await statisticsInputWaterReuseApi.submitRainFallUtilFactStatus({ + ...articleSubmitRequest, + submitList, + deleteList, + }); + } else if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputWaterReuseApi.submitTreatWaterReuseDesignConstStatus( + { + ...articleSubmitRequest, + submitList, + deleteList, + }, + ); + } else if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputWaterReuseApi.submitTreatWaterReuseBusinessApprStatus( + { + ...articleSubmitRequest, + submitList, + deleteList, + }, + ); + } else { + alert("제출 API 연동 준비 중입니다."); + return; + } + } + + if (response.status !== "success") { + alert(response.message ?? "데이터 제출 중 오류가 발생했습니다."); + return; + } + + const finalStatus = response.data?.finalStatus; + const nvpassed = response.data?.nvpassed; + + if (finalStatus === "5") { + setGlobalProgress(50); + setGlobalProgressLabel("IMR 검증 진행 중..."); + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", + stats_cd: statsCdForTab, + stats_yr: lastSearchedValues.year ?? "", + lae_sbmsn_stts_cd: + activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", + gen_verify_stts_cd: nvpassed ? "1" : "2", + }; + + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted( + imrResponse.job_id, + (id) => statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert, notifySubmitComplete: false }, + ); + const msg = + e instanceof Error && e.message + ? e.message + : "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요."; + alert(msg); + } + } else { + setGlobalProgress(100); + await reloadAfterSubmit(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitActiveGrid(); + + const requestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + + const reloadTabAfterSave = async () => { + const param = + searchParamRef.current ?? + buildWaterReuseSearchParam(lastSearchedValues); + await loadActiveTabData( + tabKey, + param, + lastSearchedValues, + true, + false, + ); + }; + + if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) { + const currentData = getTabGridRows(tabKey); + if (currentData.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.savePublicSewageTreatWaterReuse({ + ...requestBase, + userId: userInfo.userUnqId, + noApplicable: false, + saveList: currentData.map(buildPublicSewageTreatWaterReuseSaveItem), + }); + + if (response.status === "success") { + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === RecycledWaterStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildRecycledWaterStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRecycledWaterStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = await statisticsInputWaterReuseApi.saveRecycledWaterStatus({ + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildRainFallUtilFactStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.saveRainFallUtilFactStatus({ + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.saveTreatWaterReuseDesignConstStatus( + { + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }, + ); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + const sido = lastSearchedValues.sido ?? ""; + let list = sggOptions; + if (sggOptionsSidoRef.current !== sido || list.length === 0) { + list = await loadSggOptions(sido); + sggOptionsSidoRef.current = sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.saveTreatWaterReuseBusinessApprStatus( + { + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }, + ); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + } + })(); }; const updateOperateDataButtonFunc = () => { - // 운영 데이터 업데이트 로직 - tab1. 공공하수처리수 재이용 + void (async () => { + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!(await confirm("운영 데이터를 업데이트 하시겠습니까?"))) return; + + try { + showGlobalProgress({ label: "운영 데이터 업데이트 중...", progress: 0 }); + const userInfo = await getUserInfo(); + const request = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: userInfo.userUnqId, + }; + + const response = + await statisticsInputWaterReuseApi.updateWaterReuseOperationData(request); + if (response.status !== "success") { + alert(response.message || "운영 데이터 업데이트 중 오류가 발생했습니다."); + return; + } + + setGlobalProgress(50); + const param: WaterReuseRequestParams = { + statsYr: request.statsYr, + ctpvNm: request.ctpvNm, + sggNm: request.sggNm, + sewerBzmnCd: request.sewerBzmnCd, + }; + await loadActiveTabData( + PublicSewageTreatWaterReuseInpColumn.tabKey, + param, + lastSearchedValues, + true, + false, + ); + setGlobalProgress(100); + alert("운영 데이터가 업데이트되었습니다."); + } catch { + alert("운영 데이터 업데이트 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } + })(); }; - const addArticleFunc = () => { - console.log('save'); - // 항목 추가 로직 + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 추가할 수 없는 탭입니다."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + let list = sggOptions; + if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) { + list = await loadSggOptions(ctx.sido); + sggOptionsSidoRef.current = ctx.sido; + } + if (list.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + const newRow = createEmptyArticleRow(tabKey, ctx.sido, sggNm); + + flushSync(() => { + setSggOptions(list); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, [...currentRows, newRow]); + return next; + }); + }); + + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx); }; const deleteArticleFunc = () => { - console.log('save'); - // 항목 제외 로직 + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 제외할 수 없는 탭입니다."); + return; + } + + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + if (currentRows.length === 0) { + alert("삭제할 데이터가 없습니다."); + return; + } + + const checkedIndexes = getCheckedItemIndexesFromGrid(currentRows.length); + const checkedIndexSet = + checkedIndexes.length > 0 + ? new Set(checkedIndexes) + : new Set( + currentRows + .map((row, idx) => (isCheckedRow(row) ? idx : -1)) + .filter((idx) => idx >= 0), + ); + + const removedRows = currentRows.filter((_, idx) => checkedIndexSet.has(idx)); + const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx)); + if (removedRows.length === 0) { + alert("삭제할 항목을 선택해주세요."); + return; + } + + const persistedRemovedRows = removedRows.filter((row) => + isPersistedArticleRow(tabKey, row), + ); + + if (persistedRemovedRows.length > 0) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, [...(prev.get(tabKey) ?? []), ...persistedRemovedRows]); + return next; + }); + } + + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, nextRows); + return next; + }); + + const ctx = lastSearchedValues ?? searchValues; + if (ctx) { + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + nextRows.length > 0, + ctx, + ); + } }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const openVerificationHistoryDialog = () => { + setVerificationHistoryOpen(true); + }; + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of WaterReuseInpTabList) { + const tabKey = tab.tabKey; + const canEdit = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const rawColumns = (tab.columns ?? []) as ColumnConfig[]; + const base = canEdit + ? rawColumns + : rawColumns.map((col) => ({ ...col, editable: false as const })); + const columns = base.map((col) => { + let next: ColumnConfig = col; + if (col.fieldName === "ctpvNm") { + next = { ...col, editable: false as const }; + } + if ( + tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey && + col.fieldName === "sggNm" + ) { + next = { + ...col, + editable: false as const, + styleName: "rg-cell-readonly", + }; + } else if ( + ARTICLE_EDIT_TAB_KEYS.has(tabKey) && + col.fieldName === "sggNm" + ) { + next = buildSggNmColumn(col, canEdit, sggOptions); + } + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...next, tooltip: guide } : next; + }); + map.set(tabKey, { + columns, + columnLayout: tab.columnLayout ?? [], + }); + } + return map; + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab, sggOptions]); + + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + activeTabToolbar.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed; + const showSecondaryToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + + const isPublicSewageTreatTab = activeTab === PublicSewageTreatWaterReuseInpColumn.tabKey; + const isArticleEditTab = ARTICLE_EDIT_TAB_KEYS.has(activeTab); + const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false; + const showNotApplicableForActiveTab = + loadedTabKeysRef.current.has(activeTab) && + shouldShowNotApplicable( + activeTab, + tabRowDataExists(rowDataMap, activeTab), + activeTabStatus, + ); + const setIsNotApplicableForActiveTab: Dispatch> = useCallback( + (next) => { + const tabKey = activeTabRef.current; + const current = isNotApplicableByTab.get(tabKey) ?? false; + const resolved = + typeof next === "function" + ? (next as (prevState: boolean) => boolean)(current) + : next; + setIsNotApplicableForTab(tabKey, resolved); + }, + [isNotApplicableByTab, setIsNotApplicableForTab], + ); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""), + statsYr: ctx.year, + statsCd, + }; + })(); + + const verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); + return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | undefined => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors + .map((e) => { + const prefix = + e.vrfcCd === "IMR" + ? "IMR검증 - " + : e.vrfcCd === "NV" + ? "일반검증 - " + : "HB검증 - "; + return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; + }) + .filter(Boolean) + .join("\n"); + } + }; + + const { verificationSearchDefaults, searchFormRemountKey } = + useStatisticsInputPendingNavigation({ + urlPath: STATISTICS_INPUT_URL_PATH.WATER_REUSE, + onPending: (pending) => { + activeTabRef.current = pending.tabKey; + setActiveTab(pending.tabKey); + }, + onAutoSearch: (values, tabKey) => + handleSearch(values, { forcedTabKey: tabKey }), + }); return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForActiveTab && ( + + )} + {isPublicSewageTreatTab && showEnterableToolbarButtons && ( + + )} + {isArticleEditTab && showEnterableToolbarButtons && ( + <> + + + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {WaterReuseInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {WaterReuseInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + setRowDataMap((prev) => { + const next = new Map(prev); + const cur = next.get(tabKey) ?? []; + const nextRows = + typeof updater === "function" + ? updater(cur) + : updater; + next.set(tabKey, nextRows); + return next; + }); + }} + showHeaderCheckbox={ + ARTICLE_EDIT_TAB_KEYS.has(tabKey) && + tabEnterableForGrid + } + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + getGridRef={ + activeTab === tabKey ? getGridRef : undefined + } + /> + + ); + })} +
-
-
- - - - -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + ); }; -export default WaterReuseInp; \ No newline at end of file +export default WaterReuseInp; diff --git a/apps/st/src/components/statistics/management/AdmdstRenewalTab.tsx b/apps/st/src/components/statistics/management/AdmdstRenewalTab.tsx new file mode 100644 index 000000000..8e8aba373 --- /dev/null +++ b/apps/st/src/components/statistics/management/AdmdstRenewalTab.tsx @@ -0,0 +1,112 @@ +import { Button } from "@common/components"; +import DataGrid from "@/components/module/DataGrid"; +import type { ColumnInfo } from "@/components/type/DataGrid.type"; +import { + AdmdstRenewalReducedColumnConfig, + AdmdstRenewalAddedColumnConfig, +} from "@/components/util/column/management/AdmdstRenewalColumn"; + +import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog"; +import type { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab"; +import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast"; +import type { AdmdstRenewalAddedItem } from "@/components/statistics/management/types/population-tab-types"; + +const reducedColumnInfo: ColumnInfo = { + columns: AdmdstRenewalReducedColumnConfig.columns ?? [], + columnLayout: AdmdstRenewalReducedColumnConfig.columnLayout ?? [], +}; +const addedColumnInfo: ColumnInfo = { + columns: AdmdstRenewalAddedColumnConfig.columns ?? [], + columnLayout: AdmdstRenewalAddedColumnConfig.columnLayout ?? [], +}; + +type Props = { + tab: ReturnType; + statsYr: string; +}; + +const AdmdstRenewalTab = ({ tab, statsYr }: Props) => { + return ( +
+
+
+ + 감축된 행정구역(범위:동읍면) 자료 + +
+
+ +
+ +
+
+ + 추가된 행정구역 자료 + +
+
+ + +
+ { + if (fieldName !== "lawCd") return; // lawCd 셀만! + void tab.handleRowClick(fromGridRow(row), statsYr); + }} + /> +
+ + +
+ ); +}; + +export default AdmdstRenewalTab; \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/DomesticPopulationTab.tsx b/apps/st/src/components/statistics/management/DomesticPopulationTab.tsx new file mode 100644 index 000000000..02803315d --- /dev/null +++ b/apps/st/src/components/statistics/management/DomesticPopulationTab.tsx @@ -0,0 +1,136 @@ +import { Button } from "@common/components"; +import type { RealGridReact } from "realgrid-react"; +import DataGrid from "@/components/module/DataGrid"; +import type { ColumnInfo, GridViewWithEditCommit } from "@/components/type/DataGrid.type"; +import { + DomesticPopulationLeftColumnConfig, + DomesticPopulationRightColumnConfig, +} from "@/components/util/column/management/DomesticPopulationColumn"; +import { useRef } from "react"; + +import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog"; +import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab"; +import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast"; +import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface"; + + +const leftColumnInfo: ColumnInfo = { + columns: DomesticPopulationLeftColumnConfig.columns ?? [], + columnLayout: DomesticPopulationLeftColumnConfig.columnLayout ?? [], +}; +const rightColumnInfo: ColumnInfo = { + columns: DomesticPopulationRightColumnConfig.columns ?? [], + columnLayout: DomesticPopulationRightColumnConfig.columnLayout ?? [], +}; + +type DomesticTab = ReturnType>; + +type Props = { + tab: DomesticTab; + statsYr: string; + onFileButtonClick: () => void; +}; + +const DomesticPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => { + const rightGridRef = useRef(null); + + return ( +
+
+
+ + 엑셀 업로드 자료 (내국인 전체 리스트) + +
+
+ +
+ +
+ +
+
+ + 미매핑 리스트 (작년 대비) + +
+
+ +
+ { + if (fieldName === "mpinCd") return; + if (fieldName === "admdstCd") { + const gridView = rightGridRef.current?.gridView as + | GridViewWithEditCommit + | undefined; + gridView?.commit?.(); + setTimeout(() => { + tab.setRightRowData((prev) => + prev.map((r) => + r.admdstCd === row.admdstCd + ? { ...r, mpinCd: String(row.admdstCd ?? "") } + : r, + ), + ); + }, 0); + return; + } + }} + onCellDblClick={({ row, fieldName }) => { + if (fieldName !== "mpinCd") return; + void tab.handleRightRowClick(fromGridRow(row), statsYr); + }} + /> +
+ + +
+ ); +}; + +export default DomesticPopulationTab; \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/ForeignPopulationTab.tsx b/apps/st/src/components/statistics/management/ForeignPopulationTab.tsx new file mode 100644 index 000000000..c6c6e5895 --- /dev/null +++ b/apps/st/src/components/statistics/management/ForeignPopulationTab.tsx @@ -0,0 +1,135 @@ +import { Button } from "@common/components"; +import DataGrid from "@/components/module/DataGrid"; +import type { ColumnInfo, GridViewWithEditCommit } from "@/components/type/DataGrid.type"; +import { useRef } from "react"; +import type { RealGridReact } from "realgrid-react"; +import { + ForeignPopulationLeftColumnConfig, + ForeignPopulationRightColumnConfig, +} from "@/components/util/column/management/ForeignPopulationColumn"; + +import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog"; +import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab"; +import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast"; +import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types"; + +const leftColumnInfo: ColumnInfo = { + columns: ForeignPopulationLeftColumnConfig.columns ?? [], + columnLayout: ForeignPopulationLeftColumnConfig.columnLayout ?? [], +}; +const rightColumnInfo: ColumnInfo = { + columns: ForeignPopulationRightColumnConfig.columns ?? [], + columnLayout: ForeignPopulationRightColumnConfig.columnLayout ?? [], +}; + +type ForeignTab = ReturnType>; + +type Props = { + tab: ForeignTab; + statsYr: string; + onFileButtonClick: () => void; +}; + +const ForeignPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => { + const rightGridRef = useRef(null); + return ( +
+
+
+ + 엑셀 업로드 자료 (외국인 전체 리스트) + +
+
+ +
+ +
+ +
+
+ + 미매핑 리스트 (작년 대비) + +
+
+ +
+ + { + if (fieldName === "mpinCd") return; + if (fieldName === "admdstCd") { + const gridView = rightGridRef.current?.gridView as + | GridViewWithEditCommit + | undefined; + gridView?.commit?.(); + setTimeout(() => { + tab.setRightRowData((prev) => + prev.map((r) => + r.admdstCd === row.admdstCd + ? { ...r, mpinCd: String(row.admdstCd ?? "") } + : r, + ), + ); + }, 0); + return; + } + }} + onCellDblClick={({ row, fieldName }) => { + if (fieldName !== "mpinCd") return; + void tab.handleRightRowClick(fromGridRow(row), statsYr); + }} + /> +
+ + +
+ ); +}; + +export default ForeignPopulationTab; \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/LogHistory.tsx b/apps/st/src/components/statistics/management/LogHistory.tsx index b51578c0f..184a17dce 100644 --- a/apps/st/src/components/statistics/management/LogHistory.tsx +++ b/apps/st/src/components/statistics/management/LogHistory.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react"; import dayjs from "dayjs"; +import { CalendarIcon } from "lucide-react"; import useSWR from "swr"; import { useAlertStore } from "@/shared/lib"; @@ -17,9 +18,13 @@ import type { } from "@/entities/statistics/model/statistics-interface"; import { Button, - InputDatePicker, + Calendar, + Input, Label, LoadingWithAnimation, + Popover, + PopoverContent, + PopoverTrigger, Select, SelectContent, SelectItem, @@ -34,6 +39,91 @@ import { LogHistoryColumn } from "@/components/util/column/management/LogHistory import { toGridRows } from "@/components/util/grid/gridFunction"; const PAGE_SIZE = 100; +const MAX_DATE_RANGE_YEARS = 1; + +const isWithinMaxDateRange = (start: Date, end: Date) => { + const startDay = dayjs(start).startOf("day"); + const endDay = dayjs(end).startOf("day"); + return ( + !endDay.isBefore(startDay, "day") && + !endDay.isAfter(startDay.add(MAX_DATE_RANGE_YEARS, "year"), "day") + ); +}; + +type LogHistoryDateFieldProps = { + value: Date; + onChange: (date: Date) => void; + className?: string; +}; + +/** 로그 이력 전용: 날짜를 비울 수 없는 date input */ +const LogHistoryDateField = ({ + value, + onChange, + className, +}: LogHistoryDateFieldProps) => { + const [open, setOpen] = useState(false); + const [month, setMonth] = useState(value); + const dateValue = dayjs(value).format("YYYY-MM-DD"); + + useEffect(() => { + setMonth(value); + }, [value]); + + return ( +
+ { + if (!e.target.value) return; + const next = dayjs(e.target.value); + if (!next.isValid()) return; + onChange(next.toDate()); + }} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setOpen(true); + } + }} + /> + + + + + + { + if (!newDate) return; + onChange(newDate); + setOpen(false); + }} + /> + + +
+ ); +}; const getSidoListFetcher = async (): Promise> => { return statisticsApi.getSidoList(); @@ -63,13 +153,45 @@ const LogHistory = () => { string, string | number > | null>(null); - const [startDate, setStartDate] = useState(() => + const [startDate, setStartDate] = useState(() => dayjs().subtract(1, "month").startOf("day").toDate(), ); - const [endDate, setEndDate] = useState(() => + const [endDate, setEndDate] = useState(() => dayjs().endOf("day").toDate(), ); + const handleStartDateChange = (date: Date) => { + const next = dayjs(date).startOf("day"); + const end = dayjs(endDate).startOf("day"); + + if (next.isAfter(end, "day")) { + alert("시작일은 종료일보다 늦을 수 없습니다."); + return; + } + if (next.isBefore(end.subtract(MAX_DATE_RANGE_YEARS, "year"), "day")) { + alert("조회 기간은 최대 1년까지 설정할 수 있습니다."); + return; + } + + setStartDate(next.toDate()); + }; + + const handleEndDateChange = (date: Date) => { + const next = dayjs(date).startOf("day"); + const start = dayjs(startDate).startOf("day"); + + if (next.isBefore(start, "day")) { + alert("종료일은 시작일보다 빠를 수 없습니다."); + return; + } + if (next.isAfter(start.add(MAX_DATE_RANGE_YEARS, "year"), "day")) { + alert("조회 기간은 최대 1년까지 설정할 수 있습니다."); + return; + } + + setEndDate(next.toDate()); + }; + const { data: sidoList } = useSWR>( "log-history-sido-list", getSidoListFetcher, @@ -119,7 +241,7 @@ const LogHistory = () => { const handleSidoChange = (value: string) => { const selectedBsnm = - sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd ?? ""; + sidoList?.data?.sidoList.find((v) => v.sido === value)?.bsnmCd ?? ""; setSido(value); setGugun("ALL"); setBsnmCd(selectedBsnm); @@ -129,7 +251,7 @@ const LogHistory = () => { const listForBsnm = sggList?.data ?? []; const selectedBsnm = value === "ALL" - ? (sidoList?.data.sidoList.find((v) => v.sido === sido) + ? (sidoList?.data?.sidoList.find((v) => v.sido === sido) ?.bsnmCd ?? "") : (listForBsnm.find((v) => v.gugun === value)?.bsnmCd ?? ""); setGugun(value); @@ -141,10 +263,8 @@ const LogHistory = () => { const param: StatisticsLogRequest = { page: page1Based, size: PAGE_SIZE, - startDate: startDate - ? dayjs(startDate).format("YYYY-MM-DD") - : "", - endDate: endDate ? dayjs(endDate).format("YYYY-MM-DD") : "", + startDate: dayjs(startDate).format("YYYY-MM-DD"), + endDate: dayjs(endDate).format("YYYY-MM-DD"), ...(!(statsCd === "" || statsCd === "ALL") && { statsCd }), ...(sido && sido !== "ALL" && { ctpvNm: sido }), ...(gugun && gugun !== "ALL" && { sggNm: gugun }), @@ -165,6 +285,8 @@ const LogHistory = () => { const logList = logListRaw.map((l) => ({ ...l, sggNm: String(l.sggNm ?? "").trim() || "-", + ctpvNm: String(l.ctpvNm ?? "").trim() || "-", + statsYr: String(l.statsYr ?? "").trim() || "-", })); setRowData(toGridRows(logList)); @@ -196,6 +318,18 @@ const LogHistory = () => { const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); + + const start = dayjs(startDate); + const end = dayjs(endDate); + if (end.isBefore(start, "day")) { + alert("종료일은 시작일보다 빠를 수 없습니다."); + return; + } + if (!isWithinMaxDateRange(startDate, endDate)) { + alert("조회 기간은 최대 1년까지 설정할 수 있습니다."); + return; + } + await loadLogs(1); }; @@ -294,15 +428,15 @@ const LogHistory = () => {
- -
diff --git a/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx b/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx index 359eba00d..416543fa1 100644 --- a/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx +++ b/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { GridFitStyle } from "realgrid"; import { @@ -15,17 +15,25 @@ import { } from "@common/components"; import DataGrid from "@/components/module/DataGrid"; -import type { ColumnInfo } from "@/components/type/DataGrid.type"; +import type { + ColumnInfo, + DataCellClickCallbackParams, +} from "@/components/type/DataGrid.type"; import { alignLogDetailRowsToColumns, buildLogHistoryDetailColumnInfo, emptyLogHistoryDetailColumnInfo, + LOG_DETAIL_DETAILS_FIELD, + LOG_DETAIL_DETAILS_RAW_FIELD, + type LogDetailVerificationItem, + prepareLogDetailRowsForGrid, } from "@/components/util/column/management/LogHistoryDetailColumn"; -import { toGridRows } from "@/components/util/grid/gridFunction"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; -import type { StatisticsLogDetailRow } from "@/entities/statistics/model/statistics-interface"; +import { UserApi } from "@/entities/user"; import { useAlertStore } from "@/shared/lib"; +import { LogHistoryDetailsNestedDialog } from "./LogHistoryDetailsNestedDialog"; + export interface LogHistoryDetailDialogProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -45,11 +53,19 @@ export function LogHistoryDetailDialog({ const [columnInfo, setColumnInfo] = useState( emptyLogHistoryDetailColumnInfo, ); + const [userName, setUserName] = useState(""); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [selectedDetails, setSelectedDetails] = useState< + LogDetailVerificationItem[] + >([]); useEffect(() => { if (!open) { setRowData([]); setColumnInfo(emptyLogHistoryDetailColumnInfo); + setUserName(""); + setDetailsDialogOpen(false); + setSelectedDetails([]); setIsLoading(false); return; } @@ -57,23 +73,42 @@ export function LogHistoryDetailDialog({ if (!row) return; const sn = row.statsLogSn; + const rgtrId = + row.rgtrId != null && String(row.rgtrId).trim() !== "" + ? String(row.rgtrId).trim() + : ""; let cancelled = false; void (async () => { setIsLoading(true); setRowData([]); + setUserName(""); try { - const response = await statisticsApi.getLogDetail( - sn !== undefined && sn !== null ? String(sn) : "", - ); + const userLookupPromise = + rgtrId !== "" + ? UserApi.findByUserUnqId(rgtrId).catch(() => { + return null; + }) + : Promise.resolve(null); + + const [response, userResponse] = await Promise.all([ + statisticsApi.getLogDetail( + sn !== undefined && sn !== null ? String(sn) : "", + ), + userLookupPromise, + ]); + + if (userResponse) { + setUserName(userResponse.userName ?? ""); + } if (cancelled) return; if (response.status !== "success") { alert( response.message ?? - "로그 상세 정보를 불러오는데 실패했습니다.", + "로그 상세 정보를 불러오는데 실패했습니다.", ); return; } @@ -83,7 +118,7 @@ export function LogHistoryDetailDialog({ : []; const nextColumnInfo = buildLogHistoryDetailColumnInfo(list); const nextRows = alignLogDetailRowsToColumns( - toGridRows(list as StatisticsLogDetailRow[]), + prepareLogDetailRowsForGrid(list), nextColumnInfo, ); setColumnInfo(nextColumnInfo); @@ -102,6 +137,25 @@ export function LogHistoryDetailDialog({ }; }, [open, row, alert]); + const handleDataCellClick = useCallback( + ({ fieldName, row }: DataCellClickCallbackParams) => { + if (fieldName !== LOG_DETAIL_DETAILS_FIELD) return; + + const raw = row[LOG_DETAIL_DETAILS_RAW_FIELD]; + if (raw == null || String(raw).trim() === "") return; + + try { + const parsed = JSON.parse(String(raw)) as unknown; + if (!Array.isArray(parsed) || parsed.length === 0) return; + setSelectedDetails(parsed as LogDetailVerificationItem[]); + setDetailsDialogOpen(true); + } catch { + alert("상세 데이터를 불러오는데 실패했습니다."); + } + }, + [alert], + ); + const gridRemountKey = useMemo( () => columnInfo.columns.map((c) => c.fieldName).join("\u0001"), [columnInfo], @@ -110,13 +164,14 @@ export function LogHistoryDetailDialog({ const showGrid = !isLoading && rowData.length > 0; return ( - - + <> + + - 로그 상세 정보 + 로그 상세 정보(기능 실행자: {userName})
{isLoading && ( @@ -136,7 +191,8 @@ export function LogHistoryDetailDialog({ isExcelExport={false} enableSort={false} gridHeight={55} - fitStyle={GridFitStyle.NONE} + fitStyle={GridFitStyle.EVEN} + onDataCellClick={handleDataCellClick} /> ) : ( !isLoading && ( @@ -153,7 +209,13 @@ export function LogHistoryDetailDialog({ - -
+
+
+ + ); } diff --git a/apps/st/src/components/statistics/management/LogHistoryDetailsNestedDialog.tsx b/apps/st/src/components/statistics/management/LogHistoryDetailsNestedDialog.tsx new file mode 100644 index 000000000..bc6069847 --- /dev/null +++ b/apps/st/src/components/statistics/management/LogHistoryDetailsNestedDialog.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@common/components"; + +import type { LogDetailVerificationItem } from "@/components/util/column/management/LogHistoryDetailColumn"; + +export interface LogHistoryDetailsNestedDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + items: LogDetailVerificationItem[]; +} + +export function LogHistoryDetailsNestedDialog({ + open, + onOpenChange, + items, +}: LogHistoryDetailsNestedDialogProps) { + return ( + + + + 검증 상세 + +
+ {items.length === 0 ? ( +

+ 표시할 상세 데이터가 없습니다. +

+ ) : ( + items.map((item, index) => ( +
+
+

+ + 조합순번: + {" "} + {item.eqtnCombSeq ?? "-"} +

+

+ + 검증상세순번: + {" "} + {item.vrfcDtlSn ?? "-"} +

+

+ + 비교연산: + {" "} + {item.cmprOpert ?? "-"} +

+

+ + 참조통계코드: + {" "} + {item.refStatsCd ?? "-"} +

+

+ + 한글 수식: + {" "} + {item.kornArtopnEqtnCn ?? "-"} +

+

+ + 영문 수식: + {" "} + {item.engArtopnEqtnCn ?? "-"} +

+
+
+ )) + )} +
+ + + + + +
+
+ ); +} diff --git a/apps/st/src/components/statistics/management/NotEnteredStatus.tsx b/apps/st/src/components/statistics/management/NotEnteredStatus.tsx index 7b0740b27..ea0c0aaf4 100644 --- a/apps/st/src/components/statistics/management/NotEnteredStatus.tsx +++ b/apps/st/src/components/statistics/management/NotEnteredStatus.tsx @@ -213,6 +213,7 @@ function buildRowsFromStatusList( statKeys: string[], ): Record[] { const rowMap = new Map>(); + 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( diff --git a/apps/st/src/components/statistics/management/PopulationStatusChart.tsx b/apps/st/src/components/statistics/management/PopulationStatusChart.tsx new file mode 100644 index 000000000..fa0f8ba5b --- /dev/null +++ b/apps/st/src/components/statistics/management/PopulationStatusChart.tsx @@ -0,0 +1,119 @@ +import { Chart } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarController, + BarElement, + LineController, + LineElement, + PointElement, + Title, + Tooltip, + Legend, +} from "chart.js"; +import type { ChartOptions } from "chart.js"; + +ChartJS.register( + CategoryScale, + LinearScale, + BarController, + BarElement, + LineController, + LineElement, + PointElement, + Title, + Tooltip, + Legend, +); + +// 차트 설정 외부 정의 (Mixed 타입 명시) +const options: ChartOptions<"bar" | "line"> = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { stacked: true }, + y: { + stacked: true, + beginAtZero: true, + title: { display: true, text: "인구수(명)" }, + }, + y1: { + position: "right", + beginAtZero: true, + grid: { drawOnChartArea: false }, + title: { display: true, text: "총 인구(명)" }, + }, + }, + plugins: { + legend: { position: "bottom" }, + }, +}; + +export type PopulationChartData = { + X_AXIS: string[]; + TOT: number[]; + ORIGIN_MALE: (number | null)[]; + ORIGIN_FEMALE: (number | null)[]; + FOREIGN_MALE: (number | null)[]; + FOREIGN_FEMALE: (number | null)[]; +}; + +type Props = { + chartData: PopulationChartData | null; +}; + +const SERIES_META = [ + { key: "ORIGIN_MALE", name: "내국인 남성", color: "#3B82F6" }, + { key: "ORIGIN_FEMALE", name: "내국인 여성", color: "#EC4899" }, + { key: "FOREIGN_MALE", name: "외국인 남성", color: "#10B981" }, + { key: "FOREIGN_FEMALE", name: "외국인 여성", color: "#F59E0B" }, +] as const; + +const PopulationStatusChart = ({ chartData }: Props) => { + if (!chartData || chartData.X_AXIS.length === 0) { + return ( +
+ 조회된 데이터가 없습니다. 조회 버튼을 눌러주세요. +
+ ); + } + + const data = { + labels: chartData.X_AXIS.map((yr) => `${yr}년`), + datasets: [ + ...SERIES_META + .filter((s) => chartData[s.key]?.some((v) => v !== null)) + .map((s) => ({ + type: "bar" as const, + label: s.name, + data: chartData[s.key].map((v) => v ?? 0), + backgroundColor: s.color, + stack: "stack1", + })), + { + type: "line" as const, + label: "총 인구", + data: chartData.TOT, + borderColor: "#1F2937", + backgroundColor: "#1F2937", + borderDash: [5, 5], + tension: 0.1, + yAxisID: "y1", + }, + ], + }; + + return ( +
+ {/* 제네릭 타입을 통해 Mixed Chart 지원, 부모 컨테이너 크기에 맞춰 반응형으로 리사이즈됨 */} + + type="bar" + data={data} + options={options} + /> +
+ ); +}; + +export default PopulationStatusChart; diff --git a/apps/st/src/components/statistics/management/PopulationStatusTab.tsx b/apps/st/src/components/statistics/management/PopulationStatusTab.tsx new file mode 100644 index 000000000..f84103d4d --- /dev/null +++ b/apps/st/src/components/statistics/management/PopulationStatusTab.tsx @@ -0,0 +1,42 @@ +import DataGrid from "@/components/module/DataGrid"; +import type { ColumnInfo } from "@/components/type/DataGrid.type"; +import { PopulationStatusColumnConfig } from "@/components/util/column/management/PopulationStatusColumn"; + +import type { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab"; +import { asGridRowData, asGridRowSetter } from "@/components/statistics/management/lib/gridCast"; + +const columnInfo: ColumnInfo = { + columns: PopulationStatusColumnConfig.columns ?? [], + columnLayout: PopulationStatusColumnConfig.columnLayout ?? [], +}; + +type Props = { + tab: ReturnType; +}; + +const PopulationStatusTab = ({ tab }: Props) => { + return ( + <> +
+ + 행정 인구 현황 + +
+ + + + ); +}; + +export default PopulationStatusTab; \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/PopulationVizTab.tsx b/apps/st/src/components/statistics/management/PopulationVizTab.tsx new file mode 100644 index 000000000..0792148ec --- /dev/null +++ b/apps/st/src/components/statistics/management/PopulationVizTab.tsx @@ -0,0 +1,22 @@ +import PopulationStatusChart from "./PopulationStatusChart"; +import type { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab"; + +type Props = { + tab: ReturnType; +}; + +const PopulationVizTab = ({ tab }: Props) => { + return ( + <> + + 행정인구현황 시각화 (기준연도 ~ 과거 자료(최대 5년) / *단위: 명) + + +
+ +
+ + ); +}; + +export default PopulationVizTab; \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx index cb63324eb..2a813ed95 100644 --- a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx +++ b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx @@ -33,6 +33,12 @@ function rowSeTypeToCode(row: Record): "INPT" | "VRFC" return "INPT"; } +function parseExtSchdlId(value: string | number | undefined): number | undefined { + if (typeof value === "number" && Number.isFinite(value)) return value; + const parsed = parseInt(String(value ?? ""), 10); + return Number.isFinite(parsed) ? parsed : undefined; +} + function parseYmdToDate(ymd: string): Date | undefined { const s = String(ymd ?? "").replace(/\D/g, ""); if (s.length !== 8) return undefined; @@ -132,6 +138,7 @@ const StatisticsInputManagement = () => { const seCd = Number.isFinite(seCdNum) ? seCdNum : 0; const statsYr = String(row.statsYr ?? ""); const seType = rowSeTypeToCode(row); + const extSchdlId = parseExtSchdlId(row.extSchdlId); try { setIsLoading(true); @@ -139,6 +146,7 @@ const StatisticsInputManagement = () => { statsYr, seType, seCd, + ...(extSchdlId != null ? { extSchdlId } : {}), }); if (res.status !== "success" || !res.data) { @@ -147,8 +155,10 @@ const StatisticsInputManagement = () => { } const detail = res.data; + const detailExtSchdlId = parseExtSchdlId(detail.extSchdlId) ?? extSchdlId; setPeriodDialogMode("edit"); setPeriodDialogInitial({ + extSchdlId: detailExtSchdlId, statsYr: String(detail.statsYr ?? statsYr), seType: detail.seType === "VRFC" ? "VRFC" : "INPT", seCd: diff --git a/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx b/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx index 4e0c878da..8ec3c30ec 100644 --- a/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx +++ b/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx @@ -31,6 +31,7 @@ import { import { getUserInfo } from "../../../../../../packages/common/components/src/api"; export type StatisticsInputPeriodDialogValues = { + extSchdlId?: number; statsYr: string; seType: "INPT" | "VRFC"; seCd: number; @@ -146,6 +147,12 @@ export function StatisticsInputPeriodDialog({ }; if (!isCreate) { param.seCd = seCdParsed; + if ( + initial?.extSchdlId != null && + Number.isFinite(initial.extSchdlId) + ) { + param.extSchdlId = initial.extSchdlId; + } } const res = await statisticsApi.saveStatsInputPeriod(param); if (res.status === "success") { @@ -191,6 +198,9 @@ export function StatisticsInputPeriodDialog({ inptEndYmd: end, rgtrId: userInfo.userUnqId, }; + if (initial.extSchdlId != null && Number.isFinite(initial.extSchdlId)) { + param.extSchdlId = initial.extSchdlId; + } const res = await statisticsApi.deleteStatsInputPeriod(param); if (res.status === "success") { alert("삭제되었습니다."); diff --git a/apps/st/src/components/statistics/management/StatisticsPopulationManagement.tsx b/apps/st/src/components/statistics/management/StatisticsPopulationManagement.tsx index c0c5e0de1..c33feff95 100644 --- a/apps/st/src/components/statistics/management/StatisticsPopulationManagement.tsx +++ b/apps/st/src/components/statistics/management/StatisticsPopulationManagement.tsx @@ -1,11 +1,285 @@ +import { useRef, useState } from "react"; + +import { Button, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import SearchForm from "@/components/module/SearchForm"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; +import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface"; + +import { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab"; +import { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab"; +import { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab"; +import { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab"; + +import DomesticPopulationTab from "./DomesticPopulationTab"; +import ForeignPopulationTab from "./ForeignPopulationTab"; +import AdmdstRenewalTab from "./AdmdstRenewalTab"; +import PopulationStatusTab from "./PopulationStatusTab"; +import PopulationVizTab from "./PopulationVizTab"; + +import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types"; + +const TAB_LIST = [ + { key: "domestic", label: "행정인구등록" }, + { key: "foreign", label: "외국인인구등록" }, + { key: "renewal", label: "행정동갱신" }, + { key: "status", label: "행정인구현황" }, + { key: "statusViz", label: "행정인구현황 시각화" }, +]; const StatisticsPopulationManagement = () => { + const [activeTab, setActiveTab] = useState("domestic"); + + /** 검색폼은 모든 탭이 공유. 마지막 조회 조건만 ref로 기억해뒀다가 탭 전환 시 재사용 */ + const lastSearchedValuesRef = useRef(null); + + /* ── 탭별 상태/로직은 전부 훅에서 가져온다. 탭이 화면에 보이지 않아도 훅은 항상 호출되므로 + 탭을 왔다갔다 해도 데이터가 유지된다 (원본 컴포넌트와 동일한 동작) ── */ + const domesticTab = usePopulationMappingTab({ + kind: "domestic", + getList: statisticsPpltnApi.getDomesticPopulationList, + uploadExcel: statisticsPpltnApi.uploadDomesticPopulationExcel, + saveFinal: statisticsPpltnApi.saveDomesticPopulationFinal, + getUpperCodesByName: statisticsPpltnApi.getUpperCodesByName, + requiredHeaderKeywords: ["행정기관코드", "행정기관"], + invalidFileMessage: "행정인구(내국인) 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요.", + }); + + const foreignTab = usePopulationMappingTab({ + kind: "foreign", + getList: async (statsYr, opt1) => { + const res = await statisticsPpltnApi.getForeignPopulationList(statsYr, opt1); + // res.data를 확실하게 ForeignPopulationItem[]로 캐스팅 + return { ...res, data: res.data as ForeignPopulationItem[] }; +}, + uploadExcel: statisticsPpltnApi.uploadForeignPopulationExcel, + saveFinal: statisticsPpltnApi.saveForeignPopulationFinal, + getUpperCodesByName: statisticsPpltnApi.getUpperCodesByName, + requiredHeaderKeywords: ["행정동코드", "행정동명", "외국인수"], + invalidFileMessage: "외국인 인구 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요.", + }); + + const renewalTab = useAdmdstRenewalTab(); + const statusTab = usePopulationStatusTab(); + const vizTab = usePopulationVizTab(); + + const statsYr = lastSearchedValuesRef.current?.year ?? ""; + + const isLoading = + (activeTab === "domestic" && domesticTab.isLoading) || + (activeTab === "foreign" && foreignTab.isLoading) || + (activeTab === "renewal" && renewalTab.isLoading) || + (activeTab === "status" && statusTab.isLoading) || + (activeTab === "statusViz" && vizTab.isLoading); + + /* ════════════════ 공통 ════════════════ */ + const handleSearch = async (values: SearchFormValues) => { + lastSearchedValuesRef.current = values; + const yr = values.year ?? ""; + + if (activeTab === "domestic") await domesticTab.search(yr); + else if (activeTab === "foreign") await foreignTab.search(yr); + else if (activeTab === "renewal") await renewalTab.loadAll(yr); + else if (activeTab === "status") await statusTab.search(yr); + else if (activeTab === "statusViz") await vizTab.search(yr); + }; + + const handleReset = () => { + domesticTab.reset(); + foreignTab.reset(); + renewalTab.reset(); + statusTab.reset(); + vizTab.reset(); + setActiveTab("domestic"); + lastSearchedValuesRef.current = null; + }; + + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const values = lastSearchedValuesRef.current; + if (!values) return; + const yr = values.year ?? ""; + + if (tabKey === "domestic") await domesticTab.loadOnActivate(yr); + else if (tabKey === "foreign") await foreignTab.loadOnActivate(yr); + else if (tabKey === "renewal") await renewalTab.loadOnActivate(yr); + // status / statusViz는 원본에서도 탭 전환 시 자동 재조회하지 않음 (그대로 유지) + }; + return ( - <> - - +
+ void domesticTab.handleFileChange(e, statsYr)} + /> + void foreignTab.handleFileChange(e, statsYr)} + /> + +
+ + + {activeTab === "status" && ( +
+ + + + +
+ )} + + {activeTab === "statusViz" && ( +
+ + + + + + +
+ )} +
+ + {isLoading && ( +
+
+ +
+
+ )} + +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {TAB_LIST.map((tab) => ( + + {tab.label} + + ))} + + + + domesticTab.fileInputRef.current?.click()} + /> + + + + foreignTab.fileInputRef.current?.click()} + /> + + + + + + + + + + + + + + +
+
); }; -export default StatisticsPopulationManagement; +export default StatisticsPopulationManagement; \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/UpperCodeSelectDialog.tsx b/apps/st/src/components/statistics/management/UpperCodeSelectDialog.tsx new file mode 100644 index 000000000..ced0faf4d --- /dev/null +++ b/apps/st/src/components/statistics/management/UpperCodeSelectDialog.tsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@common/components"; + +interface UpperCodeSelectDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + candidates: Record[]; + codeField: string; + nameField: string; + onSelect: (item: Record) => void; + extraFields?: { field: string; header: string }[]; +} + +export const UpperCodeSelectDialog = ({ + open, + onOpenChange, + candidates, + codeField, + nameField, + extraFields, + onSelect, +}: UpperCodeSelectDialogProps) => { + const [selectedCode, setSelectedCode] = useState(null); + + const handleSelect = () => { + const item = candidates.find((c) => c[codeField] === selectedCode); + if (!item) return; + onSelect(item); + onOpenChange(false); + setSelectedCode(null); + }; + + const handleClose = () => { + onOpenChange(false); + setSelectedCode(null); + }; + + return ( + + + + 상위 코드 리스트 + +
+
${k}${k} ${safe(v)}
+ + + + + {extraFields?.map((f) => ( + + ))} + + +
+ 코드 + + 동명 + + {f.header} +
+
+ + + {candidates.length === 0 ? ( + + + + ) : ( + candidates.map((item,index) => ( + setSelectedCode(item[codeField] ?? "")} + className={`cursor-pointer border-b border-gray-200 transition-colors + ${selectedCode === item[codeField] + ? "bg-blue-100" + : "hover:bg-gray-50" + }`} + > + + + {extraFields?.map((f) => ( + + ))} + + )) + )} + +
+ 후보 코드가 없습니다. +
+ {item[codeField]} + + {item[nameField]} + + {item[f.field]} +
+
+
+ + + + + + + ); +}; \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/hooks/useAdmdstRenewalTab.ts b/apps/st/src/components/statistics/management/hooks/useAdmdstRenewalTab.ts new file mode 100644 index 000000000..a45d3923f --- /dev/null +++ b/apps/st/src/components/statistics/management/hooks/useAdmdstRenewalTab.ts @@ -0,0 +1,171 @@ +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +// TODO: 원본 파일은 `../../../../../../packages/common/components/src/api` (6단계 상위) 였음. +// 이 훅 파일은 원본 컴포넌트보다 한 단계 더 깊은 폴더(hooks/)에 위치하므로 +// 실제 프로젝트에 옮길 때 상대경로 깊이를 다시 맞춰줘야 함 (혹은 @common/components에 getUserInfo가 +// re-export 되어 있다면 그쪽으로 교체). +import { getUserInfo } from "../../../../../../../packages/common/components/src/api"; + +import { useAsyncAction } from "./useAsyncAction"; +import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface"; +import type { + AdmdstRenewalAddedItem, + AdmdstRenewalAddedSaveItem, + AdmdstRenewalReducedItem, + AdmdstRenewalReducedSaveItem, +} from "@/components/statistics/management/types/population-tab-types"; + +export function useAdmdstRenewalTab() { + const { setMessage: alert } = useAlertStore((state) => state); + const { isLoading, run } = useAsyncAction(alert); + + const [reducedRowData, setReducedRowData] = useState([]); + const [addedRowData, setAddedRowData] = useState([]); + + const [dialogOpen, setDialogOpen] = useState(false); + const [candidates, setCandidates] = useState([]); + const selectedRowIndexRef = useRef(null); + + /** OPT_1: "1" = 추가된 행정구역, "2" = 감축된 행정구역 (원본 그대로) */ + const loadAll = async (statsYr: string): Promise => { + if (!statsYr) { alert("연도를 선택해주세요."); return false; } + const result = await run(async () => { + const [addedRes, reducedRes] = await Promise.all([ + statisticsPpltnApi.searchChangedArea(statsYr, "1"), + statisticsPpltnApi.searchChangedArea(statsYr, "2"), + ]); + if (addedRes.status === "success") { + setAddedRowData(toGridRows((addedRes.data as object[]) ?? []) as unknown as AdmdstRenewalAddedItem[]); + } + if (reducedRes.status === "success") { + setReducedRowData(toGridRows((reducedRes.data as object[]) ?? []) as unknown as AdmdstRenewalReducedItem[]); + } + return true; + }); + return result ?? false; + }; + + /** 탭 전환 시 - 아직 한 번도 로드되지 않았을 때만 로드 */ + const loadOnActivate = async (statsYr: string) => { + if (reducedRowData.length === 0 && addedRowData.length === 0) await loadAll(statsYr); + }; + + const handleFinal = async (statsYr: string) => { + if (!statsYr) { alert("먼저 조회를 진행해주세요."); return; } + if (reducedRowData.length === 0 && addedRowData.length === 0) { + alert("반영할 데이터가 없습니다."); + return; + } + + + await run(async () => { + const userInfo = await getUserInfo(); +const addedList: AdmdstRenewalAddedSaveItem[] = addedRowData + .filter((row) => { + const lawCd = String(row.lawCd ?? "").trim(); + return lawCd !== "" && lawCd !== "null"; + }) + .map((row) => ({ + adstCd: String(row.adstCd ?? ""), + lawCd: String(row.lawCd ?? ""), + adstDstrNm: String(row.adstDstrNm ?? ""), + rgnCd: String(row.rgnCd ?? ""), + lv: String(row.lv ?? ""), + sido: String(row.sido ?? ""), + gugun: String(row.gugun ?? ""), + adstDong: String(row.adstDong ?? ""), + userId: userInfo.userUnqId, + })); + + + +const reducedList: AdmdstRenewalReducedSaveItem[] = reducedRowData.map((row) => ({ + adstCd: String(row.adstCd ?? ""), +})); + + const res = await statisticsPpltnApi.saveChangedArea(reducedList, addedList); + if (res.status === "success") { + await loadAll(statsYr); + alert("행정구역 감축 및 추가사항이 성공적으로 반영되었습니다."); + } else { + alert(res.message || "반영 중 오류가 발생했습니다."); + } + }); + + }; + + const handleRowClick = async (rowData: AdmdstRenewalAddedItem, _statsYr: string) => { + const adstCd = String(rowData.adstCd ?? ""); + if (!adstCd) { alert("행정동코드가 없습니다."); return; } + + const parts = adstCd.trim().split(/\s+/); + const dongNm = parts[parts.length - 1] ?? adstCd; + + await run( + async () => { + const res = await statisticsPpltnApi.getLawCdsByName(dongNm); + if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) { + setCandidates(res.data); + const rowIndex = addedRowData.findIndex((r) => r.adstCd === rowData.adstCd); + selectedRowIndexRef.current = rowIndex; + setDialogOpen(true); + } else { + alert("해당 지역명에 대한 법정동코드 후보가 없습니다."); + } + }, + { errorMessage: "법정동코드 조회 중 오류가 발생했습니다." }, + ); +}; + const handleCodeSelect = (item: Record) => { + const rowIndex = selectedRowIndexRef.current; + if (rowIndex === null) return; + setAddedRowData((prev) => + prev.map((row, idx) => + idx === rowIndex + ? { ...row, lawCd: item["lawCd"] ?? "", lawNm: item["lawNm"] ?? "", rgnCd: item["rgnCd"] ?? "", lv: item["lv"] ?? "" } + : row, + ), + ); + + selectedRowIndexRef.current = null; +}; + +const handleRefresh = async () => { + await run(async () => { + const res = await statisticsPpltnApi.refreshMaterializedViews(); + if (res.status === "success") { + alert("뷰 갱신이 완료되었습니다."); + } else { + alert("뷰 갱신 중 오류가 발생했습니다."); + } + }); +}; + const reset = () => { + setReducedRowData([]); + setAddedRowData([]); + setDialogOpen(false); + setCandidates([]); + selectedRowIndexRef.current = null; + }; + + return { + isLoading, + reducedRowData, + addedRowData, + setReducedRowData, + setAddedRowData, + dialogOpen, + setDialogOpen, + candidates, + loadAll, + loadOnActivate, + handleFinal, + handleRowClick, + handleCodeSelect, + handleRefresh, + reset, + }; +} \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/hooks/useAsyncAction.ts b/apps/st/src/components/statistics/management/hooks/useAsyncAction.ts new file mode 100644 index 000000000..fc03df2a0 --- /dev/null +++ b/apps/st/src/components/statistics/management/hooks/useAsyncAction.ts @@ -0,0 +1,41 @@ +import { useCallback, useState } from "react"; + +type AsyncActionOptions = { + /** onError가 없을 때 사용할 기본 에러 메시지 */ + errorMessage?: string; + /** 커스텀 에러 처리가 필요할 때 - 지정하면 errorMessage 대신 이 콜백이 호출됨 */ + onError?: (error: unknown) => void; +}; + +/** + * 탭별 hook(usePopulationMappingTab, useAdmdstRenewalTab 등)에서 + * 반복되던 `setIsLoading(true) → try → catch(alert) → finally(setIsLoading(false))` + * 패턴을 공통화한 훅. + * + * notify: 실패 시 사용자에게 보여줄 알럿 함수 (useAlertStore의 setMessage를 그대로 넘기면 됨) + */ +export function useAsyncAction(notify: (message: string) => void) { + const [isLoading, setIsLoading] = useState(false); + + const run = useCallback( + async (action: () => Promise, options: AsyncActionOptions = {}): Promise => { + const { errorMessage = "서버 연결 중 오류가 발생했습니다.", onError } = options; + try { + setIsLoading(true); + return await action(); + } catch (error) { + if (onError) { + onError(error); + } else { + notify(errorMessage); + } + return undefined; + } finally { + setIsLoading(false); + } + }, + [notify], + ); + + return { isLoading, run }; +} \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/hooks/usePopulationMappingTab.ts b/apps/st/src/components/statistics/management/hooks/usePopulationMappingTab.ts new file mode 100644 index 000000000..5874e9038 --- /dev/null +++ b/apps/st/src/components/statistics/management/hooks/usePopulationMappingTab.ts @@ -0,0 +1,200 @@ +import { useRef, useState } from "react"; +import * as XLSX from "xlsx"; + +import { useAlertStore } from "@/shared/lib"; +import { toGridRows } from "@/components/util/grid/gridFunction"; + +import { useAsyncAction } from "./useAsyncAction"; +import type { + PopulationMappingApiConfig, + PopulationMappingRow, + UpperCodeCandidateItem, +} from "@/components/statistics/management/types/population-tab-types"; + +/* ── 엑셀 헤더 검증 (원본 컴포넌트의 validateExcelHeader를 그대로 이동) ── */ +const validateExcelHeader = (file: File, requiredKeywords: string[]): Promise => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = e.target?.result; + const workbook = XLSX.read(data, { type: "binary" }); + const sheetName = workbook.SheetNames[0]; + if (!sheetName) { resolve(false); return; } + const sheet = workbook.Sheets[sheetName]; + if (!sheet) { resolve(false); return; } + const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 }); + const headerRow = (rows[0] as string[]).map((h) => String(h ?? "")); + const isValid = requiredKeywords.every((kw) => headerRow.some((h) => h.includes(kw))); + resolve(isValid); + } catch { + resolve(false); + } + }; + reader.readAsBinaryString(file); + }); +}; + +/** + * 내국인/외국인 탭이 공유하는 "좌측 전체 리스트 + 우측 미매핑 리스트" 패턴 훅. + * 두 탭은 API 함수와 필드명만 다를 뿐 동작이 완전히 동일해서 config로 주입받는다. + */ +export function usePopulationMappingTab( + config: PopulationMappingApiConfig, +) { + const { setMessage: alert } = useAlertStore((state) => state); + const { isLoading, run } = useAsyncAction(alert); + + const [leftRowData, setLeftRowData] = useState([]); + const [rightRowData, setRightRowData] = useState([]); + + const [dialogOpen, setDialogOpen] = useState(false); + const [candidates, setCandidates] = useState([]); + const selectedRowIndexRef = useRef(null); + + const fileInputRef = useRef(null); + + const loadLeft = async (statsYr: string) => { + const res = await config.getList(statsYr, "1"); + if (res.status === "success") setLeftRowData(toGridRows((res.data as object[]) ?? []) as unknown as TRow[]); + }; + + const loadRight = async (statsYr: string) => { + const res = await config.getList(statsYr, "2"); + if (res.status === "success") setRightRowData(toGridRows((res.data as object[]) ?? []) as unknown as TRow[]); + }; + + /** 탭 최초 진입/활성화 시 - 좌/우 모두 갱신 (업로드 직후에도 사용) */ + const loadAll = async (statsYr: string): Promise => { + if (!statsYr) { alert("연도를 선택해주세요."); return false; } + const result = await run(async () => { + await loadLeft(statsYr); + await loadRight(statsYr); + return true; + }); + return result ?? false; + }; + + /** + * 검색 버튼 클릭 시 - 좌측은 항상 갱신하고, 우측은 비어있을 때만 갱신한다. + * (사용자가 우측에서 수동으로 매핑한 값을 재검색 시 날려버리지 않기 위함 - 원본 동작 그대로 유지) + */ + const search = async (statsYr: string): Promise => { + if (!statsYr) { alert("연도를 선택해주세요."); return false; } + const result = await run(async () => { + await loadLeft(statsYr); + if (rightRowData.length === 0) await loadRight(statsYr); + return true; + }); + return result ?? false; + }; + + /** 탭 전환 시 - 아직 한 번도 로드되지 않았을 때만 로드 */ + const loadOnActivate = async (statsYr: string) => { + if (leftRowData.length === 0) await loadAll(statsYr); + }; + + const handleFileChange = async (e: React.ChangeEvent, statsYr: string) => { + const file = e.target.files?.[0]; + if (!file) return; + + const isValidExt = [".xlsx", ".xls", ".csv"].some((ext) => file.name.toLowerCase().endsWith(ext)); + if (!isValidExt) { alert("xlsx, xls, csv 파일만 업로드 가능합니다."); e.target.value = ""; return; } + + const isValidHeader = await validateExcelHeader(file, config.requiredHeaderKeywords); + if (!isValidHeader) { alert(config.invalidFileMessage); e.target.value = ""; return; } + + if (!statsYr) { alert("연도를 선택해주세요."); e.target.value = ""; return; } + + await run(async () => { + const res = await config.uploadExcel(file, statsYr); + if (res.status === "success") { + await loadAll(statsYr); + alert("업로드가 완료되었습니다."); + } else { + alert(res.message || "업로드 중 오류가 발생했습니다."); + } + }); + e.target.value = ""; + }; + + const handleFinal = async (statsYr: string) => { + if (!statsYr) { alert("먼저 조회를 진행해주세요."); return; } + if (rightRowData.length === 0) { alert("반영할 데이터가 없습니다."); return; } + + await run(async () => { + const regList = rightRowData + .filter((row) => String(row.mpinCd ?? "").trim() !== "") + .map((row) => ({ + statsYr, + admdstCd: String(row.admdstCd ?? ""), + admdstNm: String(row.admdstNm ?? ""), + subAdmdstCd: String(row.mpinCd ?? ""), + })); + + const res = await config.saveFinal(statsYr, regList); + if (res.status === "success") { + await loadLeft(statsYr); + alert("반영이 완료되었습니다."); + } else { + alert(res.message || "반영 중 오류가 발생했습니다."); + } + }); + }; +const handleRightRowClick = async (rowData: TRow, statsYr: string) => { + const admdstCd = String(rowData.admdstCd ?? ""); + if (!admdstCd) { alert("행정구역코드가 없습니다."); return; } + + await run( + async () => { + const res = await config.getUpperCodesByName(statsYr, "", admdstCd); // ← admdstCd 추가 + if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) { + setCandidates(res.data); + const rowIndex = rightRowData.findIndex((r) => r.admdstCd === rowData.admdstCd); + selectedRowIndexRef.current = rowIndex; + setDialogOpen(true); + } else { + alert("해당 지역명에 대한 상위 코드 후보가 없습니다."); + } + }, + { errorMessage: "상위 코드 조회 중 오류가 발생했습니다." }, + ); +}; + + const handleCodeSelect = (item: Record) => { + const rowIndex = selectedRowIndexRef.current; + if (rowIndex === null) return; + setRightRowData((prev) => + prev.map((row, idx) => (idx === rowIndex ? { ...row, mpinCd: item["admdstCd"] ?? "" } : row)), + ); + selectedRowIndexRef.current = null; + }; + + const reset = () => { + setLeftRowData([]); + setRightRowData([]); + setDialogOpen(false); + setCandidates([]); + selectedRowIndexRef.current = null; + }; + + return { + isLoading, + leftRowData, + rightRowData, + setLeftRowData, + setRightRowData, + fileInputRef, + dialogOpen, + setDialogOpen, + candidates, + loadAll, + search, + loadOnActivate, + handleFileChange, + handleFinal, + handleRightRowClick, + handleCodeSelect, + reset, + }; +} \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/hooks/usePopulationStatusTab.ts b/apps/st/src/components/statistics/management/hooks/usePopulationStatusTab.ts new file mode 100644 index 000000000..e4e13b46d --- /dev/null +++ b/apps/st/src/components/statistics/management/hooks/usePopulationStatusTab.ts @@ -0,0 +1,71 @@ +import { useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; +import { toGridRows } from "@/components/util/grid/gridFunction"; + +import { useAsyncAction } from "./useAsyncAction"; +import type { PopulationStatusItem } from "../types/population-tab-types"; + +export function usePopulationStatusTab() { + const { setMessage: alert } = useAlertStore((state) => state); + const { isLoading, run } = useAsyncAction(alert); + + const [rowData, setRowData] = useState([]); + + const [includeOriginMale, setIncludeOriginMale] = useState(true); + const [includeOriginFemale, setIncludeOriginFemale] = useState(true); + const [includeForeignMale, setIncludeForeignMale] = useState(true); + const [includeForeignFemale, setIncludeForeignFemale] = useState(true); + + const search = async (statsYr: string): Promise => { + if (!statsYr) { alert("연도를 선택해주세요."); return false; } + if (!includeOriginMale && !includeOriginFemale && !includeForeignMale && !includeForeignFemale) { + alert( + "체크 박스를 모두 해제하신 상태에서는\n조회가 불가능합니다.\n(최소 1개 이상의 체크 박스 선택 후\n조회를 진행해주세요.)", + ); + return false; + } + + const result = await run(async () => { + const res = await statisticsPpltnApi.searchPopByEachCondition( + statsYr, + includeOriginMale, + includeOriginFemale, + includeForeignMale, + includeForeignFemale, + ); + if (res.status === "success") { + setRowData(toGridRows((res.data as object[]) ?? []) as unknown as PopulationStatusItem[]); + return true; + } + alert(res.message || "조회 중 오류가 발생했습니다."); + return false; + }); + return result ?? false; + }; + + const reset = () => { + setRowData([]); + setIncludeOriginMale(true); + setIncludeOriginFemale(true); + setIncludeForeignMale(true); + setIncludeForeignFemale(true); + }; + + return { + isLoading, + rowData, + setRowData, + includeOriginMale, + setIncludeOriginMale, + includeOriginFemale, + setIncludeOriginFemale, + includeForeignMale, + setIncludeForeignMale, + includeForeignFemale, + setIncludeForeignFemale, + search, + reset, + }; +} \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/hooks/usePopulationVizTab.ts b/apps/st/src/components/statistics/management/hooks/usePopulationVizTab.ts new file mode 100644 index 000000000..7490d89bf --- /dev/null +++ b/apps/st/src/components/statistics/management/hooks/usePopulationVizTab.ts @@ -0,0 +1,84 @@ +import { useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; + +import { useAsyncAction } from "./useAsyncAction"; +import type { PopulationChartData } from "../PopulationStatusChart"; + +export function usePopulationVizTab() { + const { setMessage: alert } = useAlertStore((state) => state); + const { isLoading, run } = useAsyncAction(alert); + + const [year, setYear] = useState(String(new Date().getFullYear() - 1)); + + const [includeOriginMale, setIncludeOriginMale] = useState(true); + const [includeOriginFemale, setIncludeOriginFemale] = useState(true); + const [includeForeignMale, setIncludeForeignMale] = useState(true); + const [includeForeignFemale, setIncludeForeignFemale] = useState(true); + + const [chartData, setChartData] = useState(null); + + const search = async (statsYr: string): Promise => { + if (!statsYr) { alert("연도를 선택해주세요."); return false; } + setYear(statsYr); + + const result = await run(async () => { + const res = await statisticsPpltnApi.getChartDatas1( + statsYr, + includeOriginMale, + includeOriginFemale, + includeForeignMale, + includeForeignFemale, + ); + if (res.status === "success") { + setChartData(res.data as PopulationChartData); + return true; + } + alert(res.message || "조회 중 오류가 발생했습니다."); + return false; + }); + return result ?? false; + }; + + const selectAll = () => { + setIncludeOriginMale(true); + setIncludeOriginFemale(true); + setIncludeForeignMale(true); + setIncludeForeignFemale(true); + }; + + const deselectAll = () => { + setIncludeOriginMale(false); + setIncludeOriginFemale(false); + setIncludeForeignMale(false); + setIncludeForeignFemale(false); + }; + + const reset = () => { + setYear(String(new Date().getFullYear() - 1)); + setIncludeOriginMale(true); + setIncludeOriginFemale(true); + setIncludeForeignMale(true); + setIncludeForeignFemale(true); + setChartData(null); + }; + + return { + isLoading, + year, + chartData, + includeOriginMale, + setIncludeOriginMale, + includeOriginFemale, + setIncludeOriginFemale, + includeForeignMale, + setIncludeForeignMale, + includeForeignFemale, + setIncludeForeignFemale, + search, + selectAll, + deselectAll, + reset, + }; +} \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/lib/gridCast.ts b/apps/st/src/components/statistics/management/lib/gridCast.ts new file mode 100644 index 000000000..438e45542 --- /dev/null +++ b/apps/st/src/components/statistics/management/lib/gridCast.ts @@ -0,0 +1,27 @@ +import type { Dispatch, SetStateAction } from "react"; + +export type GridRow = Record; + +/** + * DataGrid가 제네릭이 아니라 rowData: Record[] 를 그대로 요구함 + * (실제 타입 에러로 확인됨: ts(2322), "missing properties from PopulationStatusItem"). + * + * 훅/탭 내부에서는 명시적 도메인 타입(PopulationStatusItem 등)을 그대로 쓰고, + * DataGrid에 넘기는 이 경계에서만 캐스팅한다. 내부 로직의 타입 안정성은 유지하면서 + * 외부 라이브러리(DataGrid)의 느슨한 타입과의 경계만 명시적으로 표시하는 용도. + */ +export function asGridRowData(data: T[]): GridRow[] { + return data as unknown as GridRow[]; +} + +export function asGridRowSetter(setter: Dispatch>): Dispatch> { + return setter as unknown as Dispatch>; +} + +/** + * DataGrid의 onRowClick 등에서 넘어오는 GridRow(Record)를 + * 도메인 타입으로 되돌릴 때 사용. `as unknown as T`를 매번 직접 쓰지 않도록 모아둠. + */ +export function fromGridRow(row: GridRow): T { + return row as unknown as T; +} \ No newline at end of file diff --git a/apps/st/src/components/statistics/management/types/population-tab-types.ts b/apps/st/src/components/statistics/management/types/population-tab-types.ts new file mode 100644 index 000000000..233fba2d1 --- /dev/null +++ b/apps/st/src/components/statistics/management/types/population-tab-types.ts @@ -0,0 +1,116 @@ +import type { ApiResponse } from "@/entities/statistics/model/statistics-interface"; +import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface"; + +/* ══════════════════════════════════════════════════ + 외국인 인구 관련 타입 + (내국인 쪽 DomesticPopulationItem과 필드명만 다르고 구조는 동일) +══════════════════════════════════════════════════ */ + +export interface ForeignPopulationItem { + statsYr: string; + admdstCd: string; + admdstNm: string; + frgnrWholPpltnCnt: number; + frgnrMlPpltnCnt: number; + frgnrFmlPpltnCnt: number; + /** 미매핑 리스트(우측) 전용 - 매핑 대상 행정구역코드(상위코드) */ + mpinCd: string; +} + +export type ForeignPopulationListResponse = ApiResponse; + +/* ══════════════════════════════════════════════════ + 행정동갱신 관련 타입 +══════════════════════════════════════════════════ */ + +export interface AdmdstRenewalReducedItem { + adstDstrNm: string; + adstCd: string; +} + +export interface AdmdstRenewalAddedItem { + adstDstrNm: string; + adstCd: string; + lawCd: string; + lawNm: string; + rgnCd?: string; + lv?: string; + sido?: string; // ← 추가 + gugun?: string; // ← 추가 + adstDong?: string; // ← 추가 +} + +/** 추가된 행정구역 반영 시 서버로 보내는 단건 페이로드 */ +export interface AdmdstRenewalAddedSaveItem { + adstCd: string; + lawCd: string; + adstDstrNm: string; + sido: string; + gugun: string; + adstDong: string; + userId: string; + rgnCd: string; + lv: string; +} + +/** 감축된 행정구역 반영 시 서버로 보내는 단건 페이로드 */ +export interface AdmdstRenewalReducedSaveItem { + adstCd: string; +} + +/* ══════════════════════════════════════════════════ + 행정인구현황 관련 타입 +══════════════════════════════════════════════════ */ + +export interface PopulationStatusItem { + statsYr: string; + admdstCd: string; + admdstNm: string; + wholPpltnCnt: number; + domplWholPpltnCnt: number; + domplMlPpltnCnt: number; + domplFmlPpltnCnt: number; + frgnrWholPpltnCnt: number; + frgnrMlPpltnCnt: number; + frgnrFmlPpltnCnt: number; +} + +export type PopulationStatusListResponse = ApiResponse; + +/* ══════════════════════════════════════════════════ + 내국인/외국인 공용 매핑 탭(좌:전체 리스트, 우:미매핑 리스트) 공통 타입 +══════════════════════════════════════════════════ */ + +export type MappingTabKind = "domestic" | "foreign"; + +/** 반영(최종 매핑 저장) 요청 시 서버로 보내는 단건 페이로드 - 내국인/외국인 공통 */ +export interface PopulationMappingRegItem { + statsYr: string; + admdstCd: string; + admdstNm: string; + subAdmdstCd: string; +} + +/** 좌/우 그리드 행이 공통으로 가져야 하는 최소 필드 */ +export interface PopulationMappingRow { + admdstCd: string; + admdstNm: string; + /** 우측(미매핑 리스트) 전용 필드. 좌측 데이터에는 존재하지 않을 수 있음 */ + mpinCd?: string; +} + +/** usePopulationMappingTab에 주입하는 API/검증 설정 - 내국인/외국인 탭이 이 설정만 다르게 넘김 */ +export interface PopulationMappingApiConfig { + kind: MappingTabKind; + /** opt1: "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측) */ + getList: (statsYr: string, opt1: "1" | "2") => Promise>; + uploadExcel: (file: File, statsYr: string) => Promise>; + saveFinal: (statsYr: string, regList: PopulationMappingRegItem[]) => Promise>; + getUpperCodesByName: (statsYr: string, admdstNm: string, admdstCd: string) => Promise>; + /** 업로드된 엑셀 헤더에 반드시 포함되어야 하는 키워드 목록 */ + requiredHeaderKeywords: string[]; + /** 헤더 검증 실패 시 노출할 안내 메시지 */ + invalidFileMessage: string; +} + +export type { UpperCodeCandidateItem }; \ No newline at end of file diff --git a/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx b/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx index 961f0a79e..9b3d7f25e 100644 --- a/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx +++ b/apps/st/src/components/statistics/review/ExcretaTreatmentFacilityRvw.tsx @@ -1,38 +1,323 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { LoadingWithAnimation } from "@common/components"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import type { + ExcretaTreatmentFacilityReviewItem, +} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface"; +import { + extractExcretaTreatmentFacilityReviewList, + extractExcretaTreatmentFacilityReviewStatsStatusInfo, +} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewOpinionSaveRequest, + ReviewerInfoRequest, +} from "@/entities/statistics/model/statistics-interface"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { ExcretaTreatmentFacilityInfoList } from "@/components/util/column/review/ExcretaTreatmentFacilityRvwColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; -// 하수도 통계 관리 07.분뇨처리 시설 +const excretaTreatmentFacilityTab = ExcretaTreatmentFacilityInfoList[0]; + +// 하수도 통계 검토 07. 분뇨처리시설 const ExcretaTreatmentFacilityRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [statsStatusInfo, setStatsStatusInfo] = useState({}); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [reviewOpinionContext, setReviewOpinionContext] = + useState(null); + const [latestReviewOpinion, setLatestReviewOpinion] = useState(""); + const [latestProcessHistory, setLatestProcessHistory] = useState(""); + const [lastSearchValues, setLastSearchValues] = useState(null); - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const columnInfo = useMemo(() => { + if (!excretaTreatmentFacilityTab) { + return { columns: [], columnLayout: [], fixedColumns: 0, rowStartNum: 0 }; + } - const approveFunc = () => console.log('approveFunc'); - const saveFunc = () => console.log('submit'); - const reviewOpinionFunc = () => console.log('reviewOpinionFunc'); - const returnFunc = () => console.log('returnFunc'); + const { columns, columnLayout, fixedColumns, rowStartNum } = excretaTreatmentFacilityTab; + return { + columnLayout, + fixedColumns, + rowStartNum, + columns: columns.map((col) => ({ + ...col, + editable: false as const, + })), + }; + }, []); + + const cellStyleCallback = useMemo( + () => buildReviewCellStyleCallback(columnInfo.columns, rowData), + [columnInfo, rowData], + ); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [statsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const submitDialogContext = useMemo(() => { + if (reviewOpinionContext == null) { + return null; + } + + const instSeCd = userInfo?.instSeCd ?? ""; + if (!instSeCd) { + return null; + } + + return { + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + instSeCd, + }; + }, [reviewOpinionContext, userInfo?.instSeCd]); + + const handleReset = () => { + setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + setLastSearchValues(null); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + setLastSearchValues(values); + + try { + setIsLoading(true); + + const response = + await statisticsManagementApi.getExcretaTreatmentFacilityReviewList({ + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + swtrmtFcltNm: values.fctName, + }); + + if (response.status === "success" && response.data) { + const rawList = extractExcretaTreatmentFacilityReviewList(response.data); + const statsInfo = extractExcretaTreatmentFacilityReviewStatsStatusInfo(response.data); + const convertedData: ExcretaTreatmentFacilityReviewItem[] = rawList.map((item) => ({ + ...item, + admdstNm: item.admdstNm ?? "", + fcltPstnCn: item.fcltPstnCn ?? "", + telNo: item.telNo ?? "", + cmptnStrtYmd: item.cmptnStrtYmd ?? "", + oprtngStrtYmd: item.oprtngStrtYmd ?? "", + prcsMthdExpln: item.prcsMthdExpln ?? "", + linkTmplntCn: item.linkTmplntCn ?? "", + wtsysNm: item.wtsysNm ?? "", + trbtrNm: item.trbtrNm ?? "", + msirbCn: item.msirbCn ?? "", + ssirbCn: item.ssirbCn ?? "", + operMthdCdCn: item.operMthdCdCn ?? "", + rmrk: item.rmrk ?? "", + })); + + setStatsStatusInfo(statsInfo); + setReviewOpinionContext({ + statsYr: values.year ?? "", + sewerBzmnCd: values.bsnmCd ?? "", + statsCd: statsInfo.statsCd ?? "", + }); + setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? ""); + setLatestProcessHistory(statsInfo.wrterOpnnCn ?? ""); + setRowData(toGridRows(convertedData as unknown as object[])); + } else { + setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + alert(response.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setLatestReviewOpinion(row?.splmntOpnnCn ?? ""); + setLatestProcessHistory(row?.wrterOpnnCn ?? ""); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setStatsStatusInfo((prev) => ({ + ...prev, + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + })); + setLatestReviewOpinion(param.splmntOpnnCn); + setLatestProcessHistory(param.wrterOpnnCn); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && } +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ + +
+ + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
- - ); }; -export default ExcretaTreatmentFacilityRvw; \ No newline at end of file +export default ExcretaTreatmentFacilityRvw; diff --git a/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx b/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx index 619085736..a116ce6a4 100644 --- a/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx +++ b/apps/st/src/components/statistics/review/PrivateSewageFacilityRvw.tsx @@ -1,51 +1,436 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractPrivateSewageFacilityReviewList, + extractPrivateSewageFacilityReviewStatsStatusInfo, + mapPrivateSewageFacilityReviewRows, + type PrivateSewageFacilityReviewRequestParams, +} from "@/entities/statistics/model/statistics-private-sewage-facility-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { PrivateSewageFacilityInfoListRvw } from "@/components/util/column/review/PrivateSewageFacilityColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; -// 하수도 통계 관리 08.개인하수처리시설 +const firstTabKey = PrivateSewageFacilityInfoListRvw[0]?.tabKey ?? "ST00080001"; + +async function fetchPrivateSewageFacilityReviewTab( + tabKey: string, + searchParam: PrivateSewageFacilityReviewRequestParams, +) { + if (tabKey === "ST00080001") { + return statisticsManagementApi.getPrivateSewageTreatFactStatusReviewList(searchParam); + } + if (tabKey === "ST00080002") { + return statisticsManagementApi.getSepticTankStatusReviewList(searchParam); + } + if (tabKey === "ST00080003") { + return statisticsManagementApi.getPrivateSewageDesigStatusReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 08. 개인하수처리시설 const PrivateSewageFacilityRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoggedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return PrivateSewageFacilityInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoggedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: PrivateSewageFacilityReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoggedRef.current.clear(); + + const response = await fetchPrivateSewageFacilityReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractPrivateSewageFacilityReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractPrivateSewageFacilityReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapPrivateSewageFacilityReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoggedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchPrivateSewageFacilityReviewTab(tabKey, searchParam); + lazyTabLoggedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractPrivateSewageFacilityReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractPrivateSewageFacilityReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapPrivateSewageFacilityReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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={reviewCellStyleCallbackByTab.get(tabKey)} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; diff --git a/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx b/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx index 6d23f3c87..f4b379e82 100644 --- a/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx +++ b/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx @@ -23,6 +23,8 @@ import { } from "@/components/statistics/review/ReviewOpinionDialog"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn"; @@ -55,6 +57,11 @@ const PublicSewageFacilityRvw = () => { }; }, []); + const cellStyleCallback = useMemo( + () => buildReviewCellStyleCallback(columnInfo.columns, rowData), + [columnInfo, rowData], + ); + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { const submit = statsStatusInfo.submitStatusCd != null ? String(statsStatusInfo.submitStatusCd) : ""; const prvn = statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; @@ -243,14 +250,18 @@ const PublicSewageFacilityRvw = () => {
)} - +
+ + +
{ const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowData, setRowData] = useState[]>([]); const [isLoading, setIsLoading] = useState(false); + const [statsStatusInfo, setStatsStatusInfo] = useState({}); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [reviewOpinionContext, setReviewOpinionContext] = + useState(null); + const [latestReviewOpinion, setLatestReviewOpinion] = useState(""); + const [latestProcessHistory, setLatestProcessHistory] = useState(""); + const [lastSearchValues, setLastSearchValues] = useState(null); const columnInfo = useMemo(() => { const { columns, columnLayout, fixedColumns, rowStartNum } = PumpingStationInfoList; @@ -36,65 +64,198 @@ const PumpingStationRvw = () => { }; }, []); + const cellStyleCallback = useMemo( + () => buildReviewCellStyleCallback(columnInfo.columns, rowData), + [columnInfo, rowData], + ); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [statsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const submitDialogContext = useMemo(() => { + if (reviewOpinionContext == null) { + return null; + } + + const instSeCd = userInfo?.instSeCd ?? ""; + if (!instSeCd) { + return null; + } + + return { + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + instSeCd, + }; + }, [reviewOpinionContext, userInfo?.instSeCd]); + const handleReset = () => { setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + setLastSearchValues(null); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); }; const handleSearch = async (values: SearchFormValues) => { - if (!values.sido || values.sido === 'ALL') { - alert('시도를 선택해주세요.'); + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); return; } + setLastSearchValues(values); + try { setIsLoading(true); const response = await statisticsManagementApi.getMediationPumpStationList({ ctpvNm: values.sido, - sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sggNm: values.gugun === "ALL" ? "" : values.gugun, sewerBzmnCd: values.bsnmCd, statsYr: values.year, - pumpstNm: values.fctName, + fcltNm: values.fctName, }); - if (response.status === 'success' && response.data) { - const rawList = response.data as MediationPumpStationItem[]; - - const convertedData = rawList.map(item => ({ + if (response.status === "success" && response.data) { + const rawList = extractMediationPumpStationList(response.data); + const statsInfo = extractMediationPumpStationStatsStatusInfo(response.data); + + const convertedData = rawList.map((item: MediationPumpStationItem) => ({ ...item, - rmrk: item.rmrk || '' + admdstNm: item.admdstNm ?? "", + pumpstNm: item.fcltNm ?? "", + impdFcltPstnCn: item.fcltPstnCn ?? "", + pumpSeNm: item.pumpSeNm ?? "", + swtrmtFcltNm: item.swtrmtFcltNm ?? "", + oprtngStrtYmd: item.oprtngStrtYmd ?? "", + exlnPumpN4RsrvCnt: item.exlnPumpN1RsrvCnt ?? 0, + rmrk: item.rmrk ?? "", })); + setStatsStatusInfo(statsInfo); + setReviewOpinionContext({ + statsYr: values.year ?? "", + sewerBzmnCd: values.bsnmCd ?? "", + statsCd: statsInfo.statsCd ?? "", + }); + setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? ""); + setLatestProcessHistory(statsInfo.wrterOpnnCn ?? ""); setRowData(toGridRows(convertedData)); } else { setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); alert(response.message || "데이터가 없습니다."); } - } catch (error) { - console.error("중계펌프장 조회 중 에러 발생:", error); - alert('데이터 조회 중 오류가 발생했습니다.'); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; - const approveFunc = () => console.log('approveFunc'); - const saveFunc = () => console.log('submit'); - const reviewOpinionFunc = () => console.log('reviewOpinionFunc'); - const returnFunc = () => console.log('returnFunc'); + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setLatestReviewOpinion(row?.splmntOpnnCn ?? ""); + setLatestProcessHistory(row?.wrterOpnnCn ?? ""); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setStatsStatusInfo((prev) => ({ + ...prev, + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + })); + setLatestReviewOpinion(param.splmntOpnnCn); + setLatestProcessHistory(param.wrterOpnnCn); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }; return (
-
-
- - - - + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && } +
-
+ )} {isLoading && (
@@ -104,16 +265,49 @@ const PumpingStationRvw = () => {
)} - + + +
+ + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
); }; -export default PumpingStationRvw; \ No newline at end of file +export default PumpingStationRvw; diff --git a/apps/st/src/components/statistics/review/ReviewEmptyCurrLegend.tsx b/apps/st/src/components/statistics/review/ReviewEmptyCurrLegend.tsx new file mode 100644 index 000000000..0028c07fb --- /dev/null +++ b/apps/st/src/components/statistics/review/ReviewEmptyCurrLegend.tsx @@ -0,0 +1,16 @@ +const ReviewEmptyCurrLegend = () => ( +
+
+
+
+ 전년도만 존재 +
+
+
+ 현년도만 존재 +
+
+
+); + +export default ReviewEmptyCurrLegend; diff --git a/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx b/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx index c491e1552..84f6aad10 100644 --- a/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx +++ b/apps/st/src/components/statistics/review/ReviewSubmitDialog.tsx @@ -65,7 +65,6 @@ export function ReviewSubmitDialog({ if (cancelled) return; if (response.status !== "success" || !response.data) { - alert("제출 정보를 불러오지 못했습니다."); setReviewerInfo(null); return; } @@ -73,7 +72,6 @@ export function ReviewSubmitDialog({ setReviewerInfo(response.data); } catch { if (!cancelled) { - alert("제출 정보를 불러오지 못했습니다."); setReviewerInfo(null); } } finally { @@ -91,7 +89,7 @@ export function ReviewSubmitDialog({ }, [open, context, alert]); const handleConfirm = async () => { - if (context == null || reviewerInfo == null) { + if (context == null) { alert("제출 정보를 확인한 뒤 다시 시도해주세요."); return; } @@ -105,8 +103,8 @@ export function ReviewSubmitDialog({ sewerBzmnCd: context.sewerBzmnCd, statsCd: context.statsCd, mdfrId: userInfo.userUnqId, - splmntOpnnCn: reviewerInfo.splmntOpnnCn ?? "", - wrterOpnnCn: reviewerInfo.wrterOpnnCn ?? "", + splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "", + wrterOpnnCn: reviewerInfo?.wrterOpnnCn ?? "", }; const response = await statisticsApi.submitReview(submitParam); @@ -177,7 +175,7 @@ export function ReviewSubmitDialog({ type="button" color="primary" onClick={() => void handleConfirm()} - disabled={isLoading || isSubmitting || reviewerInfo == null} + disabled={isLoading || isSubmitting} > 제출 diff --git a/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx b/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx index bd594d334..c9ad04528 100644 --- a/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageExcretaRelatedRvw.tsx @@ -1,53 +1,442 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractSewageExcretaRelatedReviewList, + extractSewageExcretaRelatedReviewStatsStatusInfo, + mapSewageExcretaRelatedReviewRows, + type SewageExcretaRelatedReviewRequestParams, +} from "@/entities/statistics/model/statistics-sewage-excreta-related-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewageExcretaRelatedInfoListRvw } from "@/components/util/column/review/SewageExcretaRelatedRvwColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; -// 하수도 통계 관리 12. 하수 및 분뇨 관련 산업 +const firstTabKey = SewageExcretaRelatedInfoListRvw[0]?.tabKey ?? "ST00120001"; + +async function fetchExcretaRelatedReviewTab( + tabKey: string, + searchParam: SewageExcretaRelatedReviewRequestParams, +) { + if (tabKey === "ST00120001") { + return statisticsManagementApi.getPublicSewageStaffReviewList(searchParam); + } + if (tabKey === "ST00120002") { + return statisticsManagementApi.getPublicHumanWasteStaffReviewList(searchParam); + } + if (tabKey === "ST00120003") { + return statisticsManagementApi.getHumanWasteCollectionReviewList(searchParam); + } + if (tabKey === "ST00120004") { + return statisticsManagementApi.getIndvSwtrmtRelBsnsReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 12. 하수 및 분뇨 관련 산업 const SewageExcretaRelatedRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewageExcretaRelatedInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewageExcretaRelatedReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchExcretaRelatedReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaRelatedReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewageExcretaRelatedReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewageExcretaRelatedReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchExcretaRelatedReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaRelatedReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewageExcretaRelatedReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewageExcretaRelatedReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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={reviewCellStyleCallbackByTab.get(tabKey)} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; - export default SewageExcretaRelatedRvw; diff --git a/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx b/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx index d0dbcb383..fd83c82fb 100644 --- a/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageExcretaSlurgeRvw.tsx @@ -1,52 +1,443 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractSewageExcretaSlurgeReviewList, + extractSewageExcretaSlurgeReviewStatsStatusInfo, + mapSewageExcretaSlurgeReviewRows, + type SewageExcretaSlurgeReviewRequestParams, +} from "@/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewageExcretaSludgeInfoListRvw } from "@/components/util/column/review/SewageExcretaSlurgeRvwColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; -// 하수도 통계 관리 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 +const firstTabKey = SewageExcretaSludgeInfoListRvw[0]?.tabKey ?? "ST00110001"; + +async function fetchSludgeReviewTab( + tabKey: string, + searchParam: SewageExcretaSlurgeReviewRequestParams, +) { + if (tabKey === "ST00110001") { + return statisticsManagementApi.getSludgeProcessingReviewList(searchParam); + } + if (tabKey === "ST00110002") { + return statisticsManagementApi.getSludgeDigestionTankReviewList(searchParam); + } + if (tabKey === "ST00110003") { + return statisticsManagementApi.getSludgeFacilityOperReviewList(searchParam); + } + if (tabKey === "ST00110004") { + return statisticsManagementApi.getHumanWasteSludgeReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 const SewageExcretaSlurgeRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewageExcretaSludgeInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewageExcretaSlurgeReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + swtrmtFcltNm: values.fctName, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchSludgeReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaSlurgeReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewageExcretaSlurgeReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewageExcretaSlurgeReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchSludgeReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewageExcretaSlurgeReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewageExcretaSlurgeReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewageExcretaSlurgeReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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={reviewCellStyleCallbackByTab.get(tabKey)} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; -export default SewageExcretaSlurgeRvw; \ No newline at end of file +export default SewageExcretaSlurgeRvw; diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index 295802cdc..a90af6ac2 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -1,21 +1,35 @@ import { useMemo, useRef, useState } from "react"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useUserStore } from "@/shared/lib"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; -import type { SewerPipeMtcRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + SewerPipeMtcRequestParams, + StatsStatusInfo, +} from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; -import ApproveButton from "@/components/button/ApproveButton"; +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; -import SaveButton from "@/components/button/SaveButton"; import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; - +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; const firstTabKey = SewagePipeMaintainColumnInfoList[0]?.tabKey ?? "ST00030001"; @@ -50,17 +64,36 @@ function getListDataByTab(tabKey: string, responseData: unknown): object[] { return []; } +function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null { + if (!data || typeof data !== "object") return null; + const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo; + return ssi && typeof ssi === "object" ? ssi : null; +} + // 하수도 통계 검토 03. 하수관로 유지 관리 const SewagePipeMaintainRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); const searchParamRef = useRef(null); const searchDoneRef = useRef(false); const lazyTabLoggedRef = useRef>(new Set()); + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + const columnInfo = useMemo(() => { return SewagePipeMaintainColumnInfoList.map((tab: ColumnInfoList) => ({ ...tab, @@ -72,9 +105,71 @@ const SewagePipeMaintainRvw = () => { })); }, []); + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + const handleReset = () => { setRowDataMap(new Map()); setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); searchParamRef.current = null; searchDoneRef.current = false; lazyTabLoggedRef.current.clear(); @@ -88,6 +183,7 @@ const SewagePipeMaintainRvw = () => { try { setIsLoading(true); + setLastSearchValues(values); const searchParam: SewerPipeMtcRequestParams = { statsYr: values.year, @@ -103,6 +199,12 @@ const SewagePipeMaintainRvw = () => { const response = await fetchSewagePipeMaintainReviewTab(activeTab, searchParam); if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi ?? {}); + return next; + }); const listData = getListDataByTab(activeTab, response.data); setRowDataMap((prev) => { const next = new Map(prev); @@ -110,9 +212,11 @@ const SewagePipeMaintainRvw = () => { return next; }); } else { - alert(response?.message || "데이터가 없습니다."); + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); } } catch { + setTabStatsStatusInfoMap(new Map()); alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); @@ -143,6 +247,12 @@ const SewagePipeMaintainRvw = () => { lazyTabLoggedRef.current.add(tabKey); if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi ?? {}); + return next; + }); setRowDataMap((prev) => { const next = new Map(prev); const listData = getListDataByTab(tabKey, response.data); @@ -159,18 +269,96 @@ const SewagePipeMaintainRvw = () => { } }; + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }; + return (
-
-
- { }} /> - { }} /> - { }} /> - { }} /> + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
-
+ )} {isLoading && (
@@ -181,6 +369,7 @@ const SewagePipeMaintainRvw = () => { )}
+ void handleActiveTabChange(v)} @@ -228,14 +417,39 @@ const SewagePipeMaintainRvw = () => { showHeaderCheckbox={false} fixedColumns={info.fixedColumns} enableSort={false} + cellStyleCallback={reviewCellStyleCallbackByTab.get(tabKey)} /> ); })}
+ + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
); }; -export default SewagePipeMaintainRvw; \ No newline at end of file +export default SewagePipeMaintainRvw; diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index 8bd0f9e44..02c111d38 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -32,6 +32,8 @@ import type { } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn"; +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; import { toGridRows } from "@/components/util/grid/gridFunction"; const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001"; @@ -156,6 +158,12 @@ const SewagePipeRvw = () => { })), [], ); + + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + const reviewOpinionContext = useMemo(() => { if (lastSearchValues == null) return null; return { @@ -412,6 +420,7 @@ const SewagePipeRvw = () => { )}
+ void handleActiveTabChange(v)} @@ -459,6 +468,7 @@ const SewagePipeRvw = () => { showHeaderCheckbox={false} fixedColumns={info.fixedColumns} enableSort={false} + cellStyleCallback={reviewCellStyleCallbackByTab.get(tabKey)} /> ); diff --git a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx index 4a250d2aa..efb748aa3 100644 --- a/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx +++ b/apps/st/src/components/statistics/review/SewageReservoirPumpRvw.tsx @@ -1,19 +1,74 @@ -import { useState, useMemo, useRef } from "react"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; -import ReturnButton from "@/components/button/ReturnButton"; -import { LoadingWithAnimation } from "@common/components"; +import { useMemo, useRef, useState } from "react"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useUserStore } from "@/shared/lib"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; -import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn"; -import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; -import type { SearchFormValues } from "@/components/type/SearchForm.type"; -import { toGridRows } from "@/components/util/grid/gridFunction"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import type { SewerReservoirRequestParams } from "@/entities/statistics/model/sewage-reservoir-pump-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; +import ReturnButton from "@/components/button/ReturnButton"; +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; + +const firstTabKey = SewageReservoirPumpInfoListRvw[0]?.tabKey ?? "ST00040001"; + +async function fetchSewageReservoirPumpReviewTab( + tabKey: string, + searchParam: SewerReservoirRequestParams, +) { + if (tabKey === "ST00040001") { + return statisticsManagementApi.getSewerReservoirList(searchParam); + } + if (tabKey === "ST00040002") { + return statisticsManagementApi.getPumpStationList(searchParam); + } + return null; +} + +function getListDataByTab(tabKey: string, responseData: unknown): object[] { + if (Array.isArray(responseData)) { + return responseData as object[]; + } + if (!responseData || typeof responseData !== "object") { + return []; + } + + const data = responseData as Record; + if (tabKey === "ST00040001") { + const list = data.sewerReservoirList ?? data.reservoirList; + return Array.isArray(list) ? (list as object[]) : []; + } + if (tabKey === "ST00040002") { + const list = data.pumpStationList ?? data.pumpingStationList; + return Array.isArray(list) ? (list as object[]) : []; + } + return []; +} + +function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null { + if (!data || typeof data !== "object" || Array.isArray(data)) return null; + const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo; + return ssi && typeof ssi === "object" ? ssi : null; +} /** * 하수도 통계 검토 04. 하수저류 시설 및 빗물 펌프장 @@ -22,11 +77,26 @@ import type { SewerReservoirRequestParams } from "@/entities/statistics/model/se */ const SewageReservoirPumpRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); - const [activeTab, setActiveTab] = useState('ST00040001'); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoggedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); const columnInfo = useMemo(() => { return SewageReservoirPumpInfoListRvw.map((tab: ColumnInfoList) => ({ @@ -39,44 +109,121 @@ const SewageReservoirPumpRvw = () => { })); }, []); + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const activeStatsCd = activeStatsStatusInfo.statsCd ?? activeTab; + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsCd, + }; + }, [activeStatsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsCd; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + const handleReset = () => { setRowDataMap(new Map()); - setActiveTab('ST00040001'); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoggedRef.current.clear(); }; const handleSearch = async (values: SearchFormValues) => { - if (!values.sido || values.sido === 'ALL') { - alert('시도를 선택해주세요.'); + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); return; } - setActiveTab('ST00040001'); - setRowDataMap(new Map()); - try { setIsLoading(true); + setLastSearchValues(values); - const searchParam: SewerReservoirRequestParams = { + const searchParam: SewerReservoirRequestParams = { statsYr: values.year, sewerBzmnCd: values.bsnmCd, ctpvNm: values.sido, - sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sggNm: values.gugun === "ALL" ? "" : values.gugun, }; searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoggedRef.current.clear(); - const response = await statisticsManagementApi.getSewerReservoirList(searchParam); + const response = await fetchSewageReservoirPumpReviewTab(activeTab, searchParam); - if (response?.status === 'success' && response.data) { - const newRowDataMap = new Map[]>(); - newRowDataMap.set('ST00040001', toGridRows(response.data as object[])); - setRowDataMap(newRowDataMap); + if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi ?? {}); + return next; + }); + const listData = getListDataByTab(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(listData)); + return next; + }); } else { - alert(response?.message || "데이터가 없습니다."); + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); } } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); } @@ -85,38 +232,119 @@ const SewageReservoirPumpRvw = () => { const handleActiveTabChange = async (tabKey: string) => { setActiveTab(tabKey); - const existingAData = rowDataMap.get('ST00040001'); const activeTabData = rowDataMap.get(tabKey); - - const hasAData = Array.isArray(existingAData) && existingAData.length > 0; const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; - if (hasAData && !hasActiveTabData && searchParamRef.current) { - try { - setIsLoading(true); + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoggedRef.current.has(tabKey) && + searchDoneRef.current; - let response; + if (!canLazyLoad) return; - if (tabKey === 'ST00040001') { - response = await statisticsManagementApi.getSewerReservoirList(searchParamRef.current); - } else if (tabKey === 'ST00040002') { - response = await statisticsManagementApi.getPumpStationList(searchParamRef.current); - } + const searchParam = searchParamRef.current; + if (!searchParam) return; - if (response?.status === 'success' && response.data) { - setRowDataMap(prev => { - const next = new Map(prev); - next.set(tabKey, toGridRows(response.data as object[])); - return next; - }); - } else if (response) { - alert(response?.message || "데이터가 없습니다."); - } - } catch { - alert('데이터 조회 중 오류가 발생했습니다.'); - } finally { - setIsLoading(false); + try { + setIsLoading(true); + + const response = await fetchSewageReservoirPumpReviewTab(tabKey, searchParam); + lazyTabLoggedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi ?? {}); + return next; + }); + const listData = getListDataByTab(tabKey, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, toGridRows(listData)); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); } }; @@ -124,14 +352,19 @@ const SewageReservoirPumpRvw = () => {
-
-
- { }} /> - { }} /> - { }} /> - { }} /> + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
-
+ )} {isLoading && (
@@ -141,16 +374,88 @@ const SewageReservoirPumpRvw = () => {
)} - + + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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={reviewCellStyleCallbackByTab.get(tabKey)} + /> + + ); + })} + +
+ + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> +
); }; -export default SewageReservoirPumpRvw; \ No newline at end of file +export default SewageReservoirPumpRvw; diff --git a/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx b/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx index 68a5b6cc0..30d2faaed 100644 --- a/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx +++ b/apps/st/src/components/statistics/review/SewerageFinanceRvw.tsx @@ -1,54 +1,440 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractSewerageFinanceReviewList, + extractSewerageFinanceReviewStatsStatusInfo, + mapSewerageFinanceReviewRows, + type SewerageFinanceReviewRequestParams, +} from "@/entities/statistics/model/statistics-sewerage-finance-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; - +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewerageFinanceInfoListRvw } from "@/components/util/column/review/SewerageFinanceRvwColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; +const firstTabKey = SewerageFinanceInfoListRvw[0]?.tabKey ?? "ST00090001"; -// 하수도 통계 관리 09. 하수도 재정 +async function fetchSewerageFinanceReviewTab( + tabKey: string, + searchParam: SewerageFinanceReviewRequestParams, +) { + if (tabKey === "ST00090001") { + return statisticsManagementApi.getSewerageFinanceSettleReviewList(searchParam); + } + if (tabKey === "ST00090002") { + return statisticsManagementApi.getSewerageChargeReviewList(searchParam); + } + if (tabKey === "ST00090003") { + return statisticsManagementApi.getSewerageUsageFeeReviewList(searchParam); + } + if (tabKey === "ST00090004") { + return statisticsManagementApi.getCauserBurdenReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 09. 하수도 재정 const SewerageFinanceRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoggedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewerageFinanceInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoggedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewerageFinanceReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoggedRef.current.clear(); + + const response = await fetchSewerageFinanceReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageFinanceReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewerageFinanceReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewerageFinanceReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoggedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchSewerageFinanceReviewTab(tabKey, searchParam); + lazyTabLoggedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageFinanceReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewerageFinanceReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewerageFinanceReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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={reviewCellStyleCallbackByTab.get(tabKey)} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; - -export default SewerageFinanceRvw; \ No newline at end of file +export default SewerageFinanceRvw; diff --git a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx index c13c93dd5..8aeb157c5 100644 --- a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx +++ b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx @@ -19,6 +19,8 @@ import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; import { ReviewOpinionDialog, type ReviewOpinionDialogContext, @@ -54,6 +56,11 @@ const SeweragePenetrationRateRvw = () => { }; }, []); + const cellStyleCallback = useMemo( + () => buildReviewCellStyleCallback(columnInfo.columns, rowData), + [columnInfo, rowData], + ); + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { const submit = statsStatusInfo.submitStatusCd != null ? String(statsStatusInfo.submitStatusCd) : ""; const prvn = statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; @@ -238,14 +245,18 @@ const SeweragePenetrationRateRvw = () => {
)} - +
+ + +
{ + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return SewerageServiceLevelInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: SewerageServiceLevelReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + swtrmtFcltNm: values.fctName, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchServiceLevelReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageServiceLevelReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractSewerageServiceLevelReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapSewerageServiceLevelReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + setRowDataMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchServiceLevelReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractSewerageServiceLevelReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractSewerageServiceLevelReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapSewerageServiceLevelReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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={reviewCellStyleCallbackByTab.get(tabKey)} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; - export default SewerageServiceLevelRvw; diff --git a/apps/st/src/components/statistics/review/WaterReuseRvw.tsx b/apps/st/src/components/statistics/review/WaterReuseRvw.tsx index e642a4448..d3976cf01 100644 --- a/apps/st/src/components/statistics/review/WaterReuseRvw.tsx +++ b/apps/st/src/components/statistics/review/WaterReuseRvw.tsx @@ -1,51 +1,444 @@ -import { useState } from "react"; +import { useMemo, useRef, useState } from "react"; -import { TabDataGrid } from "@rsone/components"; -import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; -import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import { useAlertStore, useUserStore } from "@/shared/lib"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { + extractWaterReuseReviewList, + extractWaterReuseReviewStatsStatusInfo, + mapWaterReuseReviewRows, + type WaterReuseReviewRequestParams, +} from "@/entities/statistics/model/statistics-water-reuse-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + ReviewerInfoRequest, + ReviewOpinionSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; + +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; +import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { WaterReuseInfoListRvw } from "@/components/util/column/review/WaterReuseRvwColumn"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; +import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; -import { WaterReuseInfoList } from "@/components/util/column/review/WaterReuseRvwColumn"; +const firstTabKey = WaterReuseInfoListRvw[0]?.tabKey ?? "ST00100001"; +async function fetchWaterReuseReviewTab( + tabKey: string, + searchParam: WaterReuseReviewRequestParams, +) { + if (tabKey === "ST00100001") { + return statisticsManagementApi.getWaterReuseReuseStatusReviewList(searchParam); + } + if (tabKey === "ST00100002") { + return statisticsManagementApi.getWaterReuseGrayWaterReviewList(searchParam); + } + if (tabKey === "ST00100003") { + return statisticsManagementApi.getWaterReuseRainwaterReviewList(searchParam); + } + if (tabKey === "ST00100004") { + return statisticsManagementApi.getWaterReuseRegStatusReviewList(searchParam); + } + if (tabKey === "ST00100005") { + return statisticsManagementApi.getWaterReuseSncnStatusReviewList(searchParam); + } + return null; +} + +// 하수도 통계 검토 10. 물 재이용 const WaterReuseRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [lastSearchValues, setLastSearchValues] = useState(null); + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); - const approveFunc = () => { - console.log('approveFunc'); - // 승인 로직 + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const lazyTabLoadedRef = useRef>(new Set()); + + const activeStatsStatusInfo = useMemo( + () => tabStatsStatusInfoMap.get(activeTab) ?? {}, + [tabStatsStatusInfoMap, activeTab], + ); + + const columnInfo = useMemo(() => { + return WaterReuseInfoListRvw.map((tab: ColumnInfoList) => ({ + ...tab, + columns: (tab.columns ?? []).map((col: ColumnConfig) => ({ + ...col, + editable: false as const, + styleName: col.styleName ?? ("rg-cell-readonly" as const), + })), + })); + }, []); + + const reviewCellStyleCallbackByTab = useMemo( + () => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap), + [columnInfo, rowDataMap], + ); + + const reviewOpinionContext = useMemo(() => { + if (lastSearchValues == null) return null; + return { + statsYr: lastSearchValues.year ?? "", + sewerBzmnCd: lastSearchValues.bsnmCd ?? "", + statsCd: activeStatsStatusInfo.statsCd ?? "", + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues]); + + const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? ""; + const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? ""; + + const submitDialogContext = useMemo(() => { + if (lastSearchValues == null) return null; + + const statsYr = lastSearchValues.year ?? ""; + const sewerBzmnCd = lastSearchValues.bsnmCd ?? ""; + const statsCd = activeStatsStatusInfo.statsCd ?? ""; + const instSeCd = userInfo?.instSeCd ?? ""; + + if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) { + return null; + } + + return { + statsYr, + sewerBzmnCd, + statsCd, + instSeCd, + }; + }, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = activeStatsStatusInfo; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [activeStatsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); + setReviewOpinionOpen(false); + setReturnDialogOpen(false); + setSubmitDialogOpen(false); + setLastSearchValues(null); + searchParamRef.current = null; + searchDoneRef.current = false; + lazyTabLoadedRef.current.clear(); }; - const saveFunc = () => { - console.log('submit'); - // 저장 로직 + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + + try { + setIsLoading(true); + setLastSearchValues(values); + + const searchParam: WaterReuseReviewRequestParams = { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + swtrmtFcltNm: values.fctName, + }; + + searchParamRef.current = searchParam; + searchDoneRef.current = true; + lazyTabLoadedRef.current.clear(); + + const response = await fetchWaterReuseReviewTab(activeTab, searchParam); + + if (response?.status === "success" && response.data) { + const ssi = extractWaterReuseReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi); + return next; + }); + const listData = extractWaterReuseReviewList(activeTab, response.data); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(activeTab, toGridRows(mapWaterReuseReviewRows(listData))); + return next; + }); + } else { + setTabStatsStatusInfoMap(new Map()); + alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + setTabStatsStatusInfoMap(new Map()); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); - // 검토의견 작성 + const handleActiveTabChange = async (tabKey: string) => { + setActiveTab(tabKey); + + const activeTabData = rowDataMap.get(tabKey); + const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0; + + const canLazyLoad = + searchParamRef.current && + !hasActiveTabData && + !lazyTabLoadedRef.current.has(tabKey) && + searchDoneRef.current; + + if (!canLazyLoad) return; + + const searchParam = searchParamRef.current; + if (!searchParam) return; + + try { + setIsLoading(true); + + const response = await fetchWaterReuseReviewTab(tabKey, searchParam); + lazyTabLoadedRef.current.add(tabKey); + + if (response?.status === "success" && response.data) { + const ssi = extractWaterReuseReviewStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi); + return next; + }); + setRowDataMap((prev) => { + const next = new Map(prev); + const listData = extractWaterReuseReviewList(tabKey, response.data); + next.set(tabKey, toGridRows(mapWaterReuseReviewRows(listData))); + return next; + }); + } else { + alert(response?.message || "데이터가 없습니다."); + } + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const returnFunc = () => { - console.log('returnFunc'); - // 반려 + const submitFunc = () => { + if (submitDialogContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + setSubmitDialogOpen(true); + }; + + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: row?.splmntOpnnCn ?? "", + wrterOpnnCn: row?.wrterOpnnCn ?? "", + }); + return next; + }); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, { + ...(next.get(activeTab) ?? {}), + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + }); + return next; + }); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && ( + submitFunc()} /> + )} +
+ )} + + {isLoading && ( +
+
+ +
+
+ )} + +
+ + void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {columnInfo.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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={reviewCellStyleCallbackByTab.get(tabKey)} + /> + + ); + })} +
- - + + { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + }} + /> + +
); }; -export default WaterReuseRvw; \ No newline at end of file +export default WaterReuseRvw; diff --git a/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx b/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx index e470b07fa..a5c7f2dae 100644 --- a/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx +++ b/apps/st/src/components/statistics/total/ExcretaTreatmentFacilityTot.tsx @@ -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[]>([]); + const [isLoading, setIsLoading] = useState(false); + const lastSearchedValuesRef = useRef(null); + + const config = ExcretaTreatmentStatusColumnConfig; + + const handleReset = () => { + setRowData([]); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (values: SearchFormValues): Promise => { + 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 ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ { + 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'; + }} + /> +
+
); }; diff --git a/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx b/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx index 1171f7b2c..0948ab60e 100644 --- a/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx +++ b/apps/st/src/components/statistics/total/PrivateSewageFacilityTot.tsx @@ -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[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = PrivateSewageColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + /** 각 탭별 엔드포인트 비동기 디스패치 및 연동 */ + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + 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 ( - <> - - +
+ + + {/* 하수관로 규칙과 100% 싱크 맞춘 중앙 정렬 소형 팝업 로딩 바 고정 */} + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + + // 단언 연산자(as) 배제 형태 완벽 적용 + const rowData = rowDataMap.get(tabKey) ?? []; + + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + 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'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/statistics/total/PumpingStationTot.tsx b/apps/st/src/components/statistics/total/PumpingStationTot.tsx index fd4cc679d..b3da0a044 100644 --- a/apps/st/src/components/statistics/total/PumpingStationTot.tsx +++ b/apps/st/src/components/statistics/total/PumpingStationTot.tsx @@ -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[]>([]); + const [isLoading, setIsLoading] = useState(false); + const lastSearchedValuesRef = useRef(null); + + const config = MediationPumpStatusColumnConfig; + + const handleReset = () => { + setRowData([]); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (values: SearchFormValues): Promise => { + 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 ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ { + 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'; + }} + /> +
+
); }; diff --git a/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx b/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx index adcc810ad..cbdf98747 100644 --- a/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx +++ b/apps/st/src/components/statistics/total/SewageExcretaRelatedTot.tsx @@ -1,10 +1,161 @@ +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"; +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[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + 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 ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {SewageExcretaIndustryColumnListTot.map((info) => ( + {info.tabLabel} + ))} + + + {SewageExcretaIndustryColumnListTot.map((info) => ( + + { + 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'; + }} + /> + + ))} + +
+
); }; diff --git a/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx b/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx index c384f1461..ffd2db631 100644 --- a/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx +++ b/apps/st/src/components/statistics/total/SewageExcretaSlurgeTot.tsx @@ -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[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = SludgeColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + +const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + 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 { + 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 { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + return ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + {info.tabLabel} + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + + return ( + + { + 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'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx b/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx index a061146c5..b3dfe7a46 100644 --- a/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx +++ b/apps/st/src/components/statistics/total/SewagePipeMaintainTot.tsx @@ -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 (
- + {isLoading && (
diff --git a/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx b/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx index 6016a7930..479e7b4b0 100644 --- a/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx +++ b/apps/st/src/components/statistics/total/SewageReservoirPumpTot.tsx @@ -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[]>>(new Map()); + + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = SewerReservoirColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + 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 ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + 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'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/statistics/total/SewerageAggregateSummary.tsx b/apps/st/src/components/statistics/total/SewerageAggregateSummary.tsx new file mode 100644 index 000000000..994285f7b --- /dev/null +++ b/apps/st/src/components/statistics/total/SewerageAggregateSummary.tsx @@ -0,0 +1,233 @@ +import { useEffect, useRef, useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import type { RealGridReact } from "realgrid-react"; + +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; +import { statisticsSewerageAggregateSummaryApi } from "@/entities/statistics/api/statistics-sewerage-aggregate-summary-service"; +import type { SewerageAggregateSummaryDownloadRequest } from "@/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface"; +import { LoadingWithAnimation } from "@common/components"; + +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; + +const columnInfo = { + columns: [ + { + name: "statsCd", + fieldName: "statsCd", + header: { text: "통계항목코드" }, + editable: false, + visible: false, + }, + { name: "tabNm", fieldName: "tabNm", header: { text: "통계항목" }, editable: false }, + ], + columnLayout: ["statsCd", "tabNm"], +}; + +type GridViewWithCheckApi = { + getCheckedRows?: (refresh?: boolean) => number[]; + getCheckedItems?: (refresh?: boolean) => number[]; +}; + +const EXCEL_DOWNLOAD_PROGRESS_MAX = 98; +const EXCEL_DOWNLOAD_PROGRESS_STEP = 1; +const EXCEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 500; +const EXCEL_DOWNLOAD_PROGRESS_LABEL = + "엑셀 파일을 생성 중입니다.\n창을 닫거나 이동하지 말고 잠시만 기다려주세요."; + +const SewerageAggregateSummary = () => { + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + const downloadProgressTimerRef = useRef | null>(null); + + useEffect(() => { + let cancelled = false; + + void (async () => { + try { + setIsLoading(true); + const response = + await statisticsSewerageAggregateSummaryApi.getAggregateSummaryTabList(); + + if (cancelled) return; + + if (response.status === "success") { + setRowData(toGridRows(response.data ?? [])); + } else { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + if (!cancelled) { + alert("데이터 조회 중 오류가 발생했습니다."); + } + } finally { + if (!cancelled) { + setIsLoading(false); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [alert]); + + useEffect(() => { + return () => { + if (downloadProgressTimerRef.current !== null) { + clearInterval(downloadProgressTimerRef.current); + } + }; + }, []); + + const stopDownloadProgressTimer = () => { + if (downloadProgressTimerRef.current !== null) { + clearInterval(downloadProgressTimerRef.current); + downloadProgressTimerRef.current = null; + } + }; + + const startDownloadProgress = () => { + stopDownloadProgressTimer(); + let currentProgress = 0; + + showGlobalProgress({ + label: EXCEL_DOWNLOAD_PROGRESS_LABEL, + progress: 0, + }); + + downloadProgressTimerRef.current = setInterval(() => { + currentProgress = Math.min( + currentProgress + EXCEL_DOWNLOAD_PROGRESS_STEP, + EXCEL_DOWNLOAD_PROGRESS_MAX, + ); + setGlobalProgress(currentProgress); + + if (currentProgress >= EXCEL_DOWNLOAD_PROGRESS_MAX) { + stopDownloadProgressTimer(); + } + }, EXCEL_DOWNLOAD_PROGRESS_INTERVAL_MS); + }; + + const handleDownload = async (values: SearchFormValues) => { + const gridView = getGridRef.current?.gridView as GridViewWithCheckApi | undefined; + const checkedIndexes = + gridView?.getCheckedRows?.(false) ?? gridView?.getCheckedItems?.(false) ?? []; + + const allRows = getGridDataRef.current?.() ?? rowData; + const statsCdList = checkedIndexes + .map((idx) => normalizeGridString(allRows[idx]?.statsCd)) + .filter((cd) => cd.length > 0); + + if (statsCdList.length === 0) { + alert("내려받을 통계항목을 선택해주세요."); + return; + } + + if (!values.year) { + alert("통계연도를 선택해주세요."); + return; + } + + if ( + !(await confirm( + `선택한 데이터 및 사용자 네트워크 환경에 따라 최대 1시간까지 시간이 소요될 수 있습니다. + 진행하시겠습니까?` + )) + ) { + return; + } + + const summaryYearCnt = + values.isSummary && values.isSummary !== "ALL" + ? Number(values.isSummary) + : 0; + + const request: SewerageAggregateSummaryDownloadRequest = { + statsCdList, + statsYr: values.year, + ctpvNm: values.sido === "ALL" ? "" : (values.sido ?? ""), + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + summaryYearCnt: Number.isFinite(summaryYearCnt) ? summaryYearCnt : 0, + }; + + try { + startDownloadProgress(); + const { blob, fileName } = + await statisticsSewerageAggregateSummaryApi.downloadAggregateSummary(request); + + stopDownloadProgressTimer(); + setGlobalProgress(100); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName || `하수도통계 집계 요약표_${values.year}.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + await new Promise((resolve) => setTimeout(resolve, 400)); + alert("저장되었습니다."); + } catch (error) { + const message = + error instanceof Error && error.message + ? error.message + : "엑셀 내려받기 중 오류가 발생했습니다."; + alert(message); + } finally { + stopDownloadProgressTimer(); + hideGlobalProgress(); + } + }; + + return ( +
+ void handleDownload(values)} + isReset={false} + /> + {isLoading && ( +
+
+ +
+
+ )} + +
+ ); +}; + +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +export default SewerageAggregateSummary; \ No newline at end of file diff --git a/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx b/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx index a73eaba2f..7749bac68 100644 --- a/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx +++ b/apps/st/src/components/statistics/total/SewerageFinanceTot.tsx @@ -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[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = SewerageFinanceColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + /** 개별 조건별 데이터 동적 라우팅 디스패치 */ + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + 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 ( - <> - - +
+ + + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + + // 요청하신 타입 단언 제거 형태 유지 + const rowData = rowDataMap.get(tabKey) ?? []; + + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + 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'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx b/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx index 452419dc2..f5a60531f 100644 --- a/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx +++ b/apps/st/src/components/statistics/total/SewerageServiceLevelTot.tsx @@ -1,131 +1,98 @@ 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[]>>(new Map()); - const [isLoading, setIsLoading] = useState(false); - const [activeTab, setActiveTab] = useState(ComplaintInpColumn.tabKey); - const lastSearchValuesRef = useRef(null); - - const columnInfo = SewerageServiceLevelInpTabList; + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(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 => { + 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[]>(); - 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 (
- + + {isLoading && (
@@ -133,58 +100,48 @@ const SewerageServiceLevelTot = () => {
)} + + +
void handleActiveTabChange(v)} className="w-full" variant="default"> - {columnInfo.map((info) => ( - - {info.tabLabel || info.tabKey} - + {SewageServiceLevelColumnListTot.map((info) => ( + {info.tabLabel} ))} - {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 ( - - { - 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"; - }} - /> - - ); - })} + + {SewageServiceLevelColumnListTot.map((info) => ( + + { + 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'; + }} + /> + + ))}
diff --git a/apps/st/src/components/statistics/total/WaterReuseTot.tsx b/apps/st/src/components/statistics/total/WaterReuseTot.tsx index 567797983..e48d12673 100644 --- a/apps/st/src/components/statistics/total/WaterReuseTot.tsx +++ b/apps/st/src/components/statistics/total/WaterReuseTot.tsx @@ -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[]>>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const lastSearchedValuesRef = useRef(null); + + const columnInfo = WaterReuseColumnListTot; + + const handleReset = () => { + setRowDataMap(new Map()); + setActiveTab(firstTabKey); + lastSearchedValuesRef.current = null; + }; + + const loadGridData = async (tabKey: string, values: SearchFormValues): Promise => { + 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 ( - <> - - +
+ + + {/* 정중앙 5rem 크기 고정 격리형 로딩 애니메이션 */} + {isLoading && ( +
+
+ +
+
+ )} + + + +
+ void handleActiveTabChange(v)} className="w-full" variant="default"> + + {columnInfo.map((info) => ( + + {info.tabLabel} + + ))} + + + {columnInfo.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + + const gridColumnInfo: ColumnInfo = { + columns: info.columns, + columnLayout: info.columnLayout, + }; + + return ( + + { + 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'; + }} + /> + + ); + })} + +
+
); }; diff --git a/apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx b/apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx index 556d3f54a..bb2f30974 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/CopyDialog.tsx @@ -63,11 +63,14 @@ const CopyDialog = ({ try { setIsLoading(true); const userInfo = await getUserInfo(); - const response = await generalverificationApiService.CopyVrfcData({ - vrfcSn: row?.vrfcSn ?? null, - targetStatsCd: selectedCode ?? '', - rgtrId: userInfo.userUnqId - }); + const response = await generalverificationApiService.CopyVrfcData( + { + vrfcSn: row?.vrfcSn ?? null, + targetStatsCd: selectedCode ?? '', + rgtrId: userInfo.userUnqId, + }, + userInfo.userUnqId, + ); if (response.status === 'success') { alert('복사되었습니다.'); diff --git a/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx index 1aa0de71f..518f2da32 100644 --- a/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx +++ b/apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx @@ -177,7 +177,12 @@ const RegistDialog = memo( if (await confirm(deleteMsg)) { try { - const response = await generalverificationApiService.DeleteVrfcMaster(vrfcSn!); + const userInfo = await getUserInfo(); + const response = + await generalverificationApiService.DeleteVrfcMaster( + vrfcSn!, + userInfo.userUnqId, + ); if (response.status === 'success') { alert('삭제되었습니다.'); diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index f02424f48..00e4fbb01 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -81,6 +81,12 @@ export type GridView = { getDataRow(itemIndex: number): number; }; +/** RealGrid gridView 편집 확정 API (realgrid-react gridView 타입에 미포함) */ +export type GridViewWithEditCommit = { + commit?: (force?: boolean) => void; + cancel?: () => void; +}; + export type MergeCallback = ( grid: GridView, column: DataColumn, @@ -116,12 +122,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 +165,38 @@ 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"; + } + | { + /** Σ(numeratorFields) / denominatorField × 10,000 (건/10,000명) */ + numeratorFields: string[]; + denominatorField: string; + type: "sumPerTenThousand"; + } + | { + /** (quotient[0]/quotient[1]) / (denominatorQuotient[0]/denominatorQuotient[1]) × 100 */ + numeratorQuotient: [string, string]; + denominatorQuotient: [string, string]; + type: "quotientRatioPercent"; + } + | { + /** (numeratorField / denominatorField) × multiplierField × 100 */ + numeratorField: string; + denominatorField: string; + multiplierField: string; + type: "quotientTimesMultiplierPercent"; }; export type ColumnConfig = { @@ -271,15 +319,30 @@ export type ColumnInfoList = { }; // 개별 셀 스타일을 동적으로 결정하기 위한 콜백 (예: 검증 오류 셀 강조) +export type CellStyleCallbackGrid = { + getValue?: (itemIndex: number, fieldName: string) => unknown; +}; + export type CellStyleCallbackParams = { rowIndex: number; fieldName: string; row: Record; + /** styleCallback 시점의 그리드 값 조회(행 단위 동적 스타일용) */ + grid?: CellStyleCallbackGrid; }; +/** RealGrid styleCallback — 문자열(styleName) 또는 editable 등 동적 스타일 객체 */ +export type CellStyleCallbackResult = + | string + | { + styleName?: string; + editable?: boolean; + readOnly?: boolean; + }; + export type CellStyleCallback = ( params: CellStyleCallbackParams, -) => string | undefined; +) => CellStyleCallbackResult | undefined; /** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */ export type CellTooltipCallbackParams = { @@ -338,6 +401,8 @@ export type DataGridProps = { /** 외부에서 gridRef에 직접 접근하기 위한 ref (setRows/addRows/setValue 직접 호출용) */ getGridRef?: React.MutableRefObject; onRowClick?: (rowData: Record) => void; // 행 클릭 이벤트 + onRowDblClick?: (rowData: Record) => void; // ← 추가 + onCellDblClick?: DataCellClickCallback; /** 데이터 셀 클릭 시 행 + 클릭한 필드명 전달 (`onRowClick`과 함께 쓸 수 있음) */ onDataCellClick?: DataCellClickCallback; /** CheckBar 행 체크 변경 */ diff --git a/apps/st/src/components/type/SearchForm.type.ts b/apps/st/src/components/type/SearchForm.type.ts index 18ce8aea7..62c7fab7a 100644 --- a/apps/st/src/components/type/SearchForm.type.ts +++ b/apps/st/src/components/type/SearchForm.type.ts @@ -30,10 +30,12 @@ export type SearchFormProps = { onReset?: () => void; onValuesChange?: (values: SearchFormValues) => void; defaultValues?: SearchFormValues; + searchButtonText?: string; + isReset?: boolean; }; export const defaultSearchFormProps: Required< - Omit + Omit > = { sido: true, gugun: true, @@ -44,4 +46,5 @@ export const defaultSearchFormProps: Required< userMenual: false, isDetail: false, isSummary: false, + isReset: true, }; \ No newline at end of file diff --git a/apps/st/src/components/util/MenuMatchList.ts b/apps/st/src/components/util/MenuMatchList.ts index ded92e5a4..0ece5c741 100644 --- a/apps/st/src/components/util/MenuMatchList.ts +++ b/apps/st/src/components/util/MenuMatchList.ts @@ -1,8 +1,11 @@ /** * statistics/input 화면과 탭(·분할 그리드) 키 매칭용 상수. - * 각 컴포넌트에 실제로 쓰이는 `tabKey`를 모두 포함합니다. + * 각 컴포넌트에 실제로 쓰이는 `tabKey`·`statsCd`를 모두 포함합니다. */ +/** `SeweragePenetrationRateInp.tsx` — 단일 그리드 */ +export const SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS = ["ST00010001"] as const; + /** `SewagePipeInp.tsx` — 메인 탭 + `ConnectionStatusInpColumn` 좌·우 그리드 서브 키 */ export const SEWAGE_PIPE_INP_TAB_KEYS = [ "ST00020001", @@ -18,16 +21,108 @@ export const SEWAGE_PIPE_INP_TAB_KEYS = [ /** `SewagePipeMaintainInp.tsx` — 보수·준설 탭 */ export const SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS = ["ST00030001", "ST00030002"] as const; -/** `SeweragePenetrationRateInp.tsx` — 단일 그리드(통계코드) */ -export const SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS = ["ST00010001"] as const; +/** `SewageReservoirPumpInp.tsx` */ +export const SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS = ["ST00040001", "ST00040002"] as const; +/** `PumpingStationInp.tsx` */ +export const PUMPING_STATION_INP_TAB_KEYS = ["ST00050001"] as const; + +/** `PublicSewageFacilityInp.tsx` */ +export const PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS = ["ST00060001", "ST00060002"] as const; + +/** `ExcretaTreatmentFacilityInp.tsx` */ +export const EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS = ["ST00070001"] as const; + +/** + * `PrivateSewageFacilityInp.tsx` + * ST00080003은 tabKey(`privateSewageDesigStatusInp`)와 다름 — `INPUT_TAB_KEY_BY_STATS_CD` 참고 + */ +export const PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS = [ + "ST00080001", + "ST00080002", + "ST00080003", +] as const; + +/** `SewerageFinanceInp.tsx` */ +export const SEWERAGE_FINANCE_INP_TAB_KEYS = [ + "ST00090001", + "ST00090002", + "ST00090003", + "ST00090004", +] as const; + +/** `WaterReuseInp.tsx` */ +export const WATER_REUSE_INP_TAB_KEYS = [ + "ST00100001", + "ST00100002", + "ST00100003", + "ST00100004", + "ST00100005", +] as const; + +/** `SewageExcretaSlurgeInp.tsx` */ +export const SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS = [ + "ST00110001", + "ST00110002", + "ST00110003", + "ST00110004", +] as const; + +/** `SewageExcretaRelatedInp.tsx` */ +export const SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS = [ + "ST00120001", + "ST00120002", + "ST00120003", + "ST00120004", +] as const; + +/** `SewerageServiceLevelInp.tsx` */ +export const SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS = [ + "ST00130001", + "ST00130002", +] as const; + +/** statsCd와 입력 화면 activeTab(tabKey)이 다른 경우 */ +export const INPUT_TAB_KEY_BY_STATS_CD: Record = { + ST00080003: "privateSewageDesigStatusInp", +}; + +export type SeweragePenetrationRateInpTabKey = + (typeof SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS)[number]; export type SewagePipeInpTabKey = (typeof SEWAGE_PIPE_INP_TAB_KEYS)[number]; -export type SewagePipeMaintainInpTabKey = (typeof SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS)[number]; -export type SeweragePenetrationRateInpTabKey = (typeof SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS)[number]; +export type SewagePipeMaintainInpTabKey = + (typeof SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS)[number]; +export type SewageReservoirPumpInpTabKey = + (typeof SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS)[number]; +export type PumpingStationInpTabKey = (typeof PUMPING_STATION_INP_TAB_KEYS)[number]; +export type PublicSewageFacilityInpTabKey = + (typeof PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS)[number]; +export type ExcretaTreatmentFacilityInpTabKey = + (typeof EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS)[number]; +export type PrivateSewageFacilityInpTabKey = + (typeof PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS)[number]; +export type SewerageFinanceInpTabKey = (typeof SEWERAGE_FINANCE_INP_TAB_KEYS)[number]; +export type WaterReuseInpTabKey = (typeof WATER_REUSE_INP_TAB_KEYS)[number]; +export type SewageExcretaSlurgeInpTabKey = + (typeof SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS)[number]; +export type SewageExcretaRelatedInpTabKey = + (typeof SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS)[number]; +export type SewerageServiceLevelInpTabKey = + (typeof SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS)[number]; /** 컴포넌트 파일명 기준 묶음 (메뉴·라우트 매칭 시 참고) */ export const STATISTICS_INPUT_TAB_KEYS_BY_SCREEN = { + SeweragePenetrationRateInp: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS, SewagePipeInp: SEWAGE_PIPE_INP_TAB_KEYS, SewagePipeMaintainInp: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS, - SeweragePenetrationRateInp: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS, + SewageReservoirPumpInp: SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS, + PumpingStationInp: PUMPING_STATION_INP_TAB_KEYS, + PublicSewageFacilityInp: PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS, + ExcretaTreatmentFacilityInp: EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS, + PrivateSewageFacilityInp: PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS, + SewerageFinanceInp: SEWERAGE_FINANCE_INP_TAB_KEYS, + WaterReuseInp: WATER_REUSE_INP_TAB_KEYS, + SewageExcretaSlurgeInp: SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS, + SewageExcretaRelatedInp: SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS, + SewerageServiceLevelInp: SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS, } as const; diff --git a/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts b/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts index 4896a2194..ecaef2339 100644 --- a/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts +++ b/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts @@ -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, styleName: 'rg-cell-readonly' }, + { 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', }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts index db0c550ff..d9943bbd6 100644 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts @@ -8,7 +8,7 @@ import { footerDecimal1, footerDecimal3,footerInt } from "@/components/util/grid export const PublicSewageFactColumn = { columns: [ // 행정구역 - { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, footer: { text: '합계' }, styleName: 'rg-cell-readonly' }, // 시설명 @@ -18,7 +18,20 @@ export const PublicSewageFactColumn = { { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false, styleName: 'rg-cell-readonly' }, // 시설용량 - { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, + { + name: 'prcsFcltCpct', + fieldName: 'prcsFcltCpct', + header: { text: '시설용량(500m³/일 이상/미만)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['prcsFcltPhysCpct', 'prcsFcltBoilCpct', 'prcsFcltAltdCpct'], + type: 'sum' as const, + }, + }, // (m³/일) { name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, @@ -56,8 +69,22 @@ export const PublicSewageFactColumn = { { name: 'dschgGrssClfgVl', fieldName: 'dschgGrssClfgVl', header: { text: '방류총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 처리특성 - { name: 'prcsRt', fieldName: 'prcsRt', header: { text: '처리효율(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, - { name: 'prcsLodQnt', fieldName: 'prcsLodQnt', header: { text: '처리부하량(kgBOD/D)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, + { + name: 'prcsRt', + fieldName: 'prcsRt', + header: { text: '처리효율(%)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + inflwField: 'inflwBodVl', + dschgField: 'dschgBodVl', + type: 'bodPrcsRt' as const, + }, + }, + { name: 'prcsLodQnt', fieldName: 'prcsLodQnt', header: { text: '처리부하량(kgBOD/D)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, { name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리방법' }, editable: false, styleName: 'rg-cell-readonly' }, // 연계처리량 @@ -86,7 +113,20 @@ export const PublicSewageFactColumn = { { name: 'goalWqNo', fieldName: 'goalWqNo', header: { text: '목표수질(BOD)' }, editable: false, styleName: 'rg-cell-readonly' }, // 에너지 자립률 - { name: 'engySrlneRt', fieldName: 'engySrlneRt', header: { text: '에너지자립률(%)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { + name: 'engySrlneRt', + fieldName: 'engySrlneRt', + header: { text: '에너지자립률(%)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + numeratorFields: ['engyItslfPrdctnQnt'], + denominatorFields: ['anlEngyWholUseQnt'], + type: 'pblswrgPopltnSuprtRate' as const, + }, + }, // 에너지 자체 생산량 { name: 'engyItslfPrdctnQnt', fieldName: 'engyItslfPrdctnQnt', header: { text: '에너지자체생산량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, @@ -97,7 +137,7 @@ export const PublicSewageFactColumn = { { name: 'itslfEngyUseQnt', fieldName: 'itslfEngyUseQnt', header: { text: '자체 에너지사용량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 하수처리량당 CO2배출량 - { name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-total' }, + { name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, // 운영주체 { name: 'operMnbdCn', fieldName: 'operMnbdCn', header: { text: '운영주체(자체/공기업/민간대행)' }, editable: false, styleName: 'rg-cell-readonly' }, @@ -123,7 +163,10 @@ export const PublicSewageFactColumn = { name: 'administrative', hideChildHeaders: false, header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'], + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, 'swtrmtFcltNm', 'fcltPstnCn', diff --git a/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts b/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts index 6a174daca..deb9d592e 100644 --- a/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts +++ b/apps/st/src/components/util/column/input/PumpingStationInpColumn.ts @@ -1,6 +1,33 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 펌프장 구분(`pumpSeNm`) */ +export const PUMPING_STATION_PUMP_SE_VALUES = ["합류식", "분류식"] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + /** * 하수도 통계 관리 > 05. 펌프장 * 단일 Tab @@ -8,69 +35,155 @@ import { ValueType } from "realgrid"; export const PumpingStationInpColumn = { columns: [ // 행정구역 - { name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 기본 정보 - { name: 'treatmentFacilityName', fieldName: '처리장명', header: { text: '처리장명' } }, - { name: 'facilityName', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'classification', fieldName: '구분(합류식 분류식)', header: { text: '구분(합류식 분류식)' } }, - { name: 'location', fieldName: '위치', header: { text: '위치' } }, - { name: 'operationDate', fieldName: '가동 년월일 yyyy-mm-dd', header: { text: '가동 년월일 yyyy-mm-dd' } }, - { name: 'planDrainageArea', fieldName: '계획배수면적(㎢)', header: { text: '계획배수면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } }, + dropdownColumn( + { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분(합류식 분류식)' } }, + PUMPING_STATION_PUMP_SE_VALUES, + ), + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치' } }, + { + 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: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 계획시간 대 최대배수량 - { name: 'fairWeatherFlow', fieldName: '청천시(A)', header: { text: '청천시(A)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'rainyWeatherFlow', fieldName: '우천시(B)', header: { text: '우천시(B)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'rainwaterFlow', fieldName: '우수량(B-A)', header: { text: '우수량(B-A)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sndyArea', fieldName: 'sndyArea', header: { text: '청천시(A)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'rainArea', fieldName: 'rainArea', header: { text: '우천시(B)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'exlnQnt', + fieldName: 'exlnQnt', + header: { text: '우수량(B-A)' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { fields: ['rainArea', 'sndyArea'], type: 'difference' as const }, + styleName: 'rg-cell-total', + }, // 배수능력 > 총 양수량 (오수) - { name: 'sewageTotalPumpCapacity', fieldName: '총양수량(m³/min)', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'drtwPumpGrssPpqty', + fieldName: 'drtwPumpGrssPpqty', + header: { text: '총양수량(m³/min)' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + type: 'pumpGrossPpqty' as const, + pumpGroups: [ + { + pumpCntField: 'drtwPumpN1Cnt', + rsrvPumpCntField: 'drtwPumpN1RsrvCnt', + perPpqtyField: 'drtwPumpN1Ppqty', + }, + { + pumpCntField: 'drtwPumpN2Cnt', + rsrvPumpCntField: 'drtwPumpN2RsrvCnt', + perPpqtyField: 'drtwPumpN2Ppqty', + }, + { + pumpCntField: 'drtwPumpN3Cnt', + rsrvPumpCntField: 'drtwPumpN3RsrvCnt', + perPpqtyField: 'drtwPumpN3Ppqty', + }, + ], + }, + }, // 배수능력 > 오수펌프(1) - { name: 'sewagePump1Diameter', fieldName: '오수펌프(1) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewagePump1Capacity', fieldName: '오수펌프(1) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'sewagePump1Count', fieldName: '오수펌프(1) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewagePump1Reserve', fieldName: '오수펌프(1) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN1DmtrArea', fieldName: 'drtwPumpN1DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN1Ppqty', fieldName: 'drtwPumpN1Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'drtwPumpN1Cnt', fieldName: 'drtwPumpN1Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN1RsrvCnt', fieldName: 'drtwPumpN1RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 배수능력 > 오수펌프(2) - { name: 'sewagePump2Diameter', fieldName: '오수펌프(2) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewagePump2Capacity', fieldName: '오수펌프(2) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'sewagePump2Count', fieldName: '오수펌프(2) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewagePump2Reserve', fieldName: '오수펌프(2) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN2DmtrArea', fieldName: 'drtwPumpN2DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN2Ppqty', fieldName: 'drtwPumpN2Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'drtwPumpN2Cnt', fieldName: 'drtwPumpN2Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN2RsrvCnt', fieldName: 'drtwPumpN2RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 배수능력 > 오수펌프(3) - { name: 'sewagePump3Diameter', fieldName: '오수펌프(3) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewagePump3Capacity', fieldName: '오수펌프(3) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'sewagePump3Count', fieldName: '오수펌프(3) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'sewagePump3Reserve', fieldName: '오수펌프(3) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN3DmtrArea', fieldName: 'drtwPumpN3DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN3Ppqty', fieldName: 'drtwPumpN3Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'drtwPumpN3Cnt', fieldName: 'drtwPumpN3Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPumpN3RsrvCnt', fieldName: 'drtwPumpN3RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 배수능력 > 총 양수량 (우수) - { name: 'rainwaterTotalPumpCapacity', fieldName: '총양수량(m³/min)', header: { text: '총양수량(m³/min)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'exlnPumpGrssPpqty', + fieldName: 'exlnPumpGrssPpqty', + header: { text: '총양수량(m³/min)' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + type: 'pumpGrossPpqty' as const, + pumpGroups: [ + { + pumpCntField: 'exlnPumpN1Cnt', + rsrvPumpCntField: 'exlnPumpN1RsrvCnt', + perPpqtyField: 'exlnPumpN1Ppqty', + }, + { + pumpCntField: 'exlnPumpN2Cnt', + rsrvPumpCntField: 'exlnPumpN2RsrvCnt', + perPpqtyField: 'exlnPumpN2Ppqty', + }, + { + pumpCntField: 'exlnPumpN3Cnt', + rsrvPumpCntField: 'exlnPumpN3RsrvCnt', + perPpqtyField: 'exlnPumpN3Ppqty', + }, + ], + }, + }, // 배수능력 > 우수펌프(1) - { name: 'rainwaterPump1Diameter', fieldName: '우수펌프(1) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwaterPump1Capacity', fieldName: '우수펌프(1) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'rainwaterPump1Count', fieldName: '우수펌프(1) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwaterPump1Reserve', fieldName: '우수펌프(1) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN1DmtrArea', fieldName: 'exlnPumpN1DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN1Ppqty', fieldName: 'exlnPumpN1Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'exlnPumpN1Cnt', fieldName: 'exlnPumpN1Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN1RsrvCnt', fieldName: 'exlnPumpN1RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 배수능력 > 우수펌프(2) - { name: 'rainwaterPump2Diameter', fieldName: '우수펌프(2) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwaterPump2Capacity', fieldName: '우수펌프(2) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'rainwaterPump2Count', fieldName: '우수펌프(2) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwaterPump2Reserve', fieldName: '우수펌프(2) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN2DmtrArea', fieldName: 'exlnPumpN2DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN2Ppqty', fieldName: 'exlnPumpN2Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'exlnPumpN2Cnt', fieldName: 'exlnPumpN2Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN2RsrvCnt', fieldName: 'exlnPumpN2RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 배수능력 > 우수펌프(3) - { name: 'rainwaterPump3Diameter', fieldName: '우수펌프(3) 구경(mm)', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwaterPump3Capacity', fieldName: '우수펌프(3) 대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'rainwaterPump3Count', fieldName: '우수펌프(3) 펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'rainwaterPump3Reserve', fieldName: '우수펌프(3) 예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN3DmtrArea', fieldName: 'exlnPumpN3DmtrArea', header: { text: '구경(mm)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN3Ppqty', fieldName: 'exlnPumpN3Ppqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'exlnPumpN3Cnt', fieldName: 'exlnPumpN3Cnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'exlnPumpN3RsrvCnt', fieldName: 'exlnPumpN3RsrvCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 연간전력 사용량 - { name: 'annualPowerUsage', fieldName: '연간전력사용량(kWh/년)', header: { text: '연간전력사용량(kWh/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '연간전력사용량(kWh/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'remarks', 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: [ @@ -78,66 +191,75 @@ export const PumpingStationInpColumn = { name: 'administrativeArea', hideChildHeaders: false, header: { text: '행정구역' }, - items: ['sido', 'sgg'], + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'treatmentFacilityName', - 'facilityName', - 'classification', - 'location', - 'operationDate', - 'planDrainageArea', + 'swtrmtFcltNm', + 'fcltNm', + 'pumpSeNm', + 'fcltPstnCn', + 'oprtngStrtYmd', + 'planMltvArea', { name: 'planFlowGroup', hideChildHeaders: false, header: { text: '계획시간대최대배수량(m³/hr)' }, - items: ['fairWeatherFlow', 'rainyWeatherFlow', 'rainwaterFlow'], + items: ['sndyArea', 'rainArea', 'exlnQnt'], }, { name: 'drainageCapability', hideChildHeaders: false, header: { text: '배수능력' }, items: [ - 'sewageTotalPumpCapacity', + 'drtwPumpGrssPpqty', { name: 'sewagePump1Group', hideChildHeaders: false, header: { text: '오수펌프(1)' }, - items: ['sewagePump1Diameter', 'sewagePump1Capacity', 'sewagePump1Count', 'sewagePump1Reserve'], + items: ['drtwPumpN1DmtrArea', 'drtwPumpN1Ppqty', 'drtwPumpN1Cnt', 'drtwPumpN1RsrvCnt'], }, { name: 'sewagePump2Group', hideChildHeaders: false, header: { text: '오수펌프(2)' }, - items: ['sewagePump2Diameter', 'sewagePump2Capacity', 'sewagePump2Count', 'sewagePump2Reserve'], + items: ['drtwPumpN2DmtrArea', 'drtwPumpN2Ppqty', 'drtwPumpN2Cnt', 'drtwPumpN2RsrvCnt'], }, { name: 'sewagePump3Group', hideChildHeaders: false, header: { text: '오수펌프(3)' }, - items: ['sewagePump3Diameter', 'sewagePump3Capacity', 'sewagePump3Count', 'sewagePump3Reserve'], + items: ['drtwPumpN3DmtrArea', 'drtwPumpN3Ppqty', 'drtwPumpN3Cnt', 'drtwPumpN3RsrvCnt'], }, - 'rainwaterTotalPumpCapacity', + 'exlnPumpGrssPpqty', { name: 'rainwaterPump1Group', hideChildHeaders: false, header: { text: '우수펌프(1)' }, - items: ['rainwaterPump1Diameter', 'rainwaterPump1Capacity', 'rainwaterPump1Count', 'rainwaterPump1Reserve'], + items: ['exlnPumpN1DmtrArea', 'exlnPumpN1Ppqty', 'exlnPumpN1Cnt', 'exlnPumpN1RsrvCnt'], }, { name: 'rainwaterPump2Group', hideChildHeaders: false, header: { text: '우수펌프(2)' }, - items: ['rainwaterPump2Diameter', 'rainwaterPump2Capacity', 'rainwaterPump2Count', 'rainwaterPump2Reserve'], + items: ['exlnPumpN2DmtrArea', 'exlnPumpN2Ppqty', 'exlnPumpN2Cnt', 'exlnPumpN2RsrvCnt'], }, { name: 'rainwaterPump3Group', hideChildHeaders: false, header: { text: '우수펌프(3)' }, - items: ['rainwaterPump3Diameter', 'rainwaterPump3Capacity', 'rainwaterPump3Count', 'rainwaterPump3Reserve'], + items: ['exlnPumpN3DmtrArea', 'exlnPumpN3Ppqty', 'exlnPumpN3Cnt', 'exlnPumpN3RsrvCnt'], }, ], }, - 'annualPowerUsage', - 'remarks', - ] + 'anlElpwrUseQnt', + 'rmrk', + 'admdstCd', + 'fcltSn', + 'admdstNm' + ], + rowStartNum: 3, + fixedColumns: 3, + tabKey: 'ST00050001', }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts b/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts index b7eaf2cca..88cdc2e40 100644 --- a/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn.ts @@ -5,66 +5,64 @@ import { ValueType } from "realgrid"; export const PrivateSewageDesigStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 개인하수도관리지역공고현황 - { name: 'MGMT_AR', fieldName: '공고지역(개소)', header: { text: '공고지역(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'APPN_SCOPE', fieldName: '지정범위(㎢)', header: { text: '지정범위(㎢)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'indvSwagePbancRgnCnt', fieldName: 'indvSwagePbancRgnCnt', header: { text: '공고지역(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'indvSwageDsgnScpLen', fieldName: 'indvSwageDsgnScpLen', header: { text: '지정범위(㎢)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 개인하수도관리지역공고현황 > 정화조 - { name: 'STTY_CNT', fieldName: '개소수', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_CPC', fieldName: '처리용량(인/일)', header: { text: '처리용량(인/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'spctnkGrssFcltCnt', fieldName: 'spctnkGrssFcltCnt', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'spctnkQnt', fieldName: 'spctnkQnt', header: { text: '처리용량(인/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 개인하수도관리지역공고현황 > 오수시설 - { name: 'WRRTN_CNT', fieldName: '개소수', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'WRRTN_CPC', fieldName: '처리용량(m³/일)', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'drtwPrcsFcltGrssFcltCnt', fieldName: 'drtwPrcsFcltGrssFcltCnt', header: { text: '개소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPrcsFcltQnt', fieldName: 'drtwPrcsFcltQnt', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // 검증 필드 (hidden) - { name: 'CHK_MGMT_AR', fieldName: 'CHK_MGMT_AR', header: { text: '' } }, - { name: 'CHK_APPN_SCOPE', fieldName: 'CHK_APPN_SCOPE', header: { text: '' } }, - { name: 'CHK_STTY_CNT', fieldName: 'CHK_STTY_CNT', header: { text: '' } }, - { name: 'CHK_STTY_CPC', fieldName: 'CHK_STTY_CPC', header: { text: '' } }, - { name: 'CHK_WRRTN_CNT', fieldName: 'CHK_WRRTN_CNT', header: { text: '' } }, - { name: 'CHK_WRRTN_CPC', fieldName: 'CHK_WRRTN_CPC', header: { text: '' } }, - - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_notice', header: { text: '개인하수도관리지역공고현황 (하수도법제34조의2)' }, items: [ - 'MGMT_AR', - 'APPN_SCOPE', + 'indvSwagePbancRgnCnt', + 'indvSwageDsgnScpLen', { name: 'group_septic', header: { text: '정화조' }, items: [ - 'STTY_CNT', - 'STTY_CPC' + 'spctnkGrssFcltCnt', + 'spctnkQnt' ] }, { name: 'group_sewage', header: { text: '오수시설' }, items: [ - 'WRRTN_CNT', - 'WRRTN_CPC' + 'drtwPrcsFcltGrssFcltCnt', + 'drtwPrcsFcltQnt' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, diff --git a/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts b/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts index 4d2b45b8f..ef1d1aa1c 100644 --- a/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn.ts @@ -5,182 +5,316 @@ import { ValueType } from "realgrid"; export const SepticTankStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, - // 총 시설 수 - { name: 'TOT_FCLTY_CNT', fieldName: '총 시설 수 (A+D)', header: { text: '총 시설 수 (A+D)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + // 총 시설 수 — (B+C)/2 + (E+F)/2 = A + D + { + name: 'grssFcltCnt', + fieldName: 'grssFcltCnt', + header: { text: '총 시설 수 (A+D)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + firstHalfSum: { + firstSumFields: [ + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt', + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt', + ], + secondSumFields: [ + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope', + ], + }, + secondHalfSum: { + firstSumFields: [ + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt', + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt', + ], + secondSumFields: [ + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope', + ], + }, + type: 'sumOfTwoHalfSumOfSums' as const, + }, + }, // 하수처리구역 내 > 총계 - { name: 'SPZI_SM', fieldName: '총계 A=(B+C)/2', header: { text: '총계 A=(B+C)/2' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaInsdWhol', + fieldName: 'staaInsdWhol', + header: { text: '총계 A=(B+C)/2' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + firstSumFields: [ + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt', + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt', + ], + secondSumFields: [ + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope', + ], + type: 'halfSumOfSums' as const, + }, + }, // 하수처리구역 내 > 처리방식별 - { name: 'SPZI_MTHD_SM', fieldName: '계(B)', header: { text: '계(B)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaInsdPrcsMthByWhol', + fieldName: 'staaInsdPrcsMthByWhol', + header: { text: '계(B)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt', + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt', + ], + type: 'sum' as const, + }, + }, // 하수처리구역 내 > 처리방식별 > 부패탱크 - { name: 'SPZI_ROT_NPFL_CNT', fieldName: '자연유하', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_ROT_ENFCT_PUMP_1', fieldName: '강제펌핑식', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdSttkNprsflCnt', fieldName: 'staaInsdSttkNprsflCnt', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step1StaaInsdSttkFrcCnt', fieldName: 'step1StaaInsdSttkFrcCnt', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 처리방식별 - { name: 'SPZI_WTROT_NPFL_CNT', fieldName: '살수여상', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_TFM', fieldName: '폭기방법', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_ARTM', fieldName: '접촉폭기', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_CTARM', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdSprktpFltrtbCnt', fieldName: 'staaInsdSprktpFltrtbCnt', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdAertMthdCnt', fieldName: 'staaInsdAertMthdCnt', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdCntcAertCnt', fieldName: 'staaInsdCntcAertCnt', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaInsdEtcCnt', fieldName: 'staaInsdEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 처리용량별 - { name: 'SPZI_PROC_CPC_SM', fieldName: '계(C)', header: { text: '계(C)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_1', fieldName: '10인조이하', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_2', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_3', fieldName: '21~30', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_4', fieldName: '31~50', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_5', fieldName: '51~199', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_6', fieldName: '200~499', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_7', fieldName: '500~999', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_8', fieldName: '1,000 이상', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaInsdPrcsCpctByWhol', + fieldName: 'staaInsdPrcsCpctByWhol', + header: { text: '계(C)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaInsdNope', fieldName: 'step1StaaInsdNope', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaInsdNope', fieldName: 'step2StaaInsdNope', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaInsdNope', fieldName: 'step3StaaInsdNope', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaInsdNope', fieldName: 'step4StaaInsdNope', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaInsdNope', fieldName: 'step5StaaInsdNope', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaInsdNope', fieldName: 'step6StaaInsdNope', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaInsdNope', fieldName: 'step7StaaInsdNope', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step8StaaInsdNope', fieldName: 'step8StaaInsdNope', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 외 > 총계 - { name: 'SPZO_SM', fieldName: '총계 D=(E+F)/2', header: { text: '총계 D=(E+F)/2' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpWhol', + fieldName: 'staaExcpWhol', + header: { text: '총계 D=(E+F)/2' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + firstSumFields: [ + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt', + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt', + ], + secondSumFields: [ + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope', + ], + type: 'halfSumOfSums' as const, + }, + }, // 하수처리구역 외 > 처리방식별 - { name: 'SPZO_MTHD_SM', fieldName: '계(E)', header: { text: '계(E)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpPrcsMthByWhol', + fieldName: 'staaExcpPrcsMthByWhol', + header: { text: '계(E)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt', + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt', + ], + type: 'sum' as const, + }, + }, // 하수처리구역 외 > 처리방식별 > 부패탱크 - { name: 'SPZO_ROT_NPFL_CNT', fieldName: '자연유하', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_ROT_ENFCT_PUMP_1', fieldName: '강제펌핑식', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpSttkNprsflCnt', fieldName: 'staaExcpSttkNprsflCnt', header: { text: '자연유하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step1StaaExcpSttkFrcCnt', fieldName: 'step1StaaExcpSttkFrcCnt', header: { text: '강제펌핑식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 외 > 처리방식별 - { name: 'SPZO_WTROT_NPFL_CNT', fieldName: '살수여상', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_TFM', fieldName: '폭기방법', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_ARTM', fieldName: '접촉폭기', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_CTARM', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpSprktpFltrtbCnt', fieldName: 'staaExcpSprktpFltrtbCnt', header: { text: '살수여상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpAertMthdCnt', fieldName: 'staaExcpAertMthdCnt', header: { text: '폭기방법' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpCntcAertCnt', fieldName: 'staaExcpCntcAertCnt', header: { text: '접촉폭기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpEtcCnt', fieldName: 'staaExcpEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 외 > 처리용량별 - { name: 'SPZO_PROC_CPC_SM', fieldName: '계(F)', header: { text: '계(F)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_1', fieldName: '10인조이하', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_2', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_3', fieldName: '21~30', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_4', fieldName: '31~50', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_5', fieldName: '51~199', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_6', fieldName: '200~499', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_7', fieldName: '500~999', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SPZO_PROC_CPC_8', fieldName: '1,000 이상', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpPrcsCpctByWhol', + fieldName: 'staaExcpPrcsCpctByWhol', + header: { text: '계(F)' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaExcpNope', fieldName: 'step1StaaExcpNope', header: { text: '10인조이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaExcpNope', fieldName: 'step2StaaExcpNope', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaExcpNope', fieldName: 'step3StaaExcpNope', header: { text: '21~30' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaExcpNope', fieldName: 'step4StaaExcpNope', header: { text: '31~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaExcpNope', fieldName: 'step5StaaExcpNope', header: { text: '51~199' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaExcpNope', fieldName: 'step6StaaExcpNope', header: { text: '200~499' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaExcpNope', fieldName: 'step7StaaExcpNope', header: { text: '500~999' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step8StaaExcpNope', fieldName: 'step8StaaExcpNope', header: { text: '1,000 이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - - // 검증 필드 (검증 여부 표시용 - 빨간색 표시) - { name: 'CHK_TOT_FCLTY_CNT', fieldName: 'CHK_TOT_FCLTY_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_SM', fieldName: 'CHK_SPZI_SM', header: { text: '' } }, - { name: 'CHK_SPZI_MTHD_SM', fieldName: 'CHK_SPZI_MTHD_SM', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_NPFL_CNT', fieldName: 'CHK_SPZI_ROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZI_ROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZI_ROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZI_ROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZI_ROT_TANK_SM', fieldName: 'CHK_SPZI_ROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_NPFL_CNT', fieldName: 'CHK_SPZI_IMHF_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_ENFCT_PUMP_1', fieldName: 'CHK_SPZI_IMHF_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_ENFCT_PUMP_2', fieldName: 'CHK_SPZI_IMHF_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_ENFCT_PUMP_3', fieldName: 'CHK_SPZI_IMHF_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZI_IMHF_TANK_SM', fieldName: 'CHK_SPZI_IMHF_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_NPFL_CNT', fieldName: 'CHK_SPZI_WTROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZI_WTROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZI_WTROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZI_WTROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZI_WTROT_TANK_SM', fieldName: 'CHK_SPZI_WTROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZI_TFM', fieldName: 'CHK_SPZI_TFM', header: { text: '' } }, - { name: 'CHK_SPZI_ARTM', fieldName: 'CHK_SPZI_ARTM', header: { text: '' } }, - { name: 'CHK_SPZI_CTARM', fieldName: 'CHK_SPZI_CTARM', header: { text: '' } }, - { name: 'CHK_SPZI_NPFL', fieldName: 'CHK_SPZI_NPFL', header: { text: '' } }, - { name: 'CHK_SPZI_ENFCT_PUMP', fieldName: 'CHK_SPZI_ENFCT_PUMP', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_SM', fieldName: 'CHK_SPZI_PROC_CPC_SM', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_1', fieldName: 'CHK_SPZI_PROC_CPC_1', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_2', fieldName: 'CHK_SPZI_PROC_CPC_2', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_3', fieldName: 'CHK_SPZI_PROC_CPC_3', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_4', fieldName: 'CHK_SPZI_PROC_CPC_4', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_5', fieldName: 'CHK_SPZI_PROC_CPC_5', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_6', fieldName: 'CHK_SPZI_PROC_CPC_6', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_7', fieldName: 'CHK_SPZI_PROC_CPC_7', header: { text: '' } }, - { name: 'CHK_SPZI_PROC_CPC_8', fieldName: 'CHK_SPZI_PROC_CPC_8', header: { text: '' } }, - { name: 'CHK_SPZO_SM', fieldName: 'CHK_SPZO_SM', header: { text: '' } }, - { name: 'CHK_SPZO_MTHD_SM', fieldName: 'CHK_SPZO_MTHD_SM', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_NPFL_CNT', fieldName: 'CHK_SPZO_ROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZO_ROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZO_ROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZO_ROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZO_ROT_TANK_SM', fieldName: 'CHK_SPZO_ROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_NPFL_CNT', fieldName: 'CHK_SPZO_IMHF_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_ENFCT_PUMP_1', fieldName: 'CHK_SPZO_IMHF_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_ENFCT_PUMP_2', fieldName: 'CHK_SPZO_IMHF_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_ENFCT_PUMP_3', fieldName: 'CHK_SPZO_IMHF_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZO_IMHF_TANK_SM', fieldName: 'CHK_SPZO_IMHF_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_NPFL_CNT', fieldName: 'CHK_SPZO_WTROT_NPFL_CNT', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_ENFCT_PUMP_1', fieldName: 'CHK_SPZO_WTROT_ENFCT_PUMP_1', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_ENFCT_PUMP_2', fieldName: 'CHK_SPZO_WTROT_ENFCT_PUMP_2', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_ENFCT_PUMP_3', fieldName: 'CHK_SPZO_WTROT_ENFCT_PUMP_3', header: { text: '' } }, - { name: 'CHK_SPZO_WTROT_TANK_SM', fieldName: 'CHK_SPZO_WTROT_TANK_SM', header: { text: '' } }, - { name: 'CHK_SPZO_TFM', fieldName: 'CHK_SPZO_TFM', header: { text: '' } }, - { name: 'CHK_SPZO_ARTM', fieldName: 'CHK_SPZO_ARTM', header: { text: '' } }, - { name: 'CHK_SPZO_CTARM', fieldName: 'CHK_SPZO_CTARM', header: { text: '' } }, - { name: 'CHK_SPZO_NPFL', fieldName: 'CHK_SPZO_NPFL', header: { text: '' } }, - { name: 'CHK_SPZO_ENFCT_PUMP', fieldName: 'CHK_SPZO_ENFCT_PUMP', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_SM', fieldName: 'CHK_SPZO_PROC_CPC_SM', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_1', fieldName: 'CHK_SPZO_PROC_CPC_1', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_2', fieldName: 'CHK_SPZO_PROC_CPC_2', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_3', fieldName: 'CHK_SPZO_PROC_CPC_3', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_4', fieldName: 'CHK_SPZO_PROC_CPC_4', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_5', fieldName: 'CHK_SPZO_PROC_CPC_5', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_6', fieldName: 'CHK_SPZO_PROC_CPC_6', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_7', fieldName: 'CHK_SPZO_PROC_CPC_7', header: { text: '' } }, - { name: 'CHK_SPZO_PROC_CPC_8', fieldName: 'CHK_SPZO_PROC_CPC_8', header: { text: '' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'TOT_FCLTY_CNT', + 'grssFcltCnt', { name: 'group_spzi', header: { text: '하수처리구역 내' }, items: [ - 'SPZI_SM', + 'staaInsdWhol', { name: 'group_spzi_mthd', header: { text: '처리방식별' }, items: [ - 'SPZI_MTHD_SM', + 'staaInsdPrcsMthByWhol', { name: 'group_spzi_rot', header: { text: '부패탱크' }, items: [ - 'SPZI_ROT_NPFL_CNT', - 'SPZI_ROT_ENFCT_PUMP_1' + 'staaInsdSttkNprsflCnt', + 'step1StaaInsdSttkFrcCnt' ] }, - 'SPZI_WTROT_NPFL_CNT', - 'SPZI_TFM', - 'SPZI_ARTM', - 'SPZI_CTARM' + 'staaInsdSprktpFltrtbCnt', + 'staaInsdAertMthdCnt', + 'staaInsdCntcAertCnt', + 'staaInsdEtcCnt' ] }, { name: 'group_spzi_proc', header: { text: '처리용량별' }, items: [ - 'SPZI_PROC_CPC_SM', - 'SPZI_PROC_CPC_1', - 'SPZI_PROC_CPC_2', - 'SPZI_PROC_CPC_3', - 'SPZI_PROC_CPC_4', - 'SPZI_PROC_CPC_5', - 'SPZI_PROC_CPC_6', - 'SPZI_PROC_CPC_7', - 'SPZI_PROC_CPC_8' + 'staaInsdPrcsCpctByWhol', + 'step1StaaInsdNope', + 'step2StaaInsdNope', + 'step3StaaInsdNope', + 'step4StaaInsdNope', + 'step5StaaInsdNope', + 'step6StaaInsdNope', + 'step7StaaInsdNope', + 'step8StaaInsdNope' ] } ] @@ -189,47 +323,49 @@ export const SepticTankStatusInpColumn = { name: 'group_spzo', header: { text: '하수처리구역 외' }, items: [ - 'SPZO_SM', + 'staaExcpWhol', { name: 'group_spzo_mthd', header: { text: '처리방식별' }, items: [ - 'SPZO_MTHD_SM', + 'staaExcpPrcsMthByWhol', { name: 'group_spzo_rot', header: { text: '부패탱크' }, items: [ - 'SPZO_ROT_NPFL_CNT', - 'SPZO_ROT_ENFCT_PUMP_1' + 'staaExcpSttkNprsflCnt', + 'step1StaaExcpSttkFrcCnt' ] }, - 'SPZO_WTROT_NPFL_CNT', - 'SPZO_TFM', - 'SPZO_ARTM', - 'SPZO_CTARM' + 'staaExcpSprktpFltrtbCnt', + 'staaExcpAertMthdCnt', + 'staaExcpCntcAertCnt', + 'staaExcpEtcCnt' ] }, { name: 'group_spzo_proc', header: { text: '처리용량별' }, items: [ - 'SPZO_PROC_CPC_SM', - 'SPZO_PROC_CPC_1', - 'SPZO_PROC_CPC_2', - 'SPZO_PROC_CPC_3', - 'SPZO_PROC_CPC_4', - 'SPZO_PROC_CPC_5', - 'SPZO_PROC_CPC_6', - 'SPZO_PROC_CPC_7', - 'SPZO_PROC_CPC_8' + 'staaExcpPrcsCpctByWhol', + 'step1StaaExcpNope', + 'step2StaaExcpNope', + 'step3StaaExcpNope', + 'step4StaaExcpNope', + 'step5StaaExcpNope', + 'step6StaaExcpNope', + 'step7StaaExcpNope', + 'step8StaaExcpNope' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 4, fixedColumns: 1, - tabKey: 'septicTankStatusInp', + tabKey: 'ST00080002', tabLabel: '정화조 현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts b/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts index 40e77978a..2c247a3de 100644 --- a/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn.ts @@ -5,65 +5,119 @@ import { ValueType } from "realgrid"; export const SewageTreatFactStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 총계 - { name: 'STTY_SM', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'wholDrtwPrcsFcltCnt', + fieldName: 'wholDrtwPrcsFcltCnt', + header: { text: '총계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaPrcsQnt', + 'step2StaaPrcsQnt', + 'step3StaaPrcsQnt', + 'step4StaaPrcsQnt', + 'step5StaaPrcsQnt', + 'step6StaaPrcsQnt', + 'step7StaaPrcsQnt', + 'step1StaaExcpPrcsQnt', + 'step2StaaExcpPrcsQnt', + 'step3StaaExcpPrcsQnt', + 'step4StaaExcpPrcsQnt', + 'step5StaaExcpPrcsQnt', + 'step6StaaExcpPrcsQnt', + 'step7StaaExcpPrcsQnt', + ], + type: 'sum' as const, + }, + }, // 오수처리시설 > 하수처리구역 내(m³/일) - { name: 'STTY_ZONE_INNER_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_2', fieldName: '2이하', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_3', fieldName: '3~10', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_11', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_21', fieldName: '21~50', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_51', fieldName: '51~100', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_101', fieldName: '101~500', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_501', fieldName: '501이상', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'wholStaaCnt', + fieldName: 'wholStaaCnt', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaPrcsQnt', + 'step2StaaPrcsQnt', + 'step3StaaPrcsQnt', + 'step4StaaPrcsQnt', + 'step5StaaPrcsQnt', + 'step6StaaPrcsQnt', + 'step7StaaPrcsQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaPrcsQnt', fieldName: 'step1StaaPrcsQnt', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaPrcsQnt', fieldName: 'step2StaaPrcsQnt', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaPrcsQnt', fieldName: 'step3StaaPrcsQnt', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaPrcsQnt', fieldName: 'step4StaaPrcsQnt', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaPrcsQnt', fieldName: 'step5StaaPrcsQnt', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaPrcsQnt', fieldName: 'step6StaaPrcsQnt', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaPrcsQnt', fieldName: 'step7StaaPrcsQnt', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 오수처리시설 > 하수처리구역 외(m³/일) - { name: 'STTY_ZONE_OUTER_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_2', fieldName: '2이하', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_3', fieldName: '3~10', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_11', fieldName: '11~20', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_21', fieldName: '21~50', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_51', fieldName: '51~100', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_101', fieldName: '101~500', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_501', fieldName: '501이상', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'staaExcpWhol', + fieldName: 'staaExcpWhol', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'step1StaaExcpPrcsQnt', + 'step2StaaExcpPrcsQnt', + 'step3StaaExcpPrcsQnt', + 'step4StaaExcpPrcsQnt', + 'step5StaaExcpPrcsQnt', + 'step6StaaExcpPrcsQnt', + 'step7StaaExcpPrcsQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'step1StaaExcpPrcsQnt', fieldName: 'step1StaaExcpPrcsQnt', header: { text: '2이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step2StaaExcpPrcsQnt', fieldName: 'step2StaaExcpPrcsQnt', header: { text: '3~10' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step3StaaExcpPrcsQnt', fieldName: 'step3StaaExcpPrcsQnt', header: { text: '11~20' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step4StaaExcpPrcsQnt', fieldName: 'step4StaaExcpPrcsQnt', header: { text: '21~50' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step5StaaExcpPrcsQnt', fieldName: 'step5StaaExcpPrcsQnt', header: { text: '51~100' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step6StaaExcpPrcsQnt', fieldName: 'step6StaaExcpPrcsQnt', header: { text: '101~500' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'step7StaaExcpPrcsQnt', fieldName: 'step7StaaExcpPrcsQnt', header: { text: '501이상' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - - // 검증 필드 (검증 여부 표시용, 빨간색 표시) - { name: 'CHK_STTY_SM', fieldName: 'CHK_STTY_SM', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_SM', fieldName: 'CHK_STTY_ZONE_INNER_SM', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_2', fieldName: 'CHK_STTY_ZONE_INNER_2', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_3', fieldName: 'CHK_STTY_ZONE_INNER_3', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_11', fieldName: 'CHK_STTY_ZONE_INNER_11', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_21', fieldName: 'CHK_STTY_ZONE_INNER_21', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_51', fieldName: 'CHK_STTY_ZONE_INNER_51', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_101', fieldName: 'CHK_STTY_ZONE_INNER_101', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_INNER_501', fieldName: 'CHK_STTY_ZONE_INNER_501', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_SM', fieldName: 'CHK_STTY_ZONE_OUTER_SM', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_2', fieldName: 'CHK_STTY_ZONE_OUTER_2', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_3', fieldName: 'CHK_STTY_ZONE_OUTER_3', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_11', fieldName: 'CHK_STTY_ZONE_OUTER_11', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_21', fieldName: 'CHK_STTY_ZONE_OUTER_21', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_51', fieldName: 'CHK_STTY_ZONE_OUTER_51', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_101', fieldName: 'CHK_STTY_ZONE_OUTER_101', header: { text: '' } }, - { name: 'CHK_STTY_ZONE_OUTER_501', fieldName: 'CHK_STTY_ZONE_OUTER_501', header: { text: '' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'STTY_SM', + 'wholDrtwPrcsFcltCnt', { name: 'group_stty', header: { text: '오수처리시설' }, @@ -72,36 +126,38 @@ export const SewageTreatFactStatusInpColumn = { name: 'group_inner', header: { text: '하수처리구역 내(m³/일)' }, items: [ - 'STTY_ZONE_INNER_SM', - 'STTY_ZONE_INNER_2', - 'STTY_ZONE_INNER_3', - 'STTY_ZONE_INNER_11', - 'STTY_ZONE_INNER_21', - 'STTY_ZONE_INNER_51', - 'STTY_ZONE_INNER_101', - 'STTY_ZONE_INNER_501' + 'wholStaaCnt', + 'step1StaaPrcsQnt', + 'step2StaaPrcsQnt', + 'step3StaaPrcsQnt', + 'step4StaaPrcsQnt', + 'step5StaaPrcsQnt', + 'step6StaaPrcsQnt', + 'step7StaaPrcsQnt' ] }, { name: 'group_outer', header: { text: '하수처리구역 외(m³/일)' }, items: [ - 'STTY_ZONE_OUTER_SM', - 'STTY_ZONE_OUTER_2', - 'STTY_ZONE_OUTER_3', - 'STTY_ZONE_OUTER_11', - 'STTY_ZONE_OUTER_21', - 'STTY_ZONE_OUTER_51', - 'STTY_ZONE_OUTER_101', - 'STTY_ZONE_OUTER_501' + 'staaExcpWhol', + 'step1StaaExcpPrcsQnt', + 'step2StaaExcpPrcsQnt', + 'step3StaaExcpPrcsQnt', + 'step4StaaExcpPrcsQnt', + 'step5StaaExcpPrcsQnt', + 'step6StaaExcpPrcsQnt', + 'step7StaaExcpPrcsQnt' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'sewageTreatFactStatusInp', + tabKey: 'ST00080001', tabLabel: '처리용량별 오수처리시설 현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts index a2b645123..e8e0991f0 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts @@ -5,74 +5,75 @@ import { ValueType } from "realgrid"; export const ExcretaRelatedIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DONG', fieldName: '행정동', header: { text: '행정동' }, visible: false }, - { name: 'RGN_NM', fieldName: '지역명', header: { text: '지역명' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 업체수(개소) - { name: 'ECSCS_SM', fieldName: '업체수(개소)', header: { text: '업체수(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'bzentyCnt', fieldName: 'bzentyCnt', header: { text: '업체수(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 종사인원(명) - { name: 'ECSCS_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'empNope', fieldName: 'empNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 시설(차량) 확보현황(대수) - { name: 'VHCLE_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'THREE_BELOW', fieldName: '3톤이하', header: { text: '3톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'FOUR_BELOW', fieldName: '4.5톤이하', header: { text: '4.5톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'EIGHT_BELOW', fieldName: '8톤이하', header: { text: '8톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'fcltSecrPrstCnt', + fieldName: 'fcltSecrPrstCnt', + header: { text: '계' }, + editable: false, footer: + footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['n3TonUdrCnt', 'n4TonUdrCnt', 'n8TonUdrCnt', 'etcCnt'], + type: 'sum' as const, + }, + }, + { name: 'n3TonUdrCnt', fieldName: 'n3TonUdrCnt', header: { text: '3톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'n4TonUdrCnt', fieldName: 'n4TonUdrCnt', header: { text: '4.5톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'n8TonUdrCnt', fieldName: 'n8TonUdrCnt', header: { text: '8톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 수집·운반 실적(m³/년) - { name: 'COLCT_TRSP_ACMSLT', fieldName: '수집·운반 실적(m³/년)', header: { text: '수집·운반 실적(m³/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'clctCnvncPrfmncQnt', fieldName: 'clctCnvncPrfmncQnt', header: { text: '수집·운반 실적(m³/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_ECSCS_SM', fieldName: 'CHK_ECSCS_SM', header: { text: 'CHK_ECSCS_SM' }, visible: false }, - { name: 'CHK_ECSCS_BSMN', fieldName: 'CHK_ECSCS_BSMN', header: { text: 'CHK_ECSCS_BSMN' }, visible: false }, - { name: 'CHK_VHCLE_SM', fieldName: 'CHK_VHCLE_SM', header: { text: 'CHK_VHCLE_SM' }, visible: false }, - { name: 'CHK_THREE_BELOW', fieldName: 'CHK_THREE_BELOW', header: { text: 'CHK_THREE_BELOW' }, visible: false }, - { name: 'CHK_FOUR_BELOW', fieldName: 'CHK_FOUR_BELOW', header: { text: 'CHK_FOUR_BELOW' }, visible: false }, - { name: 'CHK_EIGHT_BELOW', fieldName: 'CHK_EIGHT_BELOW', header: { text: 'CHK_EIGHT_BELOW' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, - { name: 'CHK_COLCT_TRSP_ACMSLT', fieldName: 'CHK_COLCT_TRSP_ACMSLT', header: { text: 'CHK_COLCT_TRSP_ACMSLT' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'ECSCS_SM', - 'ECSCS_BSMN', + 'bzentyCnt', + 'empNope', { name: 'group_vehicle_status', header: { text: '시설(차량) 확보현황(대수)' }, items: [ - 'VHCLE_SM', - 'THREE_BELOW', - 'FOUR_BELOW', - 'EIGHT_BELOW', - 'ETC' + 'fcltSecrPrstCnt', + 'n3TonUdrCnt', + 'n4TonUdrCnt', + 'n8TonUdrCnt', + 'etcCnt' ] }, - 'COLCT_TRSP_ACMSLT', - 'RMK' + 'clctCnvncPrfmncQnt', + 'rmrk', + 'admdstCd', + 'admdstNm', ], rowStartNum: 2, - fixedColumns: 1, - tabKey: 'excretaRelatedIndustryInp', + fixedColumns: 0, + tabKey: 'ST00120003', tabLabel: '분뇨수집·운반업' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts index 5e61f6afe..53fa25308 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts @@ -5,82 +5,59 @@ import { ValueType } from "realgrid"; export const PrivateSewageTreatRelatedIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_DONG', fieldName: '행정동', header: { text: '행정동' }, visible: false }, - { name: 'RGN_NM', fieldName: '지역명', header: { text: '지역명' }, visible: false }, - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 개인하수처리시설 설계·시공업 - { name: 'CNSTRCT_ENTRPS', fieldName: '업소수', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CNSTRCT_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'designMdsrCnt', fieldName: 'designMdsrCnt', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'designMdsrNope', fieldName: 'designMdsrNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 개인하수처리시설 제조업 - { name: 'MNFCTUR_ENTRPS', fieldName: '업소수', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MNFCTUR_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mfbizCnt', fieldName: 'mfbizCnt', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mfbizMdsrNope', fieldName: 'mfbizMdsrNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 개인하수처리시설 관리업 - { name: 'MGMT_ENTRPS', fieldName: '업소수', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MGMT_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mngCnt', fieldName: 'mngCnt', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mngMdsrNope', fieldName: 'mngMdsrNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: 추가 필드 (화면에 미노출이, DB 저장) - { name: 'CNSTRCT_FCT_CNT', fieldName: '설계시공시설개수', header: { text: '설계시공시설개수' }, visible: false }, - { name: 'CNSTRCT_FCT_CPCTY', fieldName: '설계시공시설용량', header: { text: '설계시공시설용량' }, visible: false }, - { name: 'MNFCTUR_STTY_CNT', fieldName: '제조정원', header: { text: '제조정원' }, visible: false }, - { name: 'MNFCTUR_WRRTN_CNT', fieldName: '제조권원', header: { text: '제조권원' }, visible: false }, - { name: 'MGMT_STTY_CNT', fieldName: '관리정원', header: { text: '관리정원' }, visible: false }, - { name: 'MGMT_WRRTN_CNT', fieldName: '관리권원', header: { text: '관리권원' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_CNSTRCT_ENTRPS', fieldName: 'CHK_CNSTRCT_ENTRPS', header: { text: 'CHK_CNSTRCT_ENTRPS' }, visible: false }, - { name: 'CHK_CNSTRCT_BSMN', fieldName: 'CHK_CNSTRCT_BSMN', header: { text: 'CHK_CNSTRCT_BSMN' }, visible: false }, - { name: 'CHK_CNSTRCT_FCT_CNT', fieldName: 'CHK_CNSTRCT_FCT_CNT', header: { text: 'CHK_CNSTRCT_FCT_CNT' }, visible: false }, - { name: 'CHK_CNSTRCT_FCT_CPCTY', fieldName: 'CHK_CNSTRCT_FCT_CPCTY', header: { text: 'CHK_CNSTRCT_FCT_CPCTY' }, visible: false }, - { name: 'CHK_MNFCTUR_ENTRPS', fieldName: 'CHK_MNFCTUR_ENTRPS', header: { text: 'CHK_MNFCTUR_ENTRPS' }, visible: false }, - { name: 'CHK_MNFCTUR_BSMN', fieldName: 'CHK_MNFCTUR_BSMN', header: { text: 'CHK_MNFCTUR_BSMN' }, visible: false }, - { name: 'CHK_MNFCTUR_STTY_CNT', fieldName: 'CHK_MNFCTUR_STTY_CNT', header: { text: 'CHK_MNFCTUR_STTY_CNT' }, visible: false }, - { name: 'CHK_MNFCTUR_WRRTN_CNT', fieldName: 'CHK_MNFCTUR_WRRTN_CNT', header: { text: 'CHK_MNFCTUR_WRRTN_CNT' }, visible: false }, - { name: 'CHK_MGMT_ENTRPS', fieldName: 'CHK_MGMT_ENTRPS', header: { text: 'CHK_MGMT_ENTRPS' }, visible: false }, - { name: 'CHK_MGMT_BSMN', fieldName: 'CHK_MGMT_BSMN', header: { text: 'CHK_MGMT_BSMN' }, visible: false }, - { name: 'CHK_MGMT_STTY_CNT', fieldName: 'CHK_MGMT_STTY_CNT', header: { text: 'CHK_MGMT_STTY_CNT' }, visible: false }, - { name: 'CHK_MGMT_WRRTN_CNT', fieldName: 'CHK_MGMT_WRRTN_CNT', header: { text: 'CHK_MGMT_WRRTN_CNT' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_design_construct', header: { text: '개인하수처리시설 설계·시공업' }, - items: ['CNSTRCT_ENTRPS', 'CNSTRCT_BSMN'] + items: ['designMdsrCnt', 'designMdsrNope'] }, { name: 'group_manufacture', header: { text: '개인하수처리시설 제조업' }, - items: ['MNFCTUR_ENTRPS', 'MNFCTUR_BSMN'] + items: ['mfbizCnt', 'mfbizMdsrNope'] }, { name: 'group_management', header: { text: '개인하수처리시설 관리업' }, - items: ['MGMT_ENTRPS', 'MGMT_BSMN'] + items: ['mngCnt', 'mngMdsrNope'] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', ], rowStartNum: 2, - fixedColumns: 1, - tabKey: 'privateSewageTreatRelatedIndustryInp', + fixedColumns: 0, + tabKey: 'ST00120004', tabLabel: '개인하수처리관련업' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts index 8df679707..7df3d801c 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts @@ -5,86 +5,107 @@ import { ValueType } from "realgrid"; export const PublicInstitutionExcretaIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 직제 - { name: 'OCGNZ', fieldName: '직 제', header: { text: '직 제' } }, + { name: 'ognzNm', fieldName: 'ognzNm', header: { text: '직 제' } }, // 직원총수 - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'empGrssCnt', + fieldName: 'empGrssCnt', + header: { text: '직원총수' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt', 'etcCnt'], + type: 'sum' as const, + }, + }, // 행정직 - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'pbadmsNope', fieldName: 'pbadmsNope', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기술직 - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - - // 기능직 (hidden) - { name: 'TCNSRV', fieldName: '기능직', header: { text: '기능직' }, visible: false }, + { + name: 'techWholNope', + fieldName: 'techWholNope', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt'], + type: 'sum' as const, + }, + }, + { name: 'engrkCnt', fieldName: 'engrkCnt', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mchnCnt', fieldName: 'mchnCnt', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'prvtmCnt', fieldName: 'prvtmCnt', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'chmengCnt', fieldName: 'chmengCnt', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'envCnt', fieldName: 'envCnt', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'enjbEtcCnt', fieldName: 'enjbEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기타 - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_OCGNZ', fieldName: 'CHK_OCGNZ', header: { text: 'CHK_OCGNZ' }, visible: false }, - { name: 'CHK_TOT_WTR_QRM', fieldName: 'CHK_TOT_WTR_QRM', header: { text: 'CHK_TOT_WTR_QRM' }, visible: false }, - { name: 'CHK_ADMINIST_JSSFC', fieldName: 'CHK_ADMINIST_JSSFC', header: { text: 'CHK_ADMINIST_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_JSSFC_SM', fieldName: 'CHK_TCHNLGY_JSSFC_SM', header: { text: 'CHK_TCHNLGY_JSSFC_SM' }, visible: false }, - { name: 'CHK_ENGRK_JSSFC', fieldName: 'CHK_ENGRK_JSSFC', header: { text: 'CHK_ENGRK_JSSFC' }, visible: false }, - { name: 'CHK_MCHN_JSSFC', fieldName: 'CHK_MCHN_JSSFC', header: { text: 'CHK_MCHN_JSSFC' }, visible: false }, - { name: 'CHK_ELCTY_JSSFC', fieldName: 'CHK_ELCTY_JSSFC', header: { text: 'CHK_ELCTY_JSSFC' }, visible: false }, - { name: 'CHK_CHMSTRY_JSSFC', fieldName: 'CHK_CHMSTRY_JSSFC', header: { text: 'CHK_CHMSTRY_JSSFC' }, visible: false }, - { name: 'CHK_ENVRN_JSSFC', fieldName: 'CHK_ENVRN_JSSFC', header: { text: 'CHK_ENVRN_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_ETC', fieldName: 'CHK_TCHNLGY_ETC', header: { text: 'CHK_TCHNLGY_ETC' }, visible: false }, - { name: 'CHK_TCNSRV', fieldName: 'CHK_TCNSRV', header: { text: 'CHK_TCNSRV' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'ognzSn', fieldName: 'ognzSn', header: { text: '조직일련번호' }, visible: false }, + { + name: '__isNewArticle', + fieldName: '__isNewArticle', + header: { text: '신규항목' }, + visible: false, + editable: false, + valueType: ValueType.NUMBER, + }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'OCGNZ', - 'TOT_WTR_QRM', - 'ADMINIST_JSSFC', + 'ognzNm', + 'empGrssCnt', + 'pbadmsNope', { name: 'group_technology', header: { text: '기술직' }, items: [ - 'TCHNLGY_JSSFC_SM', - 'ENGRK_JSSFC', - 'MCHN_JSSFC', - 'ELCTY_JSSFC', - 'CHMSTRY_JSSFC', - 'ENVRN_JSSFC', - 'TCHNLGY_ETC' + 'techWholNope', + 'engrkCnt', + 'mchnCnt', + 'prvtmCnt', + 'chmengCnt', + 'envCnt', + 'enjbEtcCnt' ] }, - 'ETC', - 'RMK' + 'etcCnt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'ognzSn', + '__isNewArticle', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'publicInstitutionExcretaIndustryInp', + tabKey: 'ST00120002', tabLabel: '공공기관 분뇨분야' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts index 9df115260..176c11a1a 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts @@ -5,86 +5,107 @@ import { ValueType } from "realgrid"; export const PublicInstitutionSewerIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 직제 - { name: 'OCGNZ', fieldName: '직 제', header: { text: '직 제' } }, + { name: 'ognzNm', fieldName: 'ognzNm', header: { text: '직 제' } }, // 직원총수 - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'empGrssCnt', + fieldName: 'empGrssCnt', + header: { text: '직원총수' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt', 'etcCnt'], + type: 'sum' as const, + }, + }, // 행정직 - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'pbadmsNope', fieldName: 'pbadmsNope', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기술직 - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - - // 기능직 (hidden) - { name: 'TCNSRV', fieldName: '기능직', header: { text: '기능직' }, visible: false }, + { + name: 'techWholNope', + fieldName: 'techWholNope', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt'], + type: 'sum' as const, + }, + }, + { name: 'engrkCnt', fieldName: 'engrkCnt', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mchnCnt', fieldName: 'mchnCnt', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'prvtmCnt', fieldName: 'prvtmCnt', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'chmengCnt', fieldName: 'chmengCnt', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'envCnt', fieldName: 'envCnt', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'enjbEtcCnt', fieldName: 'enjbEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기타 - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_OCGNZ', fieldName: 'CHK_OCGNZ', header: { text: 'CHK_OCGNZ' }, visible: false }, - { name: 'CHK_TOT_WTR_QRM', fieldName: 'CHK_TOT_WTR_QRM', header: { text: 'CHK_TOT_WTR_QRM' }, visible: false }, - { name: 'CHK_ADMINIST_JSSFC', fieldName: 'CHK_ADMINIST_JSSFC', header: { text: 'CHK_ADMINIST_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_JSSFC_SM', fieldName: 'CHK_TCHNLGY_JSSFC_SM', header: { text: 'CHK_TCHNLGY_JSSFC_SM' }, visible: false }, - { name: 'CHK_ENGRK_JSSFC', fieldName: 'CHK_ENGRK_JSSFC', header: { text: 'CHK_ENGRK_JSSFC' }, visible: false }, - { name: 'CHK_MCHN_JSSFC', fieldName: 'CHK_MCHN_JSSFC', header: { text: 'CHK_MCHN_JSSFC' }, visible: false }, - { name: 'CHK_ELCTY_JSSFC', fieldName: 'CHK_ELCTY_JSSFC', header: { text: 'CHK_ELCTY_JSSFC' }, visible: false }, - { name: 'CHK_CHMSTRY_JSSFC', fieldName: 'CHK_CHMSTRY_JSSFC', header: { text: 'CHK_CHMSTRY_JSSFC' }, visible: false }, - { name: 'CHK_ENVRN_JSSFC', fieldName: 'CHK_ENVRN_JSSFC', header: { text: 'CHK_ENVRN_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_ETC', fieldName: 'CHK_TCHNLGY_ETC', header: { text: 'CHK_TCHNLGY_ETC' }, visible: false }, - { name: 'CHK_TCNSRV', fieldName: 'CHK_TCNSRV', header: { text: 'CHK_TCNSRV' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'ognzSn', fieldName: 'ognzSn', header: { text: '조직일련번호' }, visible: false }, + { + name: '__isNewArticle', + fieldName: '__isNewArticle', + header: { text: '신규항목' }, + visible: false, + editable: false, + valueType: ValueType.NUMBER, + }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'OCGNZ', - 'TOT_WTR_QRM', - 'ADMINIST_JSSFC', + 'ognzNm', + 'empGrssCnt', + 'pbadmsNope', { name: 'group_technology', header: { text: '기술직' }, items: [ - 'TCHNLGY_JSSFC_SM', - 'ENGRK_JSSFC', - 'MCHN_JSSFC', - 'ELCTY_JSSFC', - 'CHMSTRY_JSSFC', - 'ENVRN_JSSFC', - 'TCHNLGY_ETC' + 'techWholNope', + 'engrkCnt', + 'mchnCnt', + 'prvtmCnt', + 'chmengCnt', + 'envCnt', + 'enjbEtcCnt' ] }, - 'ETC', - 'RMK' + 'etcCnt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'ognzSn', + '__isNewArticle', ], rowStartNum: 2, - fixedColumns: 1, - tabKey: 'publicInstitutionSewerIndustryInp', + fixedColumns: 0, + tabKey: 'ST00120001', tabLabel: '공공기관 하수분야' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts index c6b70ea6c..247fdd5aa 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts @@ -1,91 +1,158 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +export const DIGESTOR_OPERATING_STATUS_YN_VALUES = ["Y", "N"] as const; +export const DIGESTOR_OPERATING_STATUS_EN_YN_FIELD = "digstnEnYn" as const; + +/** 소화조 유/무가 N이면 해당 행의 입력 셀을 잠근다(digstnEnYn 제외). */ +export function isDigestorOperatingRowDisabled( + row: Record, + grid?: { getValue?: (itemIndex: number, fieldName: string) => unknown }, + rowIndex?: number, +): boolean { + const live = + grid?.getValue != null && rowIndex != null && rowIndex >= 0 + ? grid.getValue(rowIndex, DIGESTOR_OPERATING_STATUS_EN_YN_FIELD) + : undefined; + const raw = + live !== undefined && live !== null + ? live + : row[DIGESTOR_OPERATING_STATUS_EN_YN_FIELD]; + return String(raw ?? "").trim().toUpperCase() === "N"; +} + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리 > tab 2. 소화조 운영현황 export const DigestorOperatingStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리장명 - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수처리장명' }, editable: false }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리장명' }, editable: false }, // 소화조 운영현황 - { name: 'DGSTR_FNNC', fieldName: '소화조 유/무(Y/N)선택', header: { text: '소화조 유/무(Y/N)선택' } }, - { name: 'DGSTR_CPC', fieldName: '설계용량(m³)', header: { text: '설계용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_OPR_CPC', fieldName: '실제운전용량(m³)', header: { text: '실제운전용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_IN', fieldName: '투입량(m³/일)', header: { text: '투입량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_TS', fieldName: '투입 TS 농도(%)', header: { text: '투입 TS 농도(%)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_YRSTP_RN_DYN', fieldName: '가동일수(연간)', header: { text: '가동일수(연간)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_OCCUR_GAS', fieldName: '발생가스량(m³/일)', header: { text: '발생가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + dropdownColumn( + { + name: 'digstnEnYn', + fieldName: 'digstnEnYn', + header: { text: '소화조 유/무(Y/N)선택' }, + }, + DIGESTOR_OPERATING_STATUS_YN_VALUES, + ), + { name: 'digstnCpct', fieldName: 'digstnCpct', header: { text: '설계용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'actlDrvnCpct', fieldName: 'actlDrvnCpct', header: { text: '실제운전용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasInputQnt', fieldName: 'digstnGasInputQnt', header: { text: '투입량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasInputSldDnst', fieldName: 'digstnGasInputSldDnst', header: { text: '투입 TS 농도(%)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnTnkOprtngDayCnt', fieldName: 'digstnTnkOprtngDayCnt', header: { text: '가동일수(연간)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ocrnGasQnt', fieldName: 'ocrnGasQnt', header: { text: '발생가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 소화조 운영현황 > 소화가스 이용현황(m³/일) - { name: 'DGSTR_SM', fieldName: '계', header: { text: '계' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_SLE', fieldName: '판매', header: { text: '판매' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_ELTGNR', fieldName: '발전', header: { text: '발전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_ONSLF', fieldName: '자체활용', header: { text: '자체활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_ETC', fieldName: '기타(CNG등)', header: { text: '기타(CNG등)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'digstnGasUtztnWholPrst', + fieldName: 'digstnGasUtztnWholPrst', + header: { text: '계' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'digstnGasUtztnNtslQnt', + 'digstnGasUtztnEtgtQnt', + 'digstnGasUtztnItslfQnt', + 'digstnGasUtztnEtcQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'digstnGasUtztnNtslQnt', fieldName: 'digstnGasUtztnNtslQnt', header: { text: '판매' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasUtztnEtgtQnt', fieldName: 'digstnGasUtztnEtgtQnt', header: { text: '발전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasUtztnItslfQnt', fieldName: 'digstnGasUtztnItslfQnt', header: { text: '자체활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasUtztnEtcQnt', fieldName: 'digstnGasUtztnEtcQnt', header: { text: '기타(CNG등)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 소화조 운영현황 > 잉여가스량 - { name: 'DGSTR_GAS', fieldName: '잉여가스량(m³/일)', header: { text: '잉여가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'surplsGasQnt', fieldName: 'surplsGasQnt', header: { text: '잉여가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'DGSTR_RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: Check fields for change detection - { name: 'CHK_DGSTR_FNNC', fieldName: 'CHK_DGSTR_FNNC', header: { text: 'CHK_DGSTR_FNNC' }, visible: false }, - { name: 'CHK_DGSTR_IN', fieldName: 'CHK_DGSTR_IN', header: { text: 'CHK_DGSTR_IN' }, visible: false }, - { name: 'CHK_DGSTR_TS', fieldName: 'CHK_DGSTR_TS', header: { text: 'CHK_DGSTR_TS' }, visible: false }, - { name: 'CHK_DGSTR_YRSTP_RN_DYN', fieldName: 'CHK_DGSTR_YRSTP_RN_DYN', header: { text: 'CHK_DGSTR_YRSTP_RN_DYN' }, visible: false }, - { name: 'CHK_DGSTR_OCCUR_GAS', fieldName: 'CHK_DGSTR_OCCUR_GAS', header: { text: 'CHK_DGSTR_OCCUR_GAS' }, visible: false }, - { name: 'CHK_DGSTR_CPC', fieldName: 'CHK_DGSTR_CPC', header: { text: 'CHK_DGSTR_CPC' }, visible: false }, - { name: 'CHK_DGSTR_OPR_CPC', fieldName: 'CHK_DGSTR_OPR_CPC', header: { text: 'CHK_DGSTR_OPR_CPC' }, visible: false }, - { name: 'CHK_DGSTR_SM', fieldName: 'CHK_DGSTR_SM', header: { text: 'CHK_DGSTR_SM' }, visible: false }, - { name: 'CHK_DGSTR_SLE', fieldName: 'CHK_DGSTR_SLE', header: { text: 'CHK_DGSTR_SLE' }, visible: false }, - { name: 'CHK_DGSTR_ELTGNR', fieldName: 'CHK_DGSTR_ELTGNR', header: { text: 'CHK_DGSTR_ELTGNR' }, visible: false }, - { name: 'CHK_DGSTR_ONSLF', fieldName: 'CHK_DGSTR_ONSLF', header: { text: 'CHK_DGSTR_ONSLF' }, visible: false }, - { name: 'CHK_DGSTR_ETC', fieldName: 'CHK_DGSTR_ETC', header: { text: 'CHK_DGSTR_ETC' }, visible: false }, - { name: 'CHK_DGSTR_GAS', fieldName: 'CHK_DGSTR_GAS', header: { text: 'CHK_DGSTR_GAS' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'BSC_FCT_NM', + 'swtrmtFcltNm', { name: 'group_digestor_operating', header: { text: '소화조 운영현황' }, items: [ - 'DGSTR_FNNC', - 'DGSTR_CPC', - 'DGSTR_OPR_CPC', - 'DGSTR_IN', - 'DGSTR_TS', - 'DGSTR_YRSTP_RN_DYN', - 'DGSTR_OCCUR_GAS', + 'digstnEnYn', + 'digstnCpct', + 'actlDrvnCpct', + 'digstnGasInputQnt', + 'digstnGasInputSldDnst', + 'digstnTnkOprtngDayCnt', + 'ocrnGasQnt', { name: 'group_gas_usage', header: { text: '소화가스 이용현황(m³/일)' }, items: [ - 'DGSTR_SM', - 'DGSTR_SLE', - 'DGSTR_ELTGNR', - 'DGSTR_ONSLF', - 'DGSTR_ETC' + 'digstnGasUtztnWholPrst', + 'digstnGasUtztnNtslQnt', + 'digstnGasUtztnEtgtQnt', + 'digstnGasUtztnItslfQnt', + 'digstnGasUtztnEtcQnt' ] }, - 'DGSTR_GAS' + 'surplsGasQnt' ] }, - 'DGSTR_RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd' ], rowStartNum: 3, fixedColumns: 0, - tabKey: 'digestorOperatingStatusInp', + tabKey: 'ST00110002', tabLabel: '소화조 운영현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts index 7edfb7a40..60f9b14d0 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts @@ -5,79 +5,116 @@ import { ValueType } from "realgrid"; export const ExcretaSlurgeProcessingStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 분뇨처리장명 - { name: 'ECSCS_NM', fieldName: '분 뇨 처리장명', header: { text: '분 뇨 처리장명' }, editable: false }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '분뇨 처리장명' }, editable: false }, // 발생량 - { name: 'OCTY', fieldName: '발생량 (A)=(B)+(C)', header: { text: '발생량 (A)=(B)+(C)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'gnam', + fieldName: 'gnam', + header: { text: '발생량 (A)=(B)+(C)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'rcycQnt', + 'olandLfvlm', + 'incnrQnt', + 'oceanInputQnt', + 'fuelQnt', + 'etcQnt', + 'nondspsQnt' + ], + type: 'sum' as const, + }, + }, // 처 분 량(톤/년) - { name: 'PROCESS_CPCTY_SM', fieldName: '계(B)', header: { text: '계(B)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RUSE', fieldName: '재활용', header: { text: '재활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'LAND_RCLM', fieldName: '육상매립', header: { text: '육상매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'INCNR', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'OCEAN_DPSL', fieldName: '해양투기', header: { text: '해양투기' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'FUEL', fieldName: '연료화', header: { text: '연료화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholDspsQnt', + fieldName: 'wholDspsQnt', + header: { text: '계(B)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'rcycQnt', + 'olandLfvlm', + 'incnrQnt', + 'oceanInputQnt', + 'fuelQnt', + 'etcQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'olandLfvlm', fieldName: 'olandLfvlm', header: { text: '육상매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrQnt', fieldName: 'incnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'oceanInputQnt', fieldName: 'oceanInputQnt', header: { text: '해양투기' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fuelQnt', fieldName: 'fuelQnt', header: { text: '연료화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 미처분량(이월량)(C) - { name: 'CYFD_CPCTY', fieldName: '미처분량(이월량)(C)', header: { text: '미처분량(이월량)(C)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'nondspsQnt', fieldName: 'nondspsQnt', header: { text: '미처분량(이월량)(C)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 타시설연계여부 - { name: 'OUT_BSC_FCT_NM', fieldName: '타시설연계여부', header: { text: '타시설연계여부' } }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '타시설연계여부' } }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields for change detection - { name: 'CHK_ECSCS_NM', fieldName: 'CHK_ECSCS_NM', header: { text: 'CHK_ECSCS_NM' }, visible: false }, - { name: 'CHK_OCTY', fieldName: 'CHK_OCTY', header: { text: 'CHK_OCTY' }, visible: false }, - { name: 'CHK_PROCESS_CPCTY_SM', fieldName: 'CHK_PROCESS_CPCTY_SM', header: { text: 'CHK_PROCESS_CPCTY_SM' }, visible: false }, - { name: 'CHK_RUSE', fieldName: 'CHK_RUSE', header: { text: 'CHK_RUSE' }, visible: false }, - { name: 'CHK_LAND_RCLM', fieldName: 'CHK_LAND_RCLM', header: { text: 'CHK_LAND_RCLM' }, visible: false }, - { name: 'CHK_INCNR', fieldName: 'CHK_INCNR', header: { text: 'CHK_INCNR' }, visible: false }, - { name: 'CHK_OCEAN_DPSL', fieldName: 'CHK_OCEAN_DPSL', header: { text: 'CHK_OCEAN_DPSL' }, visible: false }, - { name: 'CHK_FUEL', fieldName: 'CHK_FUEL', header: { text: 'CHK_FUEL' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, - { name: 'CHK_CYFD_CPCTY', fieldName: 'CHK_CYFD_CPCTY', header: { text: 'CHK_CYFD_CPCTY' }, visible: false }, - { name: 'CHK_OUT_BSC_FCT_NM', fieldName: 'CHK_OUT_BSC_FCT_NM', header: { text: 'CHK_OUT_BSC_FCT_NM' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '시설물위치내용' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'ECSCS_NM', - 'OCTY', + 'fcltNm', + 'gnam', { name: 'group_process_amount', header: { text: '처 분 량(톤/년)' }, items: [ - 'PROCESS_CPCTY_SM', - 'RUSE', - 'LAND_RCLM', - 'INCNR', - 'OCEAN_DPSL', - 'FUEL', - 'ETC' + 'wholDspsQnt', + 'rcycQnt', + 'olandLfvlm', + 'incnrQnt', + 'oceanInputQnt', + 'fuelQnt', + 'etcQnt' ] }, - 'CYFD_CPCTY', - 'OUT_BSC_FCT_NM', - 'RMK' + 'nondspsQnt', + 'swtrmtFcltNm', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'fcltPstnCn' ], rowStartNum: 2, fixedColumns: 2, - tabKey: 'excretaSlurgeProcessingStatusInp', + tabKey: 'ST00110004', tabLabel: '분뇨찌꺼기(슬러지)처리현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts index dedf4af98..2c779ccd3 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts @@ -5,22 +5,37 @@ import { ValueType } from "realgrid"; export const SewageSlurgeProcessingFacStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, styleName: 'rg-cell-readonly' }, // 찌꺼기처리시설명 - { name: 'SLG_FCT_NM', fieldName: '찌꺼기처리시설명', header: { text: '찌꺼기처리시설명' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '찌꺼기처리시설명' } }, // 처리시설 운영현황 - { name: 'TRT_METHOD', fieldName: '처리방식', header: { text: '처리방식' } }, - { name: 'SLG_FCT_CPC', fieldName: '시설용량(톤/일)', header: { text: '시설용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'OPR_CPC', fieldName: '연간처리량(톤/년)', header: { text: '연간처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ODY_RN_TIME', fieldName: '일평균가동시간(1~24)', header: { text: '일평균가동시간(1~24)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'YRSTP_RN_DYN', fieldName: '연중 가동일(1~330)', header: { text: '연중 가동일(1~330)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'OPR_RT', fieldName: '가동율(%)', header: { text: '가동율(%)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsMthExpln', fieldName: 'prcsMthExpln', header: { text: '처리방식' } }, + { name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '시설용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlPrcsQnt', fieldName: 'anlPrcsQnt', header: { text: '연간처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dayAvgOprtngHr', fieldName: 'dayAvgOprtngHr', header: { text: '일평균가동시간(1~24)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'anlOprtngDayCnt', fieldName: 'anlOprtngDayCnt', header: { text: '연중 가동일(1~330)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'oprtngRt', + fieldName: 'oprtngRt', + header: { text: '가동율(%)' }, + footer: footerDecimal1, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorField: 'anlPrcsQnt', + denominatorField: 'fcltCpct', + multiplierField: 'anlOprtngDayCnt', + type: 'quotientTimesMultiplierPercent' as const, + }, + }, // 비고 - { name: 'SLG_FCT_RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // Hidden: 기본 정보 { name: 'STPN_SEQ', fieldName: 'STPN_SEQ', header: { text: 'STPN_SEQ' }, visible: false }, @@ -30,40 +45,42 @@ export const SewageSlurgeProcessingFacStatusInpColumn = { { name: 'BSNM_CD', fieldName: 'BSNM_CD', header: { text: 'BSNM_CD' }, visible: false }, { name: 'BSC_FCT_CD', fieldName: 'BSC_FCT_CD', header: { text: 'BSC_FCT_CD' }, visible: false }, - // Hidden: Check fields for change detection - { name: 'CHK_SLG_FCT_NM', fieldName: 'CHK_SLG_FCT_NM', header: { text: 'CHK_SLG_FCT_NM' }, visible: false }, - { name: 'CHK_TRT_METHOD', fieldName: 'CHK_TRT_METHOD', header: { text: 'CHK_TRT_METHOD' }, visible: false }, - { name: 'CHK_SLG_FCT_CPC', fieldName: 'CHK_SLG_FCT_CPC', header: { text: 'CHK_SLG_FCT_CPC' }, visible: false }, - { name: 'CHK_OPR_CPC', fieldName: 'CHK_OPR_CPC', header: { text: 'CHK_OPR_CPC' }, visible: false }, - { name: 'CHK_ODY_RN_TIME', fieldName: 'CHK_ODY_RN_TIME', header: { text: 'CHK_ODY_RN_TIME' }, visible: false }, - { name: 'CHK_YRSTP_RN_DYN', fieldName: 'CHK_YRSTP_RN_DYN', header: { text: 'CHK_YRSTP_RN_DYN' }, visible: false }, - { name: 'CHK_OPR_RT', fieldName: 'CHK_OPR_RT', header: { text: 'CHK_OPR_RT' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'SLG_FCT_NM', + 'fcltNm', { name: 'group_processing_fac_operating', header: { text: '처리시설 운영현황' }, items: [ - 'TRT_METHOD', - 'SLG_FCT_CPC', - 'OPR_CPC', - 'ODY_RN_TIME', - 'YRSTP_RN_DYN', - 'OPR_RT' + 'prcsMthExpln', + 'fcltCpct', + 'anlPrcsQnt', + 'dayAvgOprtngHr', + 'anlOprtngDayCnt', + 'oprtngRt' ] }, - 'SLG_FCT_RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', ], rowStartNum: 2, fixedColumns: 1, - tabKey: 'sewageSlurgeProcessingFacStatusInp', + tabKey: 'ST00110003', tabLabel: '하수찌꺼기 처리시설 운영현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts index 364c4e378..1d3e11f0d 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts @@ -5,212 +5,357 @@ import { ValueType } from "realgrid"; export const SlurgeProcessingFacStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리장명 - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수처리장명' }, }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리장명' }, editable: false, styleName: 'rg-cell-readonly' }, // 찌꺼기발생량(톤/년, 탈수기준) - { name: 'TOT_AMNT', fieldName: '합계', header: { text: '합계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'P_AMNT', fieldName: '자체', header: { text: '자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'OUT_AMNT', fieldName: '외부유입량', header: { text: '외부유입량' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'gnamWhol', + fieldName: 'gnamWhol', + header: { text: '합계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['itslfQnt', 'otsdInflwQnt'], + type: 'sum' as const, + }, + }, + { name: 'itslfQnt', fieldName: 'itslfQnt', header: { text: '자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdInflwQnt', fieldName: 'otsdInflwQnt', header: { text: '외부유입량' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 이송처리장 - { name: 'OUT_FCT_NM', fieldName: '이송처리장', header: { text: '이송처리장' } }, + { name: 'transfTmplntNm', fieldName: 'transfTmplntNm', header: { text: '이송처리장' } }, // 이송처리량 - 쿼리에서 못찾음 (임의) - { name: 'a', fieldName: '이송처리량(m³/년)', header: { text: '이송처리량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'transfPrcsQnt', fieldName: 'transfPrcsQnt', header: { text: '이송처리량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 함수율 - { name: 'DSP_MOIRAT', fieldName: '함수율(%, 탈수기준)', header: { text: '함수율(%, 탈수기준)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'srpFncRt', fieldName: 'srpFncRt', header: { text: '함수율(%, 탈수기준)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 찌꺼기처리량합계 - { name: 'DSP_SM', fieldName: '찌꺼기처리량합계(톤/년)', header: { text: '찌꺼기처리량합계(톤/년)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholSrpPrcsQnt', + fieldName: 'wholSrpPrcsQnt', + header: { text: '찌꺼기처리량합계(톤/년)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'itslfTs1RclmQnt', + 'itslfTs1IncnrQnt', + 'itslfTs1AltdQnt', + 'itslfTs1DryQnt', + 'itslfTs1FuelQnt', + 'itslfTs2CmpstgQnt', + 'otsdCnsgnRclmQnt', + 'otsdCnsgnIncnrQnt', + 'otsdCnsgnFuelPwplQnt', + 'otsdCnsgnPrdctRmtrlQnt', + 'otsdCnsgnFmlyrQnt', + 'otsdCnsgnCmpstgQnt', + 'cparTs1Whol', + ], + type: 'sum' as const, + } + }, // 자체 찌꺼기 처리량 > 자체찌꺼기처리량소계 - { name: 'DSP_SELF_SM', fieldName: '자체찌꺼기처리량소계', header: { text: '자체찌꺼기처리량소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'itslfTs1Whol', + fieldName: 'itslfTs1Whol', + header: { text: '자체찌꺼기처리량소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'itslfTs1RclmQnt', + 'itslfTs1IncnrQnt', + 'itslfTs1AltdQnt', + 'itslfTs1DryQnt', + 'itslfTs1FuelQnt', + 'itslfTs2CmpstgQnt', + ], + type: 'sum' as const, + } + }, // 자체 찌꺼기 처리량 > 매립 - { name: 'O_DSP_RCLM', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1RclmQnt', fieldName: 'itslfTs1RclmQnt', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 소각(1차) - { name: 'O_DSP_INCNR', fieldName: '소각(1차)', header: { text: '소각(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1IncnrQnt', fieldName: 'itslfTs1IncnrQnt', header: { text: '소각(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 소각 후 처리(2차) - 쿼리에 없음 (임의 생성) - { name: 'b1', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b2', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b3', fieldName: '제품원료', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b4', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b5', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'incnrPrcsWhol', + fieldName: 'incnrPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'incnrPrcsLfvlm', + 'incnrPrcsPrdctRmtrlQnt', + 'incnrPrcsFmlyrQnt', + ], + type: 'sum' as const, + } + }, + { name: 'incnrPrcsLfvlm', fieldName: 'incnrPrcsLfvlm', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrPrcsPrdctRmtrlQnt', fieldName: 'incnrPrcsPrdctRmtrlQnt', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrPrcsFmlyrQnt', fieldName: 'incnrPrcsFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrPrcsFcltNm', fieldName: 'incnrPrcsFcltNm', header: { text: '시설(업체명)' } }, // 자체 찌꺼기 처리량 > 고화(1차) - 쿼리에 없음 (임의 생성) - { name: 'c1', fieldName: '고화(1차)', header: { text: '고화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1AltdQnt', fieldName: 'itslfTs1AltdQnt', header: { text: '고화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 고화 후 처리(2차) - 쿼리에 없음 (임의 생성) - { name: 'c2', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'c3', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'c4', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'c5', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'stbznPrcsWhol', + fieldName: 'stbznPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'stbznPrcsLfvlm', + 'stbznPrcsFmlyrQnt', + ], + type: 'sum' as const, + } + }, + { name: 'stbznPrcsLfvlm', fieldName: 'stbznPrcsLfvlm', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'stbznPrcsFmlyrQnt', fieldName: 'stbznPrcsFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'stbznPrcsFcltNm', fieldName: 'stbznPrcsFcltNm', header: { text: '시설(업체명)' } }, // 자체 찌꺼기 처리량 > 건조(1차) - { name: 'DSP_DESIC', fieldName: '건조(1차)', header: { text: '건조(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1DryQnt', fieldName: 'itslfTs1DryQnt', header: { text: '건조(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 건조 후 처리(2차) - 쿼리에 없음 - { name: 'd1', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd2', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd3', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd4', fieldName: '연료', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd5', fieldName: '제품원료', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd6', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd7', fieldName: '퇴비화', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd8', fieldName: '시설(업체명)', header: { text: '시설(업체명)' }, editable: true }, + { + name: 'dryPrcsWhol', + fieldName: 'dryPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'dryPrcsLfvlm', + 'dryPrcsIncnrQnt', + 'dryPrcsFuelQnt', + 'dryPrcsPrdctRmtrlQnt', + 'dryPrcsFmlyrQnt', + 'dryPrcsCmpstgQnt', + ], + type: 'sum' as const, + } + }, + { name: 'dryPrcsLfvlm', fieldName: 'dryPrcsLfvlm', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsIncnrQnt', fieldName: 'dryPrcsIncnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsFuelQnt', fieldName: 'dryPrcsFuelQnt', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsPrdctRmtrlQnt', fieldName: 'dryPrcsPrdctRmtrlQnt', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsFmlyrQnt', fieldName: 'dryPrcsFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsCmpstgQnt', fieldName: 'dryPrcsCmpstgQnt', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsFcltNm', fieldName: 'dryPrcsFcltNm', header: { text: '시설(업체명)' }, editable: true }, // 자체 찌꺼기 처리량 > 탄화(1차) - 쿼리에 없음 (임의 생성) - { name: 'e1', fieldName: '탄화(1차)', header: { text: '탄화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1FuelQnt', fieldName: 'itslfTs1FuelQnt', header: { text: '탄화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 탄화 후 처리(2차) - 쿼리에 없음 (임의 생성) - { name: 'e2', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'e3', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'e4', fieldName: '연료', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'e5', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'fuelPrcsWhol', + fieldName: 'fuelPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['fuelPrcsIncnrQnt', 'fuelPrcsFuelQnt'], + type: 'sum' as const, + } + }, + { name: 'fuelPrcsIncnrQnt', fieldName: 'fuelPrcsIncnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fuelPrcsFuelQnt', fieldName: 'fuelPrcsFuelQnt', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fuelPrcsFcltNm', fieldName: 'fuelPrcsFcltNm', header: { text: '시설(업체명)' } }, // 자체 찌꺼기 처리량 > 퇴비화 - 쿼리에 없음 (임의 생성) - { name: 'f1', fieldName: '퇴비화', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs2CmpstgQnt', fieldName: 'itslfTs2CmpstgQnt', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 퇴비화 > 시설(업체명) - 쿼리에 없음 (임의 생성) - { name: 'f2', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { name: 'cmpsPrcsFcltBzentyNm', fieldName: 'cmpsPrcsFcltBzentyNm', header: { text: '시설(업체명)' } }, // 외부위탁 처리량(톤/년) - { name: 'DSP_CONSIGNMENT', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g1', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g2', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g3', fieldName: '연료', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g4', fieldName: '제품원료', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g5', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g6', fieldName: '퇴비화', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g7', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'otsdCnsgnWhol', + fieldName: 'otsdCnsgnWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'otsdCnsgnRclmQnt', + 'otsdCnsgnIncnrQnt', + 'otsdCnsgnFuelPwplQnt', + 'otsdCnsgnPrdctRmtrlQnt', + 'otsdCnsgnFmlyrQnt', + 'otsdCnsgnCmpstgQnt', + ], + type: 'sum' as const, + } + }, + { name: 'otsdCnsgnRclmQnt', fieldName: 'otsdCnsgnRclmQnt', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnIncnrQnt', fieldName: 'otsdCnsgnIncnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnFuelPwplQnt', fieldName: 'otsdCnsgnFuelPwplQnt', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnPrdctRmtrlQnt', fieldName: 'otsdCnsgnPrdctRmtrlQnt', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnFmlyrQnt', fieldName: 'otsdCnsgnFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnCmpstgQnt', fieldName: 'otsdCnsgnCmpstgQnt', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnFcltNm', fieldName: 'otsdCnsgnFcltNm', header: { text: '시설(업체명)' } }, // 수도권광역위탁처리량 - { name: 'DSP_WIDE', fieldName: '수도권광역위탁처리량(톤/년)', header: { text: '수도권광역위탁처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'cparTs1Whol', fieldName: 'cparTs1Whol', header: { text: '수도권광역위탁처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: Check fields - { name: 'CHK_TOT_AMNT', fieldName: 'CHK_TOT_AMNT', header: { text: 'CHK_TOT_AMNT' }, visible: false }, - { name: 'CHK_P_AMNT', fieldName: 'CHK_P_AMNT', header: { text: 'CHK_P_AMNT' }, visible: false }, - { name: 'CHK_OUT_AMNT', fieldName: 'CHK_OUT_AMNT', header: { text: 'CHK_OUT_AMNT' }, visible: false }, - { name: 'CHK_DSP_MOIRAT', fieldName: 'CHK_DSP_MOIRAT', header: { text: 'CHK_DSP_MOIRAT' }, visible: false }, - { name: 'CHK_DSP_SM', fieldName: 'CHK_DSP_SM', header: { text: 'CHK_DSP_SM' }, visible: false }, - { name: 'CHK_DSP_SELF_SM', fieldName: 'CHK_DSP_SELF_SM', header: { text: 'CHK_DSP_SELF_SM' }, visible: false }, - { name: 'CHK_O_DSP_RCLM', fieldName: 'CHK_O_DSP_RCLM', header: { text: 'CHK_O_DSP_RCLM' }, visible: false }, - { name: 'CHK_O_DSP_INCNR', fieldName: 'CHK_O_DSP_INCNR', header: { text: 'CHK_O_DSP_INCNR' }, visible: false }, - { name: 'CHK_DSP_DESIC', fieldName: 'CHK_DSP_DESIC', header: { text: 'CHK_DSP_DESIC' }, visible: false }, - { name: 'CHK_DSP_CONSIGNMENT', fieldName: 'CHK_DSP_CONSIGNMENT', header: { text: 'CHK_DSP_CONSIGNMENT' }, visible: false }, - { name: 'CHK_DSP_WIDE', fieldName: 'CHK_DSP_WIDE', header: { text: 'CHK_DSP_WIDE' }, visible: false }, - { name: 'CHK_OUT_FCT_NM', fieldName: 'CHK_OUT_FCT_NM', header: { text: 'CHK_OUT_FCT_NM' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'BSC_FCT_NM', + 'swtrmtFcltNm', { name: 'group_occur_amount', header: { text: '찌꺼기발생량(톤/년, 탈수기준)' }, items: [ - 'TOT_AMNT', - 'P_AMNT', - 'OUT_AMNT' + 'gnamWhol', + 'itslfQnt', + 'otsdInflwQnt' ] }, - 'OUT_FCT_NM', - 'a', - 'DSP_MOIRAT', - 'DSP_SM', + 'transfTmplntNm', + 'transfPrcsQnt', + 'srpFncRt', + 'wholSrpPrcsQnt', { name: 'group_self_treatment', header: { text: '자체 찌꺼기 처리량(톤/년)' }, items: [ - 'DSP_SELF_SM', - 'O_DSP_RCLM', - 'O_DSP_INCNR', + 'itslfTs1Whol', + 'itslfTs1RclmQnt', + 'itslfTs1IncnrQnt', { name: 'group_incinerate_2nd', header: { text: '소각 후 처리(2차)' }, items: [ - 'b1', - 'b2', - 'b3', - 'b4', - 'b5' + 'incnrPrcsWhol', + 'incnrPrcsLfvlm', + 'incnrPrcsPrdctRmtrlQnt', + 'incnrPrcsFmlyrQnt', + 'incnrPrcsFcltNm' ] }, - 'c1', + 'itslfTs1AltdQnt', { name: 'group_solidify_2nd', header: { text: '고화 후 처리(2차)' }, items: [ - 'c2', - 'c3', - 'c4', - 'c5' + 'stbznPrcsWhol', + 'stbznPrcsLfvlm', + 'stbznPrcsFmlyrQnt', + 'stbznPrcsFcltNm' ] }, - 'DSP_DESIC', + 'itslfTs1DryQnt', { name: 'group_drying_2nd', header: { text: '건조 후 처리(2차)' }, items: [ - 'd1', - 'd2', - 'd3', - 'd4', - 'd5', - 'd6', - 'd7', - 'd8' + 'dryPrcsWhol', + 'dryPrcsLfvlm', + 'dryPrcsIncnrQnt', + 'dryPrcsFuelQnt', + 'dryPrcsPrdctRmtrlQnt', + 'dryPrcsFmlyrQnt', + 'dryPrcsCmpstgQnt', + 'dryPrcsFcltNm' ] }, - 'e1', + 'itslfTs1FuelQnt', { name: 'group_carbonize_2nd', header: { text: '탄화 후 처리(2차)' }, items: [ - 'e2', - 'e3', - 'e4', - 'e5' + 'fuelPrcsWhol', + 'fuelPrcsIncnrQnt', + 'fuelPrcsFuelQnt', + 'fuelPrcsFcltNm' ] }, - 'f1', - 'f2' + 'itslfTs2CmpstgQnt', + 'cmpsPrcsFcltBzentyNm' ] }, { name: 'group_external_treatment', header: { text: '외부위탁 처리량(톤/년)' }, items: [ - 'DSP_CONSIGNMENT', - 'g1', - 'g2', - 'g3', - 'g4', - 'g5', - 'g6', - 'g7' + 'otsdCnsgnWhol', + 'otsdCnsgnRclmQnt', + 'otsdCnsgnIncnrQnt', + 'otsdCnsgnFuelPwplQnt', + 'otsdCnsgnPrdctRmtrlQnt', + 'otsdCnsgnFmlyrQnt', + 'otsdCnsgnCmpstgQnt', + 'otsdCnsgnFcltNm' ] }, - 'DSP_WIDE', - 'RMK' + 'cparTs1Whol', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd' ], rowStartNum: 3, fixedColumns: 2, - tabKey: 'slurgeProcessingFacStatusInp', + tabKey: 'ST00110001', tabLabel: '찌꺼기(슬러지)처리시설현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts index c28f12bc7..9b41b2202 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts @@ -10,7 +10,7 @@ export const AdminAreaConnectionInpColumn = { { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false }, { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false }, // 연결여부 - { name: 'isConnection', fieldName: 'isConnection', header: { text: '연결여부' }, editable: false }, + { name: 'isAreaConnection', fieldName: 'isAreaConnection', header: { text: '연결여부' }, editable: false }, // 연결하수처리장 { name: 'connectionBscFctNmList', fieldName: 'connectionBscFctNmList', header: { text: '연결하수처리장' }, editable: false }, { @@ -28,7 +28,7 @@ export const AdminAreaConnectionInpColumn = { header: { text: '행정구역' }, items: ['ctpvNm', 'sggNm', 'admdstNm'], }, - 'isConnection', + 'isAreaConnection', 'connectionBscFctNmList', 'admdstCd' ], diff --git a/apps/st/src/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn.ts b/apps/st/src/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn.ts index ebd09ca2b..695fb8213 100644 --- a/apps/st/src/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn.ts @@ -1,6 +1,33 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerInt, footerDecimal1, footerDecimal2, footerDecimal3 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +const RAINFALL_PUMP_FCLTY_KIND_VALUES = ["해당없음", "제1종", "제2종", "제3종"] as const; +const RAINFALL_PUMP_YN_VALUES = ["유", "무"] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 04.하수저류시설 및 빗물펌프장 > tab 2: 빗물펌프장 export const RainfallPumpStationInpColumn = { columns: [ @@ -10,7 +37,10 @@ export const RainfallPumpStationInpColumn = { { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } }, { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치' } }, - { name: 'fcltyKndCn', fieldName: 'fcltyKndCn', header: { text: '시설물종류' } }, + dropdownColumn( + { name: 'fcltyKndCn', fieldName: 'fcltyKndCn', header: { text: '시설물종류' } }, + RAINFALL_PUMP_FCLTY_KIND_VALUES, + ), { name: 'oprtngStrtYmd', @@ -29,14 +59,46 @@ export const RainfallPumpStationInpColumn = { { name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 유수지 - { name: 'rsvrYn', fieldName: 'rsvrYn', header: { text: '유수지유무(유,무)' } }, + dropdownColumn( + { name: 'rsvrYn', fieldName: 'rsvrYn', header: { text: '유수지유무(유,무)' } }, + RAINFALL_PUMP_YN_VALUES, + ), { name: 'rsvrImpndQnt', fieldName: 'rsvrImpndQnt', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 흡수정 { name: 'abstnRgnImpdQnt', fieldName: 'abstnRgnImpdQnt', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 펌프시설 > 총양수량(m³/min) - { name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'grssPpqty', + fieldName: 'grssPpqty', + header: { text: '총양수량(m³/min)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + type: 'pumpGrossPpqty' as const, + pumpGroups: [ + { + pumpCntField: 'pumpFcltN1PumpCnt', + rsrvPumpCntField: 'pumpFcltN1RsrvPumpCnt', + perPpqtyField: 'pumpFcltN1PerPpqty', + }, + { + pumpCntField: 'pumpFcltN2PumpCnt', + rsrvPumpCntField: 'pumpFcltN2RsrvPumpCnt', + perPpqtyField: 'pumpFcltN2PerPpqty', + }, + { + pumpCntField: 'pumpFcltN3PumpCnt', + rsrvPumpCntField: 'pumpFcltN3RsrvPumpCnt', + perPpqtyField: 'pumpFcltN3PerPpqty', + }, + ], + }, + }, // 펌프시설1 { name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } }, @@ -57,9 +119,18 @@ export const RainfallPumpStationInpColumn = { { name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 부대시설 - { name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } }, - { name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } }, - { name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } }, + dropdownColumn( + { name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } }, + RAINFALL_PUMP_YN_VALUES, + ), + dropdownColumn( + { name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } }, + RAINFALL_PUMP_YN_VALUES, + ), + dropdownColumn( + { name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } }, + RAINFALL_PUMP_YN_VALUES, + ), // 방류수역 { name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역' } }, diff --git a/apps/st/src/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn.ts b/apps/st/src/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn.ts index 1cd46cf9e..8747c5204 100644 --- a/apps/st/src/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn.ts @@ -11,6 +11,10 @@ export const SEWAGE_RESERVOIR_FCLT_STRCT_VALUES = [ "지하터널식", "기타", ] as const; +export const SEWAGE_RESERVOIR_YN_VALUES = ["유", "무"] as const; +export const SEWAGE_RESERVOIR_MLTV_MTH_VALUES = ["자연배수", "강제배수"] as const; +export const SEWAGE_RESERVOIR_IMPD_MTH_VALUES = ["직렬", "병렬"] as const; +export const SEWAGE_RESERVOIR_IMPD_PRCS_MTHD_VALUES = ["자체처리", "연계처리"] as const; function dropdownColumn( col: Pick & @@ -99,7 +103,36 @@ export const SewageReservoirInpColumn = { { name: 'impdFcltCpct', fieldName: 'impdFcltCpct', header: { text: '시설용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 펌프시설 > 총양수량 - { name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'grssPpqty', + fieldName: 'grssPpqty', + header: { text: '총양수량(m³/min)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + type: 'pumpGrossPpqty' as const, + pumpGroups: [ + { + pumpCntField: 'pumpFcltN1PumpCnt', + rsrvPumpCntField: 'pumpFcltN1RsrvPumpCnt', + perPpqtyField: 'pumpFcltN1PerPpqty', + }, + { + pumpCntField: 'pumpFcltN2PumpCnt', + rsrvPumpCntField: 'pumpFcltN2RsrvPumpCnt', + perPpqtyField: 'pumpFcltN2PerPpqty', + }, + { + pumpCntField: 'pumpFcltN3PumpCnt', + rsrvPumpCntField: 'pumpFcltN3RsrvPumpCnt', + perPpqtyField: 'pumpFcltN3PerPpqty', + }, + ], + }, + }, // 펌프시설 1 { name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } }, @@ -120,18 +153,60 @@ export const SewageReservoirInpColumn = { { name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 부대시설 - { name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } }, - { name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } }, - { name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } }, + dropdownColumn( + { + name: "prcsgFcltYn", + fieldName: "prcsgFcltYn", + header: { text: "전처리시설유무(유/무)" }, + }, + SEWAGE_RESERVOIR_YN_VALUES, + ), + dropdownColumn( + { + name: "ddrztnEqpmYn", + fieldName: "ddrztnEqpmYn", + header: { text: "탈취설비유무(유/무)" }, + }, + SEWAGE_RESERVOIR_YN_VALUES, + ), + dropdownColumn( + { + name: "oslfEtgtYn", + fieldName: "oslfEtgtYn", + header: { text: "자가발전유무(유/무)" }, + }, + SEWAGE_RESERVOIR_YN_VALUES, + ), // 배수 방식 - { name: 'mltvMthCn', fieldName: 'mltvMthCn', header: { text: '배수방식(자연배수,강제배수)' } }, - + dropdownColumn( + { + name: "mltvMthCn", + fieldName: "mltvMthCn", + header: { text: "배수방식(자연배수,강제배수)" }, + }, + SEWAGE_RESERVOIR_MLTV_MTH_VALUES, + ), + // 저류 방식 - { name: 'impdMthCn', fieldName: 'impdMthCn', header: { text: '저류방식(직렬,병렬)' } }, - + dropdownColumn( + { + name: "impdMthCn", + fieldName: "impdMthCn", + header: { text: "저류방식(직렬,병렬)" }, + }, + SEWAGE_RESERVOIR_IMPD_MTH_VALUES, + ), + // 저류수 처리방법 - { name: 'impdPrcsMthdCn', fieldName: 'impdPrcsMthdCn', header: { text: '저류수처리방법(자체처리,연계처리)' } }, + dropdownColumn( + { + name: "impdPrcsMthdCn", + fieldName: "impdPrcsMthdCn", + header: { text: "저류수처리방법(자체처리,연계처리)" }, + }, + SEWAGE_RESERVOIR_IMPD_PRCS_MTHD_VALUES, + ), // 방류 수역 { name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역(또는 연계처리장명)' } }, diff --git a/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts index 419f00d9d..9a7087cbc 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts @@ -5,46 +5,72 @@ import { ValueType } from "realgrid"; export const CauserBurdenInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 부과액 - { name: 'LEVY_CO', fieldName: '건수', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'LEVY_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'levyEvntcsCnt', fieldName: 'levyEvntcsCnt', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'levyAmt', fieldName: 'levyAmt', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 대상별 징수현황 > 계 - { name: 'TRGET_LEV_STTUS_CO', fieldName: '건수', header: { text: '건수' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'TRGET_LEV_STTUS_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'trgtByWholClctnEvntcsCnt', + fieldName: 'trgtByWholClctnEvntcsCnt', + header: { text: '건수' }, + editable: false, + footer: footerInt, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { + fields: ['mltvEqpmEvntcsCnt', 'othrCstrnOthrActrCnt'], + type: 'sum' as const, + }, + }, + { + name: 'trgtByWholClctnAmt', + fieldName: 'trgtByWholClctnAmt', + header: { text: '금액(백만원)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: ['mltvEqpmAmt', 'othrCstrnOthrActrAmt'], + type: 'sum' as const, + }, + }, // 대상별 징수현황 > 배수설비설치자 - { name: 'DRNG_EQP_CO', fieldName: '건수', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'DRNG_EQP_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'mltvEqpmEvntcsCnt', fieldName: 'mltvEqpmEvntcsCnt', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mltvEqpmAmt', fieldName: 'mltvEqpmAmt', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 대상별 징수현황 > 타공사·타행위자 - { name: 'CNTRWK_CO', fieldName: '건수', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CNTRWK_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'othrCstrnOthrActrCnt', fieldName: 'othrCstrnOthrActrCnt', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'othrCstrnOthrActrAmt', fieldName: 'othrCstrnOthrActrAmt', header: { text: '금액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - - // 참조 필드 - { name: 'AMNTBRNCSR', fieldName: 'AMNTBRNCSR', header: { text: 'AMNTBRNCSR' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_assess', header: { text: '부과액' }, - items: ['LEVY_CO', 'LEVY_AMOUNT'] + items: ['levyEvntcsCnt', 'levyAmt'] }, { name: 'group_collection', @@ -53,25 +79,27 @@ export const CauserBurdenInpColumn = { { name: 'coll_group_total', header: { text: '계' }, - items: ['TRGET_LEV_STTUS_CO', 'TRGET_LEV_STTUS_AMOUNT'] + items: ['trgtByWholClctnEvntcsCnt', 'trgtByWholClctnAmt'] }, { name: 'coll_group_inst', header: { text: '배수설비설치자(하수도법 제61조 제1항)' }, - items: ['DRNG_EQP_CO', 'DRNG_EQP_AMOUNT'] + items: ['mltvEqpmEvntcsCnt', 'mltvEqpmAmt'] }, { name: 'coll_group_otr', header: { text: '타공사·타행위자(하수도법 제61조 제2항)' }, - items: ['CNTRWK_CO', 'CNTRWK_AMOUNT'] + items: ['othrCstrnOthrActrCnt', 'othrCstrnOthrActrAmt'] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'causerBurdenInp', + tabKey: 'ST00090004', tabLabel: '원인자 부담금' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts index 5940acc81..b6bdb0f4f 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts @@ -5,45 +5,86 @@ import { ValueType } from "realgrid"; export const SewerageChargeInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 요금 - { name: 'FYER_LEVY_QY', fieldName: '연간조정량(m³)(A)', header: { text: '연간조정량(m³)(A)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'LEVY_AMOUNT', fieldName: '하수도사용료수익(원)(B)', header: { text: '하수도사용료수익(원)(B)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'AVRG_UNTPC', fieldName: 'm³당요금(원/m³) C=(B/A)', header: { text: 'm³당요금(원/m³) C=(B/A)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlLevyQnt', fieldName: 'anlLevyQnt', header: { text: '연간조정량(m³)(A)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'levyAmt', fieldName: 'levyAmt', header: { text: '하수도사용료수익(원)(B)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'avgUntprcAmt', + fieldName: 'avgUntprcAmt', + header: { text: 'm³당요금(원/m³) C=(B/A)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: ['levyAmt', 'anlLevyQnt'], + type: 'quotient' as const, + }, + }, - { name: 'SMRIZE_PRMPC', fieldName: '총괄원가(원)(D)', header: { text: '총괄원가(원)(D)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'SMRIZE_UNIT_PRMPC', fieldName: 'm³당원가(원/m³) E=(D/A)', header: { text: 'm³당원가(원/m³) E=(D/A)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RLTY_RT', fieldName: '요금현실화율(%) F=(C/E*100)', header: { text: '요금현실화율(%) F=(C/E*100)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'grssPrmcoAmt', fieldName: 'grssPrmcoAmt', header: { text: '총괄원가(원)(D)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'grssUnitPrmcoAmt', + fieldName: 'grssUnitPrmcoAmt', + header: { text: 'm³당원가(원/m³) E=(D/A)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: ['grssPrmcoAmt', 'anlLevyQnt'], + type: 'quotient' as const, + }, + }, + { + name: 'rlztnRt', + fieldName: 'rlztnRt', + header: { text: '요금현실화율(%) F=(C/E*100)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorQuotient: ['levyAmt', 'anlLevyQnt'] as [string, string], + denominatorQuotient: ['grssPrmcoAmt', 'anlLevyQnt'] as [string, string], + type: 'quotientRatioPercent' as const, + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 참조 필드 - { name: 'SEWER_RNTFEE', fieldName: 'SEWER_RNTFEE', header: { text: 'SEWER_RNTFEE' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'C_LEVY_AMOUNT', fieldName: 'C_LEVY_AMOUNT', header: { text: 'C_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'FYER_LEVY_QY', - 'LEVY_AMOUNT', - 'AVRG_UNTPC', - 'SMRIZE_PRMPC', - 'SMRIZE_UNIT_PRMPC', - 'RLTY_RT', - 'RMK' + 'anlLevyQnt', + 'levyAmt', + 'avgUntprcAmt', + 'grssPrmcoAmt', + 'grssUnitPrmcoAmt', + 'rlztnRt', + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 2, fixedColumns: 1, - tabKey: 'sewerageChargeInp', + tabKey: 'ST00090002', tabLabel: '하수도 요금' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts index 24b46e1e6..d75f5a06d 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts @@ -5,146 +5,238 @@ import { ValueType } from "realgrid"; export const SewerageFinanceSettleInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 재정별 사업비 - { name: 'FNANC_WCT_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'NTNXPN', fieldName: '중앙정부지원액', header: { text: '중앙정부지원액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - + { + name: 'fnnByWholPjtcoAmt', + fieldName: 'fnnByWholPjtcoAmt', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'cnterGovSprtAmt', + 'mctpvLcexpAmt', + 'wareaSggLclAmt', + 'sewerUseAmt', + 'orgprBrdnAmt', + 'etcIncmAmt', + 'crfwAmt', + ], + type: 'sum' as const, + }, + }, + { name: 'cnterGovSprtAmt', fieldName: 'cnterGovSprtAmt', header: { text: '중앙정부지원액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + // 재정별 사업비 > 지방비 - { name: 'LCLEXPNS', fieldName: '특·광역시, 도', header: { text: '특·광역시, 도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'LCLTY_CT', fieldName: '시,군,구 지자체', header: { text: '시,군,구 지자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'mctpvLcexpAmt', fieldName: 'mctpvLcexpAmt', header: { text: '특·광역시, 도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'wareaSggLclAmt', fieldName: 'wareaSggLclAmt', header: { text: '시,군,구 지자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 재정별 사업비 - { name: 'SEWER_RNTFEE', fieldName: '하수도사용료', header: { text: '하수도사용료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'AMNTBRNCSR', fieldName: '원인자부담금', header: { text: '원인자부담금' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_INCME', fieldName: '기타잡수입', header: { text: '기타잡수입' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'CYFD_AMOUNT', fieldName: '이월액', header: { text: '이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sewerUseAmt', fieldName: 'sewerUseAmt', header: { text: '하수도사용료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'orgprBrdnAmt', fieldName: 'orgprBrdnAmt', header: { text: '원인자부담금' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcIncmAmt', fieldName: 'etcIncmAmt', header: { text: '기타잡수입' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'crfwAmt', fieldName: 'crfwAmt', header: { text: '이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 부채액 - { name: 'DEBT_AMOUNT', fieldName: '부채액(백만원)', header: { text: '부채액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'lbltAmt', fieldName: 'lbltAmt', header: { text: '부채액(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 계 - { name: 'PRPOS_WCT_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'usgByWholPjtcoAmt', + fieldName: 'usgByWholPjtcoAmt', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + }, // 용도별사업비 > 처리장 - { name: 'PRCPLT_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_FCLTY', fieldName: '시설비', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_RPAIR', fieldName: '개보수비', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_ORPNS', fieldName: '운영유지비', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PRCPLT_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'tmplntWholAmt', + fieldName: 'tmplntWholAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['tmplntFcltAmt', 'tmplntMendngAmt', 'tmplntOperKeepAmt', 'tmplntEtcAmt'], + type: 'sum' as const, + }, + }, + { name: 'tmplntFcltAmt', fieldName: 'tmplntFcltAmt', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntMendngAmt', fieldName: 'tmplntMendngAmt', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntOperKeepAmt', fieldName: 'tmplntOperKeepAmt', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntEtcAmt', fieldName: 'tmplntEtcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 하수관로 - { name: 'SEWAGE_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_FCLTY', fieldName: '시설비', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_RPAIR', fieldName: '개보수비', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_ORPNS', fieldName: '운영유지비', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SEWAGE_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholSwagePplAmt', + fieldName: 'wholSwagePplAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'swagePplFcltAmt', + 'swagePplMendngAmt', + 'swagePplOperKeepAmt', + 'swagePplEtcAmt', + ], + type: 'sum' as const, + }, + }, + { name: 'swagePplFcltAmt', fieldName: 'swagePplFcltAmt', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swagePplMendngAmt', fieldName: 'swagePplMendngAmt', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swagePplOperKeepAmt', fieldName: 'swagePplOperKeepAmt', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swagePplEtcAmt', fieldName: 'swagePplEtcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 하수저류시설 - { name: 'SWGE_STRGE_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_FCLTY', fieldName: '시설비', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_RPAIR', fieldName: '개보수비', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ORPNS', fieldName: '운영유지비', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholSwageImpdFcltAmt', + fieldName: 'wholSwageImpdFcltAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'swageImpdFcltAmt', + 'swageImpdMendngAmt', + 'swageImpdOperKeepAmt', + 'swageImpdFcltEtcAmt', + ], + type: 'sum' as const, + }, + }, + { name: 'swageImpdFcltAmt', fieldName: 'swageImpdFcltAmt', header: { text: '시설비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swageImpdMendngAmt', fieldName: 'swageImpdMendngAmt', header: { text: '개보수비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swageImpdOperKeepAmt', fieldName: 'swageImpdOperKeepAmt', header: { text: '운영유지비' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'swageImpdFcltEtcAmt', fieldName: 'swageImpdFcltEtcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 용도별사업비 > 기타 - { name: 'ETC_SM', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ACDNT_CYFD_AMOUNT', fieldName: '사고이월액', header: { text: '사고이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'UNRCIV_AMOUNT', fieldName: '미수납액', header: { text: '미수납액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_AMOUNT', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'etcWholAmt', + fieldName: 'etcWholAmt', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['etcAcdntCrfwAmt', 'etcNrcpAmt', 'etcAmt'], + type: 'sum' as const, + }, + }, + { name: 'etcAcdntCrfwAmt', fieldName: 'etcAcdntCrfwAmt', header: { text: '사고이월액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcNrcpAmt', fieldName: 'etcNrcpAmt', header: { text: '미수납액' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcAmt', fieldName: 'etcAmt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // 참조 필드 - { name: 'B_LEVY_AMOUNT', fieldName: 'B_LEVY_AMOUNT', header: { text: 'B_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'C_LEVY_AMOUNT', fieldName: 'C_LEVY_AMOUNT', header: { text: 'C_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'D_LEVY_AMOUNT', fieldName: 'D_LEVY_AMOUNT', header: { text: 'D_LEVY_AMOUNT' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_finance', header: { text: '재정별 사업비(백만원)' }, items: [ - 'FNANC_WCT_SM', - 'NTNXPN', + 'fnnByWholPjtcoAmt', + 'cnterGovSprtAmt', { name: 'group_local_finance', header: { text: '지방비' }, - items: ['LCLEXPNS', 'LCLTY_CT'] + items: ['mctpvLcexpAmt', 'wareaSggLclAmt'] }, - 'SEWER_RNTFEE', - 'AMNTBRNCSR', - 'ETC_INCME', - 'CYFD_AMOUNT' + 'sewerUseAmt', + 'orgprBrdnAmt', + 'etcIncmAmt', + 'crfwAmt' ] }, - 'DEBT_AMOUNT', + 'lbltAmt', { name: 'group_purpose', header: { text: '용도별사업비(백만원)' }, items: [ - 'PRPOS_WCT_SM', + 'usgByWholPjtcoAmt', { name: 'group_treatment', header: { text: '처리장' }, items: [ - 'PRCPLT_SM', - 'PRCPLT_FCLTY', - 'PRCPLT_RPAIR', - 'PRCPLT_ORPNS', - 'PRCPLT_ETC' + 'tmplntWholAmt', + 'tmplntFcltAmt', + 'tmplntMendngAmt', + 'tmplntOperKeepAmt', + 'tmplntEtcAmt' ] }, { name: 'group_pipe', header: { text: '하수관로' }, items: [ - 'SEWAGE_SM', - 'SEWAGE_FCLTY', - 'SEWAGE_RPAIR', - 'SEWAGE_ORPNS', - 'SEWAGE_ETC' + 'wholSwagePplAmt', + 'swagePplFcltAmt', + 'swagePplMendngAmt', + 'swagePplOperKeepAmt', + 'swagePplEtcAmt' ] }, { name: 'group_storage', header: { text: '하수저류시설' }, items: [ - 'SWGE_STRGE_SM', - 'SWGE_STRGE_FCLTY', - 'SWGE_STRGE_RPAIR', - 'SWGE_STRGE_ORPNS', - 'SWGE_STRGE_ETC' + 'wholSwageImpdFcltAmt', + 'swageImpdFcltAmt', + 'swageImpdMendngAmt', + 'swageImpdOperKeepAmt', + 'swageImpdFcltEtcAmt' ] }, { name: 'group_etc_purpose', header: { text: '기타' }, items: [ - 'ETC_SM', - 'ACDNT_CYFD_AMOUNT', - 'UNRCIV_AMOUNT', - 'ETC_AMOUNT' + 'etcWholAmt', + 'etcAcdntCrfwAmt', + 'etcNrcpAmt', + 'etcAmt' ] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'sewerageFinanceSettleInp', + tabKey: 'ST00090001', tabLabel: '하수도재정 결산' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts index bd4be4668..e26f4dbc6 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts @@ -5,67 +5,108 @@ import { ValueType } from "realgrid"; export const SewerageUsageFeeInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 총계 > 부과액 - { name: 'LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'levyAmt', + fieldName: 'levyAmt', + header: { text: '(백만원)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: [ + 'hshusLevyAmt', + 'taskFrLevyAmt', + 'fbuLevyAmt', + 'bthhsFrSpcs1LevyAmt', + 'bthhsFrSpcs2LevyAmt', + 'induLevyAmt', + 'etcLevyAmt', + ], + type: 'sum' as const, + }, + }, // 총계 > 부과량 - { name: 'LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'levyQnt', + fieldName: 'levyQnt', + header: { text: '(천톤)' }, + editable: false, + footer: footerDecimal1, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + fields: [ + 'hshusLevyQnt', + 'taskFrLevyQnt', + 'fbuLevyQnt', + 'bthhsFrSpcs1LevyQnt', + 'bthhsFrSpcs2LevyQnt', + 'induLevyQnt', + 'etcLevyQnt', + ], + type: 'sum' as const, + }, + }, // 가정용 - { name: 'HOME_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'HOME_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'HOME_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'hshusLevyAmt', fieldName: 'hshusLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'hshusLevyQnt', fieldName: 'hshusLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'hshusUntprcAmt', fieldName: 'hshusUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 업무용 - { name: 'JOB_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'JOB_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'JOB_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'taskFrLevyAmt', fieldName: 'taskFrLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'taskFrLevyQnt', fieldName: 'taskFrLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'taskFrUntprcAmt', fieldName: 'taskFrUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 영업용(일반용) - { name: 'BSN_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BSN_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BSN_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fbuLevyAmt', fieldName: 'fbuLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fbuLevyQnt', fieldName: 'fbuLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fbuUntprcAmt', fieldName: 'fbuUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 욕탕1종(대중탕용) - { name: 'BATHS1_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BATHS1_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'BATHS1_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs1LevyAmt', fieldName: 'bthhsFrSpcs1LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs1LevyQnt', fieldName: 'bthhsFrSpcs1LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs1UntprcAmt', fieldName: 'bthhsFrSpcs1UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 욕탕2종 - { name: 'PBBATH_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PBBATH_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PBBATH_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs2LevyAmt', fieldName: 'bthhsFrSpcs2LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs2LevyQnt', fieldName: 'bthhsFrSpcs2LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'bthhsFrSpcs2UntprcAmt', fieldName: 'bthhsFrSpcs2UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 산업 및 공업용 - { name: 'INDUST_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'INDUST_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'INDUST_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'induLevyAmt', fieldName: 'induLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'induLevyQnt', fieldName: 'induLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'induUntprcAmt', fieldName: 'induUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 기타 - { name: 'ETC_LEVY_AMOUNT', fieldName: '(백만원)', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_LEVY_QY', fieldName: '(천톤)', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC_UNTPC', fieldName: '(원/톤)', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcLevyAmt', fieldName: 'etcLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcLevyQnt', fieldName: 'etcLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcLevyUntprcAmt', fieldName: 'etcLevyUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // hidden - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - - // 참조 필드 - { name: 'SEWER_RNTFEE', fieldName: 'SEWER_RNTFEE', header: { text: 'SEWER_RNTFEE' } }, - { name: 'B_LEVY_AMOUNT', fieldName: 'B_LEVY_AMOUNT', header: { text: 'B_LEVY_AMOUNT' } }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_total', @@ -74,12 +115,12 @@ export const SewerageUsageFeeInpColumn = { { name: 'total_amount', header: { text: '부과액' }, - items: ['LEVY_AMOUNT'] + items: ['levyAmt'] }, { name: 'total_qty', header: { text: '부과량' }, - items: ['LEVY_QY'] + items: ['levyQnt'] } ] }, @@ -90,17 +131,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'home_amount', header: { text: '부과액' }, - items: ['HOME_LEVY_AMOUNT'] + items: ['hshusLevyAmt'] }, { name: 'home_qty', header: { text: '부과량' }, - items: ['HOME_LEVY_QY'] + items: ['hshusLevyQnt'] }, { name: 'home_price', header: { text: '단가' }, - items: ['HOME_UNTPC'] + items: ['hshusUntprcAmt'] } ] }, @@ -111,17 +152,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'job_amount', header: { text: '부과액' }, - items: ['JOB_LEVY_AMOUNT'] + items: ['taskFrLevyAmt'] }, { name: 'job_qty', header: { text: '부과량' }, - items: ['JOB_LEVY_QY'] + items: ['taskFrLevyQnt'] }, { name: 'job_price', header: { text: '단가' }, - items: ['JOB_UNTPC'] + items: ['taskFrUntprcAmt'] } ] }, @@ -132,17 +173,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'bsn_amount', header: { text: '부과액' }, - items: ['BSN_LEVY_AMOUNT'] + items: ['fbuLevyAmt'] }, { name: 'bsn_qty', header: { text: '부과량' }, - items: ['BSN_LEVY_QY'] + items: ['fbuLevyQnt'] }, { name: 'bsn_price', header: { text: '단가' }, - items: ['BSN_UNTPC'] + items: ['fbuUntprcAmt'] } ] }, @@ -153,17 +194,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'baths1_amount', header: { text: '부과액' }, - items: ['BATHS1_LEVY_AMOUNT'] + items: ['bthhsFrSpcs1LevyAmt'] }, { name: 'baths1_qty', header: { text: '부과량' }, - items: ['BATHS1_LEVY_QY'] + items: ['bthhsFrSpcs1LevyQnt'] }, { name: 'baths1_price', header: { text: '단가' }, - items: ['BATHS1_UNTPC'] + items: ['bthhsFrSpcs1UntprcAmt'] } ] }, @@ -174,17 +215,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'pbbath_amount', header: { text: '부과액' }, - items: ['PBBATH_LEVY_AMOUNT'] + items: ['bthhsFrSpcs2LevyAmt'] }, { name: 'pbbath_qty', header: { text: '부과량' }, - items: ['PBBATH_LEVY_QY'] + items: ['bthhsFrSpcs2LevyQnt'] }, { name: 'pbbath_price', header: { text: '단가' }, - items: ['PBBATH_UNTPC'] + items: ['bthhsFrSpcs2UntprcAmt'] } ] }, @@ -195,17 +236,17 @@ export const SewerageUsageFeeInpColumn = { { name: 'indust_amount', header: { text: '부과액' }, - items: ['INDUST_LEVY_AMOUNT'] + items: ['induLevyAmt'] }, { name: 'indust_qty', header: { text: '부과량' }, - items: ['INDUST_LEVY_QY'] + items: ['induLevyQnt'] }, { name: 'indust_price', header: { text: '단가' }, - items: ['INDUST_UNTPC'] + items: ['induUntprcAmt'] } ] }, @@ -216,25 +257,27 @@ export const SewerageUsageFeeInpColumn = { { name: 'etc_amount', header: { text: '부과액' }, - items: ['ETC_LEVY_AMOUNT'] + items: ['etcLevyAmt'] }, { name: 'etc_qty', header: { text: '부과량' }, - items: ['ETC_LEVY_QY'] + items: ['etcLevyQnt'] }, { name: 'etc_price', header: { text: '단가' }, - items: ['ETC_UNTPC'] + items: ['etcLevyUntprcAmt'] } ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 3, fixedColumns: 1, - tabKey: 'sewerageUsageFeeInp', + tabKey: 'ST00090003', tabLabel: '하수도 사용료' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts b/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts index 68624e658..b98e431c7 100644 --- a/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts @@ -1,80 +1,105 @@ import { footerInt, footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +const COMPLAINT_TYPE_COUNT_FIELDS = [ + 'dschgWqCnt', + 'odorCnt', + 'ldsCnt', + 'plltnAcdntCnt', + 'swagePplCnt', + 'sewerCrgAmt', + 'etcCnt', +] as const; + // 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 1: 민원 export const ComplaintInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'ADST_DONG', fieldName: '행정구역명', header: { text: '행정구역명' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리인구(명) - { name: 'SWGE_PROC_POPLTN', fieldName: '하수처리인구(명)', header: { text: '하수처리인구(명)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'swtrmtPpltnCnt', fieldName: 'swtrmtPpltnCnt', header: { text: '하수처리인구(명)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, // 민원유형(건) - { name: 'CVPL_CM', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_BSML', fieldName: '악취', header: { text: '악취' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_DCWTR_QLTWTR', fieldName: '하수관로', header: { text: '하수관로' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_FIVW', fieldName: '배수설비', header: { text: '배수설비' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_POLLUTN_ACDNT', fieldName: '맨홀', header: { text: '맨홀' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_SSOS', fieldName: '정화조', header: { text: '정화조' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_CHRGE', fieldName: '하수도요금', header: { text: '하수도요금' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_UNKD', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'whol', + fieldName: 'whol', + header: { text: '총계' }, + editable: false, + styleName: 'rg-cell-total', + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { + fields: [...COMPLAINT_TYPE_COUNT_FIELDS], + type: 'sum' as const, + }, + }, + { name: 'dschgWqCnt', fieldName: 'dschgWqCnt', header: { text: '악취' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'odorCnt', fieldName: 'odorCnt', header: { text: '하수관로' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ldsCnt', fieldName: 'ldsCnt', header: { text: '배수설비' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'plltnAcdntCnt', fieldName: 'plltnAcdntCnt', header: { text: '맨홀' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'swagePplCnt', fieldName: 'swagePplCnt', header: { text: '정화조' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sewerCrgAmt', fieldName: 'sewerCrgAmt', header: { text: '하수도요금' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - // 기준인구당 민원건수(건/10,000명) - { name: 'STDR_POPLTN_PER_CVPL', fieldName: '기준인구당민원건수(건/10,000명)', header: { text: '기준인구당민원건수(건/10,000명)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + // 기준인구당 민원건수(건/10,000명) = 민원유형 합계 / 하수처리인구 × 10,000 + { + name: 'crtrPpltnCvlcptEvntcsCnt', + fieldName: 'crtrPpltnCvlcptEvntcsCnt', + header: { text: '기준인구당민원건수(건/10,000명)' }, + editable: false, + styleName: 'rg-cell-total', + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorFields: [...COMPLAINT_TYPE_COUNT_FIELDS], + denominatorField: 'swtrmtPpltnCnt', + type: 'sumPerTenThousand' as const, + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '작성자의견', header: { text: '작성자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_SWGE_PROC_POPLTN', fieldName: 'CHK_SWGE_PROC_POPLTN', header: { text: 'CHK_SWGE_PROC_POPLTN' }, visible: false }, - { name: 'CHK_CVPL_CM', fieldName: 'CHK_CVPL_CM', header: { text: 'CHK_CVPL_CM' }, visible: false }, - { name: 'CHK_CVPL_BSML', fieldName: 'CHK_CVPL_BSML', header: { text: 'CHK_CVPL_BSML' }, visible: false }, - { name: 'CHK_CVPL_DCWTR_QLTWTR', fieldName: 'CHK_CVPL_DCWTR_QLTWTR', header: { text: 'CHK_CVPL_DCWTR_QLTWTR' }, visible: false }, - { name: 'CHK_CVPL_FIVW', fieldName: 'CHK_CVPL_FIVW', header: { text: 'CHK_CVPL_FIVW' }, visible: false }, - { name: 'CHK_CVPL_POLLUTN_ACDNT', fieldName: 'CHK_CVPL_POLLUTN_ACDNT', header: { text: 'CHK_CVPL_POLLUTN_ACDNT' }, visible: false }, - { name: 'CHK_CVPL_SSOS', fieldName: 'CHK_CVPL_SSOS', header: { text: 'CHK_CVPL_SSOS' }, visible: false }, - { name: 'CHK_CVPL_CHRGE', fieldName: 'CHK_CVPL_CHRGE', header: { text: 'CHK_CVPL_CHRGE' }, visible: false }, - { name: 'CHK_CVPL_UNKD', fieldName: 'CHK_CVPL_UNKD', header: { text: 'CHK_CVPL_UNKD' }, visible: false }, - { name: 'CHK_STDR_POPLTN_PER_CVPL', fieldName: 'CHK_STDR_POPLTN_PER_CVPL', header: { text: 'CHK_STDR_POPLTN_PER_CVPL' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'SWGE_PROC_POPLTN', + 'swtrmtPpltnCnt', { name: 'group_complaint_type', header: { text: '민원유형(건)' }, items: [ - 'CVPL_CM', - 'CVPL_BSML', - 'CVPL_DCWTR_QLTWTR', - 'CVPL_FIVW', - 'CVPL_POLLUTN_ACDNT', - 'CVPL_SSOS', - 'CVPL_CHRGE', - 'CVPL_UNKD' + 'whol', + 'dschgWqCnt', + 'odorCnt', + 'ldsCnt', + 'plltnAcdntCnt', + 'swagePplCnt', + 'sewerCrgAmt', + 'etcCnt' ] }, - 'STDR_POPLTN_PER_CVPL', - 'RMK' + 'crtrPpltnCvlcptEvntcsCnt', + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 2, fixedColumns: 1, - tabKey: 'complaintInp', + tabKey: 'ST00130001', tabLabel: '민원' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts b/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts index 3f59e6b07..960b01d04 100644 --- a/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts @@ -1,89 +1,100 @@ import { footerInt, footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +const RESIDENT_FRIENDLY_FACILITY_AREA_FIELDS = [ + 'brkFcltArea', + 'childFrshpSpacArea', + 'bdySportsFcltArea', + 'sportsFcltArea', + 'pkltArea', + 'eduMultiFcltArea', + 'wtlndMakeArea', + 'etcCnt', +] as const; + // 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 2: 주민친화시설 export const ResidentFriendlyFactInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 시설 기본 정보 - { name: 'BSC_FCT_NM', fieldName: '처리장명', header: { text: '처리장명' }, editable: false }, - { name: 'FCT_CPC', fieldName: '시설용량(m³/일)', header: { text: '시설용량(m³/일)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '시설용량(m³/일)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, // 처리장면적(㎡) - { name: 'PRCPLT_AR', fieldName: '처리장면적(㎡)', header: { text: '처리장면적(㎡)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntArea', fieldName: 'tmplntArea', header: { text: '처리장면적(㎡)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 주민친화시설 면적(㎡) - { name: 'TOTAL_AR', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'RESTRM_AR', fieldName: '휴게시설', header: { text: '휴게시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CHILDPLAY_AR', fieldName: '어린이놀이공간', header: { text: '어린이놀이공간' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ALSFC_AR', fieldName: '체력단련시설', header: { text: '체력단련시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'PLGRND_AR', fieldName: '운동시설', header: { text: '운동시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'PRKPLCE_AR', fieldName: '주차장', header: { text: '주차장' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CTLSTT_AR', fieldName: '교육 및 문화시설', header: { text: '교육 및 문화시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'NRPRK_AR', fieldName: '습지조성(생태공원)', header: { text: '습지조성(생태공원)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ETC_AR', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'whol', + fieldName: 'whol', + header: { text: '총계' }, + editable: false, + styleName: 'rg-cell-total', + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { + fields: [...RESIDENT_FRIENDLY_FACILITY_AREA_FIELDS], + type: 'sum' as const, + }, + }, + { name: 'brkFcltArea', fieldName: 'brkFcltArea', header: { text: '휴게시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'childFrshpSpacArea', fieldName: 'childFrshpSpacArea', header: { text: '어린이놀이공간' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'bdySportsFcltArea', fieldName: 'bdySportsFcltArea', header: { text: '체력단련시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sportsFcltArea', fieldName: 'sportsFcltArea', header: { text: '운동시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'pkltArea', fieldName: 'pkltArea', header: { text: '주차장' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'eduMultiFcltArea', fieldName: 'eduMultiFcltArea', header: { text: '교육 및 문화시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'wtlndMakeArea', fieldName: 'wtlndMakeArea', header: { text: '습지조성(생태공원)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 시설 정보 - { name: 'STPN_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, - { name: 'ADST_CD', fieldName: '행정동코드', header: { text: '행정동코드' }, visible: false }, - { name: 'BSC_FCT_CD', fieldName: '기초시설코드', header: { text: '기초시설코드' }, visible: false }, - - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '작성자의견', header: { text: '작성자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: 활용율 필드 - { name: 'PRCUSE_RT', fieldName: '주민친화시설활용율(%)', header: { text: '주민친화시설활용율(%)' }, visible: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_PRCPLT_AR', fieldName: 'CHK_PRCPLT_AR', header: { text: 'CHK_PRCPLT_AR' }, visible: false }, - { name: 'CHK_TOTAL_AR', fieldName: 'CHK_TOTAL_AR', header: { text: 'CHK_TOTAL_AR' }, visible: false }, - { name: 'CHK_RESTRM_AR', fieldName: 'CHK_RESTRM_AR', header: { text: 'CHK_RESTRM_AR' }, visible: false }, - { name: 'CHK_CHILDPLAY_AR', fieldName: 'CHK_CHILDPLAY_AR', header: { text: 'CHK_CHILDPLAY_AR' }, visible: false }, - { name: 'CHK_ALSFC_AR', fieldName: 'CHK_ALSFC_AR', header: { text: 'CHK_ALSFC_AR' }, visible: false }, - { name: 'CHK_PLGRND_AR', fieldName: 'CHK_PLGRND_AR', header: { text: 'CHK_PLGRND_AR' }, visible: false }, - { name: 'CHK_PRKPLCE_AR', fieldName: 'CHK_PRKPLCE_AR', header: { text: 'CHK_PRKPLCE_AR' }, visible: false }, - { name: 'CHK_CTLSTT_AR', fieldName: 'CHK_CTLSTT_AR', header: { text: 'CHK_CTLSTT_AR' }, visible: false }, - { name: 'CHK_NRPRK_AR', fieldName: 'CHK_NRPRK_AR', header: { text: 'CHK_NRPRK_AR' }, visible: false }, - { name: 'CHK_ETC_AR', fieldName: 'CHK_ETC_AR', header: { text: 'CHK_ETC_AR' }, visible: false }, - { name: 'CHK_PRCUSE_RT', fieldName: 'CHK_PRCUSE_RT', header: { text: 'CHK_PRCUSE_RT' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'BSC_FCT_NM', - 'FCT_CPC', - 'PRCPLT_AR', + 'swtrmtFcltNm', + 'fcltCpct', + 'tmplntArea', { name: 'group_resident_friendly_facility', header: { text: '주민친화시설 면적(㎡)' }, items: [ - 'TOTAL_AR', - 'RESTRM_AR', - 'CHILDPLAY_AR', - 'ALSFC_AR', - 'PLGRND_AR', - 'PRKPLCE_AR', - 'CTLSTT_AR', - 'NRPRK_AR', - 'ETC_AR' + 'whol', + 'brkFcltArea', + 'childFrshpSpacArea', + 'bdySportsFcltArea', + 'sportsFcltArea', + 'pkltArea', + 'eduMultiFcltArea', + 'wtlndMakeArea', + 'etcCnt' ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd', ], rowStartNum: 2, fixedColumns: 3, - tabKey: 'residentFriendlyFactInp', + tabKey: 'ST00130002', tabLabel: '주민친화시설' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts index 0e6da19f4..709cc473d 100644 --- a/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts @@ -1,176 +1,323 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 법적의무대상(`lglYn`) */ +export const PUBLIC_SEWAGE_TREAT_WATER_REUSE_LGL_YN_VALUES = ["대상", "미대상"] as const; + +/** 설치유무(`instlYn`) */ +export const PUBLIC_SEWAGE_TREAT_WATER_REUSE_INSTL_YN_VALUES = ["유", "무"] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + +/** 하수처리수 재이용현황(천톤/년) — 장내·장외 세부 항목 합산 필드 */ +const PUBLIC_SEWAGE_REUSE_USE_QNT_SUM_FIELDS = [ + 'ithlClnQty', + 'ithlFrzgQty', + 'ithlLdhcUswtrQnt', + 'ithlDltnUswtrQnt', + 'ithlRuswtrQnt', + 'ithlEtcQnt', + 'ithlUnsdQnt', + 'otcntrClnRstrmQnt', + 'otcntrWshwtrQnt', + 'otcntrLdhcUswtrQnt', + 'otcntrHdrplUswtrQnt', + 'otcntrRvrKeepQnt', + 'agrcltUswtrPstnQnt', + 'ugwtrChargQnt', + 'mdsrUswtrQnt', +] as const; + // 하수도 통계 관리 > 10. 물 재이용 > tab 1: 공공하수처리수 재이용 export const PublicSewageTreatWaterReuseInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리장 - { name: 'BSC_FCT_NM', fieldName: '시설명', header: { text: '시설명' }, editable: false }, - { name: 'EQP_PRMS_DT', fieldName: '인가일(년/월/일)', header: { text: '인가일(년/월/일)' }, editable: false }, - { name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량(㎥/일)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'TPUT_TOTAL', fieldName: '연간하수처리량(천톤/년)', header: { text: '연간하수처리량(천톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RSWTR_LAW_YN', fieldName: '법적의무대상(대상/미대상)', header: { text: '법적의무대상(대상/미대상)' } }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명' }, editable: false, styleName: 'rg-cell-readonly' }, + { + name: 'sncnYmd', + fieldName: 'sncnYmd', + header: { text: '인가일(년/월/일)' }, + datetimeFormat: 'yyyy-MM-dd', + editable: false, + styleName: 'rg-cell-readonly', + }, + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(㎥/일)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'wholPrcsQnt', fieldName: 'wholPrcsQnt', header: { text: '연간하수처리량(천톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + dropdownColumn( + { + name: 'lglYn', + fieldName: 'lglYn', + header: { text: '법적의무대상(대상/미대상)' }, + }, + PUBLIC_SEWAGE_TREAT_WATER_REUSE_LGL_YN_VALUES, + ), // 재이용시설 > 설치유무 - { name: 'INST_YN', fieldName: '설치유무(유/무)', header: { text: '설치유무(유/무)' } }, + dropdownColumn( + { + name: 'instlYn', + fieldName: 'instlYn', + header: { text: '설치유무(유/무)' }, + }, + PUBLIC_SEWAGE_TREAT_WATER_REUSE_INSTL_YN_VALUES, + ), // 재이용시설 > 재이용시설 설치시 - { name: 'RSWTR_RN_DT', fieldName: '가동개시일(yyyy-mm-dd)', header: { text: '가동개시일(yyyy-mm-dd)' } }, - { name: 'RSWTR_PRMS_DT', fieldName: '설치승인일(yyyy-mm-dd)', header: { text: '설치승인일(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: 'instlAprvYmd', + fieldName: 'instlAprvYmd', + header: { text: '설치승인일(yyyy-mm-dd)' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 재이용시설 > 재이용시설 설치시 > 재처리시설 - { name: 'RSWTR_CPC', fieldName: '용량(톤/일)', header: { text: '용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RSWTR_TRT_NM', fieldName: '공법/기자재', header: { text: '공법/기자재' } }, + { name: 'rprcsFcltCpct', fieldName: 'rprcsFcltCpct', header: { text: '용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'mtcnsNm', fieldName: 'mtcnsNm', header: { text: '공법/기자재' } }, // 재이용시설 > 재이용시설 설치시 - { name: 'RSWTR_PIPE', fieldName: '공급관로길이(km)', header: { text: '공급관로길이(km)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RSWTR_PUMP', fieldName: '공급펌프(톤/일)', header: { text: '공급펌프(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtPplLen', fieldName: 'ruswtPplLen', header: { text: '공급관로길이(km)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtPumpLen', fieldName: 'ruswtPumpLen', header: { text: '공급펌프(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 하수처리수 재이용현황 > 총계 - { name: 'USE_AMNT_TOTAL', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholUseQnt', + fieldName: 'wholUseQnt', + header: { text: '총계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [...PUBLIC_SEWAGE_REUSE_USE_QNT_SUM_FIELDS], + type: 'sum' as const, + }, + }, // 하수처리수 재이용현황 > 장내용수 - { name: 'USE_AMNT_D01', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S01', fieldName: '세척수', header: { text: '세척수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S02', fieldName: '냉각수', header: { text: '냉각수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S03', fieldName: '청소수', header: { text: '청소수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S04', fieldName: '조경용수', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // (레거시) 식수대 - { name: 'USE_AMNT_S05', fieldName: '희석용수', header: { text: '희석용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S06', fieldName: '장내중수도', header: { text: '장내중수도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S07', fieldName: '장내기타용수', header: { text: '장내기타용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'ithlWshwtrQnt', + fieldName: 'ithlWshwtrQnt', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'ithlClnQty', + 'ithlFrzgQty', + 'ithlLdhcUswtrQnt', + 'ithlDltnUswtrQnt', + 'ithlRuswtrQnt', + 'ithlEtcQnt', + 'ithlUnsdQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'ithlClnQty', fieldName: 'ithlClnQty', header: { text: '세척수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlFrzgQty', fieldName: 'ithlFrzgQty', header: { text: '냉각수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlLdhcUswtrQnt', fieldName: 'ithlLdhcUswtrQnt', header: { text: '청소수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlDltnUswtrQnt', fieldName: 'ithlDltnUswtrQnt', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlRuswtrQnt', fieldName: 'ithlRuswtrQnt', header: { text: '희석용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlEtcQnt', fieldName: 'ithlEtcQnt', header: { text: '장내중수도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlUnsdQnt', fieldName: 'ithlUnsdQnt', header: { text: '장내기타용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, // 하수처리수 재이용현황 > 장외용수 - { name: 'USE_AMNT_D02', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S11', fieldName: '청소·화장실용수', header: { text: '청소·화장실용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S12', fieldName: '세척·살수용수', header: { text: '세척·살수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S13', fieldName: '조경용수', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S14', fieldName: '친수용수', header: { text: '친수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S15', fieldName: '하천등유지용수', header: { text: '하천등유지용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S16', fieldName: '농업용수', header: { text: '농업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S17', fieldName: '지하수충전', header: { text: '지하수충전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S18', fieldName: '공업용수', header: { text: '공업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'otcntrUswtrWhol', + fieldName: 'otcntrUswtrWhol', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'otcntrClnRstrmQnt', + 'otcntrWshwtrQnt', + 'otcntrLdhcUswtrQnt', + 'otcntrHdrplUswtrQnt', + 'otcntrRvrKeepQnt', + 'agrcltUswtrPstnQnt', + 'ugwtrChargQnt', + 'mdsrUswtrQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'otcntrClnRstrmQnt', fieldName: 'otcntrClnRstrmQnt', header: { text: '청소·화장실용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrWshwtrQnt', fieldName: 'otcntrWshwtrQnt', header: { text: '세척·살수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrLdhcUswtrQnt', fieldName: 'otcntrLdhcUswtrQnt', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrHdrplUswtrQnt', fieldName: 'otcntrHdrplUswtrQnt', header: { text: '친수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrRvrKeepQnt', fieldName: 'otcntrRvrKeepQnt', header: { text: '하천등유지용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'agrcltUswtrPstnQnt', fieldName: 'agrcltUswtrPstnQnt', header: { text: '농업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ugwtrChargQnt', fieldName: 'ugwtrChargQnt', header: { text: '지하수충전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'mdsrUswtrQnt', fieldName: 'mdsrUswtrQnt', header: { text: '공업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, // 재이용률 - { name: 'REUSE_RT', fieldName: '재이용률(%)', header: { text: '재이용률(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'rtlztnRt', + fieldName: 'rtlztnRt', + header: { text: '재이용률(%)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + numeratorFields: [...PUBLIC_SEWAGE_REUSE_USE_QNT_SUM_FIELDS], + denominatorFields: ['wholPrcsQnt'], + type: 'pblswrgPopltnSuprtRate' as const, + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_RSWTR_LAW_YN', fieldName: 'CHK_RSWTR_LAW_YN', header: { text: 'CHK_RSWTR_LAW_YN' }, visible: false }, - { name: 'CHK_INST_YN', fieldName: 'CHK_INST_YN', header: { text: 'CHK_INST_YN' }, visible: false }, - { name: 'CHK_RSWTR_RN_DT', fieldName: 'CHK_RSWTR_RN_DT', header: { text: 'CHK_RSWTR_RN_DT' }, visible: false }, - { name: 'CHK_RSWTR_PRMS_DT', fieldName: 'CHK_RSWTR_PRMS_DT', header: { text: 'CHK_RSWTR_PRMS_DT' }, visible: false }, - { name: 'CHK_RSWTR_CPC', fieldName: 'CHK_RSWTR_CPC', header: { text: 'CHK_RSWTR_CPC' }, visible: false }, - { name: 'CHK_RSWTR_TRT_NM', fieldName: 'CHK_RSWTR_TRT_NM', header: { text: 'CHK_RSWTR_TRT_NM' }, visible: false }, - { name: 'CHK_RSWTR_PIPE', fieldName: 'CHK_RSWTR_PIPE', header: { text: 'CHK_RSWTR_PIPE' }, visible: false }, - { name: 'CHK_RSWTR_PUMP', fieldName: 'CHK_RSWTR_PUMP', header: { text: 'CHK_RSWTR_PUMP' }, visible: false }, - { name: 'CHK_TPUT_TOTAL', fieldName: 'CHK_TPUT_TOTAL', header: { text: 'CHK_TPUT_TOTAL' }, visible: false }, - { name: 'CHK_USE_AMNT_S01', fieldName: 'CHK_USE_AMNT_S01', header: { text: 'CHK_USE_AMNT_S01' }, visible: false }, - { name: 'CHK_USE_AMNT_S02', fieldName: 'CHK_USE_AMNT_S02', header: { text: 'CHK_USE_AMNT_S02' }, visible: false }, - { name: 'CHK_USE_AMNT_S03', fieldName: 'CHK_USE_AMNT_S03', header: { text: 'CHK_USE_AMNT_S03' }, visible: false }, - { name: 'CHK_USE_AMNT_S04', fieldName: 'CHK_USE_AMNT_S04', header: { text: 'CHK_USE_AMNT_S04' }, visible: false }, - { name: 'CHK_USE_AMNT_S05', fieldName: 'CHK_USE_AMNT_S05', header: { text: 'CHK_USE_AMNT_S05' }, visible: false }, - { name: 'CHK_USE_AMNT_S06', fieldName: 'CHK_USE_AMNT_S06', header: { text: 'CHK_USE_AMNT_S06' }, visible: false }, - { name: 'CHK_USE_AMNT_S07', fieldName: 'CHK_USE_AMNT_S07', header: { text: 'CHK_USE_AMNT_S07' }, visible: false }, - { name: 'CHK_USE_AMNT_S11', fieldName: 'CHK_USE_AMNT_S11', header: { text: 'CHK_USE_AMNT_S11' }, visible: false }, - { name: 'CHK_USE_AMNT_S12', fieldName: 'CHK_USE_AMNT_S12', header: { text: 'CHK_USE_AMNT_S12' }, visible: false }, - { name: 'CHK_USE_AMNT_S13', fieldName: 'CHK_USE_AMNT_S13', header: { text: 'CHK_USE_AMNT_S13' }, visible: false }, - { name: 'CHK_USE_AMNT_S14', fieldName: 'CHK_USE_AMNT_S14', header: { text: 'CHK_USE_AMNT_S14' }, visible: false }, - { name: 'CHK_USE_AMNT_S15', fieldName: 'CHK_USE_AMNT_S15', header: { text: 'CHK_USE_AMNT_S15' }, visible: false }, - { name: 'CHK_USE_AMNT_S16', fieldName: 'CHK_USE_AMNT_S16', header: { text: 'CHK_USE_AMNT_S16' }, visible: false }, - { name: 'CHK_USE_AMNT_S17', fieldName: 'CHK_USE_AMNT_S17', header: { text: 'CHK_USE_AMNT_S17' }, visible: false }, - { name: 'CHK_USE_AMNT_S18', fieldName: 'CHK_USE_AMNT_S18', header: { text: 'CHK_USE_AMNT_S18' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_facility', header: { text: '하수처리장' }, items: [ - 'BSC_FCT_NM', - 'EQP_PRMS_DT', - 'FCT_CPC', - 'RSWTR_LAW_YN' + 'swtrmtFcltNm', + 'sncnYmd', + 'prcsFcltCpct', + 'lglYn' ] }, { name: 'group_reuse_facility', header: { text: '재이용시설' }, items: [ - 'INST_YN', + 'instlYn', { name: 'group_reuse_install', header: { text: '재이용시설 설치시' }, items: [ - 'RSWTR_RN_DT', - 'RSWTR_PRMS_DT', + 'oprtngStrtYmd', + 'instlAprvYmd', { name: 'group_treatment_facility', header: { text: '처리시설' }, items: [ - 'RSWTR_CPC', - 'RSWTR_TRT_NM' + 'rprcsFcltCpct', + 'mtcnsNm' ] }, - 'RSWTR_PIPE', - 'RSWTR_PUMP' + 'ruswtPplLen', + 'ruswtPumpLen' ] } ] }, - 'TPUT_TOTAL', + 'wholPrcsQnt', { name: 'group_reuse_current', header: { text: '하수처리수 재이용현황(천톤/년)' }, items: [ - 'USE_AMNT_TOTAL', + 'wholUseQnt', { name: 'group_indoor_use', header: { text: '장 내 용 수' }, items: [ - 'USE_AMNT_D01', - 'USE_AMNT_S01', - 'USE_AMNT_S02', - 'USE_AMNT_S03', - 'USE_AMNT_S04', - 'USE_AMNT_S05', - 'USE_AMNT_S06', - 'USE_AMNT_S07' + 'ithlWshwtrQnt', + 'ithlClnQty', + 'ithlFrzgQty', + 'ithlLdhcUswtrQnt', + 'ithlDltnUswtrQnt', + 'ithlRuswtrQnt', + 'ithlEtcQnt', + 'ithlUnsdQnt' ] }, { name: 'group_outdoor_use', header: { text: '장 외 용 수' }, items: [ - 'USE_AMNT_D02', - 'USE_AMNT_S11', - 'USE_AMNT_S12', - 'USE_AMNT_S13', - 'USE_AMNT_S14', - 'USE_AMNT_S15', - 'USE_AMNT_S16', - 'USE_AMNT_S17', - 'USE_AMNT_S18' + 'otcntrUswtrWhol', + 'otcntrClnRstrmQnt', + 'otcntrWshwtrQnt', + 'otcntrLdhcUswtrQnt', + 'otcntrHdrplUswtrQnt', + 'otcntrRvrKeepQnt', + 'agrcltUswtrPstnQnt', + 'ugwtrChargQnt', + 'mdsrUswtrQnt' ] } ] }, - 'REUSE_RT', - 'RMK' + 'rtlztnRt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd' ], rowStartNum: 4, fixedColumns: 0, - tabKey: 'publicSewageTreatWaterReuseInp', + tabKey: 'ST00100001', tabLabel: '공공하수처리수 재이용' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts index 10dc53ccd..f5189c746 100644 --- a/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts @@ -1,120 +1,169 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 건축물용도(`bdstTypCn`) */ +export const RAINFALL_UTIL_FACT_BDST_TYP_VALUES = [ + "종합운동장", + "실내체육관", + "공공업무시설", + "공공기관청사", + "공동주택", + "학교", + "대규모점포", + "골프장", + "기타", +] as const; + +/** 법적의무대상여부(`lglTrgtYn`) */ +export const RAINFALL_UTIL_FACT_LGL_TRGT_YN_VALUES = ["대상", "미대상"] as const; + +/** 이용용도(`exlnUtlzUsgExpln`) */ +export const RAINFALL_UTIL_FACT_EXLN_UTLZ_USG_VALUES = [ + "청소화장실용수", + "세척살수용수", + "조경용수", + "친수용수", + "농업용수", + "공업용수", + "기타", +] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 10. 물 재이용 > tab 3: 빗물이용시설현황 export const RainFallUtilFactStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'ST_RWR_REUSE_FCT_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 시설명 - { name: 'RWR_REUSE_FCT_NM', fieldName: '시설명', header: { text: '시설명' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } }, // 건축물용도 - { name: 'BILD_TP', fieldName: '건축물용도', header: { text: '건축물용도' } }, + dropdownColumn( + { + name: 'bdstTypCn', + fieldName: 'bdstTypCn', + header: { text: '건축물용도' }, + }, + RAINFALL_UTIL_FACT_BDST_TYP_VALUES, + ), // 위치(주소) - { name: 'ADDR', fieldName: '위치(주소)', header: { text: '위치(주소)' } }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치(주소)' } }, // 법적의무대상여부(대상/미대상) - { name: 'LAW_YN', fieldName: '법적의무대상여부(대상/미대상)', header: { text: '법적의무대상여부(대상/미대상)' } }, + dropdownColumn( + { + name: 'lglTrgtYn', + fieldName: 'lglTrgtYn', + header: { text: '법적의무대상여부(대상/미대상)' }, + }, + RAINFALL_UTIL_FACT_LGL_TRGT_YN_VALUES, + ), // 설치완료일 yyyy-mm-dd - { name: 'RN_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일 yyyy-mm-dd' } }, + { + name: 'cmptYmd', + fieldName: 'cmptYmd', + header: { text: '설치완료일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 설치비(백만원) - { name: 'INSTL_CT', fieldName: '설치비(백만원)', header: { text: '설치비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'instlAmt', fieldName: 'instlAmt', header: { text: '설치비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 집수면 종류 - { name: 'CCM', fieldName: '집수면 종류', header: { text: '집수면 종류' } }, + { name: 'rcptMynCn', fieldName: 'rcptMynCn', header: { text: '집수면 종류' } }, // 여과등처리시설유무(유:처리시설종류,무:미설치) - { name: 'TRTFC_ENNC', fieldName: '여과등처리시설유무(유:처리시설종류,무:미설치)', header: { text: '여과등처리시설유무(유:처리시설종류,무:미설치)' } }, + { name: 'filtrPrcsFcltYnExpln', fieldName: 'filtrPrcsFcltYnExpln', header: { text: '여과등처리시설유무(유:처리시설종류,무:미설치)' } }, // 저류조용량(m³) - { name: 'SGBS_CPCTY', fieldName: '저류조용량(m³)', header: { text: '저류조용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'impdCpct', fieldName: 'impdCpct', header: { text: '저류조용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 연간이용량(m³/년) - { name: 'FYER_USEQTY', fieldName: '연간이용량(m³/년)', header: { text: '연간이용량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlUseQnt', fieldName: 'anlUseQnt', header: { text: '연간이용량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 이용용도 - { name: 'RWR_PRCUSE_PRPPOS', fieldName: '이용용도', header: { text: '이용용도' } }, + dropdownColumn( + { + name: 'exlnUtlzUsgExpln', + fieldName: 'exlnUtlzUsgExpln', + header: { text: '이용용도' }, + }, + RAINFALL_UTIL_FACT_EXLN_UTLZ_USG_VALUES, + ), // 연간운영비(백만원/년) - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비(백만원/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlOperAmt', fieldName: 'anlOperAmt', header: { text: '연간운영비(백만원/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '작성자의견', header: { text: '작성자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: 용도별 이용량 (화면에 표시 안 함) - { name: 'FYER_USE_01', fieldName: '빗물사용 1', header: { text: '빗물사용 1' }, visible: false }, - { name: 'FYER_USE_02', fieldName: '빗물사용 2', header: { text: '빗물사용 2' }, visible: false }, - { name: 'FYER_USE_03', fieldName: '빗물사용 3', header: { text: '빗물사용 3' }, visible: false }, - { name: 'FYER_USE_04', fieldName: '빗물사용 4', header: { text: '빗물사용 4' }, visible: false }, - { name: 'FYER_USE_05', fieldName: '빗물사용 5', header: { text: '빗물사용 5' }, visible: false }, - { name: 'FYER_USE_06', fieldName: '빗물사용 6', header: { text: '빗물사용 6' }, visible: false }, - { name: 'FYER_USE_07', fieldName: '빗물사용 7', header: { text: '빗물사용 7' }, visible: false }, - - // Hidden: 인가일 (화면에 표시 안 함) - { name: 'PERMIT_DT', fieldName: '인가일', header: { text: '인가일' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_RWR_REUSE_FCT_NM', fieldName: 'CHK_RWR_REUSE_FCT_NM', header: { text: 'CHK_RWR_REUSE_FCT_NM' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_RN_DT', fieldName: 'CHK_RN_DT', header: { text: 'CHK_RN_DT' }, visible: false }, - { name: 'CHK_INSTL_CT', fieldName: 'CHK_INSTL_CT', header: { text: 'CHK_INSTL_CT' }, visible: false }, - { name: 'CHK_CCM', fieldName: 'CHK_CCM', header: { text: 'CHK_CCM' }, visible: false }, - { name: 'CHK_CCM_AR', fieldName: 'CHK_CCM_AR', header: { text: 'CHK_CCM_AR' }, visible: false }, - { name: 'CHK_TRTFC_ENNC', fieldName: 'CHK_TRTFC_ENNC', header: { text: 'CHK_TRTFC_ENNC' }, visible: false }, - { name: 'CHK_SGBS_CPCTY', fieldName: 'CHK_SGBS_CPCTY', header: { text: 'CHK_SGBS_CPCTY' }, visible: false }, - { name: 'CHK_FYER_USEQTY', fieldName: 'CHK_FYER_USEQTY', header: { text: 'CHK_FYER_USEQTY' }, visible: false }, - { name: 'CHK_RWR_PRCUSE_PRPPOS', fieldName: 'CHK_RWR_PRCUSE_PRPPOS', header: { text: 'CHK_RWR_PRCUSE_PRPPOS' }, visible: false }, - { name: 'CHK_FYER_OPER_CT', fieldName: 'CHK_FYER_OPER_CT', header: { text: 'CHK_FYER_OPER_CT' }, visible: false }, - { name: 'CHK_LAW_YN', fieldName: 'CHK_LAW_YN', header: { text: 'CHK_LAW_YN' }, visible: false }, - { name: 'CHK_BILD_TP', fieldName: 'CHK_BILD_TP', header: { text: 'CHK_BILD_TP' }, visible: false }, - { name: 'CHK_PERMIT_DT', fieldName: 'CHK_PERMIT_DT', header: { text: 'CHK_PERMIT_DT' }, visible: false }, - { name: 'CHK_FYER_USE_01', fieldName: 'CHK_FYER_USE_01', header: { text: 'CHK_FYER_USE_01' }, visible: false }, - { name: 'CHK_FYER_USE_02', fieldName: 'CHK_FYER_USE_02', header: { text: 'CHK_FYER_USE_02' }, visible: false }, - { name: 'CHK_FYER_USE_03', fieldName: 'CHK_FYER_USE_03', header: { text: 'CHK_FYER_USE_03' }, visible: false }, - { name: 'CHK_FYER_USE_04', fieldName: 'CHK_FYER_USE_04', header: { text: 'CHK_FYER_USE_04' }, visible: false }, - { name: 'CHK_FYER_USE_05', fieldName: 'CHK_FYER_USE_05', header: { text: 'CHK_FYER_USE_05' }, visible: false }, - { name: 'CHK_FYER_USE_06', fieldName: 'CHK_FYER_USE_06', header: { text: 'CHK_FYER_USE_06' }, visible: false }, - { name: 'CHK_FYER_USE_07', fieldName: 'CHK_FYER_USE_07', header: { text: 'CHK_FYER_USE_07' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'RWR_REUSE_FCT_NM', - 'BILD_TP', - 'ADDR', - 'LAW_YN', - 'RN_DT', - 'INSTL_CT', - 'CCM', - 'TRTFC_ENNC', - 'SGBS_CPCTY', - 'FYER_USEQTY', - 'RWR_PRCUSE_PRPPOS', - 'FYER_OPER_CT', - 'RMK' + 'fcltNm', + 'bdstTypCn', + 'fcltPstnCn', + 'lglTrgtYn', + 'cmptYmd', + 'instlAmt', + 'rcptMynCn', + 'filtrPrcsFcltYnExpln', + 'impdCpct', + 'anlUseQnt', + 'exlnUtlzUsgExpln', + 'anlOperAmt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'rainFallUtilFactStatusInp', + tabKey: 'ST00100003', tabLabel: '빗물이용시설현황' -}; \ No newline at end of file +}; diff --git a/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts index 5ec78024c..4930c51d2 100644 --- a/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts @@ -1,109 +1,194 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 시설물용도/개발사업종류(`bldgSeCn`) */ +export const RECYCLED_WATER_BLDG_SE_VALUES = [ + "숙박업", + "목욕장업", + "공장", + "발전시설", + "대규모점포", + "물류시설", + "운수시설", + "업무시설", + "교정시설", + "방송국", + "전신전화국", + "기타제반법령에의한시설물", + "관광단지의개발사업", + "도시개발사업", + "산업단지개발사업", + "택지개발사업", + "기타", +] as const; + +/** 법적의무대상여부(`lglTrgtYn`) */ +export const RECYCLED_WATER_LGL_TRGT_YN_VALUES = ["대상", "미대상"] as const; + +/** 중수도용도(`mnUsgCn`) */ +export const RECYCLED_WATER_MN_USG_VALUES = [ + "청소화장실용수", + "세척살수용수", + "조경용수", + "친수용수", + "하천등유지용수", + "공업용수", +] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 10. 물 재이용 > tab 2: 중수도 현황 export const RecycledWaterStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'RUSWTR_FCT_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 건축물명/개발사업명 - { name: 'BILD_NM', fieldName: '건축물명/개발사업명', header: { text: '건축물명/개발사업명' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '건축물명/개발사업명' } }, // 시설물용도/개발사업종류 - { name: 'BILD_TP', fieldName: '시설물용도/개발사업종류', header: { text: '시설물용도/개발사업종류' } }, + dropdownColumn( + { + name: 'bldgSeCn', + fieldName: 'bldgSeCn', + header: { text: '시설물용도/개발사업종류' }, + }, + RECYCLED_WATER_BLDG_SE_VALUES, + ), // 주소 - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '주소' } }, // 법적의무대상여부(대상/미대상) - { name: 'LAW_YN', fieldName: '법적의무대상여부(대상/미대상)', header: { text: '법적의무대상여부(대상/미대상)' } }, + dropdownColumn( + { + name: 'lglTrgtYn', + fieldName: 'lglTrgtYn', + header: { text: '법적의무대상여부(대상/미대상)' }, + }, + RECYCLED_WATER_LGL_TRGT_YN_VALUES, + ), // 설치완료일(년-월-일) - { name: 'COMP_DT', fieldName: '설치완료일(년-월-일)', header: { text: '설치완료일(년-월-일)' } }, + { + name: 'cmptnStrtYmd', + fieldName: 'cmptnStrtYmd', + header: { text: '설치완료일(년-월-일)' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 가동개시일(년-월-일) - { name: 'RN_DT', fieldName: '가동개시일(년-월-일)', header: { text: '가동개시일(년-월-일)' } }, + { + name: 'oprtngStrtYmd', + fieldName: 'oprtngStrtYmd', + header: { text: '가동개시일(년-월-일)' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 시설현황 - { name: 'TOTAR', fieldName: '건축연면적(m²)', header: { text: '건축연면적(m²)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PROC_CPCTY', fieldName: '처리용량(m³/일)', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'archWholArea', fieldName: 'archWholArea', header: { text: '건축연면적(m²)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsCpct', fieldName: 'prcsCpct', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 물 사용량(m³/일) - { name: 'WATER_USE_CPCTY', fieldName: '물 사용량(m³/일)', header: { text: '물 사용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'wtrUseQnt', fieldName: 'wtrUseQnt', header: { text: '물 사용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 중수도이용량(m³/일) - { name: 'RUSWTR_USE_CPCTY', fieldName: '중수도이용량(m³/일)', header: { text: '중수도이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtrUtztnQnt', fieldName: 'ruswtrUtztnQnt', header: { text: '중수도이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 대체수이용량(m³/일) - { name: 'SUB_USE_CPCTY', fieldName: '대체수이용량(m³/일)', header: { text: '대체수이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sbstCntUtztnQnt', fieldName: 'sbstCntUtztnQnt', header: { text: '대체수이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 중수도용도 - { name: 'RUSWTR_PRPOS', fieldName: '중수도용도', header: { text: '중수도용도' } }, + dropdownColumn( + { + name: 'mnUsgCn', + fieldName: 'mnUsgCn', + header: { text: '중수도용도' }, + }, + RECYCLED_WATER_MN_USG_VALUES, + ), // 연간운영비(백만원/년) - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비(백만원/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtrOperAmt', fieldName: 'ruswtrOperAmt', header: { text: '연간운영비(백만원/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 사용자의견, 검토자의견 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_BILD_NM', fieldName: 'CHK_BILD_NM', header: { text: 'CHK_BILD_NM' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_TOTAR', fieldName: 'CHK_TOTAR', header: { text: 'CHK_TOTAR' }, visible: false }, - { name: 'CHK_PROC_CPCTY', fieldName: 'CHK_PROC_CPCTY', header: { text: 'CHK_PROC_CPCTY' }, visible: false }, - { name: 'CHK_RUSWTR_USE_CPCTY', fieldName: 'CHK_RUSWTR_USE_CPCTY', header: { text: 'CHK_RUSWTR_USE_CPCTY' }, visible: false }, - { name: 'CHK_COMP_DT', fieldName: 'CHK_COMP_DT', header: { text: 'CHK_COMP_DT' }, visible: false }, - { name: 'CHK_RN_DT', fieldName: 'CHK_RN_DT', header: { text: 'CHK_RN_DT' }, visible: false }, - { name: 'CHK_RUSWTR_PRPOS', fieldName: 'CHK_RUSWTR_PRPOS', header: { text: 'CHK_RUSWTR_PRPOS' }, visible: false }, - { name: 'CHK_LAW_YN', fieldName: 'CHK_LAW_YN', header: { text: 'CHK_LAW_YN' }, visible: false }, - { name: 'CHK_BILD_TP', fieldName: 'CHK_BILD_TP', header: { text: 'CHK_BILD_TP' }, visible: false }, - { name: 'CHK_PERMIT_DT', fieldName: 'CHK_PERMIT_DT', header: { text: 'CHK_PERMIT_DT' }, visible: false }, - { name: 'CHK_FYER_OPER_CT', fieldName: 'CHK_FYER_OPER_CT', header: { text: 'CHK_FYER_OPER_CT' }, visible: false }, - { name: 'CHK_WATER_USE_CPCTY', fieldName: 'CHK_WATER_USE_CPCTY', header: { text: 'CHK_WATER_USE_CPCTY' }, visible: false }, - { name: 'CHK_SUB_USE_CPCTY', fieldName: 'CHK_SUB_USE_CPCTY', header: { text: 'CHK_SUB_USE_CPCTY' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'BILD_NM', - 'BILD_TP', - 'ADDR', - 'LAW_YN', - 'COMP_DT', - 'RN_DT', + 'fcltNm', + 'bldgSeCn', + 'fcltPstnCn', + 'lglTrgtYn', + 'cmptnStrtYmd', + 'oprtngStrtYmd', { name: 'group_facility_status', header: { text: '시설현황' }, items: [ - 'TOTAR', - 'PROC_CPCTY' + 'archWholArea', + 'prcsCpct' ] }, - 'WATER_USE_CPCTY', - 'RUSWTR_USE_CPCTY', - 'SUB_USE_CPCTY', - 'RUSWTR_PRPOS', - 'FYER_OPER_CT', - 'RMK' + 'wtrUseQnt', + 'ruswtrUtztnQnt', + 'sbstCntUtztnQnt', + 'mnUsgCn', + 'ruswtrOperAmt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'recycledWaterStatusInp', + tabKey: 'ST00100002', tabLabel: '중수도 현황' -}; \ No newline at end of file +}; diff --git a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts index 7144c1edc..0a2225dc8 100644 --- a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts @@ -2,56 +2,61 @@ export const TreatWaterReuseBusinessApprStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'SIDO_CD', fieldName: '시도코드', header: { text: '시도코드' }, visible: false }, - { name: 'REUSE_CMPN_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 상호(명칭) - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, + { name: 'bplcNm', fieldName: 'bplcNm', header: { text: '상호(명칭)' } }, // 대표자 - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, + { name: 'rprsvNm', fieldName: 'rprsvNm', header: { text: '대표자' } }, // 주소 - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, + { name: 'bplcPstnCn', fieldName: 'bplcPstnCn', header: { text: '주소' } }, // 인가일 - { name: 'CMPN_RGSDE', fieldName: '인가일 yyyy-mm-dd', header: { text: '인가일 yyyy-mm-dd' } }, + { + name: 'coRegYmd', + fieldName: 'coRegYmd', + header: { text: '인가일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_CMPNM', fieldName: 'CHK_CMPNM', header: { text: 'CHK_CMPNM' }, visible: false }, - { name: 'CHK_RPRSNTV', fieldName: 'CHK_RPRSNTV', header: { text: 'CHK_RPRSNTV' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_CMPN_RGSDE', fieldName: 'CHK_CMPN_RGSDE', header: { text: 'CHK_CMPN_RGSDE' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'bplcSn', fieldName: 'bplcSn', header: { text: '사업장일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'CMPNM', - 'RPRSNTV', - 'ADDR', - 'CMPN_RGSDE', - 'RMK' + 'bplcNm', + 'rprsvNm', + 'bplcPstnCn', + 'coRegYmd', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'bplcSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'treatWaterReuseBusinessApprStatusInp', + tabKey: 'ST00100005', tabLabel: '하·폐수처리수 재이용사업 인가현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts index 15a3b12b2..69fc6311c 100644 --- a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts @@ -2,56 +2,61 @@ export const TreatWaterReuseDesignConstStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false }, // editable 임의로 - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'SIDO_CD', fieldName: '시도코드', header: { text: '시도코드' }, visible: false }, - { name: 'PLCST_CMPNY_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 상호(명칭) - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, + { name: 'bplcNm', fieldName: 'bplcNm', header: { text: '상호(명칭)' } }, // 대표자 - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, + { name: 'rprsvNm', fieldName: 'rprsvNm', header: { text: '대표자' } }, // 주소 - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, + { name: 'bplcPstnCn', fieldName: 'bplcPstnCn', header: { text: '주소' } }, // 등록일 yyyy-mm-dd - { name: 'CMPN_RGSDE', fieldName: '등록일 yyyy-mm-dd', header: { text: '등록일 yyyy-mm-dd' } }, + { + name: 'coRegYmd', + fieldName: 'coRegYmd', + header: { text: '등록일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_CMPNM', fieldName: 'CHK_CMPNM', header: { text: 'CHK_CMPNM' }, visible: false }, - { name: 'CHK_RPRSNTV', fieldName: 'CHK_RPRSNTV', header: { text: 'CHK_RPRSNTV' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_CMPN_RGSDE', fieldName: 'CHK_CMPN_RGSDE', header: { text: 'CHK_CMPN_RGSDE' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'bplcSn', fieldName: 'bplcSn', header: { text: '사업장일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'CMPNM', - 'RPRSNTV', - 'ADDR', - 'CMPN_RGSDE', - 'RMK' + 'bplcNm', + 'rprsvNm', + 'bplcPstnCn', + 'coRegYmd', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'bplcSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'treatWaterReuseDesignConstStatusInp', + tabKey: 'ST00100004', tabLabel: '하·폐수처리수 재이용시설 설계시공업 등록현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/management/AdmdstRenewalColumn.ts b/apps/st/src/components/util/column/management/AdmdstRenewalColumn.ts new file mode 100644 index 000000000..8b5887a67 --- /dev/null +++ b/apps/st/src/components/util/column/management/AdmdstRenewalColumn.ts @@ -0,0 +1,75 @@ +import type { ColumnInfoList } from "@/components/type/DataGrid.type"; + +/* ══════════════════════════════════════════════════ + 16. 통계인구관리 > 행정동갱신 탭 컬럼 설정 +══════════════════════════════════════════════════ */ + +/** 좌측: 감축된 행정구역(범위:동읍면) 자료 */ +export const AdmdstRenewalReducedColumnConfig: ColumnInfoList = { + tabKey: "RENEWAL_REDUCED", + tabLabel: "감축된 행정구역(범위:동읍면) 자료", + rowStartNum: 1, + fixedColumns: 1, + columns: [ + { + name: "adstDstrNm", + fieldName: "adstDstrNm", + header: { text: "행정동명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "adstCd", + fieldName: "adstCd", + header: { text: "행정동코드" }, + width: 150, + editable: false, + styleName: "rg-cell-readonly", + }, + ], + columnLayout: ["adstDstrNm", "adstCd"], +}; + +/** 우측: 추가된 행정구역 자료 */ +export const AdmdstRenewalAddedColumnConfig: ColumnInfoList = { + tabKey: "RENEWAL_ADDED", + tabLabel: "추가된 행정구역 자료", + rowStartNum: 1, + fixedColumns: 1, + columns: [ + { + name: "adstDstrNm", + fieldName: "adstDstrNm", + header: { text: "행정동명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "adstCd", + fieldName: "adstCd", + header: { text: "행정동코드" }, + width: 150, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "lawCd", + fieldName: "lawCd", + header: { text: "법정동코드" }, + width: 150, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "lawNm", + fieldName: "lawNm", + header: { text: "법정동명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + ], + columnLayout: ["adstDstrNm", "adstCd", "lawCd", "lawNm"], +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/management/DomesticPopulationColumn.ts b/apps/st/src/components/util/column/management/DomesticPopulationColumn.ts new file mode 100644 index 000000000..460c880e9 --- /dev/null +++ b/apps/st/src/components/util/column/management/DomesticPopulationColumn.ts @@ -0,0 +1,143 @@ +import { ValueType } from "realgrid"; +import type { ColumnInfoList } from "@/components/type/DataGrid.type"; + +/* ══════════════════════════════════════════════════ + 16. 통계인구관리 > 내국인 인구 탭 컬럼 설정 +══════════════════════════════════════════════════ */ + +/** 좌측: 엑셀 업로드 자료 (내국인 전체 리스트) */ +export const DomesticPopulationLeftColumnConfig: ColumnInfoList = { + tabKey: "DOMESTIC_LEFT", + tabLabel: "엑셀 업로드 자료 (내국인 전체 리스트)", + rowStartNum: 1, + fixedColumns: 2, + columns: [ + { + name: "admdstCd", + fieldName: "admdstCd", + header: { text: "코드" }, + width: 120, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "동명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplWholPpltnCnt", + fieldName: "domplWholPpltnCnt", + header: { text: "합계" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplMlPpltnCnt", + fieldName: "domplMlPpltnCnt", + header: { text: "남성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplFmlPpltnCnt", + fieldName: "domplFmlPpltnCnt", + header: { text: "여성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + ], + columnLayout: [ + "admdstCd", + "admdstNm", + { + name: "populationGroup", + header: { text: "인구수" }, + items: ["domplWholPpltnCnt", "domplMlPpltnCnt", "domplFmlPpltnCnt"], + }, + ], +}; + +/** 우측: 미매핑 리스트 (작년 대비) */ +export const DomesticPopulationRightColumnConfig: ColumnInfoList = { + tabKey: "DOMESTIC_RIGHT", + tabLabel: "미매핑 리스트 (작년 대비)", + rowStartNum: 1, + columns: [ + { + name: "admdstCd", + fieldName: "admdstCd", + header: { text: "업로드코드" }, + width: 130, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "동명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "mpinCd", + fieldName: "mpinCd", + header: { text: "맵핑코드" }, + width: 130, + editable: true + }, + { + name: "domplWholPpltnCnt", + fieldName: "domplWholPpltnCnt", + header: { text: "합계" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplMlPpltnCnt", + fieldName: "domplMlPpltnCnt", + header: { text: "남성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplFmlPpltnCnt", + fieldName: "domplFmlPpltnCnt", + header: { text: "여성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + ], + columnLayout: [ + "admdstCd", + "admdstNm", + "mpinCd", + { + name: "populationGroup", + header: { text: "인구수" }, + items: ["domplWholPpltnCnt", "domplMlPpltnCnt", "domplFmlPpltnCnt"], + }, + ], +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/management/ForeignPopulationColumn.ts b/apps/st/src/components/util/column/management/ForeignPopulationColumn.ts new file mode 100644 index 000000000..ea2723f0e --- /dev/null +++ b/apps/st/src/components/util/column/management/ForeignPopulationColumn.ts @@ -0,0 +1,162 @@ +import { ValueType } from "realgrid"; +import type { ColumnInfoList } from "@/components/type/DataGrid.type"; + +/* ══════════════════════════════════════════════════ + 16. 통계인구관리 > 외국인 인구 탭 컬럼 설정 +══════════════════════════════════════════════════ */ + +/** 좌측: 엑셀 업로드 자료 (외국인 전체 리스트) */ +export const ForeignPopulationLeftColumnConfig: ColumnInfoList = { + tabKey: "FOREIGN_LEFT", + tabLabel: "엑셀 업로드 자료 (외국인 전체 리스트)", + rowStartNum: 1, + fixedColumns: 2, + columns: [ + { + name: "admdstCd", + fieldName: "admdstCd", + header: { text: "코드" }, + width: 120, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "동명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrWholPpltnCnt", + fieldName: "frgnrWholPpltnCnt", + header: { text: "합계" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrMlPpltnCnt", + fieldName: "frgnrMlPpltnCnt", + header: { text: "남성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrFmlPpltnCnt", + fieldName: "frgnrFmlPpltnCnt", + header: { text: "여성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + ], + columnLayout: [ + "admdstCd", + "admdstNm", + { + name: "populationGroup", + header: { text: "외국인수" }, + items: ["frgnrWholPpltnCnt", "frgnrMlPpltnCnt", "frgnrFmlPpltnCnt"], + }, + ], +}; + +/** 우측: 미매핑 리스트 (작년 대비) */ +export const ForeignPopulationRightColumnConfig: ColumnInfoList = { + tabKey: "FOREIGN_RIGHT", + tabLabel: "미매핑 리스트 (작년 대비)", + rowStartNum: 1, + fixedColumns: 3, + columns: [ + { + name: "admdstCd", + fieldName: "admdstCd", + header: { text: "업로드코드" }, + width: 130, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "sido", + fieldName: "sido", + header: { text: "시도" }, + width: 150, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "gugun", + fieldName: "gugun", + header: { text: "구군" }, + width: 150, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "동명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "mpinCd", + fieldName: "mpinCd", + header: { text: "맵핑코드" }, + width: 130, + editable: true + }, + { + name: "frgnrWholPpltnCnt", + fieldName: "frgnrWholPpltnCnt", + header: { text: "합계" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrMlPpltnCnt", + fieldName: "frgnrMlPpltnCnt", + header: { text: "남성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrFmlPpltnCnt", + fieldName: "frgnrFmlPpltnCnt", + header: { text: "여성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + ], + columnLayout: [ + "admdstCd", + "sido", + "gugun", + "admdstNm", + "mpinCd", + { + name: "populationGroup", + header: { text: "외국인수" }, + items: ["frgnrWholPpltnCnt", "frgnrMlPpltnCnt", "frgnrFmlPpltnCnt"], + }, + ], +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/management/LogHistoryDetailColumn.ts b/apps/st/src/components/util/column/management/LogHistoryDetailColumn.ts index e8c93a45d..220aa70e5 100644 --- a/apps/st/src/components/util/column/management/LogHistoryDetailColumn.ts +++ b/apps/st/src/components/util/column/management/LogHistoryDetailColumn.ts @@ -3,6 +3,28 @@ import { ValueType } from "realgrid"; import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; import type { StatisticsLogDetailRow } from "@/entities/statistics/model/statistics-interface"; +export const LOG_DETAIL_DETAILS_FIELD = "details"; +export const LOG_DETAIL_DETAILS_RAW_FIELD = "__detailsRaw"; + +export interface LogDetailVerificationColumn { + tblNm: string; + metaSn: number; + fieldNm: string; + engColNm: string; + dataScpCd: string; + kornColNm: string; +} + +export interface LogDetailVerificationItem { + columns: LogDetailVerificationColumn[]; + cmprOpert: string | null; + vrfcDtlSn: number; + refStatsCd: string | null; + eqtnCombSeq: number; + engArtopnEqtnCn: string; + kornArtopnEqtnCn: string; +} + const COMMON_FIELD_ORDER = ["rmrk", "sggNm", "admdstCd", "admdstNm"] as const; const FIELD_HEADER: Record = { @@ -10,8 +32,71 @@ const FIELD_HEADER: Record = { sggNm: "시군구", admdstCd: "행정구역코드", admdstNm: "행정구역명", + [LOG_DETAIL_DETAILS_FIELD]: "상세", }; +function parseLogDetailsValue( + value: unknown, +): LogDetailVerificationItem[] | null { + if (value == null || value === "") return null; + if (Array.isArray(value)) { + return value as LogDetailVerificationItem[]; + } + if (typeof value === "string") { + try { + const parsed: unknown = JSON.parse(value); + return Array.isArray(parsed) + ? (parsed as LogDetailVerificationItem[]) + : null; + } catch { + return null; + } + } + return null; +} + +/** details JSON 배열을 그리드 버튼 셀 + 숨김 raw 필드로 변환 */ +export function prepareLogDetailRowsForGrid( + rows: StatisticsLogDetailRow[], +): Record[] { + return rows.map((row) => { + const next: Record = {}; + + for (const [key, value] of Object.entries(row)) { + if (key === LOG_DETAIL_DETAILS_FIELD) { + const parsed = parseLogDetailsValue(value); + if (parsed && parsed.length > 0) { + next[LOG_DETAIL_DETAILS_RAW_FIELD] = JSON.stringify(parsed); + next[LOG_DETAIL_DETAILS_FIELD] = "상세보기"; + } else { + next[LOG_DETAIL_DETAILS_RAW_FIELD] = ""; + next[LOG_DETAIL_DETAILS_FIELD] = + value === null || value === undefined + ? "" + : typeof value === "object" + ? "" + : String(value); + } + continue; + } + + next[key] = + value === null || value === undefined + ? "" + : (value as string | number); + } + + if ( + LOG_DETAIL_DETAILS_FIELD in row && + !(LOG_DETAIL_DETAILS_RAW_FIELD in next) + ) { + next[LOG_DETAIL_DETAILS_RAW_FIELD] = ""; + } + + return next; + }); +} + function collectFieldNames(rows: StatisticsLogDetailRow[]): string[] { const keys = new Set(); for (const row of rows) { @@ -47,6 +132,16 @@ function buildColumns(rows: StatisticsLogDetailRow[]): ColumnConfig[] { return fieldNames.map((fieldName) => { const numeric = isNumericField(rows, fieldName); + if (fieldName === LOG_DETAIL_DETAILS_FIELD) { + return { + name: fieldName, + fieldName, + header: { text: FIELD_HEADER[fieldName] ?? fieldName }, + editable: false, + width: 100, + styleName: "rg-cell-outline-action-btn", + }; + } return { name: fieldName, fieldName, @@ -60,13 +155,33 @@ function buildColumns(rows: StatisticsLogDetailRow[]): ColumnConfig[] { }); } +function appendHiddenDetailsRawColumn(columns: ColumnConfig[]): ColumnConfig[] { + const hasDetails = columns.some( + (column) => column.fieldName === LOG_DETAIL_DETAILS_FIELD, + ); + if (!hasDetails) return columns; + + return [ + ...columns, + { + name: LOG_DETAIL_DETAILS_RAW_FIELD, + fieldName: LOG_DETAIL_DETAILS_RAW_FIELD, + header: { text: "" }, + editable: false, + visible: false, + }, + ]; +} + export function buildLogHistoryDetailColumnInfo( rows: StatisticsLogDetailRow[], ): ColumnInfo { - const columns = buildColumns(rows); + const columns = appendHiddenDetailsRawColumn(buildColumns(rows)); return { columns, - columnLayout: columns.map((c) => c.fieldName), + columnLayout: columns + .filter((column) => column.visible !== false) + .map((column) => column.fieldName), }; } diff --git a/apps/st/src/components/util/column/management/PopulationStatusColumn.ts b/apps/st/src/components/util/column/management/PopulationStatusColumn.ts new file mode 100644 index 000000000..3ea57358e --- /dev/null +++ b/apps/st/src/components/util/column/management/PopulationStatusColumn.ts @@ -0,0 +1,121 @@ +import { ValueType } from "realgrid"; +import type { ColumnInfoList } from "@/components/type/DataGrid.type"; + +/* ══════════════════════════════════════════════════ + 16. 통계인구관리 > 행정인구현황 탭 컬럼 설정 +══════════════════════════════════════════════════ */ + +export const PopulationStatusColumnConfig: ColumnInfoList = { + tabKey: "POPULATION_STATUS", + tabLabel: "행정 인구 현황", + rowStartNum: 1, + fixedColumns: 3, + columns: [ + { + name: "statsYr", + fieldName: "statsYr", + header: { text: "년도" }, + width: 90, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "admdstCd", + fieldName: "admdstCd", + header: { text: "행정구역코드" }, + width: 140, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "admdstNm", + fieldName: "admdstNm", + header: { text: "행정구역명" }, + width: 200, + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "wholPpltnCnt", + fieldName: "wholPpltnCnt", + header: { text: "총인구" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplWholPpltnCnt", + fieldName: "domplWholPpltnCnt", + header: { text: "내국인 소계" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplMlPpltnCnt", + fieldName: "domplMlPpltnCnt", + header: { text: "내국인 남성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "domplFmlPpltnCnt", + fieldName: "domplFmlPpltnCnt", + header: { text: "내국인 여성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrWholPpltnCnt", + fieldName: "frgnrWholPpltnCnt", + header: { text: "외국인 소계" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrMlPpltnCnt", + fieldName: "frgnrMlPpltnCnt", + header: { text: "외국인 남성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + { + name: "frgnrFmlPpltnCnt", + fieldName: "frgnrFmlPpltnCnt", + header: { text: "외국인 여성" }, + width: 110, + valueType: ValueType.NUMBER, + dataFormat: "#,##0", + editable: false, + styleName: "rg-cell-readonly", + }, + ], + columnLayout: [ + "statsYr", + "admdstCd", + "admdstNm", + "wholPpltnCnt", + "domplWholPpltnCnt", + "domplMlPpltnCnt", + "domplFmlPpltnCnt", + "frgnrWholPpltnCnt", + "frgnrMlPpltnCnt", + "frgnrFmlPpltnCnt", + ], +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/review/ExcretaTreatmentFacilityRvwColumn.ts b/apps/st/src/components/util/column/review/ExcretaTreatmentFacilityRvwColumn.ts index 3cf7f9343..1ce10d9ed 100644 --- a/apps/st/src/components/util/column/review/ExcretaTreatmentFacilityRvwColumn.ts +++ b/apps/st/src/components/util/column/review/ExcretaTreatmentFacilityRvwColumn.ts @@ -1,118 +1,298 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 07.분뇨처리 시설 -const rawServiceLevelList = [ - { - tabKey: 'a', - tabLabel: '민원', - rowStartNum: 2, - fixedColumns: 2, - columns: [ - /* 1. 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerDecimal1ArrayByDivision, + footerDecimal3ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; - /* 2. 하수처리 인구 */ - { name: 'POPLTN_SM', fieldName: '하수처리인구(명)', header: { text: '하수처리인구\n(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); - /* 3. 민원유형 (단위: 건) */ - { name: 'CVPL_CM', fieldName: '총계(민원)', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_DCWTR_QLTWTR', fieldName: '악취', header: { text: '악취' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_BSML', fieldName: '하수관로', header: { text: '하수관로' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_FIVW', fieldName: '배수설비', header: { text: '배수설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_POLLUTN_ACDNT', fieldName: '맨홀', header: { text: '맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_SSOS', fieldName: '정화조', header: { text: '정화조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_CHRGE', fieldName: '하수도요금', header: { text: '하수도요금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_ETC', fieldName: '기타(민원)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 성과 지표 및 비고 */ - { name: 'STDR_POPLTN_PER_CVPL', fieldName: '기준인구당 민원건수(건/10,000명)', header: { text: '기준인구당\n민원건수\n(건/10,000명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - 'POPLTN_SM', - { - name: 'complaintTypeGroup', - header: { text: '민원유형(건)' }, - items: [ - 'CVPL_CM', - 'CVPL_DCWTR_QLTWTR', - 'CVPL_BSML', - 'CVPL_FIVW', - 'CVPL_POLLUTN_ACDNT', - 'CVPL_SSOS', - 'CVPL_CHRGE', - 'CVPL_ETC' - ] - }, - 'STDR_POPLTN_PER_CVPL', - 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '주민친화시설', - rowStartNum: 2, - fixedColumns: 5, - columns: [ - /* 1. 기본 및 시설 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: '처리장명', header: { text: '처리장명' } }, - { name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - - /* 2. 처리장 전체 면적 */ - { name: 'PRCPLT_AR', fieldName: '처리장 면적(㎡)', header: { text: '처리장\n면적(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 3. 주민친화시설 면적 상세 (단위: ㎡) */ - { name: 'TOTAL_AR', fieldName: '총계(시설)', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RESTRM_AR', fieldName: '휴게시설', header: { text: '휴게시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHILDPLAY_AR', fieldName: '어린이놀이공간', header: { text: '어린이놀이공간' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ALSFC_AR', fieldName: '체력단련시설', header: { text: '체력단련시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PLGRND_AR', fieldName: '운동시설', header: { text: '운동시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PRKPLCE_AR', fieldName: '주차장', header: { text: '주차장' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CTLSTT_AR', fieldName: '교육 및 문화시설', header: { text: '교육 및\n문화시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'NRPRK_AR', fieldName: '습지조성(생태공원)', header: { text: '습지조성\n(생태공원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC_AR', fieldName: '기타(시설)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 비고 */ - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', - 'FCT_CPC', - 'YR_GBN', - 'PRCPLT_AR', - { - name: 'friendlyFacilityGroup', - header: { text: '주민친화시설 면적(㎡)' }, - items: [ - 'TOTAL_AR', - 'RESTRM_AR', - 'CHILDPLAY_AR', - 'ALSFC_AR', - 'PLGRND_AR', - 'PRKPLCE_AR', - 'CTLSTT_AR', - 'NRPRK_AR', - 'ETC_AR' - ] - }, - 'RMK' - ] + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; -export const ExcretaTreatmentFacilityInfoList = rawServiceLevelList.map(tab => ({ +// 하수도 통계 검토 07. 분뇨처리시설 +const rawExcretaTreatmentFacilityList = [ + { + tabKey: "ST00070001", + tabLabel: "분뇨처리시설", + rowStartNum: 3, + fixedColumns: 4, + columns: [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, + { name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" } }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "소재지" } }, + { name: "telNo", fieldName: "telNo", header: { text: "전화번호" } }, + { name: "cmptnStrtYmd", fieldName: "cmptnStrtYmd", header: { text: "준공일" } }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일" } }, + { + name: "wholPjtcoAmt", + fieldName: "wholPjtcoAmt", + header: { text: "사업비\n(백만원)" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { + name: "prcsFcltCpct", + fieldName: "prcsFcltCpct", + header: { text: "시설용량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { + name: "cpct", + fieldName: "cpct", + header: { text: "처리량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { + name: "oprtngDayCnt", + fieldName: "oprtngDayCnt", + header: { text: "가동일수\n(일/년)" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { name: "prcsMthdExpln", fieldName: "prcsMthdExpln", header: { text: "처리공법" } }, + { name: "linkTmplntCn", fieldName: "linkTmplntCn", header: { text: "연계처리장명" } }, + { + name: "linkPrcsQnt", + fieldName: "linkPrcsQnt", + header: { text: "연계처리량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { + name: "inflwBodWqVl", + fieldName: "inflwBodWqVl", + header: { text: "BOD" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwTocWqVl", + fieldName: "inflwTocWqVl", + header: { text: "TOC" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwSsWqVl", + fieldName: "inflwSsWqVl", + header: { text: "SS" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwTnWqVl", + fieldName: "inflwTnWqVl", + header: { text: "T-N" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "inflwTpWqVl", + fieldName: "inflwTpWqVl", + header: { text: "T-P" }, + footer: footerDecimal3ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.000", + }, + { + name: "inflwClfgCnt", + fieldName: "inflwClfgCnt", + header: { text: "대장균\n군수" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { + name: "dschgBodWqVl", + fieldName: "dschgBodWqVl", + header: { text: "BOD" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgTocWqVl", + fieldName: "dschgTocWqVl", + header: { text: "TOC" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgSsWqVl", + fieldName: "dschgSsWqVl", + header: { text: "SS" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgTnWqVl", + fieldName: "dschgTnWqVl", + header: { text: "T-N" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + { + name: "dschgTpWqVl", + fieldName: "dschgTpWqVl", + header: { text: "T-P" }, + footer: footerDecimal3ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.000", + }, + { + name: "dschgClfgCnt", + fieldName: "dschgClfgCnt", + header: { text: "대장균\n군수" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { 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: "operMthdCdCn", fieldName: "operMthdCdCn", header: { text: "운영주체\n(자체/공기업/\n민간위탁)" } }, + { + name: "wholNope", + fieldName: "wholNope", + header: { text: "직원총수\n(명)" }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", + }, + { + name: "sftyAcdntEvntcsCnt", + fieldName: "sftyAcdntEvntcsCnt", + header: { text: "안전사고\n건수\n(건/인/년)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + ], + columnLayout: [ + { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + ], + }, + "displayYear", + "fcltNm", + "fcltPstnCn", + "telNo", + "cmptnStrtYmd", + "oprtngStrtYmd", + "wholPjtcoAmt", + "prcsFcltCpct", + "cpct", + "oprtngDayCnt", + "prcsMthdExpln", + "linkTmplntCn", + "linkPrcsQnt", + { + name: "inflwWqGroup", + header: { text: "유입수질(mg/L, 개/mL)" }, + items: [ + "inflwBodWqVl", + "inflwTocWqVl", + "inflwSsWqVl", + "inflwTnWqVl", + "inflwTpWqVl", + "inflwClfgCnt", + ], + }, + { + name: "dschgWqGroup", + header: { text: "방류수질(mg/L, 개/mL)" }, + items: [ + "dschgBodWqVl", + "dschgTocWqVl", + "dschgSsWqVl", + "dschgTnWqVl", + "dschgTpWqVl", + "dschgClfgCnt", + ], + }, + { + name: "wtrAreaGroup", + header: { text: "방류수역" }, + items: ["wtsysNm", "trbtrNm", "msirbCn", "ssirbCn"], + }, + "operMthdCdCn", + "wholNope", + "sftyAcdntEvntcsCnt", + ], + }, +]; + +export const ExcretaTreatmentFacilityInfoList = rawExcretaTreatmentFacilityList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); diff --git a/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts b/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts index c626ba504..dd6df776f 100644 --- a/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts +++ b/apps/st/src/components/util/column/review/PrivateSewageFacilityColumn.ts @@ -1,139 +1,336 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 08.개인하수처리시설 +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerDecimal1ArrayByDivision, + footerDecimal2ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, +]; + +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + ], +}; + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 08. 개인하수처리시설 const rawPrivateSewageList = [ { - tabKey: 'a', - tabLabel: '처리용량별 오수처리시설 현황', + tabKey: "ST00080001", + tabLabel: "처리용량별 오수처리시설", rowStartNum: 3, fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'STTY_SM', fieldName: '총계', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_SM', fieldName: '계(처리구역 내)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_2', fieldName: '2이하(처리구역 내)', header: { text: '2이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_3', fieldName: '3~10(처리구역 내)', header: { text: '3~10' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_11', fieldName: '11~20(처리구역 내)', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_21', fieldName: '21~50(처리구역 내)', header: { text: '21~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_51', fieldName: '51~100(처리구역 내)', header: { text: '51~100' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_101', fieldName: '101~500(처리구역 내)', header: { text: '101~500' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_INNER_501', fieldName: '501이상(처리구역 내)', header: { text: '501이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_SM', fieldName: '계(처리구역 외)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_2', fieldName: '2이하(처리구역 외)', header: { text: '2이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_3', fieldName: '3~10(처리구역 외)', header: { text: '3~10' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_11', fieldName: '11~20(처리구역 외)', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_21', fieldName: '21~50(처리구역 외)', header: { text: '21~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_51', fieldName: '51~100(처리구역 외)', header: { text: '51~100' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_101', fieldName: '101~500(처리구역 외)', header: { text: '101~500' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'STTY_ZONE_OUTER_501', fieldName: '501이상(처리구역 외)', header: { text: '501이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewAddressColumns, + intCol("wholDrtwPrcsFcltCnt", "총계"), + intCol("wholStaaCnt", "계"), + intCol("step1StaaPrcsQnt", "2이하"), + intCol("step2StaaPrcsQnt", "3~10"), + intCol("step3StaaPrcsQnt", "11~20"), + intCol("step4StaaPrcsQnt", "21~50"), + intCol("step5StaaPrcsQnt", "51~100"), + intCol("step6StaaPrcsQnt", "101~500"), + intCol("step7StaaPrcsQnt", "501이상"), + intCol("staaExcpWhol", "계"), + intCol("step1StaaExcpPrcsQnt", "2이하"), + intCol("step2StaaExcpPrcsQnt", "3~10"), + intCol("step3StaaExcpPrcsQnt", "11~20"), + intCol("step4StaaExcpPrcsQnt", "21~50"), + intCol("step5StaaExcpPrcsQnt", "51~100"), + intCol("step6StaaExcpPrcsQnt", "101~500"), + intCol("step7StaaExcpPrcsQnt", "501이상"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'STTY_SM', + reviewAddressLayout, + "displayYear", + "wholDrtwPrcsFcltCnt", { - name: 'individualGroup', header: { text: '오수처리시설' }, + name: "privateSewageFacilityGroup", + header: { text: "오수처리시설" }, items: [ { - name: 'innerGroup', header: { text: '하수처리구역 내(㎥/일)' }, - items: ['STTY_ZONE_INNER_SM', 'STTY_ZONE_INNER_2', 'STTY_ZONE_INNER_3', 'STTY_ZONE_INNER_11', 'STTY_ZONE_INNER_21', 'STTY_ZONE_INNER_51', 'STTY_ZONE_INNER_101', 'STTY_ZONE_INNER_501'] + name: "insideGroup", + header: { text: "하수처리구역 내(㎥/일)" }, + items: [ + "wholStaaCnt", + "step1StaaPrcsQnt", + "step2StaaPrcsQnt", + "step3StaaPrcsQnt", + "step4StaaPrcsQnt", + "step5StaaPrcsQnt", + "step6StaaPrcsQnt", + "step7StaaPrcsQnt", + ], }, { - name: 'outerGroup', header: { text: '하수처리구역 외(㎥/일)' }, - items: ['STTY_ZONE_OUTER_SM', 'STTY_ZONE_OUTER_2', 'STTY_ZONE_OUTER_3', 'STTY_ZONE_OUTER_11', 'STTY_ZONE_OUTER_21', 'STTY_ZONE_OUTER_51', 'STTY_ZONE_OUTER_101', 'STTY_ZONE_OUTER_501'] - } - ] + name: "outsideGroup", + header: { text: "하수처리구역 외(㎥/일)" }, + items: [ + "staaExcpWhol", + "step1StaaExcpPrcsQnt", + "step2StaaExcpPrcsQnt", + "step3StaaExcpPrcsQnt", + "step4StaaExcpPrcsQnt", + "step5StaaExcpPrcsQnt", + "step6StaaExcpPrcsQnt", + "step7StaaExcpPrcsQnt", + ], + }, + ], }, - 'RMK' - ] + "rmrk", + ], }, { - tabKey: 'b', - tabLabel: '정화조현황', + tabKey: "ST00080002", + tabLabel: "정화조현황", rowStartNum: 4, - fixedColumns: 1, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TOT_FCLTY_CNT', fieldName: '총 시설 수(A+D)', header: { text: '총 시설 수\n(A+D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_SM', fieldName: '총계 A=(B+C)/2', header: { text: '총계\nA=(B+C)/2' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_NPFL', fieldName: '자연유하', header: { text: '자연유하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_ENFCT_PUMP', fieldName: '강제펌핑식', header: { text: '강제펌핑식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_TFM', fieldName: '살수여상', header: { text: '살수여상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_ARTM', fieldName: '폭기방법', header: { text: '폭기방법' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_CTARM', fieldName: '접촉폭기', header: { text: '접촉폭기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_ETC', fieldName: '기타(방식)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_SM', fieldName: '계(C)', header: { text: '계(C)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_1', fieldName: '10인조 이하', header: { text: '10인조 이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_2', fieldName: '11~20', header: { text: '11~20' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_3', fieldName: '21~30', header: { text: '21~30' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_4', fieldName: '31~50', header: { text: '31~50' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_5', fieldName: '51~199', header: { text: '51~199' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_6', fieldName: '200~499', header: { text: '200~499' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_7', fieldName: '500~999', header: { text: '500~999' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'SPZI_PROC_CPC_8', fieldName: '1,000이상', header: { text: '1,000이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } + ...reviewAddressColumns, + intCol("grssFcltCnt", "총 시설 수\n(A+D)"), + intCol("staaInsdWhol", "총계\nA=(B+C)/2"), + intCol("staaInsdPrcsCpctByWhol", "계(C)"), + intCol("staaInsdSttkNprsflCnt", "자연유하"), + intCol("step1StaaInsdSttkFrcCnt", "강제펌핑식"), + intCol("staaInsdSprktpFltrtbCnt", "살수여상"), + intCol("staaInsdAertMthdCnt", "폭기방법"), + intCol("staaInsdCntcAertCnt", "접촉폭기"), + intCol("staaInsdEtcCnt", "기타"), + intCol("step1StaaInsdNope", "10인조\n이하"), + intCol("step2StaaInsdNope", "11~20"), + intCol("step3StaaInsdNope", "21~30"), + intCol("step4StaaInsdNope", "31~50"), + intCol("step5StaaInsdNope", "51~199"), + intCol("step6StaaInsdNope", "200~499"), + intCol("step7StaaInsdNope", "500~999"), + intCol("step8StaaInsdNope", "1,000이상"), + intCol("staaExcpWhol", "총계\nD=(E+F)/2"), + intCol("staaExcpPrcsCpctByWhol", "계(F)"), + intCol("staaExcpSttkNprsflCnt", "자연유하"), + intCol("step1StaaExcpSttkFrcCnt", "강제펌핑식"), + intCol("staaExcpSprktpFltrtbCnt", "살수여상"), + intCol("staaExcpAertMthdCnt", "폭기방법"), + intCol("staaExcpCntcAertCnt", "접촉폭기"), + intCol("staaExcpEtcCnt", "기타"), + intCol("step1StaaExcpNope", "10인조\n이하"), + intCol("step2StaaExcpNope", "11~20"), + intCol("step3StaaExcpNope", "21~30"), + intCol("step4StaaExcpNope", "31~50"), + intCol("step5StaaExcpNope", "51~199"), + intCol("step6StaaExcpNope", "200~499"), + intCol("step7StaaExcpNope", "500~999"), + intCol("step8StaaExcpNope", "1,000이상"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'TOT_FCLTY_CNT', + reviewAddressLayout, + "displayYear", + "grssFcltCnt", { - name: 'innerMainGroup', header: { text: '하수처리구역 내' }, + name: "insideSewerGroup", + header: { text: "하수처리구역 내" }, items: [ - 'SPZI_SM', + "staaInsdWhol", { - name: 'innerMethodGroup', header: { text: '처리방식별' }, - items: [{ name: 'innerTankGroup', header: { text: '부패탱크' }, items: ['SPZI_NPFL', 'SPZI_ENFCT_PUMP'] }, 'SPZI_TFM', 'SPZI_ARTM', 'SPZI_CTARM', 'SPZI_ETC'] + name: "insdBGroup", + header: { text: "계(B)" }, + items: [ + "staaInsdSttkNprsflCnt", + "step1StaaInsdSttkFrcCnt", + "staaInsdSprktpFltrtbCnt", + "staaInsdAertMthdCnt", + "staaInsdCntcAertCnt", + "staaInsdEtcCnt", + ], }, { - name: 'innerCpcGroup', header: { text: '처리용량별' }, - items: ['SPZI_PROC_CPC_SM', 'SPZI_PROC_CPC_1', 'SPZI_PROC_CPC_2', 'SPZI_PROC_CPC_3', 'SPZI_PROC_CPC_4', 'SPZI_PROC_CPC_5', 'SPZI_PROC_CPC_6', 'SPZI_PROC_CPC_7', 'SPZI_PROC_CPC_8'] - } - ] + name: "insdCGroup", + header: { text: "처리용량별" }, + items: [ + "staaInsdPrcsCpctByWhol", + "step1StaaInsdNope", + "step2StaaInsdNope", + "step3StaaInsdNope", + "step4StaaInsdNope", + "step5StaaInsdNope", + "step6StaaInsdNope", + "step7StaaInsdNope", + "step8StaaInsdNope", + ], + }, + ], }, - 'RMK' - ] + { + name: "outsideSewerGroup", + header: { text: "하수처리구역 외" }, + items: [ + "staaExcpWhol", + { + name: "excpEGroup", + header: { text: "계(E)" }, + items: [ + "staaExcpSttkNprsflCnt", + "step1StaaExcpSttkFrcCnt", + "staaExcpSprktpFltrtbCnt", + "staaExcpAertMthdCnt", + "staaExcpCntcAertCnt", + "staaExcpEtcCnt", + ], + }, + { + name: "excpFGroup", + header: { text: "처리용량별" }, + items: [ + "staaExcpPrcsCpctByWhol", + "step1StaaExcpNope", + "step2StaaExcpNope", + "step3StaaExcpNope", + "step4StaaExcpNope", + "step5StaaExcpNope", + "step6StaaExcpNope", + "step7StaaExcpNope", + "step8StaaExcpNope", + ], + }, + ], + }, + "rmrk", + ], }, { - tabKey: 'c', - tabLabel: '개인하수도 관리지역 지정공고 현황', + tabKey: "ST00080003", + tabLabel: "개인하수도 관리지역 지정공고 현황", rowStartNum: 3, - fixedColumns: 1, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'MGMT_AR', fieldName: '공고지역(개소)', header: { text: '공고지역\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'APPN_SCOPE', fieldName: '지정범위(㎢)', header: { text: '지정범위\n(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'WRRTN_CNT', fieldName: '개소수(정화조)', header: { text: '개소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'WRRTN_CPC', fieldName: '처리용량(인/일)', header: { text: '처리용량\n(인/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'STTY_CNT', fieldName: '개소수(오수시설)', header: { text: '개소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'STTY_CPC', fieldName: '처리용량(㎥/일)', header: { text: '처리용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewAddressColumns, + intCol("indvSwagePbancRgnCnt", "공고지역\n(개소)"), + { + name: "indvSwageDsgnScpLen", + fieldName: "indvSwageDsgnScpLen", + header: { text: "지정범위\n(㎢)" }, + footer: footerDecimal2ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", + }, + intCol("spctnkGrssFcltCnt", "총시설수"), + { + name: "spctnkQnt", + fieldName: "spctnkQnt", + header: { text: "처리용량\n(인/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + intCol("drtwPrcsFcltGrssFcltCnt", "총시설수"), + { + name: "drtwPrcsFcltQnt", + fieldName: "drtwPrcsFcltQnt", + header: { text: "처리용량\n(㎥/일)" }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', + reviewAddressLayout, + "displayYear", { - name: 'mgmtMainGroup', header: { text: '개인하수도관리지역공고현황\n(하수도법 제34조의2)' }, + name: "groupNotice", + header: { text: "개인하수도관리지역공고현황 (하수도법제34조의2)" }, items: [ - 'MGMT_AR', 'APPN_SCOPE', - { name: 'septictankGroup', header: { text: '정화조' }, items: ['WRRTN_CNT', 'WRRTN_CPC'] }, - { name: 'sewageFacGroup', header: { text: '오수시설' }, items: ['STTY_CNT', 'STTY_CPC'] } - ] + "indvSwagePbancRgnCnt", + "indvSwageDsgnScpLen", + { + name: "septicTankGroup", + header: { text: "정화조" }, + items: ["spctnkGrssFcltCnt", "spctnkQnt"], + }, + { + name: "sewageTreatmentGroup", + header: { text: "오수처리시설" }, + items: ["drtwPrcsFcltGrssFcltCnt", "drtwPrcsFcltQnt"], + }, + ], }, - 'RMK' - ] - } + "rmrk", + ], + }, ]; -export const PrivateSewageFacilityInfoListRvw = rawPrivateSewageList.map(tab => ({ +export const PrivateSewageFacilityInfoListRvw = rawPrivateSewageList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); diff --git a/apps/st/src/components/util/column/review/PublicSewageFacilityRvwColumn.ts b/apps/st/src/components/util/column/review/PublicSewageFacilityRvwColumn.ts index d02b5360b..b6952ff38 100644 --- a/apps/st/src/components/util/column/review/PublicSewageFacilityRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PublicSewageFacilityRvwColumn.ts @@ -33,16 +33,19 @@ export const rawPublicSewageFacilityInfoList = { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, + mergeCells: true, + sortdisable: true, footer: [ { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ], }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, { name: 'displayYear', fieldName: 'displayYear', header: { text: '구분' }, + sortdisable: true, footer: [ { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, diff --git a/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts b/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts index be414d00f..8f4cd757e 100644 --- a/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageExcretaRelatedRvwColumn.ts @@ -1,135 +1,276 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 12. 하수 및 분뇨 관련 산업 -const rawExcretaRelatedList = [ - { - tabKey: 'a', - tabLabel: '공공기관 하수분야', - rowStartNum: 2, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'OCGNZ', fieldName: '직제', header: { text: '직제' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계(기술직)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타(기술직)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'OCGNZ', 'YR_GBN', 'TOT_WTR_QRM', 'ADMINIST_JSSFC', - { - name: 'techJobGroup', - header: { text: '기술직' }, - items: ['TCHNLGY_JSSFC_SM', 'ENGRK_JSSFC', 'MCHN_JSSFC', 'ELCTY_JSSFC', 'CHMSTRY_JSSFC', 'ENVRN_JSSFC', 'TCHNLGY_ETC'] - }, - 'ETC', 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '공공기관 분뇨분야', - rowStartNum: 2, - fixedColumns: 1, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'OCGNZ', fieldName: '직제', header: { text: '직제' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계(기술직)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타(기술직)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'OCGNZ', 'YR_GBN', 'TOT_WTR_QRM', 'ADMINIST_JSSFC', - { - name: 'techJobGroup', - header: { text: '기술직' }, - items: ['TCHNLGY_JSSFC_SM', 'ENGRK_JSSFC', 'MCHN_JSSFC', 'ELCTY_JSSFC', 'CHMSTRY_JSSFC', 'ENVRN_JSSFC', 'TCHNLGY_ETC'] - }, - 'ETC', 'RMK' - ] - }, - { - tabKey: 'c', - tabLabel: '분뇨수집·운반업', - rowStartNum: 2, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'ECSCS_SM', fieldName: '업체수(개소)', header: { text: '업체수(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ECSCS_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'VHCLE_SM', fieldName: '계(차량)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'THREE_BELOW', fieldName: '3톤이하', header: { text: '3톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'FOUR_BELOW', fieldName: '4.5톤이하', header: { text: '4.5톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'EIGHT_BELOW', fieldName: '8톤이하', header: { text: '8톤이하' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC_VHCLE', fieldName: '기타(차량)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // name 중복 방지를 위해 ETC_VHCLE로 수정 권장 - { name: 'COLCT_TRSP_ACMSLT', fieldName: '수집·운반실적(㎥/년)', header: { text: '수집·운반실적\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'ECSCS_SM', 'ECSCS_BSMN', - { - name: 'vehicleGroup', - header: { text: '시설(차량) 확보현황(대수)' }, - items: ['VHCLE_SM', 'THREE_BELOW', 'FOUR_BELOW', 'EIGHT_BELOW', 'ETC_VHCLE'] - }, - 'COLCT_TRSP_ACMSLT', 'RMK' - ] - }, - { - tabKey: 'd', - tabLabel: '개인하수처리관련업', - rowStartNum: 2, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'CNSTRCT_ENTRPS', fieldName: '업소수(시공업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CNSTRCT_BSMN', fieldName: '종사인원(명)_시공업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MNFCTUR_ENTRPS', fieldName: '업소수(제조업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MNFCTUR_BSMN', fieldName: '종사인원(명)_제조업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MGMT_ENTRPS', fieldName: '업소수(관리업)', header: { text: '업소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'MGMT_BSMN', fieldName: '종사인원(명)_관리업', header: { text: '종사인원(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - { name: 'constructGroup', header: { text: '개인하수처리시설 설계ㆍ시공업' }, items: ['CNSTRCT_ENTRPS', 'CNSTRCT_BSMN'] }, - { name: 'manufactureGroup', header: { text: '개인하수처리시설 제조업' }, items: ['MNFCTUR_ENTRPS', 'MNFCTUR_BSMN'] }, - { name: 'managementGroup', header: { text: '개인하수처리시설 관리업' }, items: ['MGMT_ENTRPS', 'MGMT_BSMN'] }, - 'RMK' - ] - } +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; -export const SewageExcretaRelatedInfoListRvw = rawExcretaRelatedList.map(tab => ({ - ...tab, - columns: tab.columns.map(col => ({ - ...col, - editable: false - })) -})); \ No newline at end of file +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + // { + // name: "admdstNm", + // fieldName: "admdstNm", + // header: { text: "행정구역명" }, + // mergeCells: true, + // sortdisable: true, + // }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + // "admdstNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +// 하수도 통계 검토 12. 하수 및 분뇨 관련 산업 +const rawExcretaRelatedList = [ + { + tabKey: "ST00120001", + tabLabel: "공공기관 하수분야", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + textCol("ognzNm", "직제"), + intCol("empGrssCnt", "직원총수"), + intCol("pbadmsNope", "행정직"), + intCol("techWholNope", "계"), + intCol("engrkCnt", "토목"), + intCol("mchnCnt", "기계"), + intCol("prvtmCnt", "전기"), + intCol("chmengCnt", "화공"), + intCol("envCnt", "환경"), + intCol("enjbEtcCnt", "기타"), + intCol("etcCnt", "기타"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "ognzNm", + "empGrssCnt", + "pbadmsNope", + { + name: "techJobGroup", + header: { text: "기술직" }, + items: [ + "techWholNope", + "engrkCnt", + "mchnCnt", + "prvtmCnt", + "chmengCnt", + "envCnt", + "enjbEtcCnt", + ], + }, + "etcCnt", + "rmrk", + ], + }, + { + tabKey: "ST00120002", + tabLabel: "공공기관 분뇨분야", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + textCol("ognzNm", "직제"), + intCol("empGrssCnt", "직원총수"), + intCol("pbadmsNope", "행정직"), + intCol("techWholNope", "계"), + intCol("engrkCnt", "토목"), + intCol("mchnCnt", "기계"), + intCol("prvtmCnt", "전기"), + intCol("chmengCnt", "화공"), + intCol("envCnt", "환경"), + intCol("enjbEtcCnt", "기타"), + intCol("etcCnt", "기타"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "ognzNm", + "empGrssCnt", + "pbadmsNope", + { + name: "techJobGroup", + header: { text: "기술직" }, + items: [ + "techWholNope", + "engrkCnt", + "mchnCnt", + "prvtmCnt", + "chmengCnt", + "envCnt", + "enjbEtcCnt", + ], + }, + "etcCnt", + "rmrk", + ], + }, + { + tabKey: "ST00120003", + tabLabel: "분뇨수집·운반업", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("bzentyCnt", "업체수(개소)"), + intCol("empNope", "종사인원(명)"), + intCol("fcltSecrPrstCnt", "계"), + intCol("n3TonUdrCnt", "3톤이하"), + intCol("n4TonUdrCnt", "4.5톤이하"), + intCol("n8TonUdrCnt", "8톤이하"), + intCol("etcCnt", "기타"), + decimal1Col("clctCnvncPrfmncQnt", "수집·운반실적\n(㎥/년)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "bzentyCnt", + "empNope", + { + name: "vehicleGroup", + header: { text: "시설(차량) 확보현황(대수)" }, + items: [ + "fcltSecrPrstCnt", + "n3TonUdrCnt", + "n4TonUdrCnt", + "n8TonUdrCnt", + "etcCnt", + ], + }, + "clctCnvncPrfmncQnt", + "rmrk", + ], + }, + { + tabKey: "ST00120004", + tabLabel: "개인하수처리관련업", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("designMdsrCnt", "업소수"), + intCol("designMdsrNope", "종사인원(명)"), + intCol("mfbizCnt", "업소수"), + intCol("mfbizMdsrNope", "종사인원(명)"), + intCol("mngCnt", "업소수"), + intCol("mngMdsrNope", "종사인원(명)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { + name: "constructGroup", + header: { text: "개인하수처리시설 설계ㆍ시공업" }, + items: ["designMdsrCnt", "designMdsrNope"], + }, + { + name: "manufactureGroup", + header: { text: "개인하수처리시설 제조업" }, + items: ["mfbizCnt", "mfbizMdsrNope"], + }, + { + name: "managementGroup", + header: { text: "개인하수처리시설 관리업" }, + items: ["mngCnt", "mngMdsrNope"], + }, + "rmrk", + ], + }, +]; + +export const SewageExcretaRelatedInfoListRvw = rawExcretaRelatedList.map((tab) => ({ + ...tab, + columns: tab.columns.map((col) => ({ + ...col, + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `SewageExcretaRelatedInfoListRvw` 사용 */ +export const SewageExcretaRelatedInfoList = SewageExcretaRelatedInfoListRvw; diff --git a/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts b/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts index b9dfcbf98..257c902a0 100644 --- a/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewageExcretaSlurgeRvwColumn.ts @@ -1,199 +1,385 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, +]; + +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewFacilityAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewFacilityAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + "sggNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 11. 하수 및 분뇨 찌꺼기(슬러지) 처리 const rawSludgeList = [ { - tabKey: 'a', - tabLabel: '찌꺼기 처리시설 현황', + tabKey: "ST00110001", + tabLabel: "찌꺼기(슬러지)처리시설현황", rowStartNum: 4, fixedColumns: 2, columns: [ - /* 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수\n처리장명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - - /* 찌꺼기발생량 */ - { name: 'TOT_AMNT', fieldName: '합계(발생)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'P_AMNT', fieldName: '자체(발생)', header: { text: '자체' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_AMNT', fieldName: '외부유입량(발생)', header: { text: '외부유입량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 처리 정보 */ - { name: 'OUT_FCT_NM', fieldName: '이송처리장', header: { text: '이송처리장' } }, - { name: 'OUT_AMNT', fieldName: '이송처리량(㎥/년)', header: { text: '이송처리량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DSP_MOIRAT', fieldName: '함수율(%,탈수기준)', header: { text: '함수율\n(%,\n탈수기준)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DSP_SM', fieldName: '찌꺼기 처리량 합계(톤/년)', header: { text: '찌꺼기\n처리량\n합계(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 자체 찌꺼기 처리량 */ - { name: 'I_1CH_SM', fieldName: '자체찌꺼기 처리량소계', header: { text: '자체찌꺼기\n처리량소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_REC', fieldName: '매립(자체)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC', fieldName: '소각(자체)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_SM', fieldName: '소계(소각후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_REC', fieldName: '매립(소각후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_RAW', fieldName: '제품원료(소각후)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_COV', fieldName: '복토재(소각후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_INC_2CH_FCT', fieldName: '시설(업체명)_소각후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_SOL', fieldName: '고화(자체)', header: { text: '고화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_SM', fieldName: '소계(고화후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_REC', fieldName: '매립(고화후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_COV', fieldName: '복토재(고화후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_SOL_2CH_FCT', fieldName: '시설(업체명)_고화후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_DRY', fieldName: '건조(자체)', header: { text: '건조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_SM', fieldName: '소계(건조후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_REC', fieldName: '매립(건조후)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_INC', fieldName: '소각(건조후)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_FUE', fieldName: '연료(건조후)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_RAW', fieldName: '제품원료(건조후)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_COV', fieldName: '복토재(건조후)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_COM', fieldName: '퇴비화(건조후)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_DRY_2CH_FCT', fieldName: '시설(업체명)_건조후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_CAR', fieldName: '탄화(자체)', header: { text: '탄화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_SM', fieldName: '소계(탄화후)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_INC', fieldName: '소각(탄화후)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_FUE', fieldName: '연료(탄화후)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_CAR_2CH_FCT', fieldName: '시설(업체명)_탄화후', header: { text: '시설(업체명)' } }, - { name: 'I_1CH_COM', fieldName: '퇴비화(자체)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'I_1CH_COM_FCT', fieldName: '시설(업체명)_퇴비화', header: { text: '시설(업체명)' } }, - - /* 외부위탁 처리량 */ - { name: 'O_CON_SM', fieldName: '소계(외부위탁)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_REC', fieldName: '매립(외부위탁)', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_INC', fieldName: '소각(외부위탁)', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_FUE1', fieldName: '연료(외부위탁)', header: { text: '연료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_RAW', fieldName: '제품원료(외부위탁)', header: { text: '제품원료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_COV', fieldName: '복토재(외부위탁)', header: { text: '복토재' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_COM', fieldName: '퇴비화(외부위탁)', header: { text: '퇴비화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'O_CON_FCT_NM', fieldName: '시설(업체명)_외부위탁', header: { text: '시설(업체명)' } }, - - /* 기타 */ - { name: 'OM_1CH_SM', fieldName: '수도권광역 위탁처리량(톤/년)', header: { text: '수도권광역\n위탁처리량\n(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "하수\n처리장명"), + decimal1Col("gnamWhol", "합계"), + decimal1Col("itslfQnt", "자체"), + decimal1Col("otsdInflwQnt", "외부유입량"), + textCol("transfTmplntNm", "이송처리장"), + decimal1Col("transfPrcsQnt", "이송처리량\n(㎥/년)"), + decimal1Col("srpFncRt", "함수율\n(%, 탈수기준)"), + decimal1Col("wholSrpPrcsQnt", "찌꺼기\n처리량 합계\n(톤/년)"), + decimal1Col("itslfTs1Whol", "자체찌꺼기\n처리량 소계"), + decimal1Col("itslfTs1RclmQnt", "매립"), + decimal1Col("itslfTs1IncnrQnt", "소각"), + decimal1Col("incnrPrcsWhol", "소계"), + decimal1Col("incnrPrcsLfvlm", "매립"), + decimal1Col("incnrPrcsPrdctRmtrlQnt", "제품원료"), + decimal1Col("incnrPrcsFmlyrQnt", "복토재"), + textCol("incnrPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1AltdQnt", "고화"), + decimal1Col("stbznPrcsWhol", "소계"), + decimal1Col("stbznPrcsLfvlm", "매립"), + decimal1Col("stbznPrcsFmlyrQnt", "복토재"), + textCol("stbznPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1DryQnt", "건조"), + decimal1Col("dryPrcsWhol", "소계"), + decimal1Col("dryPrcsLfvlm", "매립"), + decimal1Col("dryPrcsIncnrQnt", "소각"), + decimal1Col("dryPrcsFuelQnt", "연료"), + decimal1Col("dryPrcsPrdctRmtrlQnt", "제품원료"), + decimal1Col("dryPrcsFmlyrQnt", "복토재"), + decimal1Col("dryPrcsCmpstgQnt", "퇴비화"), + textCol("dryPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1FuelQnt", "탄화"), + decimal1Col("fuelPrcsWhol", "소계"), + decimal1Col("fuelPrcsIncnrQnt", "소각"), + decimal1Col("fuelPrcsFuelQnt", "연료"), + textCol("fuelPrcsFcltNm", "시설(업체명)"), + decimal1Col("itslfTs1CmpstgQnt", "퇴비화"), + textCol("cmpsPrcsFcltBzentyNm", "시설(업체명)"), + decimal1Col("otsdCnsgnWhol", "소계"), + decimal1Col("otsdCnsgnRclmQnt", "매립"), + decimal1Col("otsdCnsgnIncnrQnt", "소각"), + decimal1Col("otsdCnsgnFuelPwplQnt", "연료"), + decimal1Col("otsdCnsgnPrdctRmtrlQnt", "제품원료"), + decimal1Col("otsdCnsgnFmlyrQnt", "복토재"), + decimal1Col("otsdCnsgnCmpstgQnt", "퇴비화"), + textCol("otsdCnsgnFcltNm", "시설(업체명)"), + decimal1Col("cparTs2Whol", "수도권광역\n위탁처리량\n(톤/년)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', 'YR_GBN', - { name: 'genAmntGroup', header: { text: '찌꺼기발생량(톤/년,탈수기준)' }, items: ['TOT_AMNT', 'P_AMNT', 'O_AMNT'] }, - 'OUT_FCT_NM', 'OUT_AMNT', 'DSP_MOIRAT', 'DSP_SM', + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", { - name: 'internalProcessGroup', header: { text: '자체 찌꺼기 처리량(톤/년)' }, + name: "gnamGroup", + header: { text: "찌꺼기발생량(톤/년, 탈수기준)" }, + items: ["gnamWhol", "itslfQnt", "otsdInflwQnt"], + }, + "transfTmplntNm", + "transfPrcsQnt", + "srpFncRt", + "wholSrpPrcsQnt", + { + name: "itslfProcessingGroup", + header: { text: "자체 찌꺼기 처리량 현황(톤/년)" }, items: [ - 'I_1CH_SM', 'I_1CH_REC', 'I_1CH_INC', - { name: 'inc2ndGroup', header: { text: '소각 후 처리(2차)' }, items: ['I_1CH_INC_2CH_SM', 'I_1CH_INC_2CH_REC', 'I_1CH_INC_2CH_RAW', 'I_1CH_INC_2CH_COV', 'I_1CH_INC_2CH_FCT'] }, - 'I_1CH_SOL', - { name: 'sol2ndGroup', header: { text: '고화 후 처리(2차)' }, items: ['I_1CH_SOL_2CH_SM', 'I_1CH_SOL_2CH_REC', 'I_1CH_SOL_2CH_COV', 'I_1CH_SOL_2CH_FCT'] }, - 'I_1CH_DRY', - { name: 'dry2ndGroup', header: { text: '건조 후 처리(2차)' }, items: ['I_1CH_DRY_2CH_SM', 'I_1CH_DRY_2CH_REC', 'I_1CH_DRY_2CH_INC', 'I_1CH_DRY_2CH_FUE', 'I_1CH_DRY_2CH_RAW', 'I_1CH_DRY_2CH_COV', 'I_1CH_DRY_2CH_COM', 'I_1CH_DRY_2CH_FCT'] }, - 'I_1CH_CAR', - { name: 'car2ndGroup', header: { text: '탄화 후 처리(2차)' }, items: ['I_1CH_CAR_2CH_SM', 'I_1CH_CAR_2CH_INC', 'I_1CH_CAR_2CH_FUE', 'I_1CH_CAR_2CH_FCT'] }, - { name: 'compostGroup', header: { text: '퇴비화' }, items: ['I_1CH_COM_FCT'] } - ] + "itslfTs1Whol", + "itslfTs1RclmQnt", + "itslfTs1IncnrQnt", + { + name: "incnrGroup", + header: { text: "소각 후 처리(2차)" }, + items: [ + "incnrPrcsWhol", + "incnrPrcsLfvlm", + "incnrPrcsPrdctRmtrlQnt", + "incnrPrcsFmlyrQnt", + "incnrPrcsFcltNm", + ], + }, + "itslfTs1AltdQnt", + { + name: "stbznGroup", + header: { text: "고화 후 처리(2차)" }, + items: [ + "stbznPrcsWhol", + "stbznPrcsLfvlm", + "stbznPrcsFmlyrQnt", + "stbznPrcsFcltNm", + ], + }, + "itslfTs1DryQnt", + { + name: "dryGroup", + header: { text: "건조 후 처리(2차)" }, + items: [ + "dryPrcsWhol", + "dryPrcsLfvlm", + "dryPrcsIncnrQnt", + "dryPrcsFuelQnt", + "dryPrcsPrdctRmtrlQnt", + "dryPrcsFmlyrQnt", + "dryPrcsCmpstgQnt", + "dryPrcsFcltNm", + ], + }, + "itslfTs1FuelQnt", + { + name: "fuelGroup", + header: { text: "탄화 후 처리(2차)" }, + items: [ + "fuelPrcsWhol", + "fuelPrcsIncnrQnt", + "fuelPrcsFuelQnt", + "fuelPrcsFcltNm", + ], + }, + "itslfTs1CmpstgQnt", + { + name: "cmpsGroup", + header: { text: "퇴비화" }, + items: ["cmpsPrcsFcltBzentyNm"], + }, + ], }, { - name: 'externalProcessGroup', header: { text: '외부(민간)위탁 처리량(톤/년)' }, - items: ['O_CON_SM', 'O_CON_REC', 'O_CON_INC', 'O_CON_FUE1', 'O_CON_RAW', 'O_CON_COV', 'O_CON_COM', 'O_CON_FCT_NM'] + name: "otsdGroup", + header: { text: "외부(민간) 위탁 처리량(톤/년)" }, + items: [ + "otsdCnsgnWhol", + "otsdCnsgnRclmQnt", + "otsdCnsgnIncnrQnt", + "otsdCnsgnFuelPwplQnt", + "otsdCnsgnPrdctRmtrlQnt", + "otsdCnsgnFmlyrQnt", + "otsdCnsgnCmpstgQnt", + "otsdCnsgnFcltNm", + ], }, - 'OM_1CH_SM', 'RMK' - ] + "cparTs2Whol", + "rmrk", + ], }, { - tabKey: 'b', - tabLabel: '소화조 운영현황', + tabKey: "ST00110002", + tabLabel: "소화조 운영현황", rowStartNum: 3, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수\n처리장명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'DGSTR_FNNC', fieldName: '소화조 유/무(Y/N)선택', header: { text: '소화조 유/무\n(Y/N)선택' } }, - { name: 'DGSTR_CPC', fieldName: '설계용량(㎥)', header: { text: '설계\n용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_OPR_CPC', fieldName: '실제운전용량(㎥)', header: { text: '실제운전\n용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_IN', fieldName: '투입량(㎥/일)', header: { text: '투입량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_TS', fieldName: '투입 TS 농도(%)', header: { text: '투입 TS 농도\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_YRSTP_RN_DYN', fieldName: '가동일수(연간)', header: { text: '가동일수\n(연간)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_OCCUR_GAS', fieldName: '발생가스량(㎥/일)', header: { text: '발생가스량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_SM', fieldName: '계(이용현황)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_SLE', fieldName: '판매', header: { text: '판매' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_ELTGNR', fieldName: '발전', header: { text: '발전' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_ONSLF', fieldName: '자체활용', header: { text: '자체활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_ETC', fieldName: '기타(CNG등)', header: { text: '기타(CNG등)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DGSTR_GAS', fieldName: '잉여가스량(㎥/일)', header: { text: '잉여가스량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "하수\n처리장명"), + textCol("digesterYn", "소화조 유/무\n(Y/N)선택"), + intCol("dsgnCpct", "설계\n용량(㎥)"), + intCol("actlOperCpct", "실제운전\n용량(㎥)"), + decimal1Col("inputQnt", "투입량\n(㎥/일)"), + decimal1Col("tsConc", "투입 TS 농도\n(%)"), + intCol("oprtngDays", "가동일수\n(연간)"), + decimal1Col("gnrtGasQnt", "발생가스량\n(㎥/일)"), + decimal1Col("gasTotal", "계"), + decimal1Col("gasSale", "판매"), + decimal1Col("gasGen", "발전"), + decimal1Col("gasSelf", "자체활용"), + decimal1Col("gasEtc", "기타(CNG등)"), + decimal1Col("surplusGas", "잉여가스량\n(㎥/일)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', 'YR_GBN', + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", { - name: 'digesterMainGroup', header: { text: '소화조 운영현황' }, + name: "operGroup", + header: { text: "소화조 운영현황" }, items: [ - 'DGSTR_FNNC', 'DGSTR_CPC', 'DGSTR_OPR_CPC', 'DGSTR_IN', 'DGSTR_TS', 'DGSTR_YRSTP_RN_DYN', 'DGSTR_OCCUR_GAS', - { name: 'gasUsageGroup', header: { text: '소화가스 이용현황(㎥/일)' }, items: ['DGSTR_SM', 'DGSTR_SLE', 'DGSTR_ELTGNR', 'DGSTR_ONSLF', 'DGSTR_ETC'] }, - 'DGSTR_GAS' - ] + "digesterYn", + "dsgnCpct", + "actlOperCpct", + "inputQnt", + "tsConc", + "oprtngDays", + "gnrtGasQnt", + { + name: "gasUsageGroup", + header: { text: "소화가스 이용현황(㎥/일)" }, + items: ["gasTotal", "gasSale", "gasGen", "gasSelf", "gasEtc"], + }, + "surplusGas", + ], }, - 'RMK' - ] + "rmrk", + ], }, { - tabKey: 'c', - tabLabel: '하수찌꺼기 처리시설 운영현황', + tabKey: "ST00110003", + tabLabel: "하수찌꺼기 처리시설 운영현황", rowStartNum: 2, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'SLG_FCT_NM', fieldName: '찌꺼기처리시설명', header: { text: '찌꺼기처리시설명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'TRT_METHOD', fieldName: '처리방식', header: { text: '처리방식' } }, - { name: 'SLG_FCT_CPC', fieldName: '시설용량(톤/일)', header: { text: '시설용량\n(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OPR_CPC', fieldName: '연간처리량(톤/년)', header: { text: '연간처리량\n(톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ODY_RN_TIME', fieldName: '일평균 가동시간(1~24)', header: { text: '일평균\n가동시간\n(1~24)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'YRSTP_RN_DYN', fieldName: '연중 가동일(1~330)', header: { text: '연중 가동일\n(1~330)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'OPR_RT', fieldName: '가동율', header: { text: '가동율' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "찌꺼기처리\n시설명"), + textCol("prcsMthExpln", "처리방식"), + decimal1Col("fcltCpct", "시설용량\n(톤/일)"), + decimal1Col("anlPrcsQnt", "연간처리량\n(톤/년)"), + decimal1Col("dayAvgOprtngHr", "일평균\n가동시간\n(1~24)"), + intCol("anlOprtngDayCnt", "연중 가동일\n(1~330)"), + decimal1Col("oprtngRt", "가동율(%)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'SLG_FCT_NM', 'YR_GBN', - { name: 'sludgeFctOprGroup', header: { text: '처리시설 운영현황' }, items: ['TRT_METHOD', 'SLG_FCT_CPC', 'OPR_CPC', 'ODY_RN_TIME', 'YRSTP_RN_DYN', 'OPR_RT'] }, - 'RMK' - ] + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + { + name: "operStatusGroup", + header: { text: "처리시설 운영현황" }, + items: [ + "prcsMthExpln", + "fcltCpct", + "anlPrcsQnt", + "dayAvgOprtngHr", + "anlOprtngDayCnt", + "oprtngRt", + ], + }, + "rmrk", + ], }, { - tabKey: 'd', - tabLabel: '분뇨찌꺼기 처리현황', + tabKey: "ST00110004", + tabLabel: "분뇨찌꺼기(슬러지)처리현황", rowStartNum: 2, + fixedColumns: 2, columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'ECSCS_NM', fieldName: '분뇨 처리장명', header: { text: '분 뇨\n처리장명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'OCTY', fieldName: '발생량(A)=(B)+(C)(㎥/년)', header: { text: '발생량\n(A)=(B)+(C)\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PROCESS_CPCTY_SM', fieldName: '계(B)', header: { text: '계(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RUSE', fieldName: '재활용', header: { text: '재활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LAND_RCLM', fieldName: '육상매립', header: { text: '육상매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INCNR', fieldName: '소각', header: { text: '소각' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OCEAN_DPSL', fieldName: '해양투기', header: { text: '해양투기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'FUEL', fieldName: '연료화', header: { text: '연료화' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CYFD_CPCTY', fieldName: '미처분량(이월량)(C)(㎥/년)', header: { text: '미처분량\n(이월량)\n(C)\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'OUT_BSC_FCT_NM', fieldName: '타시설 연계여부', header: { text: '타시설\n연계여부' } }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, + ...reviewFacilityAddressColumns, + textCol("fcltNm", "분뇨\n처리장명"), + decimal1Col("gnam", "발생량\n(A)=(B)+(C)"), + decimal1Col("wholDspsQnt", "계(B)"), + decimal1Col("rcycQnt", "재활용"), + decimal1Col("olandLfvlm", "육상매립"), + decimal1Col("incnrQnt", "소각"), + decimal1Col("oceanInputQnt", "해양투기"), + decimal1Col("fuelQnt", "연료화"), + decimal1Col("etcQnt", "기타"), + decimal1Col("nondspsQnt", "미처분량\n(이월량)(C)"), + textCol("swtrmtFcltNm", "타시설\n연계여부"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'ECSCS_NM', 'YR_GBN', 'OCTY', - { name: 'disposalAmntGroup', header: { text: '처 분 량(㎥/년)' }, items: ['PROCESS_CPCTY_SM', 'RUSE', 'LAND_RCLM', 'INCNR', 'OCEAN_DPSL', 'FUEL', 'ETC'] }, - 'CYFD_CPCTY', 'OUT_BSC_FCT_NM', 'RMK' - ] - } + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + "gnam", + { + name: "dspsGroup", + header: { text: "처분량" }, + items: [ + "wholDspsQnt", + "rcycQnt", + "olandLfvlm", + "incnrQnt", + "oceanInputQnt", + "fuelQnt", + "etcQnt", + ], + }, + "nondspsQnt", + "swtrmtFcltNm", + "rmrk", + ], + }, ]; -export const SewageExcretaSludgeInfoListRvw = rawSludgeList.map(tab => ({ +export const SewageExcretaSludgeInfoListRvw = rawSludgeList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `SewageExcretaSludgeInfoListRvw` 사용 */ +export const SewageExcretaSludgeInfoList = SewageExcretaSludgeInfoListRvw; diff --git a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts index de413a34b..98980f88d 100644 --- a/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeMaintainRvwColumn.ts @@ -52,7 +52,7 @@ const rawMaintainList = [ ] }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, + // { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', @@ -125,7 +125,7 @@ const rawMaintainList = [ items: [ { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, 'sggNm', - 'admdstNm', + // 'admdstNm', ] }, 'displayYear', @@ -197,7 +197,7 @@ const rawMaintainList = [ ] }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, + // { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true }, { name: 'displayYear', fieldName: 'displayYear', @@ -277,7 +277,7 @@ const rawMaintainList = [ items: [ { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, // sido -> ctpvNm 'sggNm', - 'admdstNm' + // 'admdstNm' ] }, 'displayYear', diff --git a/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts b/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts index b3dc2c73e..5e5e1f372 100644 --- a/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewerageFinanceRvwColumn.ts @@ -1,215 +1,369 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 09. 하수도 재정 -const rawFinanceList = [ - { - tabKey: 'a', - tabLabel: '하수도재정 결산', - rowStartNum: 3, - fixedColumns: 1, - columns: [ - /* 1. 기본 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerAvgDecimalArrayByDivision, + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; - /* 2. 재정별 사업비 (수입) */ - { name: 'FNANC_WCT_SM', fieldName: '계(수입)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'NTNXPN', fieldName: '중앙정부 지원액', header: { text: '중앙정부\n지원액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LCLEXPNS', fieldName: '특․광역시, 도', header: { text: '특․광역시, 도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LCLTY_CT', fieldName: '시,군,구 지자체', header: { text: '시,군,구 지자체' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWER_RNTFEE', fieldName: '하수도 사용료', header: { text: '하수도\n사용료' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'AMNTBRNCSR', fieldName: '원인자 부담금', header: { text: '원인자\n부담금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_INCME', fieldName: '기타 잡수입', header: { text: '기타\n잡수입' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CYFD_AMOUNT', fieldName: '이월액', header: { text: '이월액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 3. 부채액 */ - { name: 'DEBT_AMOUNT', fieldName: '부채액(백만원)', header: { text: '부채액\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); - /* 4. 용도별 사업비 (지출) */ - { name: 'PRPOS_WCT_SM', fieldName: '계(지출)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_SM', fieldName: '소계(처리장)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_FCLTY', fieldName: '시설비(처리장)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_RPAIR', fieldName: '개보수비(처리장)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_ORPNS', fieldName: '운영 유지비(처리장)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PRCPLT_ETC', fieldName: '기타(처리장)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_SM', fieldName: '소계(관로)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_FCLTY', fieldName: '시설비(관로)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_RPAIR', fieldName: '개보수비(관로)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_ORPNS', fieldName: '운영 유지비(관로)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SEWAGE_ETC', fieldName: '기타(관로)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_SM', fieldName: '소계(저류시설)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_FCLTY', fieldName: '시설비(저류시설)', header: { text: '시설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_RPAIR', fieldName: '개보수비(저류시설)', header: { text: '개보수비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ORPNS', fieldName: '운영 유지비(저류시설)', header: { text: '운영\n유지비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SWGE_STRGE_ETC', fieldName: '기타(저류시설)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_SM', fieldName: '소계(기타지출)', header: { text: '소계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ACDNT_CYFD_AMOUNT', fieldName: '사고 이월액', header: { text: '사고\n이월액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'UNRCIV_AMOUNT', fieldName: '미수납액', header: { text: '미수납액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_AMOUNT', fieldName: '기타(지출)', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 5. 비고 */ - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'ADST_CD', - { - name: 'financeGroup', - header: { text: '재정별 사업비(백만원)' }, - items: [ - 'FNANC_WCT_SM', 'NTNXPN', - { name: 'localGroup', header: { text: '지방비' }, items: ['LCLEXPNS', 'LCLTY_CT'] }, - 'SEWER_RNTFEE', 'AMNTBRNCSR', 'ETC_INCME', 'CYFD_AMOUNT' - ] - }, - 'DEBT_AMOUNT', - { - name: 'usageGroup', - header: { text: '용도별사업비(백만원)' }, - items: [ - 'PRPOS_WCT_SM', - { name: 'plantGroup', header: { text: '처리장' }, items: ['PRCPLT_SM', 'PRCPLT_FCLTY', 'PRCPLT_RPAIR', 'PRCPLT_ORPNS', 'PRCPLT_ETC'] }, - { name: 'pipeGroup', header: { text: '하수관로' }, items: ['SEWAGE_SM', 'SEWAGE_FCLTY', 'SEWAGE_RPAIR', 'SEWAGE_ORPNS', 'SEWAGE_ETC'] }, - { name: 'storageGroup', header: { text: '하수저류시설' }, items: ['SWGE_STRGE_SM', 'SWGE_STRGE_FCLTY', 'SWGE_STRGE_RPAIR', 'SWGE_STRGE_ORPNS', 'SWGE_STRGE_ETC'] }, - { name: 'etcUsageGroup', header: { text: '기타' }, items: ['ETC_SM', 'ACDNT_CYFD_AMOUNT', 'UNRCIV_AMOUNT', 'ETC_AMOUNT'] } - ] - }, - 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '하수도 요금', - rowStartNum: 2, - fixedColumns: 1, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'FYER_LEVY_QY', fieldName: '연간조정량(㎥)(A)', header: { text: '연간조정량\n(㎥)\n(A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LEVY_AMOUNT', fieldName: '하수도사용료수익(원)(B)', header: { text: '하수도사용료수익\n(원)\n(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'AVRG_UNTPC', fieldName: '㎥당요금(원/㎥) C=(B/A)', header: { text: '㎥당요금(원/㎥)\nC=(B/A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SMRIZE_PRMPC', fieldName: '총괄원가(원)(D)', header: { text: '총괄원가(원)\n(D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SMRIZE_UNIT_PRMPC', fieldName: '㎥당원가(원/㎥) E=(D/A)', header: { text: '㎥당원가(원/㎥)\nE=(D/A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RLTY_RT', fieldName: '요금현실화율(%) F=(C/E*100)', header: { text: '요금현실화율(%)\nF=(C/E*100)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'FYER_LEVY_QY', 'LEVY_AMOUNT', 'AVRG_UNTPC', 'SMRIZE_PRMPC', 'SMRIZE_UNIT_PRMPC', 'RLTY_RT', 'RMK' - ] - }, - { - tabKey: 'c', - tabLabel: '하수도 사용료', - rowStartNum: 2, - fixedColumns: 1, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'LEVY_AMOUNT', fieldName: '부과액(총계)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'LEVY_QY', fieldName: '부과량(총계)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'HOME_LEVY_AMOUNT', fieldName: '부과액(가정용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'HOME_LEVY_QY', fieldName: '부과량(가정용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'HOME_UNTPC', fieldName: '단가(가정용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'JOB_LEVY_AMOUNT', fieldName: '부과액(업무용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'JOB_LEVY_QY', fieldName: '부과량(업무용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'JOB_UNTPC', fieldName: '단가(업무용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BSN_LEVY_AMOUNT', fieldName: '부과액(영업용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BSN_LEVY_QY', fieldName: '부과량(영업용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BSN_UNTPC', fieldName: '단가(영업용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS1_LEVY_AMOUNT', fieldName: '부과액(욕탕1)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS1_LEVY_QY', fieldName: '부과량(욕탕1)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS1_UNTPC', fieldName: '단가(욕탕1)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS2_LEVY_AMOUNT', fieldName: '부과액(욕탕2)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS2_LEVY_QY', fieldName: '부과량(욕탕2)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'BATHS2_UNTPC', fieldName: '단가(욕탕2)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INDUST_LEVY_AMOUNT', fieldName: '부과액(산업용)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INDUST_LEVY_QY', fieldName: '부과량(산업용)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'INDUST_UNTPC', fieldName: '단가(산업용)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_LEVY_AMOUNT', fieldName: '부과액(기타사용료)', header: { text: '부과액' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_LEVY_QY', fieldName: '부과량(기타사용료)', header: { text: '부과량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'ETC_UNTPC', fieldName: '단가(기타사용료)', header: { text: '단가' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - { - name: 'totalGroup', header: { text: '총계' }, - items: [{ name: 'tAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['LEVY_AMOUNT'] }, { name: 'tQyUnit', header: { text: '부과량\n(천톤)' }, items: ['LEVY_QY'] }] - }, - { - name: 'homeGroup', header: { text: '가 정 용' }, - items: [{ name: 'hAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['HOME_LEVY_AMOUNT'] }, { name: 'hQyUnit', header: { text: '부과량\n(천톤)' }, items: ['HOME_LEVY_QY'] }, { name: 'hUntUnit', header: { text: '단가\n(원/톤)' }, items: ['HOME_UNTPC'] }] - }, - { - name: 'jobGroup', header: { text: '업 무 용' }, - items: [{ name: 'jAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['JOB_LEVY_AMOUNT'] }, { name: 'jQyUnit', header: { text: '부과량\n(천톤)' }, items: ['JOB_LEVY_QY'] }, { name: 'jUntUnit', header: { text: '단가\n(원/톤)' }, items: ['JOB_UNTPC'] }] - }, - { - name: 'bsnGroup', header: { text: '영 업 용(일 반 용)' }, - items: [{ name: 'bAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BSN_LEVY_AMOUNT'] }, { name: 'bQyUnit', header: { text: '부과량\n(천톤)' }, items: ['BSN_LEVY_QY'] }, { name: 'bUntUnit', header: { text: '단가\n(원/톤)' }, items: ['BSN_UNTPC'] }] - }, - { - name: 'bath1Group', header: { text: '욕 탕 1종(대중탕용)' }, - items: [{ name: 'bt1AmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BATHS1_LEVY_AMOUNT'] }, { name: 'bt1QyUnit', header: { text: '부과량\n(천톤)' }, items: ['BATHS1_LEVY_QY'] }, { name: 'bt1UntUnit', header: { text: '단가\n(원/톤)' }, items: ['BATHS1_UNTPC'] }] - }, - { - name: 'bath2Group', header: { text: '욕 탕 2종' }, - items: [{ name: 'bt2AmtUnit', header: { text: '부과액\n(백만원)' }, items: ['BATHS2_LEVY_AMOUNT'] }, { name: 'bt2QyUnit', header: { text: '부과량\n(천톤)' }, items: ['BATHS2_LEVY_QY'] }, { name: 'bt2UntUnit', header: { text: '단가\n(원/톤)' }, items: ['BATHS2_UNTPC'] }] - }, - { - name: 'industGroup', header: { text: '산업용 및 공업용' }, - items: [{ name: 'iAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['INDUST_LEVY_AMOUNT'] }, { name: 'iQyUnit', header: { text: '부과량\n(천톤)' }, items: ['INDUST_LEVY_QY'] }, { name: 'iUntUnit', header: { text: '단가\n(원/톤)' }, items: ['INDUST_UNTPC'] }] - }, - { - name: 'etcGroup', header: { text: '기타' }, - items: [{ name: 'eAmtUnit', header: { text: '부과액\n(백만원)' }, items: ['ETC_LEVY_AMOUNT'] }, { name: 'eQyUnit', header: { text: '부과량\n(천톤)' }, items: ['ETC_LEVY_QY'] }, { name: 'eUntUnit', header: { text: '단가\n(원/톤)' }, items: ['ETC_UNTPC'] }] - }, - 'RMK' - ] - }, - { - tabKey: 'd', - tabLabel: '원인자부담금', - rowStartNum: 3, - fixedColumns: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'LEVY_CO', fieldName: '건수(부과액)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'LEVY_AMOUNT', fieldName: '금액(백만원)(부과액)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'TRGET_LEV_STTUS_CO', fieldName: '건수(계)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'TRGET_LEV_STTUS_AMOUNT', fieldName: '금액(백만원)(계)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'DRNG_EQP_CO', fieldName: '건수(배수설비)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'DRNG_EQP_AMOUNT', fieldName: '금액(백만원)(배수설비)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CNTRWK_CO', fieldName: '건수(타공사)', header: { text: '건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CNTRWK_AMOUNT', fieldName: '금액(백만원)(타공사)', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - { name: 'levyMainGroup', header: { text: '부과액' }, items: ['LEVY_CO', 'LEVY_AMOUNT'] }, - { - name: 'collectGroup', header: { text: '대상별 징수현황' }, - items: [ - { name: 'collectTotal', header: { text: '계' }, items: ['TRGET_LEV_STTUS_CO', 'TRGET_LEV_STTUS_AMOUNT'] }, - { name: 'contractorGroup', header: { text: '배수설비설치자\n(하수도법 제61조 제1항)' }, items: ['DRNG_EQP_CO', 'DRNG_EQP_AMOUNT'] }, - { name: 'otherGroup', header: { text: '타공사·타행위자\n(하수도법 제61조 제2항)' }, items: ['CNTRWK_CO', 'CNTRWK_AMOUNT'] } - ] - }, - 'RMK' - ] + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; -export const SewerageFinanceInfoListRvw = rawFinanceList.map(tab => ({ +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + // { + // name: "admdstNm", + // fieldName: "admdstNm", + // header: { text: "행정구역명" }, + // mergeCells: true, + // sortdisable: true, + // }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + // "admdstNm", + ], +}; + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 09. 하수도 재정 +const rawFinanceList = [ + { + tabKey: "ST00090001", + tabLabel: "하수도재정 결산", + rowStartNum: 3, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + decimal1Col("fnnByWholPjtcoAmt", "계"), + decimal1Col("cnterGovSprtAmt", "중앙정부\n지원액"), + decimal1Col("mctpvLcexpAmt", "특․광역시, 도"), + decimal1Col("wareaSggLclAmt", "시,군,구 지자체"), + decimal1Col("sewerUseAmt", "하수도\n사용료"), + decimal1Col("orgprBrdnAmt", "원인자\n부담금"), + decimal1Col("etcIncmAmt", "기타\n잡수입"), + decimal1Col("crfwAmt", "이월액"), + decimal1Col("lbltAmt", "부채액\n(백만원)"), + decimal1Col("usgByWholPjtcoAmt", "계"), + decimal1Col("tmplntWholAmt", "소계"), + decimal1Col("tmplntFcltAmt", "시설비"), + decimal1Col("tmplntMendngAmt", "개보수비"), + decimal1Col("tmplntOperKeepAmt", "운영\n유지비"), + decimal1Col("tmplntEtcAmt", "기타"), + decimal1Col("wholSwagePplAmt", "소계"), + decimal1Col("swagePplFcltAmt", "시설비"), + decimal1Col("swagePplMendngAmt", "개보수비"), + decimal1Col("swagePplOperKeepAmt", "운영\n유지비"), + decimal1Col("swagePplEtcAmt", "기타"), + decimal1Col("wholSwageImpdFcltAmt", "소계"), + decimal1Col("swageImpdFcltAmt", "시설비"), + decimal1Col("swageImpdMendngAmt", "개보수비"), + decimal1Col("swageImpdOperKeepAmt", "운영\n유지비"), + decimal1Col("swageImpdFcltEtcAmt", "기타"), + decimal1Col("etcWholAmt", "소계"), + decimal1Col("etcAcdntCrfwAmt", "사고\n이월액"), + decimal1Col("etcNrcpAmt", "미수납액"), + decimal1Col("etcAmt", "기타"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { + name: "financeGroup", + header: { text: "재정별 사업비(백만원)" }, + items: [ + "fnnByWholPjtcoAmt", + "cnterGovSprtAmt", + { + name: "localGroup", + header: { text: "지방비" }, + items: ["mctpvLcexpAmt", "wareaSggLclAmt"], + }, + "sewerUseAmt", + "orgprBrdnAmt", + "etcIncmAmt", + "crfwAmt", + ], + }, + "lbltAmt", + { + name: "purposeGroup", + header: { text: "용도별사업비(백만원)" }, + items: [ + "usgByWholPjtcoAmt", + { + name: "t1Group", + header: { text: "처리장" }, + items: [ + "tmplntWholAmt", + "tmplntFcltAmt", + "tmplntMendngAmt", + "tmplntOperKeepAmt", + "tmplntEtcAmt", + ], + }, + { + name: "t2Group", + header: { text: "하수관로" }, + items: [ + "wholSwagePplAmt", + "swagePplFcltAmt", + "swagePplMendngAmt", + "swagePplOperKeepAmt", + "swagePplEtcAmt", + ], + }, + { + name: "t3Group", + header: { text: "유수지 및 배수펌프장" }, + items: [ + "wholSwageImpdFcltAmt", + "swageImpdFcltAmt", + "swageImpdMendngAmt", + "swageImpdOperKeepAmt", + "swageImpdFcltEtcAmt", + ], + }, + { + name: "t4Group", + header: { text: "기타" }, + items: ["etcWholAmt", "etcAcdntCrfwAmt", "etcNrcpAmt", "etcAmt"], + }, + ], + }, + "rmrk", + ], + }, + { + tabKey: "ST00090002", + tabLabel: "하수도 요금", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("anlLevyQnt", "연간조정량(㎡)"), + intCol("levyAmt", "하수도사용료수익(원)"), + decimal1Col("avgUntprcAmt", "㎡당 요금(원/㎡)"), + intCol("grssPrmcoAmt", "총관원가(원)"), + decimal1Col("grssUnitPrmcoAmt", "㎡당원가(원/㎡)"), + { + name: "rlztnRt", + fieldName: "rlztnRt", + header: { text: "현실화율(%)" }, + footer: footerAvgDecimalArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + "anlLevyQnt", + "levyAmt", + "avgUntprcAmt", + "grssPrmcoAmt", + "grssUnitPrmcoAmt", + "rlztnRt", + "rmrk", + ], + }, + { + tabKey: "ST00090003", + tabLabel: "하수도 사용료", + rowStartNum: 3, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + decimal1Col("levyAmt", "부과액(백만원)"), + decimal1Col("levyQnt", "부과량(천톤)"), + decimal1Col("hshusLevyAmt", "부과액"), + decimal1Col("hshusLevyQnt", "부과량"), + intCol("hshusUntprcAmt", "단가"), + decimal1Col("taskFrLevyAmt", "부과액"), + decimal1Col("taskFrLevyQnt", "부과량"), + intCol("taskFrUntprcAmt", "단가"), + decimal1Col("bsnGnrlLevyAmt", "부과액(백만원)"), + decimal1Col("bsnGnrlLevyQnt", "부과량(천톤)"), + intCol("bsnGnrlUntprcAmt", "단가(원/톤)"), + decimal1Col("massBthhsFrLevyAmt", "부과액(백만원)"), + decimal1Col("massBthhsFrLevyQnt", "부과량(천톤)"), + intCol("massBthhsFrUntprcAmt", "단가(원/톤)"), + decimal1Col("bthhsFrSpcs2LevyAmt", "부과액"), + decimal1Col("bthhsFrSpcs2LevyQnt", "부과량"), + intCol("bthhsFrSpcs2UntprcAmt", "단가"), + decimal1Col("induMdsrLevyAmt", "부과액(백만원)"), + decimal1Col("induMdsrLevyQnt", "부과량(천톤)"), + intCol("induMdsrUntprcAmt", "단가(원/톤)"), + decimal1Col("etcLevyAmt", "부과액"), + decimal1Col("etcLevyQnt", "부과량"), + intCol("etcLevyUntprcAmt", "단가"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { name: "totalGroup", header: { text: "총계" }, items: ["levyAmt", "levyQnt"] }, + { + name: "g1Group", + header: { text: "가정용" }, + items: ["hshusLevyAmt", "hshusLevyQnt", "hshusUntprcAmt"], + }, + { + name: "g2Group", + header: { text: "업무용" }, + items: ["taskFrLevyAmt", "taskFrLevyQnt", "taskFrUntprcAmt"], + }, + { + name: "g3Group", + header: { text: "영업용(일반용)" }, + items: ["bsnGnrlLevyAmt", "bsnGnrlLevyQnt", "bsnGnrlUntprcAmt"], + }, + { + name: "g4Group", + header: { text: "욕탕 1종(대중탕용)" }, + items: ["massBthhsFrLevyAmt", "massBthhsFrLevyQnt", "massBthhsFrUntprcAmt"], + }, + { + name: "g5Group", + header: { text: "욕탕2종" }, + items: ["bthhsFrSpcs2LevyAmt", "bthhsFrSpcs2LevyQnt", "bthhsFrSpcs2UntprcAmt"], + }, + { + name: "g6Group", + header: { text: "산업 및 공업용" }, + items: ["induMdsrLevyAmt", "induMdsrLevyQnt", "induMdsrUntprcAmt"], + }, + { + name: "g7Group", + header: { text: "기타" }, + items: ["etcLevyAmt", "etcLevyQnt", "etcLevyUntprcAmt"], + }, + "rmrk", + ], + }, + { + tabKey: "ST00090004", + tabLabel: "원인자부담금", + rowStartNum: 3, + fixedColumns: 2, + columns: [ + ...reviewAddressColumns, + intCol("levyEvntcsCnt", "건수"), + decimal1Col("levyAmt", "금액(백만원)"), + intCol("trgtByWholClctnEvntcsCnt", "건수"), + decimal1Col("trgtByWholClctnAmt", "금액(백만원)"), + intCol("mltvEqpmEvntcsCnt", "건수"), + decimal1Col("mltvEqpmAmt", "금액(백만원)"), + intCol("othrCstrnOthrActrCnt", "건수"), + decimal1Col("othrCstrnOthrActrAmt", "금액(백만원)"), + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, + ], + columnLayout: [ + reviewAddressLayout, + "displayYear", + { name: "levyGroup", header: { text: "부과액" }, items: ["levyEvntcsCnt", "levyAmt"] }, + { + name: "collectionStatusGroup", + header: { text: "대상별 징수현황" }, + items: [ + { + name: "c1Group", + header: { text: "계" }, + items: ["trgtByWholClctnEvntcsCnt", "trgtByWholClctnAmt"], + }, + { + name: "c2Group", + header: { text: "배수설비설치자\n(하수도법 제61조 제1항)" }, + items: ["mltvEqpmEvntcsCnt", "mltvEqpmAmt"], + }, + { + name: "c3Group", + header: { text: "타공사·타행위자\n(하수도법 제61조 제2항)" }, + items: ["othrCstrnOthrActrCnt", "othrCstrnOthrActrAmt"], + }, + ], + }, + "rmrk", + ], + }, +]; + +export const SewerageFinanceInfoListRvw = rawFinanceList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); diff --git a/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts b/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts index c673bbda5..316825d5b 100644 --- a/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewerageServiceLevelRvwColumn.ts @@ -1,117 +1,213 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 13. 하수도 서비스 수준 +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerAvgDecimalArrayByDivision, + footerDecimal1ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; + +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); + + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } + + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } + + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; + +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, +]; + +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + // { + // name: "admdstNm", + // fieldName: "admdstNm", + // header: { text: "행정구역명" }, + // mergeCells: true, + // sortdisable: true, + // }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + "sggNm", + // "admdstNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const avgDecimalCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerAvgDecimalArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +// 하수도 통계 검토 13. 하수도 서비스 수준 const rawServiceLevelList = [ { - tabKey: 'a', - tabLabel: '민원', + tabKey: "ST00130001", + tabLabel: "민원", rowStartNum: 2, fixedColumns: 2, columns: [ - /* 1. 기본 정보 */ - { name: 'SIDO', fieldName: 'SIDO', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: 'GUGUN', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: 'YR_GBN', header: { text: '구분' } }, - - /* 2. 하수처리 인구 (정수 반영) */ - { name: 'POPLTN_SM', fieldName: 'POPLTN_SM', header: { text: '하수처리인구\n(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 3. 민원유형 (단위: 건 / 정수 반영) */ - { name: 'CVPL_CM', fieldName: 'CVPL_CM', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_DCWTR_QLTWTR', fieldName: 'CVPL_DCWTR_QLTWTR', header: { text: '악취' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_BSML', fieldName: 'CVPL_BSML', header: { text: '하수관로' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_FIVW', fieldName: 'CVPL_FIVW', header: { text: '배수설비' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_POLLUTN_ACDNT', fieldName: 'CVPL_POLLUTN_ACDNT', header: { text: '맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_SSOS', fieldName: 'CVPL_SSOS', header: { text: '정화조' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_CHRGE', fieldName: 'CVPL_CHRGE', header: { text: '하수도요금' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CVPL_ETC', fieldName: 'CVPL_ETC', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 성과 지표 및 비고 (소수점 1자리 반영) */ - { name: 'STDR_POPLTN_PER_CVPL', fieldName: 'STDR_POPLTN_PER_CVPL', header: { text: '기준인구당\n민원건수\n(건/10,000명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: 'RMK', header: { text: '비고' } } + ...reviewAddressColumns, + intCol("swtrmtPpltnCnt", "하수처리인구\n(명)"), + intCol("whol", "총계"), + intCol("dschgWqCnt", "악취"), + intCol("odorCnt", "하수관로"), + intCol("ldsCnt", "배수설비"), + intCol("plltnAcdntCnt", "맨홀"), + intCol("swagePplCnt", "정화조"), + intCol("sewerCrgAmt", "하수도요금"), + intCol("etcCnt", "기타"), + avgDecimalCol("crtrPpltnCvlcptEvntcsCnt", "기준인구당\n민원건수\n(건/10,000명)"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - 'POPLTN_SM', + reviewAddressLayout, + "displayYear", + "swtrmtPpltnCnt", { - name: 'complaintTypeGroup', - header: { text: '민원유형(건)' }, + name: "complaintTypeGroup", + header: { text: "민원유형(건)" }, items: [ - 'CVPL_CM', - 'CVPL_DCWTR_QLTWTR', - 'CVPL_BSML', - 'CVPL_FIVW', - 'CVPL_POLLUTN_ACDNT', - 'CVPL_SSOS', - 'CVPL_CHRGE', - 'CVPL_ETC' - ] + "whol", + "dschgWqCnt", + "odorCnt", + "ldsCnt", + "plltnAcdntCnt", + "swagePplCnt", + "sewerCrgAmt", + "etcCnt", + ], }, - 'STDR_POPLTN_PER_CVPL', - 'RMK' - ] + "crtrPpltnCvlcptEvntcsCnt", + "rmrk", + ], }, { - tabKey: 'b', - tabLabel: '주민친화시설', + tabKey: "ST00130002", + tabLabel: "주민친화시설", rowStartNum: 2, - fixedColumns: 5, + fixedColumns: 2, columns: [ - /* 1. 기본 및 시설 정보 */ - { name: 'SIDO', fieldName: 'SIDO', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: 'GUGUN', header: { text: '시군구' } }, - { name: 'BSC_FCT_NM', fieldName: 'BSC_FCT_NM', header: { text: '처리장명' } }, - { name: 'FCT_CPC', fieldName: 'FCT_CPC', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'YR_GBN', fieldName: 'YR_GBN', header: { text: '구분' } }, - - /* 2. 처리장 전체 면적 (소수점 1자리 반영) */ - { name: 'PRCPLT_AR', fieldName: 'PRCPLT_AR', header: { text: '처리장\n면적(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - - /* 3. 주민친화시설 면적 상세 (단위: ㎡ / 정수 반영) */ - { name: 'TOTAL_AR', fieldName: 'TOTAL_AR', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'RESTRM_AR', fieldName: 'RESTRM_AR', header: { text: '휴게시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CHILDPLAY_AR', fieldName: 'CHILDPLAY_AR', header: { text: '어린이놀이공간' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ALSFC_AR', fieldName: 'ALSFC_AR', header: { text: '체력단련시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PLGRND_AR', fieldName: 'PLGRND_AR', header: { text: '운동시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'PRKPLCE_AR', fieldName: 'PRKPLCE_AR', header: { text: '주차장' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'CTLSTT_AR', fieldName: 'CTLSTT_AR', header: { text: '교육 및\n문화시설' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'NRPRK_AR', fieldName: 'NRPRK_AR', header: { text: '습지조성\n(생태공원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - { name: 'ETC_AR', fieldName: 'ETC_AR', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, - - /* 4. 비고 */ - { name: 'RMK', fieldName: 'RMK', header: { text: '비고' } } + ...reviewAddressColumns, + textCol("swtrmtFcltNm", "처리장명"), + intCol("fcltCpct", "시설용량\n(㎥/일)"), + decimal1Col("tmplntArea", "처리장\n면적(㎡)"), + decimal1Col("whol", "총계"), + decimal1Col("brkFcltArea", "휴게시설"), + decimal1Col("childFrshpSpacArea", "어린이놀이공간"), + decimal1Col("bdySportsFcltArea", "체력단련시설"), + decimal1Col("sportsFcltArea", "운동시설"), + decimal1Col("pkltArea", "주차장"), + decimal1Col("eduMultiFcltArea", "교육 및\n문화시설"), + decimal1Col("wtlndMakeArea", "습지조성\n(생태공원)"), + decimal1Col("etcCnt", "기타"), + textCol("rmrk", "비고"), ], columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BSC_FCT_NM', - 'FCT_CPC', - 'YR_GBN', - 'PRCPLT_AR', + reviewAddressLayout, + "displayYear", + "swtrmtFcltNm", + "fcltCpct", + "tmplntArea", { - name: 'friendlyFacilityGroup', - header: { text: '주민친화시설 면적(㎡)' }, + name: "friendlyFacilityGroup", + header: { text: "주민친화시설 면적(㎡)" }, items: [ - 'TOTAL_AR', - 'RESTRM_AR', - 'CHILDPLAY_AR', - 'ALSFC_AR', - 'PLGRND_AR', - 'PRKPLCE_AR', - 'CTLSTT_AR', - 'NRPRK_AR', - 'ETC_AR' - ] + "whol", + "brkFcltArea", + "childFrshpSpacArea", + "bdySportsFcltArea", + "sportsFcltArea", + "pkltArea", + "eduMultiFcltArea", + "wtlndMakeArea", + "etcCnt", + ], }, - 'RMK' - ] - } + "rmrk", + ], + }, ]; -export const SewerageServiceLevelInfoListRvw = rawServiceLevelList.map(tab => ({ +export const SewerageServiceLevelInfoListRvw = rawServiceLevelList.map((tab) => ({ ...tab, - columns: tab.columns.map(col => ({ + columns: tab.columns.map((col) => ({ ...col, - editable: false - })) -})); \ No newline at end of file + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `SewerageServiceLevelInfoListRvw` 사용 */ +export const SewerageServiceLevelInfoList = SewerageServiceLevelInfoListRvw; diff --git a/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts b/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts index 04026197f..2d045707d 100644 --- a/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts +++ b/apps/st/src/components/util/column/review/WaterReuseRvwColumn.ts @@ -1,224 +1,366 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 10. 물 재이용 -const rawWaterReuseList = [ - { - tabKey: 'a', - tabLabel: '공공하수처리수 재이용', - rowStartNum: 4, - fixedColumns: 2, - columns: [ - /* 1. 기본 및 행정 정보 */ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, +import type { GridView } from "@/components/type/DataGrid.type"; +import { + footerAvgDecimalArrayByDivision, + footerDecimal1ArrayByDivision, + footerDecimal2ArrayByDivision, + footerIntArrayByDivision, +} from "@/components/util/grid/footerSummary"; - /* 2. 하수처리장 기본 정보 */ - { name: 'BSC_FCT_NM', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'EQP_PRMS_DT', fieldName: '인가일(년/월/일)', header: { text: '인가일\n(년/월/일)' } }, - { name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'TPUT_TOTAL', fieldName: '연간 하수처리량(천톤/년)', header: { text: '연간\n하수처리량\n(천톤/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RSWTR_LAW_YN', fieldName: '법적의무대상(대상/미대상)', header: { text: '법적의무대상\n(대상/미대상)' } }, +const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => { + const dataSource = grid.getDataSource(); + const uniqueValues = new Set(); - /* 3. 재이용시설 현황 */ - { name: 'INST_YN', fieldName: '설치유무(유/무)', header: { text: '설치유무\n(유/무)' } }, - { name: 'RSWTR_RN_DT', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일\nyyyy-mm-dd' } }, - { name: 'RSWTR_PRMS_DT', fieldName: '설치승인일 yyyy-mm-dd', header: { text: '설치승인일\nyyyy-mm-dd' } }, - { name: 'RSWTR_CPC', fieldName: '용량(톤/일)', header: { text: '용량(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RSWTR_TRT_NM', fieldName: '공법/기자재', header: { text: '공법/기자재' } }, - { name: 'RSWTR_PIPE', fieldName: '공급관로길이(km)', header: { text: '공급관로길이\n(km)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RSWTR_PUMP', fieldName: '공급펌프(톤/일)', header: { text: '공급펌프\n(톤/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + for (let i = 0; i < dataSource.getRowCount(); i++) { + const val = dataSource.getValue(i, "displayYear"); + if (val) uniqueValues.add(String(val)); + } - /* 4. 재이용 실적 - 총계 및 장내용수 */ - { name: 'USE_AMNT_TOTAL', fieldName: '총계', header: { text: '총계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_D01', fieldName: '계(장내)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S01', fieldName: '세척수', header: { text: '세척수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S02', fieldName: '냉각수', header: { text: '냉각수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S03', fieldName: '청소수', header: { text: '청소수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S04', fieldName: '조경용수(장내)', header: { text: '조경용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S05', fieldName: '희석용수', header: { text: '희석용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S06', fieldName: '장내중수도', header: { text: '장내중수도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S07', fieldName: '장내기타용수', header: { text: '장내기타용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + const divisionValues = Array.from(uniqueValues); + if (divisionValues.length <= divisionIndex) { + return withSuffix ? "합계" : ""; + } - /* 5. 재이용 실적 - 장외용수 */ - { name: 'USE_AMNT_D02', fieldName: '계(장외)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S11', fieldName: '청소·화장실 용수', header: { text: '청소·화장실\n용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S12', fieldName: '세척·살수용수', header: { text: '세척·살수용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S13', fieldName: '조경용수(장외)', header: { text: '조경용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S14', fieldName: '친수용수', header: { text: '친수용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S15', fieldName: '하천등 유지용수', header: { text: '하천등 유지용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S16', fieldName: '농업용수', header: { text: '농업용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S17', fieldName: '지하수충전', header: { text: '지하수충전' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'USE_AMNT_S18', fieldName: '공업용수', header: { text: '공업용수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + const label = divisionValues[divisionIndex] ?? ""; + return withSuffix ? `${label}합계` : label; +}; - /* 6. 지표 및 비고 */ - { name: 'REUSE_RT', fieldName: '재이용률(%)', header: { text: '재이용률\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', - { - name: 'sewagePlantGroup', - header: { text: '하수처리장' }, - items: ['BSC_FCT_NM', 'EQP_PRMS_DT', 'FCT_CPC', 'TPUT_TOTAL', 'RSWTR_LAW_YN'] - }, - { - name: 'reuseFctGroup', - header: { text: '재이용시설' }, - items: [ - 'INST_YN', - { - name: 'installDetailGroup', - header: { text: '재이용시설 설치시' }, - items: [ - 'RSWTR_RN_DT', - 'RSWTR_PRMS_DT', - { - name: 'reProcessGroup', - header: { text: '재처리시설' }, - items: ['RSWTR_CPC', 'RSWTR_TRT_NM'] - }, - 'RSWTR_PIPE', - 'RSWTR_PUMP' - ] - } - ] - }, - { - name: 'reuseStatusGroup', - header: { text: '하수처리수 재이용현황(천톤/년)' }, - items: [ - 'USE_AMNT_TOTAL', - { - name: 'internalUseGroup', - header: { text: '장 내 용 수' }, - items: [ - 'USE_AMNT_D01', 'USE_AMNT_S01', 'USE_AMNT_S02', 'USE_AMNT_S03', - 'USE_AMNT_S04', 'USE_AMNT_S05', 'USE_AMNT_S06', 'USE_AMNT_S07' - ] - }, - { - name: 'externalUseGroup', - header: { text: '장 외 용 수' }, - items: [ - 'USE_AMNT_D02', 'USE_AMNT_S11', 'USE_AMNT_S12', 'USE_AMNT_S13', - 'USE_AMNT_S14', 'USE_AMNT_S15', 'USE_AMNT_S16', 'USE_AMNT_S17', 'USE_AMNT_S18' - ] - } - ] - }, - 'REUSE_RT', - 'RMK' - ] - }, - { - tabKey: 'b', - tabLabel: '중수도 현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'BILD_NM', fieldName: '건축물명/개발사업명', header: { text: '건축물명/\n개발사업명' } }, - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'BILD_TP', fieldName: '시설물용도/개발사업종류', header: { text: '시설물용도/\n개발사업종류' } }, - { name: 'LAW_YN', fieldName: '법적의무 대상여부(대상/미대상)', header: { text: '법적의무\n대상여부\n(대상/미대상)' } }, - { name: 'COMP_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일\nyyyy-mm-dd' } }, - { name: 'RN_DT', fieldName: '가동개시일 yyyy-mm-dd', header: { text: '가동개시일\nyyyy-mm-dd' } }, - { name: 'TOTAR', fieldName: '건축연면적(㎡)', header: { text: '건축연면적\n(㎡)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'PROC_CPCTY', fieldName: '처리용량(㎥/일)', header: { text: '처리용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'WATER_USE_CPCTY', fieldName: '물 사용량(㎥/년)', header: { text: '물 사용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RUSWTR_USE_SM', fieldName: '중수도 이용량(㎥/년)', header: { text: '중수도\n이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'SUB_USE_CPCTY', fieldName: '대체수 이용량(㎥/년)', header: { text: '대체수\n이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RUSWTR_PRPOS', fieldName: '중수도용도', header: { text: '중수도용도' } }, - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비\n(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'BILD_NM', 'ADDR', 'YR_GBN', 'BILD_TP', 'LAW_YN', 'COMP_DT', 'RN_DT', - { - name: 'fctStatusGroup', - header: { text: '시설현황' }, - items: ['TOTAR', 'PROC_CPCTY'] - }, - 'WATER_USE_CPCTY', 'RUSWTR_USE_SM', 'SUB_USE_CPCTY', 'RUSWTR_PRPOS', 'FYER_OPER_CT', 'RMK' - ] - }, - { - tabKey: 'c', - tabLabel: '빗물이용시설 현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'RWR_REUSE_FCT_NM', fieldName: '시설명', header: { text: '시설명' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'BILD_TP', fieldName: '건축물용도', header: { text: '건축물용도' } }, - { name: 'ADDR', fieldName: '위치(주소)', header: { text: '위치\n(주소)' } }, - { name: 'LAW_YN', fieldName: '법적의무 대상여부(대상/미대상)', header: { text: '법적의무\n대상여부\n(대상/미대상)' } }, - { name: 'RN_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일\nyyyy-mm-dd' } }, - { name: 'INSTL_CT', fieldName: '설치비(백만원)', header: { text: '설치비\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'CCM', fieldName: '집수면 종류', header: { text: '집수면 종류' } }, - { name: 'TRTFC_ENNC', fieldName: '여과등 처리시설 유무', header: { text: '여과등\n처리시설\n유무' } }, - { name: 'SGBS_CPCTY', fieldName: '저류조 용량(㎥)', header: { text: '저류조 용량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'FYER_USEQTY', fieldName: '연간 이용량(㎥/년)', header: { text: '연간 이용량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RWR_PRCUSE_PRPPOS', fieldName: '이용용도', header: { text: '이용용도' } }, - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비\n(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'RWR_REUSE_FCT_NM', 'YR_GBN', 'BILD_TP', 'ADDR', 'LAW_YN', 'RN_DT', 'INSTL_CT', 'CCM', 'TRTFC_ENNC', 'SGBS_CPCTY', 'FYER_USEQTY', 'RWR_PRCUSE_PRPPOS', 'FYER_OPER_CT', 'RMK' - ] - }, - { - tabKey: 'd', - tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 등록현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, - { name: 'CMPN_RGSDE', fieldName: '등록일 yyyy-mm-dd', header: { text: '등록일\nyyyy-mm-dd' } }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' } }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' } }, - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK' - ] - }, - { - tabKey: 'e', - tabLabel: '하ㆍ폐수처리수 재이용시설 시공업 인가현황', - rowStartNum: 2, - columns: [ - { name: 'SIDO', fieldName: '시도', header: { text: '시도' } }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - { name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } }, - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, - { name: 'CMPN_RGSDE', fieldName: '인가일(년/월/일)', header: { text: '인가일\n(년/월/일)' } }, - { name: 'RMK', fieldName: '비고', header: { text: '비고' } } - ], - columnLayout: [ - { name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] }, - 'YR_GBN', 'CMPNM', 'RPRSNTV', 'ADDR', 'CMPN_RGSDE', 'RMK' - ] - } +const divisionFooterCallbacks = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) }, ]; -export const WaterReuseInfoList = rawWaterReuseList.map(tab => ({ - ...tab, - columns: tab.columns.map(col => ({ - ...col, - editable: false - })) -})); \ No newline at end of file +const divisionFooterLabelOnly = [ + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) }, + { valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) }, +]; + +const reviewFacilityAddressColumns = [ + { + name: "ctpvNm", + fieldName: "ctpvNm", + header: { text: "시도" }, + mergeCells: true, + sortdisable: true, + footer: divisionFooterCallbacks, + }, + { + name: "sggNm", + fieldName: "sggNm", + header: { text: "시군구" }, + mergeCells: true, + sortdisable: true, + }, + { + name: "displayYear", + fieldName: "displayYear", + header: { text: "구분" }, + sortdisable: true, + footer: divisionFooterLabelOnly, + }, +]; + +const reviewFacilityAddressLayout = { + name: "areaGroup", + header: { text: "행정구역" }, + items: [ + { column: "ctpvNm", footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + "sggNm", + ], +}; + +const textCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, +}); + +const decimal1Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal1ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", +}); + +const decimal2Col = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerDecimal2ArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.00", +}); + +const intCol = (name: string, header: string) => ({ + name, + fieldName: name, + header: { text: header }, + footer: footerIntArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0", +}); + +// 하수도 통계 검토 10. 물 재이용 +const rawWaterReuseList = [ + { + tabKey: "ST00100001", + tabLabel: "공공하수처리수 재이용", + rowStartNum: 4, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("swtrmtFcltNm", "시설명"), + textCol("sncnYmd", "인가일\n(년/월/일)"), + decimal1Col("prcsFcltCpct", "시설용량\n(㎥/일)"), + decimal1Col("prcsQnt", "연간\n하수처리량\n(천톤/년)"), + textCol("lglYn", "법적의무대상\n(대상/미대상)"), + textCol("instlYn", "설치유무\n(유/무)"), + textCol("oprtngStrtYmd", "가동개시일\nyyyy-mm-dd"), + textCol("instlAprvYmd", "설치승인일\nyyyy-mm-dd"), + decimal1Col("rprcsFcltCpct", "용량(톤/일)"), + textCol("mtcnsNm", "공법/기자재"), + decimal2Col("ruswtPplLen", "공급관로길이\n(km)"), + decimal1Col("ruswtPumpLen", "공급펌프\n(톤/일)"), + decimal1Col("wholUseQnt", "총계"), + decimal1Col("ithlWshwtrQnt", "계"), + decimal1Col("ithlClnQty", "세척수"), + decimal1Col("ithlFrzgQty", "냉각수"), + decimal1Col("ithlLdhcUswtrQnt", "청소수"), + decimal1Col("ithlDltnUswtrQnt", "조경용수"), + decimal1Col("ithlRuswtrQnt", "희석용수"), + decimal1Col("ithlEtcQnt", "장내중수도"), + decimal1Col("ithlUnsdQnt", "장내기타용수"), + decimal1Col("otcntrUswtrWhol", "계"), + decimal1Col("otcntrClnRstrmQnt", "청소·화장실\n용수"), + decimal1Col("otcntrWshwtrQnt", "세척·살수용수"), + decimal1Col("otcntrLdhcUswtrQnt", "조경용수"), + decimal1Col("otcntrHdrplUswtrQnt", "친수용수"), + decimal1Col("otcntrRvrKeepQnt", "하천등 유지용수"), + decimal1Col("agrcltUswtrPstnQnt", "농업용수"), + decimal1Col("ugwtrChargQnt", "지하수충전"), + decimal1Col("mdsrUswtrQnt", "공업용수"), + { + name: "rtlztnRt", + fieldName: "rtlztnRt", + header: { text: "재이용률\n(%)" }, + footer: footerAvgDecimalArrayByDivision, + valueType: ValueType.NUMBER, + numberFormat: "#,##0.0", + }, + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + { + name: "seweragePlantGroup", + header: { text: "하수처리장" }, + items: ["swtrmtFcltNm", "sncnYmd", "prcsFcltCpct", "prcsQnt", "lglYn"], + }, + { + name: "reuseFacilityGroup", + header: { text: "재이용시설" }, + items: [ + "instlYn", + { + name: "installationGroup", + header: { text: "재이용시설 설치시" }, + items: [ + "oprtngStrtYmd", + "instlAprvYmd", + { + name: "reProcessingFacilityGroup", + header: { text: "재처리시설" }, + items: ["rprcsFcltCpct", "mtcnsNm"], + }, + "ruswtPplLen", + "ruswtPumpLen", + ], + }, + ], + }, + { + name: "reuseStatusGroup", + header: { text: "하수처리수 재이용현황(천톤/년)" }, + items: [ + "wholUseQnt", + { + name: "internalWaterGroup", + header: { text: "장 내 용 수" }, + items: [ + "ithlWshwtrQnt", + "ithlClnQty", + "ithlFrzgQty", + "ithlLdhcUswtrQnt", + "ithlDltnUswtrQnt", + "ithlRuswtrQnt", + "ithlEtcQnt", + "ithlUnsdQnt", + ], + }, + { + name: "externalWaterGroup", + header: { text: "장 외 용 수" }, + items: [ + "otcntrUswtrWhol", + "otcntrClnRstrmQnt", + "otcntrWshwtrQnt", + "otcntrLdhcUswtrQnt", + "otcntrHdrplUswtrQnt", + "otcntrRvrKeepQnt", + "agrcltUswtrPstnQnt", + "ugwtrChargQnt", + "mdsrUswtrQnt", + ], + }, + ], + }, + "rtlztnRt", + "rmrk", + ], + }, + { + tabKey: "ST00100002", + tabLabel: "중수도 현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("fcltNm", "건축물명/\n개발사업명"), + textCol("bldgSeCn", "시설물용도/\n개발사업종류"), + textCol("fcltPstnCn", "주소"), + textCol("lglTrgtYn", "법적의무\n대상여부\n(대상/미대상)"), + textCol("cmptnStrtYmd", "설치완료일\nyyyy-mm-dd"), + textCol("oprtngStrtYmd", "가동개시일\nyyyy-mm-dd"), + intCol("archWholArea", "건축연면적\n(㎡)"), + decimal1Col("prcsCpct", "처리용량\n(㎥/일)"), + intCol("wtrUseQnt", "물 사용량\n(㎥/년)"), + intCol("ruswtrUtztnQnt", "중수도\n이용량\n(㎥/년)"), + intCol("sbstCntUtztnQnt", "대체수\n이용량\n(㎥/년)"), + textCol("mnUsgCn", "중수도용도"), + decimal1Col("ruswtrOperAmt", "연간운영비\n(백만원/년)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + "bldgSeCn", + "fcltPstnCn", + "lglTrgtYn", + "cmptnStrtYmd", + "oprtngStrtYmd", + { + name: "facilityStatusGroup", + header: { text: "시설현황" }, + items: ["archWholArea", "prcsCpct"], + }, + "wtrUseQnt", + "ruswtrUtztnQnt", + "sbstCntUtztnQnt", + "mnUsgCn", + "ruswtrOperAmt", + "rmrk", + ], + }, + { + tabKey: "ST00100003", + tabLabel: "빗물이용시설 현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("fcltNm", "시설명"), + textCol("bdstTypCn", "건축물용도"), + textCol("fcltPstnCn", "위치\n(주소)"), + textCol("lglTrgtYn", "법적의무\n대상여부\n(대상/미대상)"), + textCol("instlCmptnYmd", "설치완료일\nyyyy-mm-dd"), + decimal1Col("instlAmt", "설치비\n(백만원)"), + textCol("rcptMynCn", "집수면 종류"), + textCol("filtrPrcsFcltYnExpln", "여과등\n처리시설\n유무"), + intCol("impdCpct", "저류조 용량\n(㎥)"), + intCol("anlUseQnt", "연간 이용량\n(㎥/년)"), + textCol("exlnUtlzUsgExpln", "이용용도"), + decimal1Col("anlOperAmt", "연간운영비\n(백만원/년)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "fcltNm", + "bdstTypCn", + "fcltPstnCn", + "lglTrgtYn", + "instlCmptnYmd", + "instlAmt", + "rcptMynCn", + "filtrPrcsFcltYnExpln", + "impdCpct", + "anlUseQnt", + "exlnUtlzUsgExpln", + "anlOperAmt", + "rmrk", + ], + }, + { + tabKey: "ST00100004", + tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 등록현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("bplcNm", "상호(명칭)"), + textCol("rprsvNm", "대표자"), + textCol("bplcPstnCn", "주소"), + textCol("regDt", "등록일\nyyyy-mm-dd"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "regDt", + "rmrk", + ], + }, + { + tabKey: "ST00100005", + tabLabel: "하ㆍ폐수처리수 재이용시설 시공업 인가현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + ...reviewFacilityAddressColumns, + textCol("bplcNm", "상호(명칭)"), + textCol("rprsvNm", "대표자"), + textCol("bplcPstnCn", "주소"), + textCol("coRegYmd", "인가일\n(년/월/일)"), + textCol("rmrk", "비고"), + ], + columnLayout: [ + reviewFacilityAddressLayout, + "displayYear", + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "coRegYmd", + "rmrk", + ], + }, +]; + +export const WaterReuseInfoListRvw = rawWaterReuseList.map((tab) => ({ + ...tab, + columns: tab.columns.map((col) => ({ + ...col, + editable: false, + styleName: "rg-cell-readonly", + })), +})); + +/** @deprecated `WaterReuseInfoListRvw` 사용 */ +export const WaterReuseInfoList = WaterReuseInfoListRvw; diff --git a/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts b/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts index f4c0ddae2..83b4ad7a1 100644 --- a/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts +++ b/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts @@ -192,11 +192,6 @@ export const PublicSewageFacilityColumn = { { name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' }, editable: false }, { name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수\n(명)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - - // hidden - { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false }, - { name: 'rowType', fieldName: 'rowType', header: { text: '데이터구분' }, visible: false }, - { name: 'statsYr', fieldName: 'statsYr', header: { text: '통계연도' }, visible: false }, ].map((col) => ({ ...col, editable: false })), columnLayout: [ @@ -291,8 +286,5 @@ export const PublicSewageFacilityColumn = { 'intgPrcsFcltCn', 'wholNope', 'sftyAcdntEvntcsCnt', - 'admdstCd', - 'rowType', - 'statsYr', ], }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts index 0cf31e2b1..d9e3e79af 100644 --- a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts +++ b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts @@ -813,8 +813,6 @@ const rawSewagePipeTotList = [ { name: "yy25Step3MnhlCnt", fieldName: "yy25Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, { name: "yy25Step4MnhlCnt", fieldName: "yy25Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, { name: "yy25Step5MnhlCnt", fieldName: "yy25Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, - - { name: "rmrk", fieldName: "rmrk", header: { text: "비고" }, editable: false }, ], columnLayout: [ { name: "addrGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm"] }, @@ -922,8 +920,7 @@ const rawSewagePipeTotList = [ { name: "yy25MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy25WholMnhlCnt", "yy25Step1MnhlCnt", "yy25Step2MnhlCnt", "yy25Step3MnhlCnt", "yy25Step4MnhlCnt"] }, "yy25Step5MnhlCnt", ], - }, - "rmrk", + } ], }, diff --git a/apps/st/src/components/util/column/total/SewerExcretaTreatmentStatusColumn.ts b/apps/st/src/components/util/column/total/SewerExcretaTreatmentStatusColumn.ts new file mode 100644 index 000000000..0f5ded556 --- /dev/null +++ b/apps/st/src/components/util/column/total/SewerExcretaTreatmentStatusColumn.ts @@ -0,0 +1,63 @@ +import { ValueType } from "realgrid"; + +export const ExcretaTreatmentStatusColumnConfig = { + tabKey: "ST00070001", + tabLabel: "분뇨처리시설", + rowStartNum: 2, + fixedColumns: 2, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "소재지" }, editable: false }, + { name: "telNo", fieldName: "telNo", header: { text: "전화번호" }, editable: false }, + { name: "cmptnStrtYmd", fieldName: "cmptnStrtYmd", header: { text: "준공일" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일" }, editable: false }, + { name: "wholPjtcoAmt", fieldName: "wholPjtcoAmt", header: { text: "사업비\n(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prcsFcltCpct", fieldName: "prcsFcltCpct", header: { text: "시설용량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "cpct", fieldName: "cpct", header: { text: "처리량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "oprtngDayCnt", fieldName: "oprtngDayCnt", header: { text: "가동일수\n(일/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prcsMthdExpln", fieldName: "prcsMthdExpln", header: { text: "처리공법" }, editable: false }, + { name: "linkTmplntCn", fieldName: "linkTmplntCn", header: { text: "연계처리장명" }, editable: false }, + { name: "linkPrcsQnt", fieldName: "linkPrcsQnt", header: { text: "연계처리량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 유입수질(mg/L, 개/mL) 그룹 소속 + { name: "inflwBodWqVl", fieldName: "inflwBodWqVl", header: { text: "BOD" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwTocWqVl", fieldName: "inflwTocWqVl", header: { text: "TOC" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwSsWqVl", fieldName: "inflwSsWqVl", header: { text: "SS" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwTnWqVl", fieldName: "inflwTnWqVl", header: { text: "T-N" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "inflwTpWqVl", fieldName: "inflwTpWqVl", header: { text: "T-P" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.000", editable: false }, + { name: "inflwClfgCnt", fieldName: "inflwClfgCnt", header: { text: "대장균\n군수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 방류수질(mg/L, 개/mL) 그룹 소속 + { name: "dschgBodWqVl", fieldName: "dschgBodWqVl", header: { text: "BOD" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgTocWqVl", fieldName: "dschgTocWqVl", header: { text: "TOC" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgSsWqVl", fieldName: "dschgSsWqVl", header: { text: "SS" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgTnWqVl", fieldName: "dschgTnWqVl", header: { text: "T-N" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "dschgTpWqVl", fieldName: "dschgTpWqVl", header: { text: "T-P" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.000", editable: false }, + { name: "dschgClfgCnt", fieldName: "dschgClfgCnt", header: { text: "대장균\n군수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 방류수역 그룹 소속 + { name: "wtsysNm", fieldName: "wtsysNm", header: { text: "수계" }, editable: false }, + { name: "trbtrNm", fieldName: "trbtrNm", header: { text: "지류" }, editable: false }, + { name: "msirbCn", fieldName: "msirbCn", header: { text: "중권역" }, editable: false }, + { name: "ssirbCn", fieldName: "ssirbCn", header: { text: "소권역" }, editable: false }, + + // 단일 미그룹 성격 컬럼들 + { name: "operMthdCdCn", fieldName: "operMthdCdCn", header: { text: "운영주체\n(자체/공기업/\n민간위탁)" }, editable: false }, + { name: "wholNope", fieldName: "wholNope", header: { text: "직원총수\n(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "sftyAcdntEvntcsCnt", fieldName: "sftyAcdntEvntcsCnt", header: { text: "안전사고\n건수\n(건/인/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", "fcltPstnCn", "telNo", "cmptnStrtYmd", "oprtngStrtYmd", "wholPjtcoAmt", "prcsFcltCpct", "cpct", "oprtngDayCnt", "prcsMthdExpln", "linkTmplntCn", "linkPrcsQnt", + { name: "inflwWqGroup", header: { text: "유입수질(mg/L, 개/mL)" }, items: ["inflwBodWqVl", "inflwTocWqVl", "inflwSsWqVl", "inflwTnWqVl", "inflwTpWqVl", "inflwClfgCnt"] }, + { name: "dschgWqGroup", header: { text: "방류수질(mg/L, 개/mL)" }, items: ["dschgBodWqVl", "dschgTocWqVl", "dschgSsWqVl", "dschgTnWqVl", "dschgTpWqVl", "dschgClfgCnt"] }, + { name: "wtrAreaGroup", header: { text: "방류수역" }, items: ["wtsysNm", "trbtrNm", "msirbCn", "ssirbCn"] }, + "operMthdCdCn", "wholNope", "sftyAcdntEvntcsCnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/SewerMediationPumpStatusColumn.ts b/apps/st/src/components/util/column/total/SewerMediationPumpStatusColumn.ts new file mode 100644 index 000000000..66451c04b --- /dev/null +++ b/apps/st/src/components/util/column/total/SewerMediationPumpStatusColumn.ts @@ -0,0 +1,103 @@ +import { ValueType } from "realgrid"; + +export const MediationPumpStatusColumnConfig = { + tabKey: "ST00050001", + tabLabel: "중계펌프장", + rowStartNum: 2, + fixedColumns: 3, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "펌프장명" }, editable: false }, + { name: "pumpSeNm", fieldName: "pumpSeNm", header: { text: "구분\n(합류식,\n분류식)" }, editable: false }, + { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시\n년월일\nyyyy-mm-dd" }, editable: false }, + { name: "planMltvArea", fieldName: "planMltvArea", header: { text: "계획배수면적(㏊)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 계획시간대최대배수량(㎥/일) 그룹 소속 + { name: "sndyArea", fieldName: "sndyArea", header: { text: "청천시" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rainArea", fieldName: "rainArea", header: { text: "우천시" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnQnt", fieldName: "exlnQnt", header: { text: "우수량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 오수펌프 그룹 내부 소속 + { name: "drtwPumpGrssPpqty", fieldName: "drtwPumpGrssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 오수펌프 > 오수펌프1 그룹 소속 + { name: "drtwPumpN1DmtrArea", fieldName: "drtwPumpN1DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN1Ppqty", fieldName: "drtwPumpN1Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "drtwPumpN1Cnt", fieldName: "drtwPumpN1Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN1RsrvCnt", fieldName: "drtwPumpN1RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 오수펌프 > 오수펌프2 그룹 소속 + { name: "drtwPumpN2DmtrArea", fieldName: "drtwPumpN2DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN2Ppqty", fieldName: "drtwPumpN2Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "drtwPumpN2Cnt", fieldName: "drtwPumpN2Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN2RsrvCnt", fieldName: "drtwPumpN2RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 오수펌프 > 오수펌프3 그룹 소속 + { name: "drtwPumpN3DmtrArea", fieldName: "drtwPumpN3DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN3Ppqty", fieldName: "drtwPumpN3Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "drtwPumpN3Cnt", fieldName: "drtwPumpN3Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPumpN3RsrvCnt", fieldName: "drtwPumpN3RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 우수펌프 그룹 내부 소속 + { name: "exlnPumpGrssPpqty", fieldName: "exlnPumpGrssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 우수펌프 > 우수펌프1 그룹 소속 + { name: "exlnPumpN1DmtrArea", fieldName: "exlnPumpN1DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN1Ppqty", fieldName: "exlnPumpN1Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnPumpN1Cnt", fieldName: "exlnPumpN1Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN1RsrvCnt", fieldName: "exlnPumpN1RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 우수펌프 > 우수펌프2 그룹 소속 + { name: "exlnPumpN2DmtrArea", fieldName: "exlnPumpN2DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN2Ppqty", fieldName: "exlnPumpN2Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnPumpN2Cnt", fieldName: "exlnPumpN2Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN2RsrvCnt", fieldName: "exlnPumpN2RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 우수펌프 > 우수펌프3 그룹 소속 + { name: "exlnPumpN3DmtrArea", fieldName: "exlnPumpN3DmtrArea", header: { text: "구경(mm)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN3Ppqty", fieldName: "exlnPumpN3Ppqty", header: { text: "양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "exlnPumpN3Cnt", fieldName: "exlnPumpN3Cnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnPumpN3RsrvCnt", fieldName: "exlnPumpN3RsrvCnt", header: { text: "예비대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 단일 컬럼 + { name: "anlElpwrUseQnt", fieldName: "anlElpwrUseQnt", header: { text: "연간전력\n사용량\n(㎾h/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swtrmtFcltNm", + "fcltNm", + "pumpSeNm", + "fcltPstnCn", + "oprtngStrtYmd", + "planMltvArea", + { name: "planDrainGroup", header: { text: "계획시간대최대배수량(㎥/일)" }, items: ["sndyArea", "rainArea", "exlnQnt"] }, + { + name: "drtwPumpGroup", + header: { text: "오수펌프" }, + items: [ + "drtwPumpGrssPpqty", + { name: "drtwPumpN1Group", header: { text: "오수펌프(1)" }, items: ["drtwPumpN1DmtrArea", "drtwPumpN1Ppqty", "drtwPumpN1Cnt", "drtwPumpN1RsrvCnt"] }, + { name: "drtwPumpN2Group", header: { text: "오수펌프(2)" }, items: ["drtwPumpN2DmtrArea", "drtwPumpN2Ppqty", "drtwPumpN2Cnt", "drtwPumpN2RsrvCnt"] }, + { name: "drtwPumpN3Group", header: { text: "오수펌프(3)" }, items: ["drtwPumpN3DmtrArea", "drtwPumpN3Ppqty", "drtwPumpN3Cnt", "drtwPumpN3RsrvCnt"] } + ] + }, + { + name: "exlnPumpGroup", + header: { text: "우수펌프" }, + items: [ + "exlnPumpGrssPpqty", + { name: "exlnPumpN1Group", header: { text: "우수펌프(1)" }, items: ["exlnPumpN1DmtrArea", "exlnPumpN1Ppqty", "exlnPumpN1Cnt", "exlnPumpN1RsrvCnt"] }, + { name: "exlnPumpN2Group", header: { text: "우수펌프(2)" }, items: ["exlnPumpN2DmtrArea", "exlnPumpN2Ppqty", "exlnPumpN2Cnt", "exlnPumpN2RsrvCnt"] }, + { name: "exlnPumpN3Group", header: { text: "우수펌프(3)" }, items: ["exlnPumpN3DmtrArea", "exlnPumpN3Ppqty", "exlnPumpN3Cnt", "exlnPumpN3RsrvCnt"] } + ] + }, + "anlElpwrUseQnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts index 01b224322..7ebb6a520 100644 --- a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts +++ b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts @@ -135,13 +135,7 @@ export const SeweragePentrationRateTotColumn = { valueType: ValueType.NUMBER, dataFormat: '#,##0.0', excludeFromExcelExport: true, - }, - - // 비고 - { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - - { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false }, - { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false }, + } ].map((col) => ({ ...col, editable: false })), columnLayout: [ @@ -203,9 +197,6 @@ export const SeweragePentrationRateTotColumn = { 'staaPsplyRt', 'altdPrcsPsplyRt', 'sewerInstlRt', - 'rurPsplyRt', - 'rmrk', - 'swagePplPsplyRt', - 'admdstCd', + 'rurPsplyRt' ] }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column.ts new file mode 100644 index 000000000..666c671b8 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column.ts @@ -0,0 +1,31 @@ +import { ValueType } from "realgrid"; + +export const HumanWasteCollectionTab3ColumnConfig = { + tabKey: "ST00120003", + tabLabel: "분뇨수집·운반업", + rowStartNum: 3, + fixedColumns: 3, + 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: "bzentyCnt", fieldName: "bzentyCnt", header: { text: "업체수(개소)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "empNope", fieldName: "empNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "fcltSecrPrstCnt", fieldName: "fcltSecrPrstCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "n3TonUdrCnt", fieldName: "n3TonUdrCnt", header: { text: "3톤이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "n4TonUdrCnt", fieldName: "n4TonUdrCnt", header: { text: "4.5톤이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "n8TonUdrCnt", fieldName: "n8TonUdrCnt", header: { text: "8톤이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "clctCnvncPrfmncQnt", fieldName: "clctCnvncPrfmncQnt", header: { text: "수집·운반실적(m³/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "bzentyCnt", "empNope", + { + name: "fcltGroup", + header: { text: "시설(차량) 확보현황(대수)" }, + items: ["fcltSecrPrstCnt", "n3TonUdrCnt", "n4TonUdrCnt", "n8TonUdrCnt", "etcCnt"] + }, + "clctCnvncPrfmncQnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column.ts new file mode 100644 index 000000000..18d99e765 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column.ts @@ -0,0 +1,25 @@ +import { ValueType } from "realgrid"; + +export const IndividualSewageTab4ColumnConfig = { + tabKey: "ST00120004", + tabLabel: "개인하수처리관련업", + rowStartNum: 3, + fixedColumns: 1, + 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: "designMdsrCnt", fieldName: "designMdsrCnt", header: { text: "업소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "designMdsrNope", fieldName: "designMdsrNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mfbizCnt", fieldName: "mfbizCnt", header: { text: "업소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mfbizMdsrNope", fieldName: "mfbizMdsrNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mngCnt", fieldName: "mngCnt", header: { text: "업소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mngMdsrNope", fieldName: "mngMdsrNope", header: { text: "종사인원(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "designGroup", header: { text: "개인하수처리시설 설계·시공업" }, items: ["designMdsrCnt", "designMdsrNope"] }, + { name: "mfbizGroup", header: { text: "개인하수처리시설 제조업" }, items: ["mfbizCnt", "mfbizMdsrNope"] }, + { name: "mngGroup", header: { text: "개인하수처리시설 관리업" }, items: ["mngCnt", "mngMdsrNope"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column.ts new file mode 100644 index 000000000..99d35f1ba --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column.ts @@ -0,0 +1,36 @@ +import { ValueType } from "realgrid"; + +export const PublicHumanWasteStaffTab2ColumnConfig = { + tabKey: "ST00120002", + tabLabel: "공공기관 분뇨분야", + rowStartNum: 3, + fixedColumns: 2, + 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: "ognzNm", fieldName: "ognzNm", header: { text: "직제" }, editable: false }, + { name: "empGrssCnt", fieldName: "empGrssCnt", header: { text: "직원총수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pbadmsNope", fieldName: "pbadmsNope", header: { text: "행정직" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "techWholNope", fieldName: "techWholNope", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "engrkCnt", fieldName: "engrkCnt", header: { text: "토목" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mchnCnt", fieldName: "mchnCnt", header: { text: "기계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prvtmCnt", fieldName: "prvtmCnt", header: { text: "전기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "chmengCnt", fieldName: "chmengCnt", header: { text: "화공" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "envCnt", fieldName: "envCnt", header: { text: "환경" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "enjbEtcCnt", fieldName: "enjbEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "ognzNm", + "empGrssCnt", + "pbadmsNope", + { + name: "techGroup", + header: { text: "기술직" }, + items: ["techWholNope", "engrkCnt", "mchnCnt", "prvtmCnt", "chmengCnt", "envCnt", "enjbEtcCnt"] + }, + "etcCnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column.ts b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column.ts new file mode 100644 index 000000000..90c1a789b --- /dev/null +++ b/apps/st/src/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column.ts @@ -0,0 +1,35 @@ +import { ValueType } from "realgrid"; + +export const PublicSewageStaffTab1ColumnConfig = { + tabKey: "ST00120001", + tabLabel: "공공기관 하수분야", + rowStartNum: 3, + fixedColumns: 2, + 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: "ognzNm", fieldName: "ognzNm", header: { text: "직제" }, editable: false }, + { name: "empGrssCnt", fieldName: "empGrssCnt", header: { text: "직원총수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pbadmsNope", fieldName: "pbadmsNope", header: { text: "행정직" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + // 기술직 + { name: "techWholNope", fieldName: "techWholNope", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "engrkCnt", fieldName: "engrkCnt", header: { text: "토목" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mchnCnt", fieldName: "mchnCnt", header: { text: "기계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prvtmCnt", fieldName: "prvtmCnt", header: { text: "전기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "chmengCnt", fieldName: "chmengCnt", header: { text: "화공" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "envCnt", fieldName: "envCnt", header: { text: "환경" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "enjbEtcCnt", fieldName: "enjbEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "ognzNm", "empGrssCnt", "pbadmsNope", + { + name: "techGroup", + header: { text: "기술직" }, + items: ["techWholNope", "engrkCnt", "mchnCnt", "prvtmCnt", "chmengCnt", "envCnt", "enjbEtcCnt"] + }, + "etcCnt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts new file mode 100644 index 000000000..c4af5d5a6 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column.ts @@ -0,0 +1,46 @@ +import { ValueType } from "realgrid"; + +export const PrivateSewageTab1ColumnConfig = { + tabKey: "ST00080001", + tabLabel: "처리용량별 오수처리시설", + rowStartNum: 3, + fixedColumns: 3, + 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: "wholDrtwPrcsFcltCnt", fieldName: "wholDrtwPrcsFcltCnt", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 내 + { name: "wholStaaCnt", fieldName: "wholStaaCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaPrcsQnt", fieldName: "step1StaaPrcsQnt", header: { text: "2이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaPrcsQnt", fieldName: "step2StaaPrcsQnt", header: { text: "3~10" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaPrcsQnt", fieldName: "step3StaaPrcsQnt", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaPrcsQnt", fieldName: "step4StaaPrcsQnt", header: { text: "21~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaPrcsQnt", fieldName: "step5StaaPrcsQnt", header: { text: "51~100" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaPrcsQnt", fieldName: "step6StaaPrcsQnt", header: { text: "101~500" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaPrcsQnt", fieldName: "step7StaaPrcsQnt", header: { text: "501이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 외 + { name: "staaExcpWhol", fieldName: "staaExcpWhol", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaExcpPrcsQnt", fieldName: "step1StaaExcpPrcsQnt", header: { text: "2이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaExcpPrcsQnt", fieldName: "step2StaaExcpPrcsQnt", header: { text: "3~10" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaExcpPrcsQnt", fieldName: "step3StaaExcpPrcsQnt", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaExcpPrcsQnt", fieldName: "step4StaaExcpPrcsQnt", header: { text: "21~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaExcpPrcsQnt", fieldName: "step5StaaExcpPrcsQnt", header: { text: "51~100" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaExcpPrcsQnt", fieldName: "step6StaaExcpPrcsQnt", header: { text: "101~500" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaExcpPrcsQnt", fieldName: "step7StaaExcpPrcsQnt", header: { text: "501이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "wholDrtwPrcsFcltCnt", + { + name: "privateSewageFacilityGroup", + header: { text: "오수처리시설" }, + items: [ + { name: "insideGroup", header: { text: "하수처리구역 내(㎥/일)" }, items: ["wholStaaCnt", "step1StaaPrcsQnt", "step2StaaPrcsQnt", "step3StaaPrcsQnt", "step4StaaPrcsQnt", "step5StaaPrcsQnt", "step6StaaPrcsQnt", "step7StaaPrcsQnt"] }, + { name: "outsideGroup", header: { text: "하수처리구역 외(㎥/일)" }, items: ["staaExcpWhol", "step1StaaExcpPrcsQnt", "step2StaaExcpPrcsQnt", "step3StaaExcpPrcsQnt", "step4StaaExcpPrcsQnt", "step5StaaExcpPrcsQnt", "step6StaaExcpPrcsQnt", "step7StaaExcpPrcsQnt"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column.ts new file mode 100644 index 000000000..801b59d0a --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column.ts @@ -0,0 +1,72 @@ +import { ValueType } from "realgrid"; + +export const PrivateSewageTab2ColumnConfig = { + tabKey: "ST00080002", + tabLabel: "정화조현황", + rowStartNum: 4, + fixedColumns: 2, + 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: "grssFcltCnt", fieldName: "grssFcltCnt", header: { text: "총 시설 수\n(A+D)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 내 소속 항목들 + { name: "staaInsdWhol", fieldName: "staaInsdWhol", header: { text: "총계\nA=(B+C)/2" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdPrcsCpctByWhol", fieldName: "staaInsdPrcsCpctByWhol", header: { text: "계(C)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdSttkNprsflCnt", fieldName: "staaInsdSttkNprsflCnt", header: { text: "자연유하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaInsdSttkFrcCnt", fieldName: "step1StaaInsdSttkFrcCnt", header: { text: "강제펌핑식" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdSprktpFltrtbCnt", fieldName: "staaInsdSprktpFltrtbCnt", header: { text: "살수여상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdAertMthdCnt", fieldName: "staaInsdAertMthdCnt", header: { text: "폭기방법" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdCntcAertCnt", fieldName: "staaInsdCntcAertCnt", header: { text: "접촉폭기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaInsdEtcCnt", fieldName: "staaInsdEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaInsdNope", fieldName: "step1StaaInsdNope", header: { text: "10인조\n이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaInsdNope", fieldName: "step2StaaInsdNope", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaInsdNope", fieldName: "step3StaaInsdNope", header: { text: "21~30" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaInsdNope", fieldName: "step4StaaInsdNope", header: { text: "31~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaInsdNope", fieldName: "step5StaaInsdNope", header: { text: "51~199" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaInsdNope", fieldName: "step6StaaInsdNope", header: { text: "200~499" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaInsdNope", fieldName: "step7StaaInsdNope", header: { text: "500~999" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step8StaaInsdNope", fieldName: "step8StaaInsdNope", header: { text: "1,000이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 하수처리구역 외 소속 항목들 + { name: "staaExcpWhol", fieldName: "staaExcpWhol", header: { text: "총계\nD=(E+F)/2" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpPrcsCpctByWhol", fieldName: "staaExcpPrcsCpctByWhol", header: { text: "계(F)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpSttkNprsflCnt", fieldName: "staaExcpSttkNprsflCnt", header: { text: "자연유하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaExcpSttkFrcCnt", fieldName: "step1StaaExcpSttkFrcCnt", header: { text: "강제펌핑식" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpSprktpFltrtbCnt", fieldName: "staaExcpSprktpFltrtbCnt", header: { text: "살수여상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpAertMthdCnt", fieldName: "staaExcpAertMthdCnt", header: { text: "폭기방법" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpCntcAertCnt", fieldName: "staaExcpCntcAertCnt", header: { text: "접촉폭기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "staaExcpEtcCnt", fieldName: "staaExcpEtcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step1StaaExcpNope", fieldName: "step1StaaExcpNope", header: { text: "10인조\n이하" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step2StaaExcpNope", fieldName: "step2StaaExcpNope", header: { text: "11~20" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step3StaaExcpNope", fieldName: "step3StaaExcpNope", header: { text: "21~30" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step4StaaExcpNope", fieldName: "step4StaaExcpNope", header: { text: "31~50" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step5StaaExcpNope", fieldName: "step5StaaExcpNope", header: { text: "51~199" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step6StaaExcpNope", fieldName: "step6StaaExcpNope", header: { text: "200~499" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step7StaaExcpNope", fieldName: "step7StaaExcpNope", header: { text: "500~999" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "step8StaaExcpNope", fieldName: "step8StaaExcpNope", header: { text: "1,000이상" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "grssFcltCnt", + { + name: "insideSewerGroup", + header: { text: "하수처리구역 내" }, + items: [ + "staaInsdWhol", + { name: "insdBGroup", header: { text: "계(B)" }, items: ["staaInsdSttkNprsflCnt", "step1StaaInsdSttkFrcCnt", "staaInsdSprktpFltrtbCnt", "staaInsdAertMthdCnt", "staaInsdCntcAertCnt", "staaInsdEtcCnt"] }, + { name: "insdCGroup", header: { text: "처리용량별" }, items: ["staaInsdPrcsCpctByWhol", "step1StaaInsdNope", "step2StaaInsdNope", "step3StaaInsdNope", "step4StaaInsdNope", "step5StaaInsdNope", "step6StaaInsdNope", "step7StaaInsdNope", "step8StaaInsdNope"] } + ] + }, + { + name: "outsideSewerGroup", + header: { text: "하수처리구역 외" }, + items: [ + "staaExcpWhol", + { name: "excpEGroup", header: { text: "계(E)" }, items: ["staaExcpSttkNprsflCnt", "step1StaaExcpSttkFrcCnt", "staaExcpSprktpFltrtbCnt", "staaExcpAertMthdCnt", "staaExcpCntcAertCnt", "staaExcpEtcCnt"] }, + { name: "excpFGroup", header: { text: "처리용량별" }, items: ["staaExcpPrcsCpctByWhol", "step1StaaExcpNope", "step2StaaExcpNope", "step3StaaExcpNope", "step4StaaExcpNope", "step5StaaExcpNope", "step6StaaExcpNope", "step7StaaExcpNope", "step8StaaExcpNope"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column.ts b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column.ts new file mode 100644 index 000000000..98a629759 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column.ts @@ -0,0 +1,48 @@ +import { ValueType } from "realgrid"; + +export const PrivateSewageTab3ColumnConfig = { + tabKey: "ST00080003", // 08번 개인하수처리시설 TAB3 매핑 키 고정 + tabLabel: "개인하수도 관리지역 지정공고 현황", + rowStartNum: 3, // 3단 다단 레이아웃 스펙에 맞추어 3으로 설정합니다. + fixedColumns: 1, + 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: "indvSwagePbancRgnCnt", fieldName: "indvSwagePbancRgnCnt", header: { text: "공고지역\n(개소)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "indvSwageDsgnScpLen", fieldName: "indvSwageDsgnScpLen", header: { text: "지정범위\n(㎢)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + + // 정화조 그룹 소속 (개소수, 처리용량) + { name: "spctnkGrssFcltCnt", fieldName: "spctnkGrssFcltCnt", header: { text: "총시설수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "spctnkQnt", fieldName: "spctnkQnt", header: { text: "처리용량\n(인/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 오수처리시설 그룹 소속 (개소수, 처리용량) + { name: "drtwPrcsFcltGrssFcltCnt", fieldName: "drtwPrcsFcltGrssFcltCnt", header: { text: "총시설수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "drtwPrcsFcltQnt", fieldName: "drtwPrcsFcltQnt", header: { text: "처리용량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + // 행정구역 묶음 + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + + // 메인 마스터 지표 + "indvSwagePbancRgnCnt", + "indvSwageDsgnScpLen", + + // 정화조 다단 묶음 + { + name: "septicTankGroup", + header: { text: "정화조" }, + items: ["spctnkGrssFcltCnt", "spctnkQnt"] + }, + + // 오수처리시설 다단 묶음 + { + name: "sewageTreatmentGroup", + header: { text: "오수처리시설" }, + items: ["drtwPrcsFcltGrssFcltCnt", "drtwPrcsFcltQnt"] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn.ts b/apps/st/src/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn.ts new file mode 100644 index 000000000..64ce85216 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn.ts @@ -0,0 +1,96 @@ +import { ValueType } from "realgrid"; + +export const PumpStationStatusColumnConfig = { + tabKey: "ST00040002", + tabLabel: "빗물펌프장", + rowStartNum: 2, + fixedColumns: 2, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치" }, editable: false }, + { name: "fcltyKndCn", fieldName: "fcltyKndCn", header: { text: "시설물종류" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시\n년월일\nyyyy-mm-dd" }, editable: false }, + { name: "planDesignFreqCnt", fieldName: "planDesignFreqCnt", header: { text: "계획설계빈도" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "wtrStrgZoneArea", fieldName: "wtrStrgZoneArea", header: { text: "집수구역면적(㎢)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + + // 유수지 그룹 소속 + { name: "rsvrYn", fieldName: "rsvrYn", header: { text: "유수지유무\n(유,무)" }, editable: false }, + { name: "rsvrImpndQnt", fieldName: "rsvrImpndQnt", header: { text: "저류량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 흡수정 그룹 소속 + { name: "abstnRgnImdQnt", fieldName: "abstnRgnImdQnt", header: { text: "저류량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 단일 컬럼 + { name: "grssPpqty", fieldName: "grssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 펌프시설 > 펌프시설1 그룹 소속 + { name: "pumpFcltN1DmtrExpln", fieldName: "pumpFcltN1DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN1PerPpqty", fieldName: "pumpFcltN1PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN1PumpCnt", fieldName: "pumpFcltN1PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN1RsrvPumpCnt", fieldName: "pumpFcltN1RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설2 그룹 소속 + { name: "pumpFcltN2DmtrExpln", fieldName: "pumpFcltN2DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN2PerPpqty", fieldName: "pumpFcltN2PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN2PumpCnt", fieldName: "pumpFcltN2PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN2RsrvPumpCnt", fieldName: "pumpFcltN2RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설3 그룹 소속 + { name: "pumpFcltN3DmtrExpln", fieldName: "pumpFcltN3DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN3PerPpqty", fieldName: "pumpFcltN3PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN3PumpCnt", fieldName: "pumpFcltN3PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN3RsrvPumpCnt", fieldName: "pumpFcltN3RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 부대시설 그룹 소속 + { name: "prcsgFcltYn", fieldName: "prcsgFcltYn", header: { text: "전처리\n시설\n유무\n(유/무)" }, editable: false }, + { name: "ddrztnEqpmYn", fieldName: "ddrztnEqpmYn", header: { text: "탈취설비\n유무\n(유/무)" }, editable: false }, + { name: "oslfEtgtYn", fieldName: "oslfEtgtYn", header: { text: "자가발전\n유무\n(유/무)" }, editable: false }, + + // 단일 컬럼 + { name: "dschgWtrsExpln", fieldName: "dschgWtrsExpln", header: { text: "방류수역" }, editable: false }, + + // 최종처분량(ton/년) 그룹 소속 + { name: "prcptnLastDspsQnt", fieldName: "prcptnLastDspsQnt", header: { text: "침사" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ctmnLastDspsQnt", fieldName: "ctmnLastDspsQnt", header: { text: "협잡물" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 최종처분방법 그룹 소속 + { name: "prcptnLastDspsMthdExpln", fieldName: "prcptnLastDspsMthdExpln", header: { text: "침사" }, editable: false }, + { name: "ctmnLastDspsMthdExpln", fieldName: "ctmnLastDspsMthdExpln", header: { text: "협잡물" }, editable: false }, + + // 운영실적 그룹 소속 + { name: "oprtngDayCnt", fieldName: "oprtngDayCnt", header: { text: "가동일수\n(일/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "grssMltvQnt", fieldName: "grssMltvQnt", header: { text: "총배수량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "fcltPstnCn", + "fcltyKndCn", + "oprtngStrtYmd", + "planDesignFreqCnt", + "wtrStrgZoneArea", + { name: "rsvrGroup", header: { text: "유수지" }, items: ["rsvrYn", "rsvrImpndQnt"] }, + { name: "abstnRgnGroup", header: { text: "흡수정" }, items: ["abstnRgnImdQnt"] }, + { + name: "pumpFcltyGroup", + header: { text: "펌프시설" }, + items: [ + "grssPpqty", + { name: "pump1SubGroup", header: { text: "펌프시설1" }, items: ["pumpFcltN1DmtrExpln", "pumpFcltN1PerPpqty", "pumpFcltN1PumpCnt", "pumpFcltN1RsrvPumpCnt"] }, + { name: "pump2SubGroup", header: { text: "펌프시설2" }, items: ["pumpFcltN2DmtrExpln", "pumpFcltN2PerPpqty", "pumpFcltN2PumpCnt", "pumpFcltN2RsrvPumpCnt"] }, + { name: "pump3SubGroup", header: { text: "펌프시설3" }, items: ["pumpFcltN3DmtrExpln", "pumpFcltN3PerPpqty", "pumpFcltN3PumpCnt", "pumpFcltN3RsrvPumpCnt"] } + ] + }, + { name: "subFcltGroup", header: { text: "부대시설" }, items: ["prcsgFcltYn", "ddrztnEqpmYn", "oslfEtgtYn"] }, + "dschgWtrsExpln", + { name: "lastDspsGroup", header: { text: "최종처분량(ton/년)" }, items: ["prcptnLastDspsQnt", "ctmnLastDspsQnt"] }, + { name: "lastMthdGroup", header: { text: "최종처분방법" }, items: ["prcptnLastDspsMthdExpln", "ctmnLastDspsMthdExpln"] }, + { name: "oprtngGroup", header: { text: "운영실적" }, items: ["oprtngDayCnt", "grssMltvQnt"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn.ts b/apps/st/src/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn.ts new file mode 100644 index 000000000..311cb6867 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn.ts @@ -0,0 +1,101 @@ +import { ValueType } from "realgrid"; + +export const ReservoirStatusColumnConfig = { + tabKey: "ST00040001", + tabLabel: "하수저류시설", + rowStartNum: 2, + fixedColumns: 2, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시\n년월일\nyyyy-mm-dd" }, editable: false }, + { name: "planDesignFreqCnt", fieldName: "planDesignFreqCnt", header: { text: "계획설계빈도" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 설치목적 그룹 소속 (O,X로 표기, 중복체크가능) + { name: "fldngPrvtnYn", fieldName: "fldngPrvtnYn", header: { text: "침수예방" }, editable: false }, + { name: "wqPlltnMtgtYn", fieldName: "wqPlltnMtgtYn", header: { text: "수질오염저감" }, editable: false }, + { name: "rtlztnUsgYn", fieldName: "rtlztnUsgYn", header: { text: "하수\n재이용\n용도" }, editable: false }, + + // 단일 컬럼들 + { name: "fcltStrctCn", fieldName: "fcltStrctCn", header: { text: "시설구조\n(일반지하식,\n지하터널식,\n기타)" }, editable: false }, + { name: "wtrStrgZoneArea", fieldName: "wtrStrgZoneArea", header: { text: "접수구역면적(㎢)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "impdFcltCpct", fieldName: "impdFcltCpct", header: { text: "시설용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "grssPpqty", fieldName: "grssPpqty", header: { text: "총양수량\n(㎥/min)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 펌프시설 > 펌프시설1 그룹 소속 + { name: "pumpFcltN1DmtrExpln", fieldName: "pumpFcltN1DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN1PerPpqty", fieldName: "pumpFcltN1PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN1PumpCnt", fieldName: "pumpFcltN1PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN1RsrvPumpCnt", fieldName: "pumpFcltN1RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설2 그룹 소속 + { name: "pumpFcltN2DmtrExpln", fieldName: "pumpFcltN2DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN2PerPpqty", fieldName: "pumpFcltN2PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN2PumpCnt", fieldName: "pumpFcltN2PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN2RsrvPumpCnt", fieldName: "pumpFcltN2RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 펌프시설 > 펌프시설3 그룹 소속 + { name: "pumpFcltN3DmtrExpln", fieldName: "pumpFcltN3DmtrExpln", header: { text: "구경(mm)" }, editable: false }, + { name: "pumpFcltN3PerPpqty", fieldName: "pumpFcltN3PerPpqty", header: { text: "대당 양수량\n(㎥/min·대)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pumpFcltN3PumpCnt", fieldName: "pumpFcltN3PumpCnt", header: { text: "펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "pumpFcltN3RsrvPumpCnt", fieldName: "pumpFcltN3RsrvPumpCnt", header: { text: "예비펌프대수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 부대시설 그룹 소속 + { name: "prcsgFcltYn", fieldName: "prcsgFcltYn", header: { text: "전처리\n시설\n유무\n(유/무)" }, editable: false }, + { name: "ddrztnEqpmYn", fieldName: "ddrztnEqpmYn", header: { text: "탈취설비\n유무\n(유/무)" }, editable: false }, + { name: "oslfEtgtYn", fieldName: "oslfEtgtYn", header: { text: "자가발전\n유무\n(유/무)" }, editable: false }, + + // 단일 컬럼들 + { name: "mltvMthCn", fieldName: "mltvMthCn", header: { text: "배수방식\n(자연배수,\n강제배수)" }, editable: false }, + { name: "impdMthCn", fieldName: "impdMthCn", header: { text: "저류방식\n(직렬,병렬)" }, editable: false }, + { name: "impdPrcsMthdCn", fieldName: "impdPrcsMthdCn", header: { text: "저류수\n처리방법\n(자체처리,\n연계처리)" }, editable: false }, + { name: "dschgWtrsExpln", fieldName: "dschgWtrsExpln", header: { text: "방류수역\n(또는\n연계처리장명)" }, editable: false }, + + // 최종처분량(ton/년) 그룹 소속 + { name: "prcptnLastDspsQnt", fieldName: "prcptnLastDspsQnt", header: { text: "침사" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ctmnLastDspsQnt", fieldName: "ctmnLastDspsQnt", header: { text: "협잡물" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 최종처분방법 그룹 소속 + { name: "prcptnLastDspsMthdExpln", fieldName: "prcptnLastDspsMthdExpln", header: { text: "침사" }, editable: false }, + { name: "ctmnLastDspsMthdExpln", fieldName: "ctmnLastDspsMthdExpln", header: { text: "협잡물" }, editable: false }, + + // 운영실적 그룹 소속 + { name: "oprtngDayCnt", fieldName: "oprtngDayCnt", header: { text: "가동일수\n(일/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "grssMltvQnt", fieldName: "grssMltvQnt", header: { text: "총배수량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "fcltPstnCn", + "oprtngStrtYmd", + "planDesignFreqCnt", + { name: "purposeGroup", header: { text: "설치목적(O,X로 표기, 중복체크가능)" }, items: ["fldngPrvtnYn", "wqPlltnMtgtYn", "rtlztnUsgYn"] }, + "fcltStrctCn", + "wtrStrgZoneArea", + "impdFcltCpct", + { + name: "pumpFcltyGroup", + header: { text: "펌프시설" }, + items: [ + "grssPpqty", + { name: "pump1SubGroup", header: { text: "펌프시설1" }, items: ["pumpFcltN1DmtrExpln", "pumpFcltN1PerPpqty", "pumpFcltN1PumpCnt", "pumpFcltN1RsrvPumpCnt"] }, + { name: "pump2SubGroup", header: { text: "펌프시설2" }, items: ["pumpFcltN2DmtrExpln", "pumpFcltN2PerPpqty", "pumpFcltN2PumpCnt", "pumpFcltN2RsrvPumpCnt"] }, + { name: "pump3SubGroup", header: { text: "펌프시설3" }, items: ["pumpFcltN3DmtrExpln", "pumpFcltN3PerPpqty", "pumpFcltN3PumpCnt", "pumpFcltN3RsrvPumpCnt"] } + ] + }, + { name: "subFcltGroup", header: { text: "부대시설" }, items: ["prcsgFcltYn", "ddrztnEqpmYn", "oslfEtgtYn"] }, + "mltvMthCn", + "impdMthCn", + "impdPrcsMthdCn", + "dschgWtrsExpln", + { name: "lastDspsGroup", header: { text: "최종처분량(ton/년)" }, items: ["prcptnLastDspsQnt", "ctmnLastDspsQnt"] }, + { name: "lastMthdGroup", header: { text: "최종처분방법" }, items: ["prcptnLastDspsMthdExpln", "ctmnLastDspsMthdExpln"] }, + { name: "oprtngGroup", header: { text: "운영실적" }, items: ["oprtngDayCnt", "grssMltvQnt"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column.ts new file mode 100644 index 000000000..2424c4ce7 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column.ts @@ -0,0 +1,24 @@ +import { ValueType } from "realgrid"; + +export const SewerageFeeTab2ColumnConfig = { + tabKey: "ST00090002", + tabLabel: "하수도 요금", + rowStartNum: 2, + fixedColumns: 3, + 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: "anlLevyQnt", fieldName: "anlLevyQnt", header: { text: "연간조정량(㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "levyAmt", fieldName: "levyAmt", header: { text: "하수도사용료수익(원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "avgUntprcAmt", fieldName: "avgUntprcAmt", header: { text: "㎡당 요금(원/㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "grssPrmcoAmt", fieldName: "grssPrmcoAmt", header: { text: "총관원가(원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "grssUnitPrmcoAmt", fieldName: "grssUnitPrmcoAmt", header: { text: "㎡당원가(원/㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rlztnRt", fieldName: "rlztnRt", header: { text: "현실화율(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "anlLevyQnt", "levyAmt", "avgUntprcAmt", "grssPrmcoAmt", "grssUnitPrmcoAmt", "rlztnRt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column.ts new file mode 100644 index 000000000..78589a22d --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column.ts @@ -0,0 +1,58 @@ +import { ValueType } from "realgrid"; + +export const SewerageFinancialTab1ColumnConfig = { + tabKey: "ST00090001", + tabLabel: "하수도재정 결산", + rowStartNum: 3, + fixedColumns: 1, + 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: "crfwAmt", fieldName: "crfwAmt", header: { text: "이월액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "lbltAmt", fieldName: "lbltAmt", header: { text: "부채액\n(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "usgByWholPjtcoAmt", fieldName: "usgByWholPjtcoAmt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "tmplntWholAmt", fieldName: "tmplntWholAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntFcltAmt", fieldName: "tmplntFcltAmt", header: { text: "시설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntMendngAmt", fieldName: "tmplntMendngAmt", header: { text: "개보수비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntOperKeepAmt", fieldName: "tmplntOperKeepAmt", header: { text: "운영\n유지비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "tmplntEtcAmt", fieldName: "tmplntEtcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "wholSwagePplAmt", fieldName: "wholSwagePplAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplFcltAmt", fieldName: "swagePplFcltAmt", header: { text: "시설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplMendngAmt", fieldName: "swagePplMendngAmt", header: { text: "개보수비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplOperKeepAmt", fieldName: "swagePplOperKeepAmt", header: { text: "운영\n유지비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swagePplEtcAmt", fieldName: "swagePplEtcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "wholSwageImdpFcltAmt", fieldName: "wholSwageImdpFcltAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdFcltAmt", fieldName: "swageImpdFcltAmt", header: { text: "시설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdMendngAmt", fieldName: "swageImpdMendngAmt", header: { text: "개보수비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdOperKeepAmt", fieldName: "swageImpdOperKeepAmt", header: { text: "운영\n유지비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swageImpdFcltEtcAmt", fieldName: "swageImpdFcltEtcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "etcWholAmt", fieldName: "etcWholAmt", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcAcdntCrfwAmt", fieldName: "etcAcdntCrfwAmt", header: { text: "사고\n이월액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcNrcpAmt", fieldName: "etcNrcpAmt", header: { text: "미수납액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcAmt", fieldName: "etcAmt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "crfwAmt", + + { + name: "purposeGroup", + header: { text: "용도별사업비(백만원)" }, + items: [ + + "usgByWholPjtcoAmt", + { name: "t1Group", header: { text: "처리장" }, items: ["tmplntWholAmt", "tmplntFcltAmt", "tmplntMendngAmt", "tmplntOperKeepAmt", "tmplntEtcAmt"] }, + { name: "t2Group", header: { text: "하수관로" }, items: ["wholSwagePplAmt", "swagePplFcltAmt", "swagePplMendngAmt", "swagePplOperKeepAmt", "swagePplEtcAmt"] }, + { name: "t3Group", header: { text: "유수지 및 배수펌프장" }, items: ["wholSwageImdpFcltAmt", "swageImpdFcltAmt", "swageImpdMendngAmt", "swageImpdOperKeepAmt", "swageImpdFcltEtcAmt"] }, + { name: "t4Group", header: { text: "기타" }, items: ["etcWholAmt", "etcAcdntCrfwAmt", "etcNrcpAmt", "etcAmt"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts new file mode 100644 index 000000000..af0afc329 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column.ts @@ -0,0 +1,38 @@ +import { ValueType } from "realgrid"; + +export const SewerageOrgCauseTab4ColumnConfig = { + tabKey: "ST00090004", + tabLabel: "원인자부담금", + rowStartNum: 3, + fixedColumns: 3, + 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: "levyEvntcsCnt", fieldName: "levyEvntcsCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "levyAmt", fieldName: "levyAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "trgtByWholClctnEvntcsCnt", fieldName: "trgtByWholClctnEvntcsCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "trgtByWholClctnAmt", fieldName: "trgtByWholClctnAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "mltvEqpmEvntcsCnt", fieldName: "mltvEqpmEvntcsCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mltvEqpmAmt", fieldName: "mltvEqpmAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "othrCstrnOthrActrCnt", fieldName: "othrCstrnOthrActrCnt", header: { text: "건수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "othrCstrnOthrActrAmt", fieldName: "othrCstrnOthrActrAmt", header: { text: "금액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "levyGroup", header: { text: "부과액" }, items: ["levyEvntcsCnt", "levyAmt"] }, + { + name: "collectionStatusGroup", + header: { text: "대상별 징수현황" }, + items: [ + { name: "c1Group", header: { text: "계" }, items: ["trgtByWholClctnEvntcsCnt", "trgtByWholClctnAmt"] }, + { name: "c2Group", header: { text: "배수설비설치자\n(하수도법 제61조 제1항)" }, items: ["mltvEqpmEvntcsCnt", "mltvEqpmAmt"] }, + { name: "c3Group", header: { text: "타공사·타행위자\n(하수도법 제61조 제2항)" }, items: ["othrCstrnOthrActrCnt", "othrCstrnOthrActrAmt"] } + ] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts new file mode 100644 index 000000000..5bfe56b94 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column.ts @@ -0,0 +1,62 @@ +import { ValueType } from "realgrid"; + +export const SewerageUsageFeeTab3ColumnConfig = { + tabKey: "ST00090003", + tabLabel: "하수도 사용료", + rowStartNum: 3, + fixedColumns: 3, + 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: "levyAmt", fieldName: "levyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "levyQnt", fieldName: "levyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 가정용 + { name: "hshusLevyAmt", fieldName: "hshusLevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "hshusLevyQnt", fieldName: "hshusLevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "hshusUntprcAmt", fieldName: "hshusUntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 업무용 + { name: "taskFrLevyAmt", fieldName: "taskFrLevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "taskFrLevyQnt", fieldName: "taskFrLevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "taskFrUntprcAmt", fieldName: "taskFrUntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 영업용(일반용) + { name: "bsnGnrlLevyAmt", fieldName: "bsnGnrlLevyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bsnGnrlLevyQnt", fieldName: "bsnGnrlLevyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bsnGnrlUntprcAmt", fieldName: "bsnGnrlUntprcAmt", header: { text: "단가(원/톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 욕탕 1종 + { name: "massBthhsFrLevyAmt", fieldName: "massBthhsFrLevyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "massBthhsFrLevyQnt", fieldName: "massBthhsFrLevyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "massBthhsFrUntprcAmt", fieldName: "massBthhsFrUntprcAmt", header: { text: "단가(원/톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 욕탕 2종 + { name: "bthhsFrSpcs2LevyAmt", fieldName: "bthhsFrSpcs2LevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bthhsFrSpcs2LevyQnt", fieldName: "bthhsFrSpcs2LevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bthhsFrSpcs2UntprcAmt", fieldName: "bthhsFrSpcs2UntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 산업 및 공업용 + { name: "induMdsrLevyAmt", fieldName: "induMdsrLevyAmt", header: { text: "부과액(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "induMdsrLevyQnt", fieldName: "induMdsrLevyQnt", header: { text: "부과량(천톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "induMdsrUntprcAmt", fieldName: "induMdsrUntprcAmt", header: { text: "단가(원/톤)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 기타 + { name: "etcLevyAmt", fieldName: "etcLevyAmt", header: { text: "부과액" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcLevyQnt", fieldName: "etcLevyQnt", header: { text: "부과량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcLevyUntprcAmt", fieldName: "etcLevyUntprcAmt", header: { text: "단가" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "totalGroup", header: { text: "총계" }, items: ["levyAmt", "levyQnt"] }, + { name: "g1Group", header: { text: "가정용" }, items: ["hshusLevyAmt", "hshusLevyQnt", "hshusUntprcAmt"] }, + { name: "g2Group", header: { text: "업무용" }, items: ["taskFrLevyAmt", "taskFrLevyQnt", "taskFrUntprcAmt"] }, + { name: "g3Group", header: { text: "영업용(일반용)" }, items: ["bsnGnrlLevyAmt", "bsnGnrlLevyQnt", "bsnGnrlUntprcAmt"] }, + { name: "g4Group", header: { text: "욕탕 1종(대중탕용)" }, items: ["massBthhsFrLevyAmt", "massBthhsFrLevyQnt", "massBthhsFrUntprcAmt"] }, + { name: "g5Group", header: { text: "욕탕2종" }, items: ["bthhsFrSpcs2LevyAmt", "bthhsFrSpcs2LevyQnt", "bthhsFrSpcs2UntprcAmt"] }, + { name: "g6Group", header: { text: "산업 및 공업용" }, items: ["induMdsrLevyAmt", "induMdsrLevyQnt", "induMdsrUntprcAmt"] }, + { name: "g7Group", header: { text: "기타" }, items: ["etcLevyAmt", "etcLevyQnt", "etcLevyUntprcAmt"] } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column.ts new file mode 100644 index 000000000..6987edb66 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column.ts @@ -0,0 +1,48 @@ +import { ValueType } from "realgrid"; + +export const SewerageGrayWaterTab2ColumnConfig = { + tabKey: "ST00100002", + tabLabel: "중수도 현황", + rowStartNum: 2, // 2단 그룹 헤더이므로 rowStartNum은 2로 지정합니다. + fixedColumns: 1, + 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 }, + + // 단일 마스터 컬럼들 (image_103ea5 스펙 반영) + { name: "fcltNm", fieldName: "fcltNm", header: { text: "건축물명/\n개발사업명" }, editable: false }, + { name: "bldgSeCn", fieldName: "bldgSeCn", header: { text: "시설물용도/\n개발사업종류" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "주소" }, editable: false }, + { name: "lglTrgtYn", fieldName: "lglTrgtYn", header: { text: "법적의무\n대상여부\n(대상/미대상)" }, editable: false }, + { name: "cmptnStrtYmd", fieldName: "cmptnStrtYmd", header: { text: "설치완료일\nyyyy-mm-dd" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일\nyyyy-mm-dd" }, editable: false }, + + // 시설현황 그룹 소속 + { name: "archWholArea", fieldName: "archWholArea", header: { text: "건축연면적\n(㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "prcsCpct", fieldName: "prcsCpct", header: { text: "처리용량\n(㎡/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 이용 현황 소속 컬럼들 + { name: "wtrUseQnt", fieldName: "wtrUseQnt", header: { text: "물 사용량\n(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "ruswtrUtztnQnt", fieldName: "ruswtrUtztnQnt", header: { text: "중수도\n이용량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "sbstCntUtztnQnt", fieldName: "sbstCntUtztnQnt", header: { text: "대체수\n이용량(㎡/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "mnUsgCn", fieldName: "mnUsgCn", header: { text: "중수도용도" }, editable: false }, + { name: "ruswtrOperAmt", fieldName: "ruswtrOperAmt", header: { text: "연간운영비\n(백만원/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "bldgSeCn", + "fcltPstnCn", + "lglTrgtYn", + "cmptnStrtYmd", + "oprtngStrtYmd", + { name: "facilityStatusGroup", header: { text: "시설현황" }, items: ["archWholArea", "prcsCpct"] }, + "wtrUseQnt", + "ruswtrUtztnQnt", + "sbstCntUtztnQnt", + "mnUsgCn", + "ruswtrOperAmt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column.ts new file mode 100644 index 000000000..84d3d0a7e --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column.ts @@ -0,0 +1,33 @@ +import { ValueType } from "realgrid"; + +export const SewerageRainwaterTab3ColumnConfig = { + tabKey: "ST00100003", + tabLabel: "빗물이용시설현황", + rowStartNum: 2, + fixedColumns: 3, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false }, + { name: "bdstTypCn", fieldName: "bdstTypCn", header: { text: "건축물용도" }, editable: false }, + { name: "fcltPstnCn", fieldName: "fcltPstnCn", header: { text: "위치(주소)" }, editable: false }, + { name: "lglTrgtYn", fieldName: "lglTrgtYn", header: { text: "법적의무 대상여부\n(대상/미대상)" }, editable: false }, + { name: "instlCmptnYmd", fieldName: "instlCmptnYmd", header: { text: "설치완료일\n(yyyy-mm-dd)" }, editable: false }, + { name: "instlAmt", fieldName: "instlAmt", header: { text: "설치비\n(백만원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rcptMynCn", fieldName: "rcptMynCn", header: { text: "집수면 종류" }, editable: false }, + { name: "filtrPrcsFcltYnExpln", fieldName: "filtrPrcsFcltYnExpln", header: { text: "여과등 처리시설 유무" }, editable: false }, + { name: "impdCpct", fieldName: "impdCpct", header: { text: "저류조 용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 이용 현황 그룹 소속 + { name: "anlUseQnt", fieldName: "anlUseQnt", header: { text: "연간 이용량\n(㎥/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "exlnUtlzUsgExpln", fieldName: "exlnUtlzUsgExpln", header: { text: "이용용도" }, editable: false }, + { name: "anlOperAmt", fieldName: "anlOperAmt", header: { text: "연간운영비\n(백만원/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", "bdstTypCn", "fcltPstnCn", "lglTrgtYn", "instlCmptnYmd", "instlAmt", "rcptMynCn", "filtrPrcsFcltYnExpln", "impdCpct", + "anlUseQnt", "exlnUtlzUsgExpln", "anlOperAmt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageReuseTab1Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageReuseTab1Column.ts new file mode 100644 index 000000000..6534fa651 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageReuseTab1Column.ts @@ -0,0 +1,89 @@ +import { ValueType } from "realgrid"; + +export const SewerageReuseTab1ColumnConfig = { + tabKey: "ST00100001", + tabLabel: "공공하수처리수 재이용", + rowStartNum: 4, // 3단 계층 다단 헤더 구조이므로 4로 유지합니다. + fixedColumns: 1, + 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: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "시설명" }, editable: false }, + { name: "sncnYmd", fieldName: "sncnYmd", header: { text: "인가일\n(년/월/일)" }, editable: false }, + { name: "prcsFcltCpct", fieldName: "prcsFcltCpct", header: { text: "시설용량\n(㎥/일)" }, editable: false }, + { name: "prcsQnt", fieldName: "prcsQnt", header: { text: "연간\n하수처리량\n(천톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "lglYn", fieldName: "lglYn", header: { text: "법적의무대상\n(대상/미대상)" }, editable: false }, + + // 재이용시설 설치시 소속 + { name: "instlYn", fieldName: "instlYn", header: { text: "설치유무\n(유/무)" }, editable: false }, + { name: "oprtngStrtYmd", fieldName: "oprtngStrtYmd", header: { text: "가동개시일\nyyyy-mm-dd" }, editable: false }, + { name: "instlAprvYmd", fieldName: "instlAprvYmd", header: { text: "설치승인일\nyyyy-mm-dd" }, editable: false }, + { name: "rprcsFcltCpct", fieldName: "rprcsFcltCpct", header: { text: "용량(톤/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "mtcnsNm", fieldName: "mtcnsNm", header: { text: "공법/기자재" }, editable: false }, + { name: "ruswtPplLen", fieldName: "ruswtPplLen", header: { text: "공급관로길이\n(km)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false }, + { name: "ruswtPumpLen", fieldName: "ruswtPumpLen", header: { text: "공급펌프\n(톤/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 하수처리수 재이용현황 + { name: "wholUseQnt", fieldName: "wholUseQnt", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlWshwtrQnt", fieldName: "ithlWshwtrQnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlClnQty", fieldName: "ithlClnQty", header: { text: "세척수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlFrzgQty", fieldName: "ithlFrzgQty", header: { text: "냉각수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlLdhcUswtrQnt", fieldName: "ithlLdhcUswtrQnt", header: { text: "청소수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlDltnUswtrQnt", fieldName: "ithlDltnUswtrQnt", header: { text: "조경용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlRuswtrQnt", fieldName: "ithlRuswtrQnt", header: { text: "희석용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlEtcQnt", fieldName: "ithlEtcQnt", header: { text: "장내중수도" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ithlUnsdQnt", fieldName: "ithlUnsdQnt", header: { text: "장내기타용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "otcntrUswtrWhol", fieldName: "otcntrUswtrWhol", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrClnRstrmQnt", fieldName: "otcntrClnRstrmQnt", header: { text: "청소·화장실\n용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrWshwtrQnt", fieldName: "otcntrWshwtrQnt", header: { text: "세척·살수용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrLdhcUswtrQnt", fieldName: "otcntrLdhcUswtrQnt", header: { text: "조경용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrHdrplUswtrQnt", fieldName: "otcntrHdrplUswtrQnt", header: { text: "친수용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otcntrRvrKeepQnt", fieldName: "otcntrRvrKeepQnt", header: { text: "하천유지용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "agrcltUswtrPstnQnt", fieldName: "agrcltUswtrPstnQnt", header: { text: "농업용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "ugwtrChargQnt", fieldName: "ugwtrChargQnt", header: { text: "지하수충전" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "mdsrUswtrQnt", fieldName: "mdsrUswtrQnt", header: { text: "공업용수" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "rtlztnRt", fieldName: "rtlztnRt", header: { text: "재이용률\n(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + { name: "seweragePlantGroup", header: { text: "하수처리장" }, items: ["swtrmtFcltNm", "sncnYmd", "prcsFcltCpct", "prcsQnt", "lglYn"] }, + { + name: "reuseFacilityGroup", + header: { text: "재이용시설" }, + items: [ + "instlYn", + { + name: "installationGroup", + header: { text: "재이용시설 설치시" }, + items: [ + "oprtngStrtYmd", + "instlAprvYmd", + { + // ★ 핵심 교정: 이미지 명세에 맞춰 내부에 서브 그룹 헤더 추가 완료 + name: "reProcessingFacilityGroup", + header: { text: "재처리시설" }, + items: ["rprcsFcltCpct", "mtcnsNm"] + }, + "ruswtPplLen", + "ruswtPumpLen" + ] + } + ] + }, + { + name: "reuseStatusGroup", + header: { text: "하수처리수 재이용현황(천톤/년)" }, + items: [ + "wholUseQnt", + { name: "internalWaterGroup", header: { text: "장내 용수" }, items: ["ithlWshwtrQnt", "ithlClnQty", "ithlFrzgQty", "ithlLdhcUswtrQnt", "ithlDltnUswtrQnt", "ithlRuswtrQnt", "ithlEtcQnt", "ithlUnsdQnt"] }, + { name: "externalWaterGroup", header: { text: "장외 용수" }, items: ["otcntrUswtrWhol", "otcntrClnRstrmQnt", "otcntrWshwtrQnt", "otcntrLdhcUswtrQnt", "otcntrHdrplUswtrQnt", "otcntrRvrKeepQnt", "agrcltUswtrPstnQnt", "ugwtrChargQnt", "mdsrUswtrQnt"] } + ] + }, + "rtlztnRt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column.ts new file mode 100644 index 000000000..2d51a5354 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column.ts @@ -0,0 +1,28 @@ + + + +export const SewerageWwtRtlztnRegTab4ColumnConfig = { + tabKey: "ST00100004", + tabLabel: "하·폐수처리수재이용시설설계시공업등록현황", + rowStartNum: 2, // 행정구역 2단 묶음 헤더 구조이므로 2로 지정합니다. + fixedColumns: 1, + 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 }, + + // 메인 마스터 문자열 지표들 (화면 그리드 명세서 순서 100% 매칭) + { name: "bplcNm", fieldName: "bplcNm", header: { text: "상호(명칭)" }, editable: false }, + { name: "rprsvNm", fieldName: "rprsvNm", header: { text: "대표자" }, editable: false }, + { name: "bplcPstnCn", fieldName: "bplcPstnCn", header: { text: "주소" }, editable: false }, + { name: "regDt", fieldName: "regDt", header: { text: "등록일\nyyyy-mm-dd" }, editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "regDt" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column.ts b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column.ts new file mode 100644 index 000000000..148bc1927 --- /dev/null +++ b/apps/st/src/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column.ts @@ -0,0 +1,25 @@ +export const SewerageWwtRtlztnSncnTab5ColumnConfig = { + tabKey: "ST00100005", + tabLabel: "하·폐수처리수재이용시설사업인가현황", + rowStartNum: 2, // 행정구역 2단 레이아웃에 최적화 + fixedColumns: 1, + 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: "bplcNm", fieldName: "bplcNm", header: { text: "상호(명칭)" }, editable: false }, + { name: "rprsvNm", fieldName: "rprsvNm", header: { text: "대표자" }, editable: false }, + { name: "bplcPstnCn", fieldName: "bplcPstnCn", header: { text: "주소" }, editable: false }, + { name: "coRegYmd", fieldName: "coRegYmd", header: { text: "인가일\n(년/월/일)" }, editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "bplcNm", + "rprsvNm", + "bplcPstnCn", + "coRegYmd" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts b/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts index 8ed48c355..3d06e995a 100644 --- a/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts +++ b/apps/st/src/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn.ts @@ -1,77 +1,36 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 1: 민원 -export const ComplaintInpColumn = { - columns: [ - { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } }, - { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } }, - { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } }, - { - name: "swtrmtPpltnCnt", - fieldName: "swtrmtPpltnCnt", - header: { text: "하수처리장민원건수(건)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "whol", - fieldName: "whol", - header: { text: "합계" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { name: "odorCnt", fieldName: "odorCnt", header: { text: "악취" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "swagePplCnt", fieldName: "swagePplCnt", header: { text: "하수관거" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "dschgWqCnt", fieldName: "dschgWqCnt", header: { text: "방류수수질" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "ldsCnt", fieldName: "ldsCnt", header: { text: "퇴적토" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "plltnAcdntCnt", fieldName: "plltnAcdntCnt", header: { text: "오염사고" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "sewerCrgAmt", fieldName: "sewerCrgAmt", header: { text: "하수도요금" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { - name: "crtrPpltnCvlcptEvntcsCnt", - fieldName: "crtrPpltnCvlcptEvntcsCnt", - header: { text: "인구10만명당 민원건수(건)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.00", - }, - { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, - { name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false }, - { name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false }, - { name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false }, - ].map((col) => ({ ...col, editable: false })), - - columnLayout: [ - { - name: "groupA", - hideChildHeaders: false, - header: { text: "행정구역" }, - items: ["ctpvNm", "sggNm", "admdstNm"], - }, - { - name: "groupB", - hideChildHeaders: false, - header: { text: "민원건수(건)" }, - items: [ - "whol", - "odorCnt", - "swagePplCnt", - "dschgWqCnt", - "ldsCnt", - "plltnAcdntCnt", - "sewerCrgAmt", - "etcCnt", - ], - }, - "swtrmtPpltnCnt", - "crtrPpltnCvlcptEvntcsCnt", - "rmrk", - "statsYr", - "admdstCd", - "rowType", - ], - - rowStartNum: 2, - fixedColumns: 1, +export const SewageComplaintTab1ColumnConfig = { tabKey: "ST00130001", tabLabel: "민원", + rowStartNum: 3, // 행정구역 3단 헤더 + fixedColumns: 1, + 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: "swtrmtPpltnCnt", fieldName: "swtrmtPpltnCnt", header: { text: "하수처리인구\n(명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // 민원유형 + { name: "whol", fieldName: "whol", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "odorCnt", fieldName: "odorCnt", header: { text: "악취" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "swagePplCnt", fieldName: "swagePplCnt", header: { text: "하수관로" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "dschgWqCnt", fieldName: "dschgWqCnt", header: { text: "배수설비" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "ldsCnt", fieldName: "ldsCnt", header: { text: "맨홀" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "plltnAcdntCnt", fieldName: "plltnAcdntCnt", header: { text: "정화조" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "sewerCrgAmt", fieldName: "sewerCrgAmt", header: { text: "하수도요금" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + { name: "crtrPpltnCvlcptEvntcsCnt", fieldName: "crtrPpltnCvlcptEvntcsCnt", header: { text: "기준인구당\n민원건수\n(건/10,000명)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.00", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swtrmtPpltnCnt", + { + name: "complaintGroup", + header: { text: "민원유형(건)" }, + items: ["whol", "odorCnt", "swagePplCnt", "dschgWqCnt", "ldsCnt", "plltnAcdntCnt", "sewerCrgAmt", "etcCnt"] + }, + "crtrPpltnCvlcptEvntcsCnt" + ] }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts b/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts index 1a121f49d..82890e8b9 100644 --- a/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts +++ b/apps/st/src/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn.ts @@ -1,81 +1,34 @@ import { ValueType } from "realgrid"; -// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 2: 주민친화시설 -export const ResidentFriendlyFactTotColumn = { - columns: [ - { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } }, - { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } }, - { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } }, - { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" } }, - { - name: "fcltCpct", - fieldName: "fcltCpct", - header: { text: "시설용량(m³/일)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { - name: "tmplntArea", - fieldName: "tmplntArea", - header: { text: "처리장면적(㎡)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.00", - }, - { - name: "whol", - fieldName: "whol", - header: { text: "총계" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0", - }, - { name: "brkFcltArea", fieldName: "brkFcltArea", header: { text: "휴게시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "childFrshpSpacArea", fieldName: "childFrshpSpacArea", header: { text: "어린이놀이공간" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "bdySportsFcltArea", fieldName: "bdySportsFcltArea", header: { text: "체력단련시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "sportsFcltArea", fieldName: "sportsFcltArea", header: { text: "운동시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "pkltArea", fieldName: "pkltArea", header: { text: "주차장" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "eduMultiFcltArea", fieldName: "eduMultiFcltArea", header: { text: "교육 및 문화시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "wtlndMakeArea", fieldName: "wtlndMakeArea", header: { text: "습지조성(생태공원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" }, - { name: "rmrk", fieldName: "rmrk", header: { text: "비고" } }, - { name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false }, - { name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false }, - { name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false }, - ].map((col) => ({ ...col, editable: false })), - - columnLayout: [ - { - name: "groupA", - hideChildHeaders: false, - header: { text: "행정구역" }, - items: ["ctpvNm", "sggNm", "admdstNm"], - }, - "swtrmtFcltNm", - "fcltCpct", - "tmplntArea", - { - name: "groupB", - hideChildHeaders: false, - header: { text: "주민친화시설 면적(㎡)" }, - items: [ - "whol", - "brkFcltArea", - "childFrshpSpacArea", - "bdySportsFcltArea", - "sportsFcltArea", - "pkltArea", - "eduMultiFcltArea", - "wtlndMakeArea", - "etcCnt", - ], - }, - "rmrk", - "statsYr", - "admdstCd", - "rowType", - ], - - rowStartNum: 2, - fixedColumns: 3, +export const ResidentFriendlyFacilityTab2ColumnConfig = { tabKey: "ST00130002", tabLabel: "주민친화시설", + rowStartNum: 3, // 행정구역 3단 헤더 + fixedColumns: 1, + 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: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" }, editable: false }, + { name: "tmplntArea", fieldName: "tmplntArea", header: { text: "처리장\n면적(㎡)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + // 주민친화시설 면적 + { name: "whol", fieldName: "whol", header: { text: "총계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "brkFcltArea", fieldName: "brkFcltArea", header: { text: "휴게시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "childFrshpSpacArea", fieldName: "childFrshpSpacArea", header: { text: "어린이놀이공간" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "bdySportsFcltArea", fieldName: "bdySportsFcltArea", header: { text: "체력단련시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "sportsFcltArea", fieldName: "sportsFcltArea", header: { text: "운동시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "pkltArea", fieldName: "pkltArea", header: { text: "주차장" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "eduMultiFcltArea", fieldName: "eduMultiFcltArea", header: { text: "교육 및\n문화시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "wtlndMakeArea", fieldName: "wtlndMakeArea", header: { text: "습지조성\n(생태공원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swtrmtFcltNm", "tmplntArea", + { + name: "facilityGroup", + header: { text: "주민친화시설 면적(㎡)" }, + items: ["whol", "brkFcltArea", "childFrshpSpacArea", "bdySportsFcltArea", "sportsFcltArea", "pkltArea", "eduMultiFcltArea", "wtlndMakeArea", "etcCnt"] + } + ] }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/DigestionTankTab2Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/DigestionTankTab2Column.ts new file mode 100644 index 000000000..06d7f7f17 --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/DigestionTankTab2Column.ts @@ -0,0 +1,44 @@ +import { ValueType } from "realgrid"; + +export const DigestionTankTab2ColumnConfig = { + tabKey: "ST00110002", + tabLabel: "소화조 운영현황", + rowStartNum: 3, // 행정구역, 하수처리장명 등 기본 정보 이후 다단 헤더가 있으므로 3단 지정 + fixedColumns: 1, + 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: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "하수\n처리장명" }, editable: false }, + + { name: "digstnEnYn", fieldName: "digstnEnYn", header: { text: "소화조 유/무\n(Y/N)선택" }, editable: false }, + { name: "digstnCpct", fieldName: "digstnCpct", header: { text: "설계\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "actlDrvnCpct", fieldName: "actlDrvnCpct", header: { text: "실제운전\n용량(㎥)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "digstnGasInputQnt", fieldName: "digstnGasInputQnt", header: { text: "투입량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "digstnGasInputSldDnst", fieldName: "digstnGasInputSldDnst", header: { text: "투입 TS 농도\n(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "digstnTnkOprtngDayCnt", fieldName: "digstnTnkOprtngDayCnt", header: { text: "가동일수\n(연간)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "ocrnGasQnt", fieldName: "ocrnGasQnt", header: { text: "발생가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 소화가스 이용현황(하위 항목) + { name: "digstnGasUtztnWholPrst", fieldName: "digstnGasUtztnWholPrst", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "digstnGasUtztnNtslQnt", fieldName: "digstnGasUtztnNtslQnt", header: { text: "판매" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "digstnGasUtztnEtgtQnt", fieldName: "digstnGasUtztnEtgtQnt", header: { text: "발전" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "digstnGasUtztnItslfQnt", fieldName: "digstnGasUtztnItslfQnt", header: { text: "자체활용" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "digstnGasUtztnEtcQnt", fieldName: "digstnGasUtztnEtcQnt", header: { text: "기타(CNG등)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + { name: "surplsGasQnt", fieldName: "surplsGasQnt", header: { text: "잉여가스량\n(㎥/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swtrmtFcltNm", + { name: "operGroup", header: { text: "소화조 운영현황" }, items: [ + "digstnEnYn", "digstnCpct", "actlDrvnCpct", "digstnGasInputQnt", "digstnGasInputSldDnst", "digstnTnkOprtngDayCnt", "ocrnGasQnt", + { + name: "gasUsageGroup", + header: { text: "소화가스 이용현황(㎥/일)" }, + items: ["digstnGasUtztnWholPrst", "digstnGasUtztnNtslQnt", "digstnGasUtztnEtgtQnt", "digstnGasUtztnItslfQnt", "digstnGasUtztnEtcQnt"] + }, + "surplsGasQnt" + ] }, + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column.ts new file mode 100644 index 000000000..df6a57742 --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column.ts @@ -0,0 +1,37 @@ +import { ValueType } from "realgrid"; + +export const HumanWasteSludgeTab4ColumnConfig = { + tabKey: "ST00110004", + tabLabel: "분뇨찌꺼기(슬러지)처리현황", + rowStartNum: 2, // 행정구역 2단 레이아웃 + fixedColumns: 1, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "분뇨\n처리장명" }, editable: false }, + + { name: "gnam", fieldName: "gnam", header: { text: "발생량\n(A)=(B)+(C)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "wholDspsQnt", fieldName: "wholDspsQnt", header: { text: "계(B)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "rcycQnt", fieldName: "rcycQnt", header: { text: "재활용" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "olandLfvlm", fieldName: "olandLfvlm", header: { text: "육상매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrQnt", fieldName: "incnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "oceanInputQnt", fieldName: "oceanInputQnt", header: { text: "해양투기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelQnt", fieldName: "fuelQnt", header: { text: "연료화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "etcQnt", fieldName: "etcQnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "nondspsQnt", fieldName: "nondspsQnt", header: { text: "미처분량\n(이월량)(C)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "타시설\n연계여부" }, editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + "gnam", + { + name: "dspsGroup", + header: { text: "처분량" }, + items: ["wholDspsQnt", "rcycQnt", "olandLfvlm", "incnrQnt", "oceanInputQnt", "fuelQnt", "etcQnt"] + }, + "nondspsQnt", + "swtrmtFcltNm" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column.ts new file mode 100644 index 000000000..c7a5786ce --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column.ts @@ -0,0 +1,30 @@ +import { ValueType } from "realgrid"; + +export const SewageSludgeOperTab3ColumnConfig = { + tabKey: "ST00110003", + tabLabel: "하수찌꺼기 처리시설 운영현황", + rowStartNum: 2, + fixedColumns: 1, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "찌꺼기처리\n시설명" }, editable: false }, + + { name: "prcsMthExpln", fieldName: "prcsMthExpln", header: { text: "처리방식" }, editable: false }, + { name: "fcltCpct", fieldName: "fcltCpct", header: { text: "시설용량\n(톤/일)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "anlPrcsQnt", fieldName: "anlPrcsQnt", header: { text: "연간처리량\n(톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dayAvgOprtngHr", fieldName: "dayAvgOprtngHr", header: { text: "일평균\n가동시간\n(1~24)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "anlOprtngDayCnt", fieldName: "anlOprtngDayCnt", header: { text: "연중 가동일\n(1~330)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "oprtngRt", fieldName: "oprtngRt", header: { text: "가동율(%)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + { + name: "operStatusGroup", + header: { text: "처리시설 운영현황" }, + items: ["prcsMthExpln", "fcltCpct", "anlPrcsQnt", "dayAvgOprtngHr", "anlOprtngDayCnt", "oprtngRt"] + } + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column.ts b/apps/st/src/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column.ts new file mode 100644 index 000000000..d61e6a535 --- /dev/null +++ b/apps/st/src/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column.ts @@ -0,0 +1,103 @@ +import { ValueType } from "realgrid"; + +export const SludgeProcessingTab1ColumnConfig = { + tabKey: "ST00110001", + tabLabel: "찌꺼기(슬러지)처리시설현황", + rowStartNum: 4, // 3단 그룹 대단위 레이아웃 명세이므로 4로 바인딩합니다. + fixedColumns: 1, + 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: "fcltNm", fieldName: "fcltNm", header: { text: "하수\n처리장명" }, editable: false }, + + // 찌꺼기발생량 + { name: "gnamWhol", fieldName: "gnamWhol", header: { text: "합계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "itslfQnt", fieldName: "itslfQnt", header: { text: "자체" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdInflwQnt", fieldName: "otsdInflwQnt", header: { text: "외부유입량" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 이송 및 함수율 지표들 + { name: "transfTmplntNm", fieldName: "transfTmplntNm", header: { text: "이송처리장" }, editable: false }, + { name: "transfPrcsQnt", fieldName: "transfPrcsQnt", header: { text: "이송처리량\n(㎥/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "srpFncRt", fieldName: "srpFncRt", header: { text: "함수율\n(%, 탈수기준)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "wholSrpPrcsQnt", fieldName: "wholSrpPrcsQnt", header: { text: "찌꺼기\n처리량 합계\n(톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 자체 처리 1차 대문 항목들 + { name: "itslfTs1Whol", fieldName: "itslfTs1Whol", header: { text: "자체찌꺼기\n처리량 소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "itslfTs1RclmQnt", fieldName: "itslfTs1RclmQnt", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + + // 소각 후 처리(2차) 서브 항목들 + { name: "itslfTs1IncnrQnt", fieldName: "itslfTs1IncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsWhol", fieldName: "incnrPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsLfvlm", fieldName: "incnrPrcsLfvlm", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsPrdctRmtrlQnt", fieldName: "incnrPrcsPrdctRmtrlQnt", header: { text: "제품원료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsFmlyrQnt", fieldName: "incnrPrcsFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "incnrPrcsFcltNm", fieldName: "incnrPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 고화 처리군 + { name: "itslfTs1AltdQnt", fieldName: "itslfTs1AltdQnt", header: { text: "고화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsWhol", fieldName: "stbznPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsLfvlm", fieldName: "stbznPrcsLfvlm", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsFmlyrQnt", fieldName: "stbznPrcsFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "stbznPrcsFcltNm", fieldName: "stbznPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 건조 처리군 + { name: "itslfTs1DryQnt", fieldName: "itslfTs1DryQnt", header: { text: "건조" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsWhol", fieldName: "dryPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsLfvlm", fieldName: "dryPrcsLfvlm", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsIncnrQnt", fieldName: "dryPrcsIncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsFuelQnt", fieldName: "dryPrcsFuelQnt", header: { text: "연료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsPrdctRmtrlQnt", fieldName: "dryPrcsPrdctRmtrlQnt", header: { text: "제품원료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsFmlyrQnt", fieldName: "dryPrcsFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsCmpstgQnt", fieldName: "dryPrcsCmpstgQnt", header: { text: "퇴비화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dryPrcsFcltNm", fieldName: "dryPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 탄화 처리군 + { name: "itslfTs1FuelQnt", fieldName: "itslfTs1FuelQnt", header: { text: "탄화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsWhol", fieldName: "fuelPrcsWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsIncnrQnt", fieldName: "fuelPrcsIncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsFuelQnt", fieldName: "fuelPrcsFuelQnt", header: { text: "연료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "fuelPrcsFcltNm", fieldName: "fuelPrcsFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 퇴비화 독립군 + { name: "itslfTs1CmpstgQnt", fieldName: "itslfTs1CmpstgQnt", header: { text: "퇴비화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "cmpsPrcsFcltBzentyNm", fieldName: "cmpsPrcsFcltBzentyNm", header: { text: "시설(업체명)" }, editable: false }, + + // 외부(민간) 위탁 처리현황 + { name: "otsdCnsgnWhol", fieldName: "otsdCnsgnWhol", header: { text: "소계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnRclmQnt", fieldName: "otsdCnsgnRclmQnt", header: { text: "매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnIncnrQnt", fieldName: "otsdCnsgnIncnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnFuelPwplQnt", fieldName: "otsdCnsgnFuelPwplQnt", header: { text: "연료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnPrdctRmtrlQnt", fieldName: "otsdCnsgnPrdctRmtrlQnt", header: { text: "제품원료" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnFmlyrQnt", fieldName: "otsdCnsgnFmlyrQnt", header: { text: "복토재" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnCmpstgQnt", fieldName: "otsdCnsgnCmpstgQnt", header: { text: "퇴비화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "otsdCnsgnFcltNm", fieldName: "otsdCnsgnFcltNm", header: { text: "시설(업체명)" }, editable: false }, + + // 수도권 광역 위탁 처리량 + { name: "cparTs2Whol", fieldName: "cparTs2Whol", header: { text: "수도권광역\n위탁처리량\n(톤/년)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "fcltNm", + { name: "gnamGroup", header: { text: "찌꺼기발생량(톤/년, 탈수기준)" }, items: ["gnamWhol", "itslfQnt", "otsdInflwQnt"] }, + "transfTmplntNm", "transfPrcsQnt", "srpFncRt", "wholSrpPrcsQnt", + { + name: "itslfProcessingGroup", + header: { text: "자체 찌꺼기 처리량 현황(톤/년)" }, + items: [ + "itslfTs1Whol", "itslfTs1RclmQnt","itslfTs1IncnrQnt", + { name: "incnrGroup", header: { text: "소각 후 처리(2차)" }, items: ["incnrPrcsWhol", "incnrPrcsLfvlm", "incnrPrcsPrdctRmtrlQnt", "incnrPrcsFmlyrQnt", "incnrPrcsFcltNm"] }, + "itslfTs1AltdQnt", + { name: "stbznGroup", header: { text: "고화 후 처리(2차)" }, items: [ "stbznPrcsWhol", "stbznPrcsLfvlm", "stbznPrcsFmlyrQnt", "stbznPrcsFcltNm"] }, + "itslfTs1DryQnt", + { name: "dryGroup", header: { text: "건조 후 처리(2차)" }, items: ["dryPrcsWhol", "dryPrcsLfvlm", "dryPrcsIncnrQnt", "dryPrcsFuelQnt", "dryPrcsPrdctRmtrlQnt", "dryPrcsFmlyrQnt", "dryPrcsCmpstgQnt", "dryPrcsFcltNm"] }, + "itslfTs1FuelQnt", + { name: "fuelGroup", header: { text: "탄화 후 처리(2차)" }, items: ["fuelPrcsWhol", "fuelPrcsIncnrQnt", "fuelPrcsFuelQnt", "fuelPrcsFcltNm"] }, + "itslfTs1CmpstgQnt", + { name: "cmpsGroup", header: { text: "퇴비화" }, items: ["cmpsPrcsFcltBzentyNm"] } + ] + }, + { name: "otsdGroup", header: { text: "외부(민간) 위탁 처리량(톤/년)" }, items: ["otsdCnsgnWhol", "otsdCnsgnRclmQnt", "otsdCnsgnIncnrQnt", "otsdCnsgnFuelPwplQnt", "otsdCnsgnPrdctRmtrlQnt", "otsdCnsgnFmlyrQnt", "otsdCnsgnCmpstgQnt", "otsdCnsgnFcltNm"] }, + "cparTs2Whol" + ] +}; \ No newline at end of file diff --git a/apps/st/src/components/util/grid/computedFieldCallbacks.ts b/apps/st/src/components/util/grid/computedFieldCallbacks.ts index 453cfa8c6..0506bee8a 100644 --- a/apps/st/src/components/util/grid/computedFieldCallbacks.ts +++ b/apps/st/src/components/util/grid/computedFieldCallbacks.ts @@ -43,6 +43,65 @@ export function createAvgValueCallback(fieldNames: string[]): DataFieldValueCall }; } +function sumFieldValues( + fields: string[], + fieldNamesArr: string[], + values: unknown[], +): number { + let sum = 0; + for (const f of fields) { + const idx = fieldNamesArr.indexOf(f); + if (idx >= 0) sum += toNum(values[idx]); + } + return sum; +} + +/** halfSumOfSums 한 건 — (sum(first) + sum(second)) / 2, 소수 내림 */ +function computeHalfSumOfSums( + firstSumFields: string[], + secondSumFields: string[], + fieldNamesArr: string[], + values: unknown[], +): number { + const firstSum = sumFieldValues(firstSumFields, fieldNamesArr, values); + const secondSum = sumFieldValues(secondSumFields, fieldNamesArr, values); + return Math.floor((firstSum + secondSum) / 2); +} + +export type HalfSumOfSumsGroup = { + firstSumFields: string[]; + secondSumFields: string[]; +}; + +/** 두 그룹 합계의 평균 — (sum(first) + sum(second)) / 2 */ +export function createHalfSumOfSumsValueCallback( + firstSumFields: string[], + secondSumFields: string[], +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => + computeHalfSumOfSums(firstSumFields, secondSumFields, fieldNamesArr, values); +} + +/** halfSumOfSums 2건의 합 — (B+C)/2 + (E+F)/2 */ +export function createSumOfTwoHalfSumOfSumsValueCallback( + firstGroup: HalfSumOfSumsGroup, + secondGroup: HalfSumOfSumsGroup, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => + computeHalfSumOfSums( + firstGroup.firstSumFields, + firstGroup.secondSumFields, + fieldNamesArr, + values, + ) + + computeHalfSumOfSums( + secondGroup.firstSumFields, + secondGroup.secondSumFields, + fieldNamesArr, + values, + ); +} + /** 차이값: fields[0] − fields[1] (예: 매설연장 − 철거연장) */ export function createDifferenceValueCallback(fields: string[]): DataFieldValueCallback { const a = fields[0] ?? ""; @@ -179,6 +238,115 @@ export function createSwrgInstlrtRateValueCallback( }; } +/** 처리효율: (유입BOD − 방류BOD) / 유입BOD */ +export function createBodPrcsRtValueCallback( + inflwField: string, + dschgField: string, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const inflwIdx = fieldNamesArr.indexOf(inflwField); + const dschgIdx = fieldNamesArr.indexOf(dschgField); + const inflw = inflwIdx >= 0 ? toNum(values[inflwIdx]) : 0; + const dschg = dschgIdx >= 0 ? toNum(values[dschgIdx]) : 0; + if (inflw === 0) return 0; + return (inflw - dschg) / inflw; + }; +} + +/** 총양수량: Σ(펌프대수 + 예비펌프대수) × 대당 양수량 */ +export function createPumpGrossPpqtyValueCallback( + pumpGroups: Array<{ + pumpCntField: string; + rsrvPumpCntField: string; + perPpqtyField: string; + }>, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + let total = 0; + for (const group of pumpGroups) { + const pumpIdx = fieldNamesArr.indexOf(group.pumpCntField); + const rsrvIdx = fieldNamesArr.indexOf(group.rsrvPumpCntField); + const perIdx = fieldNamesArr.indexOf(group.perPpqtyField); + const pumpCnt = pumpIdx >= 0 ? toNum(values[pumpIdx]) : 0; + const rsrvCnt = rsrvIdx >= 0 ? toNum(values[rsrvIdx]) : 0; + const perPpqty = perIdx >= 0 ? toNum(values[perIdx]) : 0; + total += (pumpCnt + rsrvCnt) * perPpqty; + } + return total; + }; +} + +/** (a/b) / (c/d) × 100 — 각 quotient 분모 0이면 0 */ +export function createQuotientRatioPercentValueCallback( + numeratorQuotient: [string, string], + denominatorQuotient: [string, string], +): DataFieldValueCallback { + const [numField, numDenField] = numeratorQuotient; + const [denNumField, denDenField] = denominatorQuotient; + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const getVal = (field: string) => { + const idx = fieldNamesArr.indexOf(field); + return idx >= 0 ? toNum(values[idx]) : 0; + }; + const numDen = getVal(numDenField); + const denDen = getVal(denDenField); + const denNum = getVal(denNumField); + if (numDen === 0 || denDen === 0 || denNum === 0) return 0; + const numerator = getVal(numField) / numDen; + const denominator = denNum / denDen; + if (denominator === 0) return 0; + return (numerator / denominator) * 100; + }; +} + +/** Σ(numeratorFields) / denominatorField × 10,000 — 기준인구당 건수(건/10,000명) */ +export function createSumPerTenThousandValueCallback( + numeratorFields: string[], + denominatorField: string, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const sum = sumFieldValues(numeratorFields, fieldNamesArr, values); + const denIdx = fieldNamesArr.indexOf(denominatorField); + const den = denIdx >= 0 ? toNum(values[denIdx]) : 0; + if (den === 0) return 0; + return truncateToFirstDecimal((sum / den) * 10000); + }; +} + +/** 나눗셈: fields[0] / fields[1] (분모 0이면 0) */ +export function createQuotientValueCallback(fields: string[]): DataFieldValueCallback { + const numeratorField = fields[0] ?? ""; + const denominatorField = fields[1] ?? ""; + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + if (!numeratorField || !denominatorField) return 0; + const numIdx = fieldNamesArr.indexOf(numeratorField); + const denIdx = fieldNamesArr.indexOf(denominatorField); + const numerator = numIdx >= 0 ? toNum(values[numIdx]) : 0; + const denominator = denIdx >= 0 ? toNum(values[denIdx]) : 0; + if (denominator === 0) return 0; + return numerator / denominator; + }; +} + +/** (numerator / denominator) × multiplier × 100 — 분모 0이면 0 */ +export function createQuotientTimesMultiplierPercentValueCallback( + numeratorField: string, + denominatorField: string, + multiplierField: string, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const getVal = (field: string) => { + const idx = fieldNamesArr.indexOf(field); + return idx >= 0 ? toNum(values[idx]) : 0; + }; + const denominator = getVal(denominatorField); + if (denominator === 0) return 0; + const quotient = getVal(numeratorField) / denominator; + const multiplier = getVal(multiplierField); + return truncateToFirstDecimal(quotient * multiplier * 100); + }; +} + /** 연산 타입에 맞는 valueCallback 반환 */ export function getComputedValueCallback(config: ComputedFieldConfig): DataFieldValueCallback { switch (config.type) { @@ -190,11 +358,28 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField return createMaxValueCallback(config.fields); case 'difference': return createDifferenceValueCallback(config.fields); + case 'quotient': + return createQuotientValueCallback(config.fields); + case 'quotientRatioPercent': + return createQuotientRatioPercentValueCallback( + config.numeratorQuotient, + config.denominatorQuotient, + ); case 'differenceOfSums': return createDifferenceOfSumsValueCallback( config.minuendFields, config.subtrahendFields, ); + case 'halfSumOfSums': + return createHalfSumOfSumsValueCallback( + config.firstSumFields, + config.secondSumFields, + ); + case 'sumOfTwoHalfSumOfSums': + return createSumOfTwoHalfSumOfSumsValueCallback( + config.firstHalfSum, + config.secondHalfSum, + ); case 'maxOfFieldAndSum': return createMaxOfFieldAndSumValueCallback( config.baseField, @@ -209,5 +394,20 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField return createCoverageRateValueCallback(config.numeratorFields, config.denominatorFields); case 'swrgInstlrtRate': return createSwrgInstlrtRateValueCallback(config.numeratorFields, config.denominatorFields, config.thirdFields); + case 'pumpGrossPpqty': + return createPumpGrossPpqtyValueCallback(config.pumpGroups); + case 'bodPrcsRt': + return createBodPrcsRtValueCallback(config.inflwField, config.dschgField); + case 'sumPerTenThousand': + return createSumPerTenThousandValueCallback( + config.numeratorFields, + config.denominatorField, + ); + case 'quotientTimesMultiplierPercent': + return createQuotientTimesMultiplierPercentValueCallback( + config.numeratorField, + config.denominatorField, + config.multiplierField, + ); } } diff --git a/apps/st/src/components/util/grid/gridFunction.ts b/apps/st/src/components/util/grid/gridFunction.ts index 28755a7f8..d6e36375a 100644 --- a/apps/st/src/components/util/grid/gridFunction.ts +++ b/apps/st/src/components/util/grid/gridFunction.ts @@ -1,3 +1,86 @@ +import type { CellStyleCallback, ColumnConfig } from "@/components/type/DataGrid.type"; + +/** 검토(review) 그리드: rowType이 CURR인 행(당해 연도) 흰색 배경 */ +export const reviewRowTypeCurrCellStyleCallback: CellStyleCallback = ({ row }) => { + if (String(row.rowType ?? "") === "CURR") { + return "rg-cell-rowtype-curr"; + } + return undefined; +}; + +/** 검토(review) CURR 행 스타일 — 지정 컬럼은 제외(merge/fixed 영역 깜빡임 방지) */ +export function createReviewRowTypeCurrCellStyleCallback(options?: { + excludeFieldNames?: readonly string[]; +}): CellStyleCallback { + const excluded = + options?.excludeFieldNames != null + ? new Set(options.excludeFieldNames) + : null; + + return ({ row, fieldName }) => { + if (excluded?.has(fieldName)) { + return undefined; + } + if (String(row.rowType ?? "") === "CURR") { + return "rg-cell-rowtype-curr"; + } + return undefined; + }; +} + +/** mergeCells 컬럼은 CURR 스타일 제외(merge 영역 깜빡임 방지) */ +function collectMergeCellFieldNames( + columns: readonly Pick[], +): string[] { + return columns + .filter((col) => col.mergeCells === true && col.fieldName) + .map((col) => col.fieldName); +} + +export function buildReviewRowTypeCurrCellStyleExcludeMergeCells( + columns: readonly Pick[], +): CellStyleCallback { + return createReviewRowTypeCurrCellStyleCallback({ + excludeFieldNames: collectMergeCellFieldNames(columns), + }); +} + +export function buildReviewRowTypeCurrCellStyleCallbackByTab( + tabs: readonly { + tabKey: string; + columns?: readonly Pick[]; + }[], +): Map { + const map = new Map(); + for (const tab of tabs) { + map.set( + tab.tabKey, + buildReviewRowTypeCurrCellStyleExcludeMergeCells(tab.columns ?? []), + ); + } + return map; +} + +/** 여러 cellStyleCallback 결과를 styleName으로 병합 */ +export function composeCellStyleCallbacks( + ...callbacks: CellStyleCallback[] +): CellStyleCallback { + return (params) => { + const styleNames: string[] = []; + for (const cb of callbacks) { + const result = cb(params); + if (!result) continue; + if (typeof result === "string") { + styleNames.push(result); + } else if (result.styleName) { + styleNames.push(result.styleName); + } + } + const styleName = styleNames.join(" ").trim(); + return styleName ? styleName : undefined; + }; +} + /** * 객체 배열을 그리드 row 형식으로 변환 (null/undefined → ""). * 어떤 엔티티 타입이든 공통 사용 가능. diff --git a/apps/st/src/components/util/grid/reviewEmptyCurrCellStyle.ts b/apps/st/src/components/util/grid/reviewEmptyCurrCellStyle.ts new file mode 100644 index 000000000..65a125475 --- /dev/null +++ b/apps/st/src/components/util/grid/reviewEmptyCurrCellStyle.ts @@ -0,0 +1,183 @@ +import type { CellStyleCallback, ColumnConfig } from "@/components/type/DataGrid.type"; +import { + buildReviewRowTypeCurrCellStyleExcludeMergeCells, + composeCellStyleCallbacks, +} from "@/components/util/grid/gridFunction"; + +const DISPLAY_YEAR_FIELD = "displayYear"; + +/** CURR 미입력 + OLD(전년) 데이터 있음 */ +export const REVIEW_CURR_EMPTY_WARN_STYLE = "rg-cell-review-curr-empty-warn"; +/** CURR 미입력 + OLD 행 없음 */ +export const REVIEW_CURR_EMPTY_NEW_STYLE = "rg-cell-review-curr-empty-new"; + +function getColumnFieldNames(columns: readonly Pick[]): string[] { + return columns.map((col) => col.fieldName).filter(Boolean); +} + +function getFieldNamesBeforeDivision(columns: readonly Pick[]): string[] { + const fields = getColumnFieldNames(columns); + const divisionIndex = fields.indexOf(DISPLAY_YEAR_FIELD); + if (divisionIndex <= 0) return []; + return fields.slice(0, divisionIndex); +} + +function getFieldNamesAfterDivision(columns: readonly Pick[]): string[] { + const fields = getColumnFieldNames(columns); + const divisionIndex = fields.indexOf(DISPLAY_YEAR_FIELD); + if (divisionIndex < 0 || divisionIndex >= fields.length - 1) return []; + return fields.slice(divisionIndex + 1); +} + +function getDivisionHighlightFieldNames( + columns: readonly Pick[], +): Set { + const divisionColumn = columns.find((col) => col.fieldName === DISPLAY_YEAR_FIELD); + return new Set( + [DISPLAY_YEAR_FIELD, divisionColumn?.name, divisionColumn?.fieldName].filter( + Boolean, + ) as string[], + ); +} + +function buildRowGroupKey( + row: Record, + groupKeyFields: readonly string[], +): string { + return groupKeyFields.map((field) => String(row[field] ?? "")).join("\0"); +} + +export function isEmptyOrZeroGridValue(value: string | number | undefined): boolean { + if (value === null || value === undefined || value === "") return true; + if (typeof value === "number") return value === 0; + const trimmed = String(value).trim(); + if (trimmed === "") return true; + const numeric = Number(trimmed.replace(/,/g, "")); + return !Number.isNaN(numeric) && numeric === 0; +} + +export function isRowEmptyOrZeroAfterDivision( + row: Record, + dataFieldNames: readonly string[], +): boolean { + if (dataFieldNames.length === 0) return false; + return dataFieldNames.every((field) => isEmptyOrZeroGridValue(row[field])); +} + +type EmptyCurrHighlight = + | typeof REVIEW_CURR_EMPTY_WARN_STYLE + | typeof REVIEW_CURR_EMPTY_NEW_STYLE; + +function buildOldRowByGroupKey( + rows: readonly Record[], + groupKeyFields: readonly string[], +): Map> { + const map = new Map>(); + for (const row of rows) { + if (String(row.rowType ?? "") !== "OLD") continue; + map.set(buildRowGroupKey(row, groupKeyFields), row); + } + return map; +} + +function buildOldRowIndexByGroupKey( + rows: readonly Record[], + groupKeyFields: readonly string[], +): Map { + const map = new Map(); + rows.forEach((row, index) => { + if (String(row.rowType ?? "") !== "OLD") return; + map.set(buildRowGroupKey(row, groupKeyFields), index); + }); + return map; +} + +function resolveEmptyCurrHighlight( + currRow: Record, + oldRowByGroupKey: ReadonlyMap>, + groupKeyFields: readonly string[], + dataFieldNames: readonly string[], +): EmptyCurrHighlight | undefined { + if (String(currRow.rowType ?? "") !== "CURR") return undefined; + if (!isRowEmptyOrZeroAfterDivision(currRow, dataFieldNames)) return undefined; + + const groupKey = buildRowGroupKey(currRow, groupKeyFields); + const oldRow = oldRowByGroupKey.get(groupKey); + + if (!oldRow) { + return REVIEW_CURR_EMPTY_NEW_STYLE; + } + if (isRowEmptyOrZeroAfterDivision(oldRow, dataFieldNames)) { + return undefined; + } + return REVIEW_CURR_EMPTY_WARN_STYLE; +} + +/** + * 검토 그리드 — CURR 행의 구분 이후 값이 모두 비어있거나 0일 때 + * 구분(displayYear): OLD 있음 → OLD·CURR 노란색, OLD 없음 → CURR 하늘색, 둘 다 없음 → 미적용 + */ +export function buildReviewEmptyCurrCellStyleCallback( + columns: readonly Pick[], + rows: readonly Record[], +): CellStyleCallback { + const groupKeyFields = getFieldNamesBeforeDivision(columns); + const dataFieldNames = getFieldNamesAfterDivision(columns); + const highlightFieldNames = getDivisionHighlightFieldNames(columns); + const oldRowByGroupKey = buildOldRowByGroupKey(rows, groupKeyFields); + const oldRowIndexByGroupKey = buildOldRowIndexByGroupKey(rows, groupKeyFields); + + const highlightByRowIndex = new Map(); + rows.forEach((row, index) => { + if (String(row.rowType ?? "") !== "CURR") return; + + const highlight = resolveEmptyCurrHighlight( + row, + oldRowByGroupKey, + groupKeyFields, + dataFieldNames, + ); + if (!highlight) return; + + highlightByRowIndex.set(index, highlight); + + if (highlight === REVIEW_CURR_EMPTY_WARN_STYLE) { + const oldIndex = oldRowIndexByGroupKey.get(buildRowGroupKey(row, groupKeyFields)); + if (oldIndex !== undefined) { + highlightByRowIndex.set(oldIndex, highlight); + } + } + }); + + return ({ rowIndex, fieldName }) => { + if (highlightFieldNames.size === 0 || !highlightFieldNames.has(fieldName)) { + return undefined; + } + return highlightByRowIndex.get(rowIndex); + }; +} + +export function buildReviewCellStyleCallback( + columns: readonly Pick[], + rows: readonly Record[], +): CellStyleCallback { + const base = buildReviewRowTypeCurrCellStyleExcludeMergeCells(columns); + const emptyCurr = buildReviewEmptyCurrCellStyleCallback(columns, rows); + return composeCellStyleCallbacks(base, emptyCurr); +} + +export function buildReviewCellStyleCallbackByTab( + tabs: readonly { + tabKey: string; + columns?: readonly Pick[]; + }[], + rowDataMap: ReadonlyMap[]>, +): Map { + const map = new Map(); + for (const tab of tabs) { + const columns = tab.columns ?? []; + const rows = rowDataMap.get(tab.tabKey) ?? []; + map.set(tab.tabKey, buildReviewCellStyleCallback(columns, rows)); + } + return map; +} diff --git a/apps/st/src/components/util/grid/ymdFormat.ts b/apps/st/src/components/util/grid/ymdFormat.ts index 888611424..d0c5f60af 100644 --- a/apps/st/src/components/util/grid/ymdFormat.ts +++ b/apps/st/src/components/util/grid/ymdFormat.ts @@ -5,11 +5,69 @@ export function formatCompactYmdForGrid(ymd: string | number | null | undefined) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`; } -/** 그리드 값 → API 8자리 일자(yyyyMMdd) */ -export function toCompactYmd(value: string | number | null | undefined): string { - const s = String(value ?? "").replace(/\D/g, ""); - if (s.length !== 8) return ""; - return s; +function isValidCompactYmdDigits(ymd: string): boolean { + if (!/^\d{8}$/.test(ymd)) return false; + const y = Number(ymd.slice(0, 4)); + const m = Number(ymd.slice(4, 6)); + const d = Number(ymd.slice(6, 8)); + if (y < 1000 || y > 9999 || m < 1 || m > 12 || d < 1 || d > 31) return false; + const dt = new Date(y, m - 1, d); + return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d; +} + +function formatDateToCompactYmd(date: Date): string { + if (Number.isNaN(date.getTime())) return ""; + const y = date.getFullYear(); + const m = date.getMonth() + 1; + const d = date.getDate(); + if (y < 1000 || y > 9999) return ""; + return `${y}${String(m).padStart(2, "0")}${String(d).padStart(2, "0")}`; +} + +/** + * 그리드·API 값 → 8자리 일자(yyyyMMdd). + * RealGrid datetime은 Date 객체로 올 수 있어, 숫자만 이어붙이는 방식은 사용하지 않습니다. + */ +export function toCompactYmd( + value: string | number | Date | null | undefined, +): string { + if (value == null || value === "") return ""; + + if (value instanceof Date) { + return formatDateToCompactYmd(value); + } + + if (typeof value === "number" && Number.isFinite(value)) { + if (value >= 19000101 && value <= 21001231) { + const compact = String(Math.trunc(value)).padStart(8, "0").slice(-8); + return isValidCompactYmdDigits(compact) ? compact : ""; + } + if (value > 1e11) { + return formatDateToCompactYmd(new Date(value)); + } + } + + const trimmed = String(value).trim(); + const isoLike = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})/); + if (isoLike) { + const compact = `${isoLike[1]}${isoLike[2]}${isoLike[3]}`; + return isValidCompactYmdDigits(compact) ? compact : ""; + } + + const digits = trimmed.replace(/\D/g, ""); + if (digits.length === 8 && isValidCompactYmdDigits(digits)) { + return digits; + } + + return ""; +} + +/** 그리드·API 값 → 8자리 일자(yyyyMMdd). 없거나 유효하지 않으면 null */ +export function toCompactYmdOrNull( + value: string | number | Date | null | undefined, +): string | null { + const compact = toCompactYmd(value); + return compact === "" ? null : compact; } /** 설치목적 등 O/X 표기 → 대문자 O 또는 X (그 외는 원문 유지) */ diff --git a/apps/st/src/components/util/gridExcelExport.ts b/apps/st/src/components/util/gridExcelExport.ts index ce86d6492..983d4ba5a 100644 --- a/apps/st/src/components/util/gridExcelExport.ts +++ b/apps/st/src/components/util/gridExcelExport.ts @@ -1,6 +1,64 @@ -import type { ColumnConfig } from "@/components/type/DataGrid.type"; +import type { + ColumnConfig, + ColumnLayoutItem, +} from "@/components/type/DataGrid.type"; import type { GridView } from "realgrid"; +function flattenColumnLayoutNames(layout: ColumnLayoutItem[]): string[] { + const result: string[] = []; + for (const item of layout) { + if (typeof item === "string") { + result.push(item); + continue; + } + if (item && typeof item === "object") { + if ("column" in item && typeof item.column === "string") { + result.push(item.column); + continue; + } + if ("items" in item && Array.isArray(item.items)) { + result.push( + ...flattenColumnLayoutNames( + item.items as ColumnLayoutItem[], + ), + ); + } + } + } + return result; +} + +/** 엑셀 가져오기 — N번째 열을 그리드 N번째 fieldName에 매핑 (보내기 컬럼 순서와 동일) */ +export function getExcelImportColumnFieldNames( + columns: ColumnConfig[], + columnLayout?: ColumnLayoutItem[], +): string[] { + const excludedNames = new Set( + columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name), + ); + const fieldByName = new Map( + columns + .filter((c) => c.fieldName) + .map((c) => [c.name, c.fieldName as string]), + ); + + const layoutNames = + columnLayout && columnLayout.length > 0 + ? flattenColumnLayoutNames(columnLayout) + : columns.map((c) => c.name); + + const seen = new Set(); + const fieldNames: string[] = []; + for (const name of layoutNames) { + if (excludedNames.has(name) || seen.has(name)) continue; + const fieldName = fieldByName.get(name); + if (!fieldName) continue; + seen.add(name); + fieldNames.push(fieldName); + } + return fieldNames; +} + export type ExcelExportVisibilitySnapshot = Map; /** 엑셀보내기 직전: 제외 컬럼 숨김, 그리드용 숨김(visible:false) 컬럼은 export에만 표시 */ diff --git a/apps/st/src/components/util/input/verification-failed-flag.ts b/apps/st/src/components/util/input/verification-failed-flag.ts new file mode 100644 index 000000000..22c81dad7 --- /dev/null +++ b/apps/st/src/components/util/input/verification-failed-flag.ts @@ -0,0 +1,17 @@ +import type { StatisticsInputCommonInfoBase } from "@/entities/statistics/model/statistics-interface"; + +export function isVerificationFailedFromCommonInfo( + commonInfo: Pick | null | undefined, +): boolean { + return commonInfo?.isVerificationFailed === true; +} + +export function patchVerificationFailedByTab( + prev: Map, + tabKey: string, + commonInfo: Pick | null | undefined, +): Map { + const next = new Map(prev); + next.set(tabKey, isVerificationFailedFromCommonInfo(commonInfo)); + return next; +} diff --git a/apps/st/src/entities/post/api/post-queries.ts b/apps/st/src/entities/post/api/post-queries.ts index 22c14e419..33809944c 100644 --- a/apps/st/src/entities/post/api/post-queries.ts +++ b/apps/st/src/entities/post/api/post-queries.ts @@ -2,30 +2,14 @@ import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import type { RecentPostSearchParams } from "../model/post-interface"; +import type { UserPostSearchParams } from "../model/post-interface"; import { postApi } from "./post-service"; -export function useRecentPostList(params?: RecentPostSearchParams) { +export function useUserPostList(params?: UserPostSearchParams) { return useQuery({ - queryKey: createRecentPostListQueryKey(params), - queryFn: () => postApi.getRecentPostList(params), + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params], + queryFn: () => postApi.getUserPostList(params), refetchOnWindowFocus: false, }); } - -function createRecentPostListQueryKey(params?: RecentPostSearchParams) { - return [ - ENDPOINT.BOARD_SERVICE.POSTS, - "recentPosts", - params?.page ?? "", - params?.size ?? "", - params?.sort ?? "", - params?.keywordType ?? "", - params?.keyword ?? "", - params?.bbsId ?? "", - params?.delYn ?? "", - params?.frDateYmd ?? "", - params?.toDateYmd ?? "", - ]; -} diff --git a/apps/st/src/entities/post/api/post-service.ts b/apps/st/src/entities/post/api/post-service.ts index 5a8a77df6..da66f8462 100644 --- a/apps/st/src/entities/post/api/post-service.ts +++ b/apps/st/src/entities/post/api/post-service.ts @@ -1,22 +1,22 @@ import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; +import { ENDPOINT, ST_DASHBOARD_BBS_ID } from "@/shared/config"; import type { PostService, - RecentPostListResponse, - RecentPostSearchParams, + UserPostListResponse, + UserPostSearchParams, } from "../model/post-interface"; -async function getRecentPostList( - params?: RecentPostSearchParams, -): Promise { - const response = await client.get( - `${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`, +async function getUserPostList( + params?: UserPostSearchParams, +): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/user/${ST_DASHBOARD_BBS_ID}`, { params }, ); return response.data; } export const postApi: PostService = { - getRecentPostList, + getUserPostList, }; diff --git a/apps/st/src/entities/post/index.ts b/apps/st/src/entities/post/index.ts index 451271145..cb9293561 100644 --- a/apps/st/src/entities/post/index.ts +++ b/apps/st/src/entities/post/index.ts @@ -1,8 +1,3 @@ -export { useRecentPostList } from "./api/post-queries"; +export { useUserPostList } from "./api/post-queries"; export { postApi } from "./api/post-service"; -export { - type PostService, - type PstRecentListResponse, - type RecentPostListResponse, - type RecentPostSearchParams, -} from "./model/post-interface"; +export { type PostService, type UserPost } from "./model/post-interface"; diff --git a/apps/st/src/entities/post/model/post-interface.ts b/apps/st/src/entities/post/model/post-interface.ts index 9282b2f35..fa9aa8fe5 100644 --- a/apps/st/src/entities/post/model/post-interface.ts +++ b/apps/st/src/entities/post/model/post-interface.ts @@ -1,15 +1,17 @@ import type { PageableList, PageableParams } from "@/shared/model"; -export interface RecentPostSearchParams extends PageableParams { +export interface UserPostSearchParams extends PageableParams { keywordType?: string; keyword?: string; - bbsId?: number; - delYn?: string; - frDateYmd?: string; - toDateYmd?: string; + popupYn?: string; + upndFixYn?: string; + prdPstgYn?: string; + siteId?: string; + bbsTypeCd?: string; + ansUseYn?: string; } -export interface PstRecentListResponse { +export interface UserPost { rowNo: number; bbsId: number; pstNo: number; @@ -29,14 +31,12 @@ export interface PstRecentListResponse { regDt: string; rgtrNm: string; cmntCnt: number; - bbsTypeCd: string; - bbsNm: string; } -export type RecentPostListResponse = PageableList; +export type UserPostListResponse = PageableList; export interface PostService { - getRecentPostList( - params?: RecentPostSearchParams, - ): Promise; + getUserPostList( + params?: UserPostSearchParams, + ): Promise; } diff --git a/apps/st/src/entities/statistics/api/general-verification-service.ts b/apps/st/src/entities/statistics/api/general-verification-service.ts index 95c96b467..4e3f3a478 100644 --- a/apps/st/src/entities/statistics/api/general-verification-service.ts +++ b/apps/st/src/entities/statistics/api/general-verification-service.ts @@ -75,10 +75,16 @@ export class GeneralverificationApiService implements GeneralverificationService } public async DeleteVrfcMaster( - param: number, + vrfcSn: number, + userUnqId: string, ): Promise { const response = await stClient.delete( - `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`, + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${vrfcSn}`, + { + headers: { + userId: userUnqId, + }, + }, ); return response.data; } @@ -141,9 +147,16 @@ export class GeneralverificationApiService implements GeneralverificationService public async CopyVrfcData( param: CopyVerifyDataRequest, + userUnqId: string, ): Promise { const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/copy?targetStatsCd=${param.targetStatsCd}&rgtrId=${param.rgtrId}`, + undefined, + { + headers: { + userId: userUnqId, + }, + }, ); return response.data; } diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service.ts new file mode 100644 index 000000000..e4489bc1a --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service.ts @@ -0,0 +1,53 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type ExcretaTreatmentFacilityApiResponse, + type ExcretaTreatmentFacilityRequestParams, + type ExcretaTreatmentFacilitySaveRequest, + type ExcretaTreatmentFacilitySaveResponse, + type ExcretaTreatmentFacilitySubmitRequest, + type ExcretaTreatmentFacilitySubmitResponse, + type StatisticsInputExcretaTreatmentFacilityService, +} from "../../model/input/statistics-input-excreta-treatment-facility-interface"; + +export class StatisticsInputExcretaTreatmentFacilityApiService + implements StatisticsInputExcretaTreatmentFacilityService +{ + /** 분뇨처리시설 조회 — ST00070001 */ + public async getExcretaTreatmentFacilityList( + params: ExcretaTreatmentFacilityRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/excreta-treatment-facility`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 분뇨처리시설 저장 — ST00070001 */ + public async saveExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/excreta-treatment-facility/save`, + request, + ); + return response.data; + } + + /** 분뇨처리시설 제출 — ST00070001 */ + public async submitExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/excreta-treatment-facility/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputExcretaTreatmentFacilityApi = + new StatisticsInputExcretaTreatmentFacilityApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-private-sewage-facility-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-private-sewage-facility-service.ts new file mode 100644 index 000000000..bcb23018c --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-private-sewage-facility-service.ts @@ -0,0 +1,133 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type PrivateSewageDesigStatusApiResponse, + type PrivateSewageDesigStatusRequestParams, + type PrivateSewageDesigStatusSaveRequest, + type PrivateSewageDesigStatusSaveResponse, + type PrivateSewageDesigStatusSubmitRequest, + type PrivateSewageDesigStatusSubmitResponse, + type SepticTankStatusApiResponse, + type SepticTankStatusRequestParams, + type SepticTankStatusSaveRequest, + type SepticTankStatusSaveResponse, + type SepticTankStatusSubmitRequest, + type SepticTankStatusSubmitResponse, + type SewageTreatFactStatusApiResponse, + type SewageTreatFactStatusRequestParams, + type SewageTreatFactStatusSaveRequest, + type SewageTreatFactStatusSaveResponse, + type SewageTreatFactStatusSubmitRequest, + type SewageTreatFactStatusSubmitResponse, + type StatisticsInputPrivateSewageFacilityService, +} from "../../model/input/statistics-input-private-sewage-facility-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/private-sewage-facility`; + +export class StatisticsInputPrivateSewageFacilityApiService + implements StatisticsInputPrivateSewageFacilityService +{ + /** 처리용량별 오수처리시설 현황 조회 — ST00080001 */ + public async getSewageTreatFactStatusList( + params: SewageTreatFactStatusRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/sewage-treat-fact-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 처리용량별 오수처리시설 현황 저장 — ST00080001 */ + public async saveSewageTreatFactStatus( + request: SewageTreatFactStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-treat-fact-status/save`, + request, + ); + return response.data; + } + + /** 처리용량별 오수처리시설 현황 제출 — ST00080001 */ + public async submitSewageTreatFactStatus( + request: SewageTreatFactStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-treat-fact-status/submit`, + request, + ); + return response.data; + } + + /** 정화조 현황 조회 — ST00080002 */ + public async getSepticTankStatusList( + params: SepticTankStatusRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/septic-tank-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 정화조 현황 저장 — ST00080002 */ + public async saveSepticTankStatus( + request: SepticTankStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/septic-tank-status/save`, + request, + ); + return response.data; + } + + /** 정화조 현황 제출 — ST00080002 */ + public async submitSepticTankStatus( + request: SepticTankStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/septic-tank-status/submit`, + request, + ); + return response.data; + } + + /** 개인하수도 관리지역 지정공고 현황 조회 — ST00080003 */ + public async getPrivateSewageDesigStatusList( + params: PrivateSewageDesigStatusRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/desig-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 개인하수도 관리지역 지정공고 현황 저장 — ST00080003 */ + public async savePrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/desig-status/save`, + request, + ); + return response.data; + } + + /** 개인하수도 관리지역 지정공고 현황 제출 — ST00080003 */ + public async submitPrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/desig-status/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputPrivateSewageFacilityApi = + new StatisticsInputPrivateSewageFacilityApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-pumping-station-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-pumping-station-service.ts new file mode 100644 index 000000000..7538acb7d --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-pumping-station-service.ts @@ -0,0 +1,50 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type PumpingStationApiResponse, + type PumpingStationRequestParams, + type PumpingStationSaveRequest, + type PumpingStationSaveResponse, + type PumpingStationSubmitRequest, + type PumpingStationSubmitResponse, + type StatisticsInputPumpingStationService, +} from "../../model/input/statistics-input-pumping-station-interface"; + +export class StatisticsInputPumpingStationApiService implements StatisticsInputPumpingStationService { + /** 펌프장 조회 — ST00050001 */ + public async getPumpingStationList( + params: PumpingStationRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/pumping-station`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 펌프장 저장 — ST00050001 */ + public async savePumpingStationList( + request: PumpingStationSaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/pumping-station/save`, + request, + ); + return response.data; + } + + /** 펌프장 제출 — ST00050001 */ + public async submitPumpingStationList( + request: PumpingStationSubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/pumping-station/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputPumpingStationApi = new StatisticsInputPumpingStationApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts index 78dd39e9c..fce1895d4 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts @@ -1,5 +1,6 @@ -import { ENDPOINT } from "@/shared/config"; import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; import { type StatisticsInputSewSupplyRateService, @@ -15,7 +16,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe public async getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate`, - { params } + withAuthHeader({ params }), ); return response.data; } diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-related-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-related-service.ts new file mode 100644 index 000000000..61baeca8e --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-related-service.ts @@ -0,0 +1,163 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import type { + ExcretaRelatedIndustryApiResponse, + ExcretaRelatedIndustrySaveRequest, + ExcretaRelatedIndustrySubmitRequest, + PrivateSewageTreatRelatedIndustryApiResponse, + PrivateSewageTreatRelatedIndustrySaveRequest, + PrivateSewageTreatRelatedIndustrySubmitRequest, + PublicInstitutionExcretaIndustryApiResponse, + PublicInstitutionExcretaIndustrySaveRequest, + PublicInstitutionExcretaIndustrySubmitRequest, + PublicInstitutionSewerIndustryApiResponse, + PublicInstitutionSewerIndustrySaveRequest, + PublicInstitutionSewerIndustrySubmitRequest, + SewageExcretaRelatedRequestParams, + SewageExcretaRelatedSaveResponse, + SewageExcretaRelatedSubmitResponse, + StatisticsInputSewageExcretaRelatedService, +} from "../../model/input/statistics-input-sewage-excreta-related-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewage-excreta-related`; + +export class StatisticsInputSewageExcretaRelatedApiService + implements StatisticsInputSewageExcretaRelatedService +{ + /** 공공기관 하수분야 조회 — ST00120001 */ + public async getPublicInstitutionSewerIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/public-institution-sewer-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 공공기관 하수분야 저장 — ST00120001 */ + public async savePublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-sewer-industry/save`, + request, + ); + return response.data; + } + + /** 공공기관 하수분야 제출 — ST00120001 */ + public async submitPublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-sewer-industry/submit`, + request, + ); + return response.data; + } + + /** 공공기관 분뇨분야 조회 — ST00120002 */ + public async getPublicInstitutionExcretaIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/public-institution-excreta-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 공공기관 분뇨분야 저장 — ST00120002 */ + public async savePublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-excreta-industry/save`, + request, + ); + return response.data; + } + + /** 공공기관 분뇨분야 제출 — ST00120002 */ + public async submitPublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-excreta-industry/submit`, + request, + ); + return response.data; + } + + /** 분뇨수집·운반업 조회 — ST00120003 */ + public async getExcretaRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/excreta-related-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 분뇨수집·운반업 저장 — ST00120003 */ + public async saveExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-related-industry/save`, + request, + ); + return response.data; + } + + /** 분뇨수집·운반업 제출 — ST00120003 */ + public async submitExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-related-industry/submit`, + request, + ); + return response.data; + } + + /** 개인하수처리관련업 조회 — ST00120004 */ + public async getPrivateSewageTreatRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/private-sewage-treat-related-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 개인하수처리관련업 저장 — ST00120004 */ + public async savePrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/private-sewage-treat-related-industry/save`, + request, + ); + return response.data; + } + + /** 개인하수처리관련업 제출 — ST00120004 */ + public async submitPrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/private-sewage-treat-related-industry/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputSewageExcretaRelatedApi = + new StatisticsInputSewageExcretaRelatedApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service.ts new file mode 100644 index 000000000..4d47e0e24 --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service.ts @@ -0,0 +1,182 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import type { + DigestorOperatingStatusApiResponse, + DigestorOperatingStatusSaveRequest, + DigestorOperatingStatusSaveResponse, + DigestorOperatingStatusSubmitRequest, + DigestorOperatingStatusSubmitResponse, + ExcretaSlurgeProcessingStatusApiResponse, + ExcretaSlurgeProcessingStatusSaveRequest, + ExcretaSlurgeProcessingStatusSaveResponse, + ExcretaSlurgeProcessingStatusSubmitRequest, + ExcretaSlurgeProcessingStatusSubmitResponse, + PublicSewageFacilitiesListApiResponse, + PublicSewageFacilitiesListRequest, + SewageExcretaSlurgeRequestParams, + SewageSlurgeProcessingFacStatusApiResponse, + SewageSlurgeProcessingFacStatusSaveRequest, + SewageSlurgeProcessingFacStatusSaveResponse, + SewageSlurgeProcessingFacStatusSubmitRequest, + SewageSlurgeProcessingFacStatusSubmitResponse, + SlurgeProcessingFacStatusApiResponse, + SlurgeProcessingFacStatusSaveRequest, + SlurgeProcessingFacStatusSaveResponse, + SlurgeProcessingFacStatusSubmitRequest, + SlurgeProcessingFacStatusSubmitResponse, + StatisticsInputSewageExcretaSlurgeService, +} from "../../model/input/statistics-input-sewage-excreta-slurge-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewage-excreta-slurge`; + +export class StatisticsInputSewageExcretaSlurgeApiService + implements StatisticsInputSewageExcretaSlurgeService +{ + /** 찌꺼기(슬러지)처리시설현황 조회 — ST00110001 */ + public async getSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/slurge-processing-fac-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 찌꺼기(슬러지)처리시설현황 저장 — ST00110001 */ + public async saveSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/slurge-processing-fac-status/save`, + request, + ); + return response.data; + } + + /** 찌꺼기(슬러지)처리시설현황 제출 — ST00110001 */ + public async submitSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/slurge-processing-fac-status/submit`, + request, + ); + return response.data; + } + + /** 소화조 운영현황 조회 — ST00110002 */ + public async getDigestorOperatingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/digestor-operating-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 소화조 운영현황 저장 — ST00110002 */ + public async saveDigestorOperatingStatus( + request: DigestorOperatingStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/digestor-operating-status/save`, + request, + ); + return response.data; + } + + /** 소화조 운영현황 제출 — ST00110002 */ + public async submitDigestorOperatingStatus( + request: DigestorOperatingStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/digestor-operating-status/submit`, + request, + ); + return response.data; + } + + /** 하수찌꺼기 처리시설 운영현황 조회 — ST00110003 */ + public async getSewageSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/sewage-slurge-processing-fac-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수찌꺼기 처리시설 운영현황 저장 — ST00110003 */ + public async saveSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-slurge-processing-fac-status/save`, + request, + ); + return response.data; + } + + /** 하수찌꺼기 처리시설 운영현황 제출 — ST00110003 */ + public async submitSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-slurge-processing-fac-status/submit`, + request, + ); + return response.data; + } + + /** 분뇨찌꺼기(슬러지)처리현황 조회 — ST00110004 */ + public async getExcretaSlurgeProcessingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/excreta-slurge-processing-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 분뇨찌꺼기(슬러지)처리현황 저장 — ST00110004 */ + public async saveExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-slurge-processing-status/save`, + request, + ); + return response.data; + } + + /** 분뇨찌꺼기(슬러지)처리현황 제출 — ST00110004 */ + public async submitExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-slurge-processing-status/submit`, + request, + ); + return response.data; + } + + /** 공공하수처리시설 목록 조회 */ + public async getPublicSewageFacilitiesList( + params: PublicSewageFacilitiesListRequest, + ): Promise { + const response = await stClient.get( + `${BASE}/public-sewage-facilities`, + withAuthHeader({ params }), + ); + return response.data; + } +} + +export const statisticsInputSewageExcretaSlurgeApi = + new StatisticsInputSewageExcretaSlurgeApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts index 49229becb..cc366f08d 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service.ts @@ -1,4 +1,5 @@ import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import type { @@ -20,7 +21,7 @@ export class SewagePipeMaintainApiService implements SewagePipeMaintainService { public async getSewagePipeMaintainRepair(param: SewagePipeMaintainRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/repair`, - { params: param } + withAuthHeader({ params: param }) ); return response.data; }; @@ -28,7 +29,7 @@ export class SewagePipeMaintainApiService implements SewagePipeMaintainService { public async getSewagePipeMaintainDredging(param: SewagePipeMaintainRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/dredging`, - { params: param } + withAuthHeader({ params: param }) ); return response.data; }; diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts index 3241efcb7..9e187ce30 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service.ts @@ -1,11 +1,20 @@ import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import { type StatisticsInputSewageReservoirPumpService, type SewageReservoirApiResponse, type SewageReservoirRequestParams, + type SewageReservoirSaveRequest, + type SewageReservoirSaveResponse, + type SewageReservoirSubmitRequest, + type SewageReservoirSubmitResponse, type RainfallPumpStationApiResponse, + type RainfallPumpStationSaveRequest, + type RainfallPumpStationSaveResponse, + type RainfallPumpStationSubmitRequest, + type RainfallPumpStationSubmitResponse, } from "../../model/input/statistics-input-sewage-reservoir-pump-interface"; export class StatisticsInputSewageReservoirPumpApiService implements StatisticsInputSewageReservoirPumpService { @@ -13,7 +22,7 @@ export class StatisticsInputSewageReservoirPumpApiService implements StatisticsI public async getSewageReservoirList(params: SewageReservoirRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/sewage-reservoir`, - { params }, + withAuthHeader({ params }), ); return response.data; } @@ -24,7 +33,51 @@ export class StatisticsInputSewageReservoirPumpApiService implements StatisticsI ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/rainfall-pump`, - { params }, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수저류시설 저장 — ST00040001 */ + public async saveSewageReservoir( + request: SewageReservoirSaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/sewage-reservoir/save`, + request, + ); + return response.data; + } + + /** 하수저류시설 제출 — ST00040001 */ + public async submitSewageReservoir( + request: SewageReservoirSubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/sewage-reservoir/submit`, + request, + ); + return response.data; + } + + /** 빗물펌프장 저장 — ST00040002 */ + public async saveRainfallPumpStation( + request: RainfallPumpStationSaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/rainfall-pump/save`, + request, + ); + return response.data; + } + + /** 빗물펌프장 제출 — ST00040002 */ + public async submitRainfallPumpStation( + request: RainfallPumpStationSubmitRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/rainfall-pump/submit`, + request, ); return response.data; } diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts new file mode 100644 index 000000000..1bac7543c --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts @@ -0,0 +1,170 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type CauserBurdenApiResponse, + type CauserBurdenRequestParams, + type CauserBurdenSaveRequest, + type CauserBurdenSaveResponse, + type CauserBurdenSubmitRequest, + type SewerageChargeApiResponse, + type SewerageChargeRequestParams, + type SewerageChargeSaveRequest, + type SewerageChargeSaveResponse, + type SewerageChargeSubmitRequest, + type SewerageFinanceSettleApiResponse, + type SewerageFinanceSettleRequestParams, + type SewerageFinanceSettleSaveRequest, + type SewerageFinanceSettleSaveResponse, + type SewerageFinanceSettleSubmitRequest, + type SewerageFinanceSettleSubmitResponse, + type SewerageFinanceSubmitResponse, + type SewerageUsageFeeApiResponse, + type SewerageUsageFeeRequestParams, + type SewerageUsageFeeSaveRequest, + type SewerageUsageFeeSaveResponse, + type SewerageUsageFeeSubmitRequest, + type StatisticsInputSewerageFinanceService, +} from "../../model/input/statistics-input-sewerage-finance-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-finance`; + +export class StatisticsInputSewerageFinanceApiService + implements StatisticsInputSewerageFinanceService +{ + /** 하수도재정 결산 조회 — ST00090001 */ + public async getSewerageFinanceSettleList( + params: SewerageFinanceSettleRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/settle`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수도재정 결산 저장 — ST00090001 */ + public async saveSewerageFinanceSettle( + request: SewerageFinanceSettleSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/settle/save`, + request, + ); + return response.data; + } + + /** 하수도재정 결산 제출 — ST00090001 */ + public async submitSewerageFinanceSettle( + request: SewerageFinanceSettleSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/settle/submit`, + request, + ); + return response.data; + } + + /** 하수도 요금 조회 — ST00090002 */ + public async getSewerageChargeList( + params: SewerageChargeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/charge`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수도 요금 저장 — ST00090002 */ + public async saveSewerageCharge( + request: SewerageChargeSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/charge/save`, + request, + ); + return response.data; + } + + /** 하수도 요금 제출 — ST00090002 */ + public async submitSewerageCharge( + request: SewerageChargeSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/charge/submit`, + request, + ); + return response.data; + } + + /** 하수도 사용료 조회 — ST00090003 */ + public async getSewerageUsageFeeList( + params: SewerageUsageFeeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/usage-fee`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수도 사용료 저장 — ST00090003 */ + public async saveSewerageUsageFee( + request: SewerageUsageFeeSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/usage-fee/save`, + request, + ); + return response.data; + } + + /** 하수도 사용료 제출 — ST00090003 */ + public async submitSewerageUsageFee( + request: SewerageUsageFeeSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/usage-fee/submit`, + request, + ); + return response.data; + } + + /** 원인자 부담금 조회 — ST00090004 */ + public async getCauserBurdenList( + params: CauserBurdenRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/causer-burden`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 원인자 부담금 저장 — ST00090004 */ + public async saveCauserBurden( + request: CauserBurdenSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/causer-burden/save`, + request, + ); + return response.data; + } + + /** 원인자 부담금 제출 — ST00090004 */ + public async submitCauserBurden( + request: CauserBurdenSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/causer-burden/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputSewerageFinanceApi = + new StatisticsInputSewerageFinanceApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-service-level-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-service-level-service.ts new file mode 100644 index 000000000..478ad0696 --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-service-level-service.ts @@ -0,0 +1,94 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type SewerageServiceLevelComplaintApiResponse, + type SewerageServiceLevelComplaintRequestParams, + type SewerageServiceLevelComplaintSaveRequest, + type SewerageServiceLevelComplaintSaveResponse, + type SewerageServiceLevelComplaintSubmitRequest, + type SewerageServiceLevelResidentFriendlyFactApiResponse, + type SewerageServiceLevelResidentFriendlyFactRequestParams, + type SewerageServiceLevelResidentFriendlyFactSaveRequest, + type SewerageServiceLevelResidentFriendlyFactSaveResponse, + type SewerageServiceLevelResidentFriendlyFactSubmitRequest, + type SewerageServiceLevelSubmitResponse, + type StatisticsInputSewerageServiceLevelService, +} from "../../model/input/statistics-input-sewerage-service-level-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-service-level`; + +export class StatisticsInputSewerageServiceLevelApiService + implements StatisticsInputSewerageServiceLevelService +{ + /** 민원 조회 — ST00130001 */ + public async getComplaintList( + params: SewerageServiceLevelComplaintRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/complaint`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 주민친화시설 조회 — ST00130002 */ + public async getResidentFriendlyFactList( + params: SewerageServiceLevelResidentFriendlyFactRequestParams, + ): Promise { + const response = + await stClient.get( + `${BASE}/resident-friendly-fact`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 민원 저장 — ST00130001 */ + public async saveComplaintList( + request: SewerageServiceLevelComplaintSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/complaint/save`, + request, + ); + return response.data; + } + + /** 주민친화시설 저장 — ST00130002 */ + public async saveResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/resident-friendly-fact/save`, + request, + ); + return response.data; + } + + /** 민원 제출 — ST00130001 */ + public async submitComplaintList( + request: SewerageServiceLevelComplaintSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/complaint/submit`, + request, + ); + return response.data; + } + + /** 주민친화시설 제출 — ST00130002 */ + public async submitResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/resident-friendly-fact/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputSewerageServiceLevelApi = + new StatisticsInputSewerageServiceLevelApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts new file mode 100644 index 000000000..87ec895be --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts @@ -0,0 +1,217 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type PublicSewageTreatWaterReuseApiResponse, + type PublicSewageTreatWaterReuseSaveRequest, + type PublicSewageTreatWaterReuseSaveResponse, + type PublicSewageTreatWaterReuseSubmitRequest, + type PublicSewageTreatWaterReuseSubmitResponse, + type RainFallUtilFactStatusApiResponse, + type RainFallUtilFactStatusSaveRequest, + type RainFallUtilFactStatusSaveResponse, + type RainFallUtilFactStatusSubmitRequest, + type RainFallUtilFactStatusSubmitResponse, + type RecycledWaterStatusApiResponse, + type RecycledWaterStatusSaveRequest, + type RecycledWaterStatusSaveResponse, + type RecycledWaterStatusSubmitRequest, + type RecycledWaterStatusSubmitResponse, + type StatisticsInputWaterReuseService, + type TreatWaterReuseBusinessApprStatusApiResponse, + type TreatWaterReuseBusinessApprStatusSaveRequest, + type TreatWaterReuseBusinessApprStatusSaveResponse, + type TreatWaterReuseBusinessApprStatusSubmitRequest, + type TreatWaterReuseBusinessApprStatusSubmitResponse, + type TreatWaterReuseDesignConstStatusApiResponse, + type TreatWaterReuseDesignConstStatusSaveRequest, + type TreatWaterReuseDesignConstStatusSaveResponse, + type TreatWaterReuseDesignConstStatusSubmitRequest, + type TreatWaterReuseDesignConstStatusSubmitResponse, + type WaterReuseOperationDataUpdateRequest, + type WaterReuseOperationDataUpdateResponse, + type WaterReuseRequestParams, +} from "../../model/input/statistics-input-water-reuse-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/water-reuse`; + +export class StatisticsInputWaterReuseApiService implements StatisticsInputWaterReuseService { + /** 공공하수처리수 재이용 조회 — ST00100001 */ + public async getPublicSewageTreatWaterReuseList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/public-sewage`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 공공하수처리수 재이용 저장 — ST00100001 */ + public async savePublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-sewage/save`, + request, + ); + return response.data; + } + + /** 공공하수처리수 재이용 제출 — ST00100001 */ + public async submitPublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-sewage/submit`, + request, + ); + return response.data; + } + + /** 중수도 현황 조회 — ST00100002 */ + public async getRecycledWaterStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/recycled-water`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 중수도 현황 저장 — ST00100002 */ + public async saveRecycledWaterStatus( + request: RecycledWaterStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/recycled-water/save`, + request, + ); + return response.data; + } + + /** 중수도 현황 제출 — ST00100002 */ + public async submitRecycledWaterStatus( + request: RecycledWaterStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/recycled-water/submit`, + request, + ); + return response.data; + } + + /** 빗물이용시설현황 조회 — ST00100003 */ + public async getRainFallUtilFactStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/rainfall-util-fact-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 빗물이용시설현황 저장 — ST00100003 */ + public async saveRainFallUtilFactStatus( + request: RainFallUtilFactStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/rainfall-util-fact-status/save`, + request, + ); + return response.data; + } + + /** 빗물이용시설현황 제출 — ST00100003 */ + public async submitRainFallUtilFactStatus( + request: RainFallUtilFactStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/rainfall-util-fact-status/submit`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 조회 — ST00100004 */ + public async getTreatWaterReuseDesignConstStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/treat-water-reuse-design-const-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 저장 — ST00100004 */ + public async saveTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-design-const-status/save`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 제출 — ST00100004 */ + public async submitTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-design-const-status/submit`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용사업 인가현황 조회 — ST00100005 */ + public async getTreatWaterReuseBusinessApprStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/treat-water-reuse-business-appr-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하·폐수처리수 재이용사업 인가현황 저장 — ST00100005 */ + public async saveTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-business-appr-status/save`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용사업 인가현황 제출 — ST00100005 */ + public async submitTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-business-appr-status/submit`, + request, + ); + return response.data; + } + + /** 공공하수처리수 재이용 운영 데이터 업데이트 — ST00100001 */ + public async updateWaterReuseOperationData( + request: WaterReuseOperationDataUpdateRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/operation-data/update`, + request, + ); + return response.data; + } +} + +export const statisticsInputWaterReuseApi = new StatisticsInputWaterReuseApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts index 29b766b06..bc81d6a8b 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts @@ -1,4 +1,5 @@ import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import type { @@ -8,6 +9,8 @@ import type { PublicSewageFacilitySaveResponse, PublicSewageFacilitySubmitRequest, PublicSewageFacilitySubmitResponse, + PublicSewageFacilityOperationDataUpdateRequest, + PublicSewageFacilityOperationDataUpdateResponse, PublicSewageMainFacilityMappingSaveRequest, PublicSewageMainFacilityMappingSaveResponse, PublicSewageMainFacilityResponse, @@ -20,7 +23,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics public async getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -28,7 +31,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics public async getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -38,7 +41,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility/mapping-targets`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -72,6 +75,16 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics ); return response.data; } + + public async updatePublicSewageFacilityOperationData( + param: PublicSewageFacilityOperationDataUpdateRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/operation-data/update`, + param, + ); + return response.data; + } } export const statisticsInputPublicSewageFacilityApiService = new StatisticsInputPublicSewageFacilityApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts index 24b271824..c506a5085 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts @@ -1,5 +1,6 @@ -import { ENDPOINT } from "@/shared/config"; import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; import type { SeweragePipeFacilityAdminAreaListRequest, SeweragePipeFacilityAdminAreaListResponse, @@ -39,7 +40,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getRegionalFactStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get ( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -47,7 +48,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getFactInstallStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get ( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -55,7 +56,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getPipeTypeStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get ( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -63,7 +64,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getPipeDiameterStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -71,7 +72,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getManholeStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -79,7 +80,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getConnectionStatus(param: SeweragePipeRequestParams): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -87,7 +88,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getFacilitiesList(param: SeweragePipeFacilitiesListRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/facilities`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } @@ -95,7 +96,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS public async getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/areas`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } diff --git a/apps/st/src/entities/statistics/api/statistics-management-service.ts b/apps/st/src/entities/statistics/api/statistics-management-service.ts index 9bb1156e6..93bed5573 100644 --- a/apps/st/src/entities/statistics/api/statistics-management-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-management-service.ts @@ -35,6 +35,34 @@ import { type PublicSewageFacilityReviewApiResponse, type PublicSewageFacilityReviewRequestParams, } from "../model/statistics-public-sewage-facility-review-interface"; +import { + type ExcretaTreatmentFacilityReviewApiResponse, + type ExcretaTreatmentFacilityReviewRequestParams, +} from "../model/statistics-excreta-treatment-facility-review-interface"; +import { + type PrivateSewageFacilityReviewApiResponse, + type PrivateSewageFacilityReviewRequestParams, +} from "../model/statistics-private-sewage-facility-review-interface"; +import { + type SewerageFinanceReviewApiResponse, + type SewerageFinanceReviewRequestParams, +} from "../model/statistics-sewerage-finance-review-interface"; +import { + type WaterReuseReviewApiResponse, + type WaterReuseReviewRequestParams, +} from "../model/statistics-water-reuse-review-interface"; +import { + type SewageExcretaSlurgeReviewApiResponse, + type SewageExcretaSlurgeReviewRequestParams, +} from "../model/statistics-sewage-excreta-slurge-review-interface"; +import { + type SewageExcretaRelatedReviewApiResponse, + type SewageExcretaRelatedReviewRequestParams, +} from "../model/statistics-sewage-excreta-related-review-interface"; +import { + type SewerageServiceLevelReviewApiResponse, + type SewerageServiceLevelReviewRequestParams, +} from "../model/statistics-sewerage-service-level-review-interface"; export class StatisticsManagementApiService { @@ -80,6 +108,20 @@ export class StatisticsManagementApiService { return response.data; } + /** + * 07. 분뇨처리시설 검토 목록 조회 + * @param params (sido, gugun, yr, bsnmCd, fctName) + */ + public async getExcretaTreatmentFacilityReviewList( + params: ExcretaTreatmentFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/excreta-treatment`, + { params }, + ); + return response.data; + } + /** * 02. 하수관로 지역별 시설현황 목록 조회 (Tab1) * @param params (sido, gugun, yr, bsnmCd) @@ -216,7 +258,7 @@ export class StatisticsManagementApiService { sggNm?: string; statsYr?: string; sewerBzmnCd?: string; - pumpstNm?: string; + fcltNm?: string; } ): Promise { const response = await stClient.get( @@ -226,6 +268,292 @@ export class StatisticsManagementApiService { return response.data; } + /** + * 08. 개인하수처리시설 tab1. 처리용량별 오수처리시설 현황 검토 조회 — ST00080001 + */ + public async getPrivateSewageTreatFactStatusReviewList( + params: PrivateSewageFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/private-sevage`, + { params }, + ); + return response.data; + } + + /** + * 08. 개인하수처리시설 tab2. 정화조 현황 검토 조회 — ST00080002 + */ + public async getSepticTankStatusReviewList( + params: PrivateSewageFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/private-tank-status`, + { params }, + ); + return response.data; + } + + /** + * 08. 개인하수처리시설 tab3. 개인하수도 관리지역 지정공고 검토 조회 — ST00080003 + */ + public async getPrivateSewageDesigStatusReviewList( + params: PrivateSewageFacilityReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/private-mgmt-area`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab1. 하수도재정 결산 검토 조회 — ST00090001 + */ + public async getSewerageFinanceSettleReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-financial`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab2. 하수도 요금 검토 조회 — ST00090002 + */ + public async getSewerageChargeReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-fee`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab3. 하수도 사용료 검토 조회 — ST00090003 + */ + public async getSewerageUsageFeeReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-usage`, + { params }, + ); + return response.data; + } + + /** + * 09. 하수도 재정 tab4. 원인자부담금 검토 조회 — ST00090004 + */ + public async getCauserBurdenReviewList( + params: SewerageFinanceReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewerage-org-cause`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab1. 공공하수처리수 재이용 검토 조회 — ST00100001 + */ + public async getWaterReuseReuseStatusReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/pbl-swage-ruswt`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab2. 중수도 현황 검토 조회 — ST00100002 + */ + public async getWaterReuseGrayWaterReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/gray-water-status`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab3. 빗물이용시설 현황 검토 조회 — ST00100003 + */ + public async getWaterReuseRainwaterReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/water-reuse`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab4. 하·폐수처리수 재이용시설 시공업 등록현황 검토 조회 — ST00100004 + */ + public async getWaterReuseRegStatusReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/wwt-rtlztn-reg`, + { params }, + ); + return response.data; + } + + /** + * 10. 물 재이용 tab5. 하·폐수처리수 재이용시설 시공업 인가현황 검토 조회 — ST00100005 + */ + public async getWaterReuseSncnStatusReviewList( + params: WaterReuseReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/wwt-rtlztn-aprv`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab1. 찌꺼기(슬러지)처리시설현황 검토 조회 — ST00110001 + */ + public async getSludgeProcessingReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-processing`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab2. 소화조 운영현황 검토 조회 — ST00110002 + */ + public async getSludgeDigestionTankReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-digestion-tank`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab3. 하수찌꺼기 처리시설 운영현황 검토 조회 — ST00110003 + */ + public async getSludgeFacilityOperReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-facility-oper`, + { params }, + ); + return response.data; + } + + /** + * 11. 하수 및 분뇨 찌꺼기 tab4. 분뇨찌꺼기(슬러지)처리현황 검토 조회 — ST00110004 + */ + public async getHumanWasteSludgeReviewList( + params: SewageExcretaSlurgeReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sludge-human-waste`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab1. 공공기관 하수분야 검토 조회 — ST00120001 + */ + public async getPublicSewageStaffReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/public-sewage-staff`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab2. 공공기관 분뇨분야 검토 조회 — ST00120002 + */ + public async getPublicHumanWasteStaffReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/public-human-waste-staff`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab3. 분뇨수집·운반업 검토 조회 — ST00120003 + */ + public async getHumanWasteCollectionReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/human-waste-collection`, + { params }, + ); + return response.data; + } + + /** + * 12. 하수 및 분뇨 관련 산업 tab4. 개인하수처리관련업 검토 조회 — ST00120004 + */ + public async getIndvSwtrmtRelBsnsReviewList( + params: SewageExcretaRelatedReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/indv-swtrmt-rel-bsns`, + { params }, + ); + return response.data; + } + + /** + * 13. 하수도 서비스 수준 tab1. 민원 검토 조회 — ST00130001 + */ + public async getSewageComplaintReviewList( + params: SewerageServiceLevelReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/sewage-complaint`, + { params }, + ); + return response.data; + } + + /** + * 13. 하수도 서비스 수준 tab2. 주민친화시설 검토 조회 — ST00130002 + */ + public async getResidentFriendlyFacilityReviewList( + params: SewerageServiceLevelReviewRequestParams, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.REVIEW}/resident-friendly-facility`, + { params }, + ); + return response.data; + } + public async getReviewHistory(params: ReviewHistoryRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/history`, diff --git a/apps/st/src/entities/statistics/api/statistics-ppltn-service.ts b/apps/st/src/entities/statistics/api/statistics-ppltn-service.ts new file mode 100644 index 000000000..84565434d --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-ppltn-service.ts @@ -0,0 +1,250 @@ +import { ENDPOINT } from "@/shared/config"; +import { stClient } from "@/shared/api/client"; + +import type { + DomesticPopulationListResponse, + DomesticPopulationFinalResponse, + UpperCodesByNameResponse, + DomesticPopulationMappingItem, +} from "../model/ppltn/statistics-domestic-population-interface"; + +// 1. 응답 데이터의 타입을 정의합니다 (이미 정의된 것이 있다면 그것을 사용하세요) +interface ChartDataResponse { + status: string; + data: { + X_AXIS: string[]; + TOT: number[]; + ORIGIN_MALE: (number | null)[]; + ORIGIN_FEMALE: (number | null)[]; + FOREIGN_MALE: (number | null)[]; + FOREIGN_FEMALE: (number | null)[]; + }; + message?: string; +} + +export class StatisticsPpltnApiService { + + /** 내국인 엑셀 임시 업로드 */ + public async uploadDomesticPopulationExcel( + file: File, + statsYr: string, + ): Promise<{ status: string; message?: string }> { + const formData = new FormData(); + formData.append("file", file); + formData.append("statsYr", statsYr); + + const response = await stClient.post<{ status: string; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-excel`, + formData, + { headers: { "Content-Type": "multipart/form-data" } }, + ); + return response.data; + } + + /** 내국인 임시 적재 리스트 조회 + * @param opt1 "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측) + */ + public async getDomesticPopulationList( + statsYr: string, + opt1: "1" | "2", + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-list`, + { params: { statsYr, opt1 } }, + ); + return response.data; + } + + /** 내국인 최종 마스터 반영 */ + public async saveDomesticPopulationFinal( + statsYr: string, + regList: DomesticPopulationMappingItem[], + ): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-final`, + { YR: statsYr, regList }, + ); + return response.data; + } + /** 미매핑 행 클릭 시 상위 코드 후보 조회 */ + + public async getUpperCodesByName( + statsYr: string, + admdstNm: string, + admdstCd: string + ): Promise { + // GET 요청으로 변경하고 params 객체를 추가합니다. + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.PPLTN}/upper-codes-by-name`, + { + params: { statsYr, admdstNm,admdstCd } + }, + ); + return response.data; + } + + /* ════════════════════════════════════ + 외국인 + ════════════════════════════════════ */ + + /** 외국인 엑셀 임시 업로드 */ + public async uploadForeignPopulationExcel( + file: File, + statsYr: string, + ): Promise<{ status: string; message?: string }> { + const formData = new FormData(); + formData.append("file", file); + formData.append("statsYr", statsYr); + const response = await stClient.post<{ status: string; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-excel`, + formData, + { headers: { "Content-Type": "multipart/form-data" } }, + ); + return response.data; + } + + /** 외국인 임시 적재 리스트 조회 + * @param opt1 "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측) + */ + public async getForeignPopulationList( + statsYr: string, + opt1: "1" | "2", + ): Promise<{ status: string; data: object[]; message?: string }> { + const response = await stClient.get<{ status: string; data: object[]; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-list`, + { params: { statsYr, opt1 } }, + ); + return response.data; + } + + /** 외국인 최종 마스터 반영 */ + public async saveForeignPopulationFinal( + statsYr: string, + regList: DomesticPopulationMappingItem[], + ): Promise<{ status: string; message?: string }> { + const response = await stClient.post<{ status: string; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-final`, + { statsYr, regList }, + ); + return response.data; + } + + + /* ════════════════════════════════════ + 행정동갱신 + ════════════════════════════════════ */ + + /** 행정동 갱신 — 감축/추가 행정구역 조회 + * @param opt1 "1" = 추가된 행정구역, "2" = 감축된 행정구역 + */ + public async searchChangedArea( + statsYr: string, + opt1: "1" | "2", + ): Promise<{ status: string; data: object[]; message?: string }> { + const response = await stClient.get<{ status: string; data: object[]; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/search`, + { params: { statsYr, opt1 } }, + ); + return response.data; + } + + /** 행정동 갱신 — 감축/추가 반영 */ + public async saveChangedArea( + reducedList: object[], + addedList: object[], + ): Promise<{ status: string; message?: string }> { + const response = await stClient.post<{ status: string; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/save`, + { reducedList, addedList }, + ); + return response.data; + } + + // /** 행정동 갱신 — 법정동코드 후보 조회 (행정동명 클릭 시) */ + // public async getRenewalUpperCodesByName( + // statsYr: string, + // admdstNm: string, + // ): Promise { + // const response = await stClient.get( + // `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/upper-codes-by-name`, + // { + // params: { statsYr, admdstNm } + // }, + // ); + // return response.data; + // } + + /** 행정동 갱신 — 법정동코드 후보 조회 */ +public async getLawCdsByName( + adstCd : string, +): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/law-codes-by-name`, + { + params: { adstCd } + }, + ); + return response.data; +} + /* ════════════════════════════════════ + 행정인구현황 + ════════════════════════════════════ */ + + /** 행정인구현황 — 체크박스 조건에 따른 인구 현황 조회 */ + public async searchPopByEachCondition( + statsYr: string, + oMale: boolean, + oFemale: boolean, + fMale: boolean, + fFemale: boolean, + ): Promise<{ status: string; data: object[]; message?: string }> { + const response = await stClient.get<{ status: string; data: object[]; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/pop-by-condition`, + { + params: { + statsYr, + oMale: oMale ? "TRUE" : "FALSE", + oFemale: oFemale ? "TRUE" : "FALSE", + fMale: fMale ? "TRUE" : "FALSE", + fFemale: fFemale ? "TRUE" : "FALSE", + } + }, + ); + return response.data; + } + + /** 행정인구현황 시각화 — 기준연도 기준 최대 5년치 차트 데이터 조회 */ + public async getChartDatas1( + statsYr: string, + oMale: boolean, + oFemale: boolean, + fMale: boolean, + fFemale: boolean, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/chart-datas`, + { + params: { + statsYr, + oMale: oMale ? "TRUE" : "FALSE", + oFemale: oFemale ? "TRUE" : "FALSE", + fMale: fMale ? "TRUE" : "FALSE", + fFemale: fFemale ? "TRUE" : "FALSE", + } + }, + ); + return response.data; + } + + /** 행정동 갱신 — MATERIALIZED VIEW 갱신 */ + public async refreshMaterializedViews(): Promise<{ status: string; message?: string }> { + const response = await stClient.post<{ status: string; message?: string }>( + `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/refresh`, + ); + return response.data; + } +} + + + +export const statisticsPpltnApi = new StatisticsPpltnApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index 570bd9ce6..557c47150 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -1,4 +1,5 @@ import { client, stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; import { ENDPOINT } from "@/shared/config"; import { @@ -92,7 +93,7 @@ export class StatisticsApiService implements StatisticsService { ): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/list`, - { params }, + withAuthHeader({ params }), ); return response.data; } @@ -100,7 +101,7 @@ export class StatisticsApiService implements StatisticsService { public async getStatsInputPeriodDetail(params: StatsInputPeriodDetailRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/detail`, - { params }, + withAuthHeader({ params }), ); return response.data; } @@ -112,6 +113,7 @@ export class StatisticsApiService implements StatisticsService { const response = await stClient.post( `${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`, param, + withAuthHeader(), ); return response.data; } @@ -122,7 +124,7 @@ export class StatisticsApiService implements StatisticsService { ): Promise { const response = await stClient.delete( `${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`, - { data: param }, + withAuthHeader({ data: param }), ); return response.data; } @@ -293,7 +295,7 @@ export class StatisticsApiService implements StatisticsService { ...(statsCd && statsCd !== "ALL" ? { statsCd } : {}), }; const response = await stClient.get( - `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`, + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/input`, { params }, ); return response.data; @@ -310,7 +312,7 @@ export class StatisticsApiService implements StatisticsService { public async getReviewerInfo(param: ReviewerInfoRequest): Promise { const response = await stClient.get( `${ENDPOINT.ST_SERVICE.REVIEW}/reviewer-info`, - { params: param }, + withAuthHeader({ params: param }), ); return response.data; } diff --git a/apps/st/src/entities/statistics/api/statistics-sewerage-aggregate-summary-service.ts b/apps/st/src/entities/statistics/api/statistics-sewerage-aggregate-summary-service.ts new file mode 100644 index 000000000..1cbade954 --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-sewerage-aggregate-summary-service.ts @@ -0,0 +1,104 @@ +import { stClient } from "@/shared/api/client"; + +import { + SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME, + type SewerageAggregateSummaryDownloadRequest, + type SewerageAggregateSummaryDownloadResult, + type SewerageAggregateSummaryTabListResponse, + type StatisticsSewerageAggregateSummaryService, +} from "../model/statistics-total/statistics-sewerage-aggregate-summary-interface"; + +const INDICATORS_URL = "/st-management-service/api/v1/summary-excel/indicators"; +/** middleware `/server` rewrite 대신 장시간 blob용 API Route 프록시 */ +const DOWNLOAD_URL = "/api/st/summary-excel/download"; +/** 확인 안내와 동일 — 최대 1시간 대기 */ +const DOWNLOAD_TIMEOUT_MS = 3_600_000; + +function parseDownloadFileName(contentDisposition: string): string | undefined { + if (!contentDisposition) return undefined; + + const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1].trim()); + } + + const plainMatch = contentDisposition.match(/filename\s*=\s*"?([^";]+)"?/i); + return plainMatch?.[1]?.trim(); +} + +async function toExcelBlob(data: Blob): Promise { + if (data.type === SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME) { + return data; + } + return new Blob([data], { type: SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME }); +} + +async function parseBlobErrorMessage(data: Blob): Promise { + const fallback = "엑셀 내려받기 중 오류가 발생했습니다."; + try { + const text = await data.text(); + const json = JSON.parse(text) as { message?: string }; + return json.message?.trim() || fallback; + } catch { + return fallback; + } +} + +export class StatisticsSewerageAggregateSummaryApiService + implements StatisticsSewerageAggregateSummaryService +{ + /** 하수도 통계 총괄표 — 통계항목(탭) 목록 */ + public async getAggregateSummaryTabList(): Promise { + const response = + await stClient.get(INDICATORS_URL); + return response.data; + } + + /** 하수도 통계 총괄표 — 엑셀 내려받기 (`Content-Type`: xlsx, `Content-Disposition`: 파일명) */ + public async downloadAggregateSummary( + request: SewerageAggregateSummaryDownloadRequest, + ): Promise { + try { + const response = await fetch(DOWNLOAD_URL, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + Accept: SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME, + }, + body: JSON.stringify(request), + signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS), + }); + + const contentType = String(response.headers.get("content-type") ?? ""); + const contentDisposition = String(response.headers.get("content-disposition") ?? ""); + const data = await response.blob(); + + if (!response.ok) { + throw new Error(await parseBlobErrorMessage(data)); + } + + if (contentType.includes("application/json")) { + throw new Error(await parseBlobErrorMessage(data)); + } + + return { + blob: await toExcelBlob(data), + fileName: parseDownloadFileName(contentDisposition), + }; + } catch (error) { + if (error instanceof DOMException && error.name === "TimeoutError") { + throw new Error( + "엑셀 생성 시간이 초과되었습니다. 선택 항목을 줄이거나 잠시 후 다시 시도해주세요.", + ); + } + if (error instanceof Error) { + throw error; + } + throw new Error("엑셀 내려받기 중 오류가 발생했습니다."); + } + } +} + +export const statisticsSewerageAggregateSummaryApi = + new StatisticsSewerageAggregateSummaryApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-total-service.ts b/apps/st/src/entities/statistics/api/statistics-total-service.ts index e5efbb39b..be7f839b9 100644 --- a/apps/st/src/entities/statistics/api/statistics-total-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-total-service.ts @@ -23,6 +23,62 @@ import type { import type { PublicSewageInquirySummaryResponse } from "../model/statistics-total/statistics-sewage-public-total-interface"; +import type { + SewerReservoirMaintainTotalRequest, + SewerReservoirStatusTotalResponse, + SewerPumpStationStatusTotalResponse +} from "../model/statistics-total/statistics-sewage-reservoir-total-interface"; + +import type { + MediationPumpMaintainTotalRequest, + MediationPumpStatusTotalResponse +} from "../model/statistics-total/statistics-mediation-pump-total-interface"; + +import type { + ExcretaTreatmentMaintainTotalRequest, + ExcretaTreatmentStatusTotalResponse +} from "../model/statistics-total/statistics-excreta-treatment-total-interface"; + + +import type { + PrivateSewageFacilitySummaryRequest, + PrivateSewageTab1Response, + PrivateSewageTab2Response, + PrivateSewageTab3Response +} from "../model/statistics-total/statistics-private-sewage-total-interface"; + +import type { + SewerageFinanceSummaryRequest, + SewerageFinanceSettlementResponse, + SewerageFeeResponse, + SewerageUsageFeeResponse, + SewerageOrgCauseResponse +} from "../model/statistics-total/statistics-sewerage-finance-total-interface"; + +import type { + WaterReuseSummaryRequest, + SewerageReuseStatusTab1Response, + GrayWaterStatusTab2Response, + RainwaterUtilizationStatusTab3Response, + SewageWwtRtlztnRegStatusTab4Response, + SewageWwtRtlztnSncnStatusTab5Response +} from "../model/statistics-total/statistics-sewerage-reuse-total-interface"; + +import type { + SludgeSummaryRequest, + SludgeProcessingTab1Response, + DigestionTankOperTab2Response, + SewageSludgeOperTab3Response, + HumanWasteSludgeTab4Response +} from "../model/statistics-total/statistics-sludge-total-interface"; + +import type { + SewerageIndustry, + PublicSewageStaffTab1Response, + PublicHumanWasteStaffTab2Response, + HumanWasteCollectionTab3Response, + IndividualSewageTab4Response +} from "../model/statistics-total/statistics-sewerage-industry-interface"; export class StatisticsTotalApiService { @@ -42,21 +98,6 @@ export class StatisticsTotalApiService { return response.data; } - public async getSewerageServiceLevelComplaint(params: SewerageServiceLevelRequest): Promise { - const response = await stClient.get ( - `${ENDPOINT.ST_SERVICE.TOTAL}/sewage-complaint`, - { params: params } - ); - return response.data; - } - - public async getSewerageServiceLevelResidentFriendlyFact(params: SewerageServiceLevelRequest): Promise { - const response = await stClient.get ( - `${ENDPOINT.ST_SERVICE.TOTAL}/resident-friendly-facility`, - { params: params } - ); - return response.data; - } public async getSewagePipeFactInstallStatus( params: SewagePipeTotalRequest, @@ -122,7 +163,40 @@ export class StatisticsTotalApiService { return response.data; } - /** ST00060001 —공공하수처리시설 */ + /** ★ ST00040001 — 하수저류시설 현황(합계) API */ + public async getSewerReservoirStatusTotalList( + params: SewerReservoirMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-reservoir-pump/reservoir-status`, + { params }, + ); + return response.data; + } + + /** ★ ST00040002 — 빗물펌프장 현황(합계) API */ + public async getSewerPumpStationStatusTotalList( + params: SewerReservoirMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-reservoir-pump/pump-station-status`, + { params }, + ); + return response.data; + } + + /** ST00050001 — 중계펌프장 현황(합계) API */ + public async getMediationPumpStationStatusTotalList( + params: MediationPumpMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/pump-station-status`, + { params }, + ); + return response.data; + } + /** + * ST00060001 —공공하수처리시설 */ public async getSewagePublicFacillityTotal( params: SewagePipeMaintainTotalRequest, ): Promise { @@ -132,6 +206,229 @@ export class StatisticsTotalApiService { ); return response.data; } -} + + + /** ST00070001 — 분뇨처리시설 현황(합계) API */ + public async getExcretaTreatmentStatusTotalList( + params: ExcretaTreatmentMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/excreta-treatment-status`, + { params }, + ); + return response.data; + } + + /** ST00080001 — 개인하수처리시설 > 처리용량별 오수처리시설(Tab1) */ + public async getPrivateSewageTab1List( + params: PrivateSewageFacilitySummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/private-sevage`, + { params }, + ); + return response.data; + } + + /** ST00080002 — 개인하수처리시설 > 정화조현황(Tab2) */ + public async getPrivateSewageTab2List( + params: PrivateSewageFacilitySummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/private-tank-status`, + { params }, + ); + return response.data; + } + + /** ST00080003 — 개인하수처리시설 > 개인하수도 관리지역 지정공고 현황(Tab3) */ + public async getPrivatePrivateMgmtAreaList( + params: PrivateSewageFacilitySummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/private-mgmt-area`, + { params }, + ); + return response.data; + } + + /** ST00090001 — 하수도 재정결산 조회(Tab1) */ + public async getSewerageFinancialList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-financial`, { params }); + return response.data; + } + + /** ST00090002 — 하수도 요금 조회(Tab2) */ + public async getSewerageFeeList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-fee`, { params }); + return response.data; + } + + /** ST00090003 — 하수도 사용료 조회(Tab3) */ + public async getSewerageUsageList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-usage`, { params }); + return response.data; + } + + /** ST00090004 — 원인자 부담금 조회(Tab4) */ + public async getSewerageOrgCauseList(params: SewerageFinanceSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/sewerage-org-cause`, { params }); + return response.data; + } + + /** ST00100001 — 물재이용 > 공공하수처리수 재이용 현황 조회(Tab1) */ + public async getSewerageReuseTab1List( + params: WaterReuseSummaryRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/pbl-swage-ruswt`, + { params }, + ); + return response.data; + } + /** + * ST00100002 — 물재이용 > TAB2 중수도 현황 조회 + */ + async getGrayWaterStatusTab2List( + params: WaterReuseSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/gray-water-status`, + { params }, + ); + return response.data; + + } + /** ★ ST00100003 — 물재이용 > TAB3 빗물이용시설현황 조회 */ + async getRainwaterStatusTab3List(params: WaterReuseSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/water-reuse`, { params }); + return response.data; + } + + /** ★ ST00100004 — 물재이용 > TAB4 하·폐수처리수 재이용시설 시공업 등록현황 조회 */ + async getWwtRtlztnRegStatusTab4List(params: WaterReuseSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/wwt-rtlztn-reg`, { params }); + return response.data; + } + + /** ★ ST00100005 — 물재이용 > TAB5 하·폐수처리수 재이용시설 시공업 인가현황 조회 */ + async getWwtRtlztnSncnStatusTab5List(params: WaterReuseSummaryRequest): Promise { + const response = await stClient.get(`${ENDPOINT.ST_SERVICE.TOTAL}/wwt-rtlztn-aprv`, { params }); + return response.data; + } + + /** + * ST00110001 — 하수및분뇨찌꺼기 > TAB1 찌꺼기처리시설현황 조회 + */ + async getSludgeProcessingTab1List( + params: SludgeSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-processing`, + { params }, + ); + return response.data; + } + /** + * ST00110002 — 하수및분뇨찌꺼기 > TAB2 소화조 운영현황 조회 + */ + async getSludgeTab2List( + params: SludgeSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-digestion-tank`, + { params }, + ); + return response.data; + } + + /** + * ST00110003 — 하수및분뇨찌꺼기 > TAB3 하수찌꺼기 처리시설 운영현황 조회 + */ + async getSludgeTab3List( + params: SludgeSummaryRequest + ): Promise { + // 제네릭을 추가하여 unknown 에러를 해결합니다. + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-facility-oper`, + { params } + ); + return response.data; + } + + /** + * ST00110004 — 하수및분뇨찌꺼기 > TAB4 분뇨찌꺼기(슬러지)처리현황 + */ + async getSludgeTab4List( + params: SludgeSummaryRequest + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sludge-human-waste`, + { params }, + ); + return response.data; + } + + /** ST00120001[TAB1] 공공기관 하수분야 조회 */ + async getPublicSewageStaffTab1List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/public-sewage-staff`, + { params } + ); + return response.data; + } + /** ST00120002[TAB2] 공공기관 분뇨분야 조회 */ + async getPublicHumanWasteStaffTab2List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/public-human-waste-staff`, + { params } + ); + return response.data; + } + + /** [TAB3] 분뇨수집·운반업 조회 */ + async getHumanWasteCollectionTab3List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/human-waste-collection`, + { params } + ); + return response.data; + } + + /** [TAB4] 개인하수처리관련업 조회 */ + async getIndividualSewageTab4List( + params: SewerageIndustry + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/indv-swtrmt-rel-bsns`, + { params } + ); + return response.data; + } + + /** [TAB1] 하수도서비스수준 - 민원 조회 */ + public async getSewerageServiceLevelComplaint(params: SewerageServiceLevelRequest): Promise { + const response = await stClient.get ( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewage-complaint`, + { params: params } + ); + return response.data; + } + /** [TAB1] 하수도서비스수준 - 주민친화시설 조회 */ + public async getSewerageServiceLevelResidentFriendlyFact(params: SewerageServiceLevelRequest): Promise { + const response = await stClient.get ( + `${ENDPOINT.ST_SERVICE.TOTAL}/resident-friendly-facility`, + { params: params } + ); + return response.data; + } + } + export const statisticsTotalStatusApi = new StatisticsTotalApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/verification-input-report-service.ts b/apps/st/src/entities/statistics/api/verification-input-report-service.ts new file mode 100644 index 000000000..b8fba2b79 --- /dev/null +++ b/apps/st/src/entities/statistics/api/verification-input-report-service.ts @@ -0,0 +1,42 @@ +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +import { stClient } from "@/shared/api/client"; +import { ENDPOINT } from "@/shared/config"; + +import type { + VerificationInputReportRequest, + VerificationInputReportResponse, + VerificationInputReportService, +} from "../model/verification-input-report-interface"; + +/** 이 API 전용 헤더 키 (백엔드 스펙: `token-id`) */ +const TOKEN_ID_HEADER = "token-id"; + +export class VerificationInputReportApiService implements VerificationInputReportService { + public async getVerificationInputReport( + param: VerificationInputReportRequest, + ): Promise { + const userInfo = await getUserInfo(); + const { statsCd, page, size, ...rest } = param; + + const params: VerificationInputReportRequest = { + ...rest, + ...(statsCd && statsCd !== "ALL" ? { statsCd } : {}), + ...(page != null ? { page } : {}), + ...(size != null ? { size } : {}), + }; + + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/input/report`, + { + params, + headers: { + [TOKEN_ID_HEADER]: userInfo.userUnqId, + }, + }, + ); + return response.data; + } +} + +export const verificationInputReportApi = new VerificationInputReportApiService(); diff --git a/apps/st/src/entities/statistics/model/general-verification-interface.ts b/apps/st/src/entities/statistics/model/general-verification-interface.ts index a3fcd66c0..07c0fbea3 100644 --- a/apps/st/src/entities/statistics/model/general-verification-interface.ts +++ b/apps/st/src/entities/statistics/model/general-verification-interface.ts @@ -275,7 +275,10 @@ export interface GeneralverificationService { RegistVrfcMaster( param: RegistVrfcMasterRequest, ): Promise; - DeleteVrfcMaster(param: number): Promise; + DeleteVrfcMaster( + vrfcSn: number, + userUnqId: string, + ): Promise; getVrfcOperators(): Promise; getVrfcDetail(param: number): Promise; getVerifyColumnList( @@ -292,6 +295,7 @@ export interface GeneralverificationService { ): Promise; CopyVrfcData( param: CopyVerifyDataRequest, + userUnqId: string, ): Promise; getVerifyResultList( param: VerifyResultListRequest, diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts new file mode 100644 index 000000000..82a721d35 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts @@ -0,0 +1,169 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 분뇨처리시설(ST00070001) — `ExcretaTreatFactInpColumn` */ +export const EXCRETA_TREATMENT_FACILITY_STATS_CD = "ST00070001" as const; + +/** 검증 결과 한 건 */ +export interface ExcretaTreatmentFacilityVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type ExcretaTreatmentFacilityVerificationResult = Record< + string, + ExcretaTreatmentFacilityVerificationResultItem[] +>; + +/** 분뇨처리시설 조회 `data.commonInfo` */ +export interface ExcretaTreatmentFacilityCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: ExcretaTreatmentFacilityVerificationResult; +} + +/** + * 분뇨처리시설 조회 `data.excretaTreatmentFacilityList[]` 한 행. + * `ExcretaTreatFactInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface ExcretaTreatmentFacilityItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + fcltPstnCn: string; + telNo: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + wholPjtcoAmt: number; + prcsFcltCpct: number; + cpct: number; + oprtngDayCnt: number; + prcsMthdExpln: string; + linkTmplntCn: string; + linkPrcsQnt: number; + inflwBodWqVl: number; + inflwTocWqVl: number; + inflwSsWqVl: number; + inflwTnWqVl: number; + inflwTpWqVl: number; + inflwClfgCnt: number; + dschgBodWqVl: number; + dschgTocWqVl: number; + dschgSsWqVl: number; + dschgTnWqVl: number; + dschgTpWqVl: number; + dschgClfgCnt: number; + wtsysNm: string; + trbtrNm: string; + msirbCn: string; + ssirbCn: string; + operMthdCdCn: string; + wholNope: number; + sftyAcdntEvntcsCnt: number; + rmrk: string | null; + verificationResult?: ExcretaTreatmentFacilityVerificationResult; +} + +export interface ExcretaTreatmentFacilityResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: ExcretaTreatmentFacilityCommonInfo; + excretaTreatmentFacilityList: ExcretaTreatmentFacilityItem[]; +} + +export type ExcretaTreatmentFacilityApiResponse = + ApiResponse & { + message: string; + }; + +/** 분뇨처리시설(ST00070001) 조회 요청 파라미터 */ +export interface ExcretaTreatmentFacilityRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; + fcltNm?: string; +} + +/** + * 분뇨처리시설(ST00070001) 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·검증결과는 저장 요청에서 제외. + */ +export type ExcretaTreatmentFacilitySaveItem = Omit< + ExcretaTreatmentFacilityItem, + "sggNm" | "verificationResult" | "cmptnStrtYmd" | "oprtngStrtYmd" +> & { + cmptnStrtYmd: string | null; + oprtngStrtYmd: string | null; +}; + +/** 분뇨처리시설(ST00070001) 저장 API 요청 */ +export interface ExcretaTreatmentFacilitySaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: ExcretaTreatmentFacilitySaveItem[]; + deleteList: ExcretaTreatmentFacilitySaveItem[]; +} + +export interface ExcretaTreatmentFacilitySaveResponse { + status: string; + message?: string; + data?: string; +} + +export type ExcretaTreatmentFacilitySubmitItem = ExcretaTreatmentFacilitySaveItem; + +/** 분뇨처리시설(ST00070001) 제출 API 요청 */ +export interface ExcretaTreatmentFacilitySubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + + superAdmin?: string; + noApplicable?: boolean; + submitList: ExcretaTreatmentFacilitySubmitItem[]; + deleteList: ExcretaTreatmentFacilitySubmitItem[]; +} + +export interface ExcretaTreatmentFacilitySubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputExcretaTreatmentFacilityService { + getExcretaTreatmentFacilityList( + params: ExcretaTreatmentFacilityRequestParams, + ): Promise; + saveExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySaveRequest, + ): Promise; + submitExcretaTreatmentFacilityList( + request: ExcretaTreatmentFacilitySubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts new file mode 100644 index 000000000..b0285ac8d --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts @@ -0,0 +1,453 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 처리용량별 오수처리시설 현황(ST00080001) — `SewageTreatFactStatusInpColumn.tabKey` */ +export const SEWAGE_TREAT_FACT_STATUS_STATS_CD = "ST00080001" as const; + +/** 검증 결과 한 건 */ +export interface PrivateSewageFacilityVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type PrivateSewageFacilityVerificationResult = Record< + string, + PrivateSewageFacilityVerificationResultItem[] +>; + +/** ST00080001 조회 `data.commonInfo` */ +export interface SewageTreatFactStatusCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +/** + * ST00080001 조회 `data.sewageTreatFactStatusList[]` 한 행. + * `SewageTreatFactStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewageTreatFactStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + wholDrtwPrcsFcltCnt: number; + wholStaaCnt: number; + step1StaaPrcsQnt: number; + step2StaaPrcsQnt: number; + step3StaaPrcsQnt: number; + step4StaaPrcsQnt: number; + step5StaaPrcsQnt: number; + step6StaaPrcsQnt: number; + step7StaaPrcsQnt: number; + staaExcpWhol: number; + step1StaaExcpPrcsQnt: number; + step2StaaExcpPrcsQnt: number; + step3StaaExcpPrcsQnt: number; + step4StaaExcpPrcsQnt: number; + step5StaaExcpPrcsQnt: number; + step6StaaExcpPrcsQnt: number; + step7StaaExcpPrcsQnt: number; + rmrk: string | null; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +export interface SewageTreatFactStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageTreatFactStatusCommonInfo; + sewageTreatFactStatusList: SewageTreatFactStatusItem[]; +} + +export type SewageTreatFactStatusApiResponse = ApiResponse & { + message: string; +}; + +/** ST00080001 조회 요청 파라미터 */ +export interface SewageTreatFactStatusRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 정화조 현황(ST00080002) — `SepticTankStatusInpColumn.tabKey` */ +export const SEPTIC_TANK_STATUS_STATS_CD = "ST00080002" as const; + +/** ST00080002 조회 `data.commonInfo` */ +export interface SepticTankStatusCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +/** + * ST00080002 조회 `data.septicTankStatusList[]` 한 행. + * `SepticTankStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SepticTankStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + grssFcltCnt: number; + staaInsdWhol: number; + staaInsdPrcsMthByWhol: number; + staaInsdSttkNprsflCnt: number; + step1StaaInsdSttkFrcCnt: number; + staaInsdSprktpFltrtbCnt: number; + staaInsdAertMthdCnt: number; + staaInsdCntcAertCnt: number; + staaInsdEtcCnt: number; + staaInsdPrcsCpctByWhol: number; + step1StaaInsdNope: number; + step2StaaInsdNope: number; + step3StaaInsdNope: number; + step4StaaInsdNope: number; + step5StaaInsdNope: number; + step6StaaInsdNope: number; + step7StaaInsdNope: number; + step8StaaInsdNope: number; + staaExcpWhol: number; + staaExcpPrcsMthByWhol: number; + staaExcpSttkNprsflCnt: number; + step1StaaExcpSttkFrcCnt: number; + staaExcpSprktpFltrtbCnt: number; + staaExcpAertMthdCnt: number; + staaExcpCntcAertCnt: number; + staaExcpEtcCnt: number; + staaExcpPrcsCpctByWhol: number; + step1StaaExcpNope: number; + step2StaaExcpNope: number; + step3StaaExcpNope: number; + step4StaaExcpNope: number; + step5StaaExcpNope: number; + step6StaaExcpNope: number; + step7StaaExcpNope: number; + step8StaaExcpNope: number; + rmrk: string | null; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +export interface SepticTankStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SepticTankStatusCommonInfo; + septicTankStatusList: SepticTankStatusItem[]; +} + +export type SepticTankStatusApiResponse = ApiResponse & { + message: string; +}; + +/** ST00080002 조회 요청 파라미터 */ +export interface SepticTankStatusRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 개인하수도 관리지역 지정공고 현황(ST00080003) — `PrivateSewageDesigStatusInpColumn` */ +export const PRIVATE_SEWAGE_DESIG_STATUS_STATS_CD = "ST00080003" as const; + +/** ST00080003 조회 `data.commonInfo` */ +export interface PrivateSewageDesigStatusCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +/** + * ST00080003 조회 `data.privateSewageDesigStatusList[]` 한 행. + * `PrivateSewageDesigStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PrivateSewageDesigStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; + rmrk: string | null; + verificationResult?: PrivateSewageFacilityVerificationResult; +} + +export interface PrivateSewageDesigStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: PrivateSewageDesigStatusCommonInfo; + privateSewageDesigStatusList: PrivateSewageDesigStatusItem[]; +} + +export type PrivateSewageDesigStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00080003 조회 요청 파라미터 */ +export interface PrivateSewageDesigStatusRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00080001 저장·제출 API `saveList` / `submitList` 한 행. + * `sggNm`·계산필드(`wholDrtwPrcsFcltCnt`, `wholStaaCnt`, `staaExcpWhol`)·검증결과는 제외. + */ +export interface SewageTreatFactStatusSaveItem { + admdstCd: string; + admdstNm: string; + step1StaaPrcsQnt: number; + step2StaaPrcsQnt: number; + step3StaaPrcsQnt: number; + step4StaaPrcsQnt: number; + step5StaaPrcsQnt: number; + step6StaaPrcsQnt: number; + step7StaaPrcsQnt: number; + step1StaaExcpPrcsQnt: number; + step2StaaExcpPrcsQnt: number; + step3StaaExcpPrcsQnt: number; + step4StaaExcpPrcsQnt: number; + step5StaaExcpPrcsQnt: number; + step6StaaExcpPrcsQnt: number; + step7StaaExcpPrcsQnt: number; + rmrk: string | null; +} + +/** ST00080001 저장 API 요청 */ +export interface SewageTreatFactStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SewageTreatFactStatusSaveItem[]; +} + +export interface SewageTreatFactStatusSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00080001 제출 API 요청 */ +export interface SewageTreatFactStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: SewageTreatFactStatusSaveItem[]; +} + +export interface SewageTreatFactStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00080002 저장·제출 API `saveList` / `submitList` 한 행. + * `sggNm`·계산필드(`grssFcltCnt`, `staaInsdWhol`, `staaInsdPrcsMthByWhol`, `staaInsdPrcsCpctByWhol`, + * `staaExcpWhol`, `staaExcpPrcsMthByWhol`, `staaExcpPrcsCpctByWhol`)·검증결과는 제외. + */ +export interface SepticTankStatusSaveItem { + admdstCd: string; + admdstNm: string; + staaInsdSttkNprsflCnt: number; + step1StaaInsdSttkFrcCnt: number; + staaInsdSprktpFltrtbCnt: number; + staaInsdAertMthdCnt: number; + staaInsdCntcAertCnt: number; + staaInsdEtcCnt: number; + step1StaaInsdNope: number; + step2StaaInsdNope: number; + step3StaaInsdNope: number; + step4StaaInsdNope: number; + step5StaaInsdNope: number; + step6StaaInsdNope: number; + step7StaaInsdNope: number; + step8StaaInsdNope: number; + staaExcpSttkNprsflCnt: number; + step1StaaExcpSttkFrcCnt: number; + staaExcpSprktpFltrtbCnt: number; + staaExcpAertMthdCnt: number; + staaExcpCntcAertCnt: number; + staaExcpEtcCnt: number; + step1StaaExcpNope: number; + step2StaaExcpNope: number; + step3StaaExcpNope: number; + step4StaaExcpNope: number; + step5StaaExcpNope: number; + step6StaaExcpNope: number; + step7StaaExcpNope: number; + step8StaaExcpNope: number; + rmrk: string | null; +} + +/** ST00080002 저장 API 요청 */ +export interface SepticTankStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SepticTankStatusSaveItem[]; +} + +export interface SepticTankStatusSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00080002 제출 API 요청 */ +export interface SepticTankStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: SepticTankStatusSaveItem[]; +} + +export interface SepticTankStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00080003 저장·제출 API `saveList` / `submitList` 한 행. + * `sggNm`·검증결과는 제외. + */ +export interface PrivateSewageDesigStatusSaveItem { + admdstCd: string; + admdstNm: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; + rmrk: string | null; +} + +/** ST00080003 저장 API 요청 */ +export interface PrivateSewageDesigStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: PrivateSewageDesigStatusSaveItem[]; +} + +export interface PrivateSewageDesigStatusSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00080003 제출 API 요청 */ +export interface PrivateSewageDesigStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: PrivateSewageDesigStatusSaveItem[]; +} + +export interface PrivateSewageDesigStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputPrivateSewageFacilityService { + getSewageTreatFactStatusList( + params: SewageTreatFactStatusRequestParams, + ): Promise; + saveSewageTreatFactStatus( + request: SewageTreatFactStatusSaveRequest, + ): Promise; + submitSewageTreatFactStatus( + request: SewageTreatFactStatusSubmitRequest, + ): Promise; + getSepticTankStatusList( + params: SepticTankStatusRequestParams, + ): Promise; + saveSepticTankStatus( + request: SepticTankStatusSaveRequest, + ): Promise; + submitSepticTankStatus( + request: SepticTankStatusSubmitRequest, + ): Promise; + getPrivateSewageDesigStatusList( + params: PrivateSewageDesigStatusRequestParams, + ): Promise; + savePrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSaveRequest, + ): Promise; + submitPrivateSewageDesigStatus( + request: PrivateSewageDesigStatusSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts new file mode 100644 index 000000000..e9cfe9b99 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts @@ -0,0 +1,177 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 펌프장(ST00050001) — `PumpingStationInpColumn` tabKey */ +export const PUMPING_STATION_STATS_CD = "ST00050001" as const; + +/** 검증 결과 한 건 */ +export interface PumpingStationVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type PumpingStationVerificationResult = Record< + string, + PumpingStationVerificationResultItem[] +>; + +/** 펌프장 조회 `data.commonInfo` */ +export interface PumpingStationCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: PumpingStationVerificationResult; +} + +/** + * 펌프장 조회 `data.pumpingStationList[]` 한 행. + * `PumpingStationInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PumpingStationItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltNm: string; + fcltNm: string; + pumpSeNm: string; + fcltPstnCn: string; + oprtngStrtYmd: string; + planMltvArea: number; + sndyArea: number; + rainArea: number; + exlnQnt: number; + drtwPumpGrssPpqty: number; + drtwPumpN1DmtrArea: number; + drtwPumpN1Ppqty: number; + drtwPumpN1Cnt: number; + drtwPumpN1RsrvCnt: number; + drtwPumpN2DmtrArea: number; + drtwPumpN2Ppqty: number; + drtwPumpN2Cnt: number; + drtwPumpN2RsrvCnt: number; + drtwPumpN3DmtrArea: number; + drtwPumpN3Ppqty: number; + drtwPumpN3Cnt: number; + drtwPumpN3RsrvCnt: number; + exlnPumpGrssPpqty: number; + exlnPumpN1DmtrArea: number; + exlnPumpN1Ppqty: number; + exlnPumpN1Cnt: number; + exlnPumpN1RsrvCnt: number; + exlnPumpN2DmtrArea: number; + exlnPumpN2Ppqty: number; + exlnPumpN2Cnt: number; + exlnPumpN2RsrvCnt: number; + exlnPumpN3DmtrArea: number; + exlnPumpN3Ppqty: number; + exlnPumpN3Cnt: number; + exlnPumpN3RsrvCnt: number; + anlElpwrUseQnt: number; + rmrk: string | null; + verificationResult?: PumpingStationVerificationResult; +} + +export interface PumpingStationResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: PumpingStationCommonInfo; + pumpingStationList: PumpingStationItem[]; +} + +export type PumpingStationApiResponse = ApiResponse & { + message: string; +}; + +/** 펌프장(ST00050001) 조회 요청 파라미터 */ +export interface PumpingStationRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; + fcltNm?: string; +} + +/** + * 펌프장(ST00050001) 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·계산필드·검증결과는 저장 요청에서 제외. + */ +export type PumpingStationSaveItem = Omit< + PumpingStationItem, + | "sggNm" + | "verificationResult" + | "exlnQnt" + | "drtwPumpGrssPpqty" + | "exlnPumpGrssPpqty" + | "oprtngStrtYmd" +> & { + oprtngStrtYmd: string | null; +}; + +/** 펌프장(ST00050001) 저장 API 요청 */ +export interface PumpingStationSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: PumpingStationSaveItem[]; + deleteList: PumpingStationSaveItem[]; +} + +export interface PumpingStationSaveResponse { + status: string; + message?: string; + data?: string; +} + +export type PumpingStationSubmitItem = PumpingStationSaveItem; + +/** 펌프장(ST00050001) 제출 API 요청 */ +export interface PumpingStationSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + + superAdmin?: string; + noApplicable?: boolean; + submitList: PumpingStationSubmitItem[]; + deleteList: PumpingStationSubmitItem[]; +} + +export interface PumpingStationSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputPumpingStationService { + getPumpingStationList( + params: PumpingStationRequestParams, + ): Promise; + savePumpingStationList( + request: PumpingStationSaveRequest, + ): Promise; + submitPumpingStationList( + request: PumpingStationSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts new file mode 100644 index 000000000..fbfc464b3 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts @@ -0,0 +1,328 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 하수·분뇨관련산업 — 공공기관 하수분야(ST00120001) `PublicInstitutionSewerIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD = "ST00120001" as const; + +/** 검증 결과 한 건 */ +export interface SewageExcretaRelatedVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewageExcretaRelatedVerificationResult = Record< + string, + SewageExcretaRelatedVerificationResultItem[] +>; + +/** 조회 `data.commonInfo` */ +export interface SewageExcretaRelatedCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +/** 하수·분뇨관련산업 조회 요청 파라미터 */ +export interface SewageExcretaRelatedRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00120001 조회 `data.publicInstitutionSewerIndustryList[]` 한 행. + * `PublicInstitutionSewerIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PublicInstitutionSewerIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + ognzSn: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface PublicInstitutionSewerIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + publicInstitutionSewerIndustryList: PublicInstitutionSewerIndustryItem[]; +} + +export type PublicInstitutionSewerIndustryApiResponse = + ApiResponse & { + message: string; + }; + +/** 하수·분뇨관련산업 — 공공기관 분뇨분야(ST00120002) `PublicInstitutionExcretaIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_PUBLIC_INSTITUTION_EXCRETA_STATS_CD = "ST00120002" as const; + +/** + * ST00120002 조회 `data.publicInstitutionExcretaIndustryList[]` 한 행. + * `PublicInstitutionExcretaIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PublicInstitutionExcretaIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + ognzSn: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface PublicInstitutionExcretaIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + publicInstitutionExcretaIndustryList: PublicInstitutionExcretaIndustryItem[]; +} + +export type PublicInstitutionExcretaIndustryApiResponse = + ApiResponse & { + message: string; + }; + +/** 하수·분뇨관련산업 — 분뇨수집·운반업(ST00120003) `ExcretaRelatedIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_EXCRETA_RELATED_INDUSTRY_STATS_CD = "ST00120003" as const; + +/** + * ST00120003 조회 `data.excretaRelatedIndustryList[]` 한 행. + * `ExcretaRelatedIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface ExcretaRelatedIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + bzentyCnt: number; + empNope: number; + fcltSecrPrstCnt: number; + n3TonUdrCnt: number; + n4TonUdrCnt: number; + n8TonUdrCnt: number; + etcCnt: number; + clctCnvncPrfmncQnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface ExcretaRelatedIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + excretaRelatedIndustryList: ExcretaRelatedIndustryItem[]; +} + +export type ExcretaRelatedIndustryApiResponse = ApiResponse & { + message: string; +}; + +/** 하수·분뇨관련산업 — 개인하수처리관련업(ST00120004) `PrivateSewageTreatRelatedIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_PRIVATE_SEWAGE_TREAT_RELATED_INDUSTRY_STATS_CD = "ST00120004" as const; + +/** + * ST00120004 조회 `data.privateSewageTreatRelatedIndustryList[]` 한 행. + * `PrivateSewageTreatRelatedIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PrivateSewageTreatRelatedIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + designMdsrCnt: number; + designMdsrNope: number; + mfbizCnt: number; + mfbizMdsrNope: number; + mngCnt: number; + mngMdsrNope: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface PrivateSewageTreatRelatedIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + privateSewageTreatRelatedIndustryList: PrivateSewageTreatRelatedIndustryItem[]; +} + +export type PrivateSewageTreatRelatedIndustryApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00120001·ST00120002 저장 — `saveList` / `deleteList` 항목 (`sggNm`·계산필드·검증결과 제외) */ +export type PublicInstitutionSewerIndustrySaveItem = Omit< + PublicInstitutionSewerIndustryItem, + "sggNm" | "verificationResult" | "empGrssCnt" | "techWholNope" +>; + +export type PublicInstitutionExcretaIndustrySaveItem = Omit< + PublicInstitutionExcretaIndustryItem, + "sggNm" | "verificationResult" | "empGrssCnt" | "techWholNope" +>; + +/** ST00120003·ST00120004 저장 — `saveList` 항목 (`sggNm`·계산필드·검증결과 제외) */ +export type ExcretaRelatedIndustrySaveItem = Omit< + ExcretaRelatedIndustryItem, + "sggNm" | "verificationResult" | "fcltSecrPrstCnt" +>; + +export type PrivateSewageTreatRelatedIndustrySaveItem = Omit< + PrivateSewageTreatRelatedIndustryItem, + "sggNm" | "verificationResult" +>; + +export interface SewageExcretaRelatedSaveRequestBase { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; +} + +export interface PublicInstitutionSewerIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + noApplicable?: boolean; + saveList: PublicInstitutionSewerIndustrySaveItem[]; + deleteList: PublicInstitutionSewerIndustrySaveItem[]; +} + +export interface PublicInstitutionExcretaIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + noApplicable?: boolean; + saveList: PublicInstitutionExcretaIndustrySaveItem[]; + deleteList: PublicInstitutionExcretaIndustrySaveItem[]; +} + +export interface ExcretaRelatedIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + saveList: ExcretaRelatedIndustrySaveItem[]; +} + +export interface PrivateSewageTreatRelatedIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + saveList: PrivateSewageTreatRelatedIndustrySaveItem[]; +} + +export type SewageExcretaRelatedSaveResponse = ApiResponse & { + message?: string; +}; + +export interface SewageExcretaRelatedSubmitRequestBase extends SewageExcretaRelatedSaveRequestBase { + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; +} + +export type PublicInstitutionSewerIndustrySubmitItem = PublicInstitutionSewerIndustrySaveItem; + +export interface PublicInstitutionSewerIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + noApplicable?: boolean; + submitList: PublicInstitutionSewerIndustrySubmitItem[]; + deleteList: PublicInstitutionSewerIndustrySubmitItem[]; +} + +export type PublicInstitutionExcretaIndustrySubmitItem = PublicInstitutionExcretaIndustrySaveItem; + +export interface PublicInstitutionExcretaIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + noApplicable?: boolean; + submitList: PublicInstitutionExcretaIndustrySubmitItem[]; + deleteList: PublicInstitutionExcretaIndustrySubmitItem[]; +} + +export type ExcretaRelatedIndustrySubmitItem = ExcretaRelatedIndustrySaveItem; + +export interface ExcretaRelatedIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + submitList: ExcretaRelatedIndustrySubmitItem[]; +} + +export type PrivateSewageTreatRelatedIndustrySubmitItem = PrivateSewageTreatRelatedIndustrySaveItem; + +export interface PrivateSewageTreatRelatedIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + submitList: PrivateSewageTreatRelatedIndustrySubmitItem[]; +} + +export interface SewageExcretaRelatedSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputSewageExcretaRelatedService { + /** 공공기관 하수분야 조회 — ST00120001 */ + getPublicInstitutionSewerIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 공공기관 하수분야 저장 — ST00120001 */ + savePublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySaveRequest, + ): Promise; + /** 공공기관 하수분야 제출 — ST00120001 */ + submitPublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySubmitRequest, + ): Promise; + /** 공공기관 분뇨분야 조회 — ST00120002 */ + getPublicInstitutionExcretaIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 공공기관 분뇨분야 저장 — ST00120002 */ + savePublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySaveRequest, + ): Promise; + /** 공공기관 분뇨분야 제출 — ST00120002 */ + submitPublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySubmitRequest, + ): Promise; + /** 분뇨수집·운반업 조회 — ST00120003 */ + getExcretaRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 분뇨수집·운반업 저장 — ST00120003 */ + saveExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySaveRequest, + ): Promise; + /** 분뇨수집·운반업 제출 — ST00120003 */ + submitExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySubmitRequest, + ): Promise; + /** 개인하수처리관련업 조회 — ST00120004 */ + getPrivateSewageTreatRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 개인하수처리관련업 저장 — ST00120004 */ + savePrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySaveRequest, + ): Promise; + /** 개인하수처리관련업 제출 — ST00120004 */ + submitPrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts new file mode 100644 index 000000000..1ad5e2294 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts @@ -0,0 +1,539 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 슬러지 처리 — 찌꺼기(슬러지)처리시설현황(ST00110001) `SlurgeProcessingFacStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD = "ST00110001" as const; + +/** 슬러지 처리 — 소화조 운영현황(ST00110002) `DigestorOperatingStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD = "ST00110002" as const; + +/** 슬러지 처리 — 하수찌꺼기 처리시설 운영현황(ST00110003) `SewageSlurgeProcessingFacStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD = "ST00110003" as const; + +/** 슬러지 처리 — 분뇨찌꺼기(슬러지)처리현황(ST00110004) `ExcretaSlurgeProcessingStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD = "ST00110004" as const; + +/** 검증 결과 한 건 */ +export interface SewageExcretaSlurgeVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewageExcretaSlurgeVerificationResult = Record< + string, + SewageExcretaSlurgeVerificationResultItem[] +>; + +/** 조회 `data.commonInfo` */ +export interface SewageExcretaSlurgeCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +/** 슬러지 처리 조회 요청 파라미터 */ +export interface SewageExcretaSlurgeRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + fcltNm?: string; + mode?: string; +} + +/** + * ST00110001 조회 `data.slurgeProcessingFacStatusList[]` 한 행. + * `SlurgeProcessingFacStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SlurgeProcessingFacStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltNm: string; + swtrmtFcltCd: string; + gnamWhol: number; + itslfQnt: number; + otsdInflwQnt: number; + transfTmplntNm: string; + transfPrcsQnt: number; + srpFncRt: number; + wholSrpPrcsQnt: number; + itslfTs1Whol: number; + itslfTs1RclmQnt: number; + itslfTs1IncnrQnt: number; + incnrPrcsWhol: number; + incnrPrcsLfvlm: number; + incnrPrcsPrdctRmtrlQnt: number; + incnrPrcsFmlyrQnt: number; + incnrPrcsFcltNm: string; + itslfTs1AltdQnt: number; + stbznPrcsWhol: number; + stbznPrcsLfvlm: number; + stbznPrcsFmlyrQnt: number; + stbznPrcsFcltNm: string; + itslfTs1DryQnt: number; + dryPrcsWhol: number; + dryPrcsLfvlm: number; + dryPrcsIncnrQnt: number; + dryPrcsFuelQnt: number; + dryPrcsPrdctRmtrlQnt: number; + dryPrcsFmlyrQnt: number; + dryPrcsCmpstgQnt: number; + dryPrcsFcltNm: string; + itslfTs1FuelQnt: number; + fuelPrcsWhol: number; + fuelPrcsIncnrQnt: number; + fuelPrcsFuelQnt: number; + fuelPrcsFcltNm: string; + itslfTs2CmpstgQnt: number; + cmpsPrcsFcltBzentyNm: string; + otsdCnsgnWhol: number; + otsdCnsgnRclmQnt: number; + otsdCnsgnIncnrQnt: number; + otsdCnsgnFuelPwplQnt: number; + otsdCnsgnPrdctRmtrlQnt: number; + otsdCnsgnFmlyrQnt: number; + otsdCnsgnCmpstgQnt: number; + otsdCnsgnFcltNm: string; + cparTs1Whol: number; + rmrk: string | null; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface SlurgeProcessingFacStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + slurgeProcessingFacStatusList: SlurgeProcessingFacStatusItem[]; +} + +export type SlurgeProcessingFacStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110001 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·계산필드·검증결과는 저장 요청에서 제외. + */ +export type SlurgeProcessingFacStatusSaveItem = Omit< + SlurgeProcessingFacStatusItem, + | "sggNm" + | "verificationResult" + | "gnamWhol" + | "wholSrpPrcsQnt" + | "itslfTs1Whol" + | "incnrPrcsWhol" + | "stbznPrcsWhol" + | "dryPrcsWhol" + | "fuelPrcsWhol" + | "otsdCnsgnWhol" +>; + +/** ST00110001 저장 API 요청 */ +export interface SlurgeProcessingFacStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SlurgeProcessingFacStatusSaveItem[]; + deleteList: SlurgeProcessingFacStatusSaveItem[]; +} + +export type SlurgeProcessingFacStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type SlurgeProcessingFacStatusSubmitItem = SlurgeProcessingFacStatusSaveItem; + +/** ST00110001 제출 API 요청 */ +export interface SlurgeProcessingFacStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: SlurgeProcessingFacStatusSubmitItem[]; + deleteList: SlurgeProcessingFacStatusSubmitItem[]; +} + +export interface SlurgeProcessingFacStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00110002 조회 `data.digestorOperatingStatusList[]` 한 행. + * `DigestorOperatingStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface DigestorOperatingStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + digstnEnYn: string; + digstnCpct: number; + actlDrvnCpct: number; + digstnGasInputQnt: number; + digstnGasInputSldDnst: number; + digstnTnkOprtngDayCnt: number; + ocrnGasQnt: number; + digstnGasUtztnWholPrst: number; + digstnGasUtztnNtslQnt: number; + digstnGasUtztnEtgtQnt: number; + digstnGasUtztnItslfQnt: number; + digstnGasUtztnEtcQnt: number; + surplsGasQnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface DigestorOperatingStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + digestorOperatingStatusList: DigestorOperatingStatusItem[]; +} + +export type DigestorOperatingStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110002 저장 — `saveList` 항목. + * `sggNm`·계산필드·검증결과는 저장 요청에서 제외. + */ +export type DigestorOperatingStatusSaveItem = Omit< + DigestorOperatingStatusItem, + "sggNm" | "verificationResult" | "digstnGasUtztnWholPrst" +>; + +/** ST00110002 저장 API 요청 */ +export interface DigestorOperatingStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: DigestorOperatingStatusSaveItem[]; + deleteList?: DigestorOperatingStatusSaveItem[]; +} + +export type DigestorOperatingStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type DigestorOperatingStatusSubmitItem = DigestorOperatingStatusSaveItem; + +/** ST00110002 제출 API 요청 */ +export interface DigestorOperatingStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: DigestorOperatingStatusSubmitItem[]; +} + +export interface DigestorOperatingStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00110003 조회 `data.sewageSlurgeProcessingFacStatusList[]` 한 행. + * `SewageSlurgeProcessingFacStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewageSlurgeProcessingFacStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + prcsMthExpln: string; + fcltCpct: number; + anlPrcsQnt: number; + dayAvgOprtngHr: number; + anlOprtngDayCnt: number; + oprtngRt: number; + rmrk: string | null; + fcltPstnCn: string; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface SewageSlurgeProcessingFacStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + sewageSlurgeProcessingFacStatusList: SewageSlurgeProcessingFacStatusItem[]; +} + +export type SewageSlurgeProcessingFacStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110003 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·계산필드·검증결과·`fcltPstnCn`은 저장 요청에서 제외. + */ +export type SewageSlurgeProcessingFacStatusSaveItem = Omit< + SewageSlurgeProcessingFacStatusItem, + "sggNm" | "verificationResult" | "oprtngRt" | "fcltPstnCn" +>; + +/** ST00110003 저장 API 요청 */ +export interface SewageSlurgeProcessingFacStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SewageSlurgeProcessingFacStatusSaveItem[]; + deleteList: SewageSlurgeProcessingFacStatusSaveItem[]; +} + +export type SewageSlurgeProcessingFacStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type SewageSlurgeProcessingFacStatusSubmitItem = + SewageSlurgeProcessingFacStatusSaveItem; + +/** ST00110003 제출 API 요청 */ +export interface SewageSlurgeProcessingFacStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: SewageSlurgeProcessingFacStatusSubmitItem[]; + deleteList: SewageSlurgeProcessingFacStatusSubmitItem[]; +} + +export interface SewageSlurgeProcessingFacStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00110004 조회 `data.excretaSlurgeProcessingStatusList[]` 한 행. + * `ExcretaSlurgeProcessingStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface ExcretaSlurgeProcessingStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + fcltPstnCn: string; + gnam: number; + wholDspsQnt: number; + rcycQnt: number; + olandLfvlm: number; + incnrQnt: number; + oceanInputQnt: number; + fuelQnt: number; + etcQnt: number; + nondspsQnt: number; + swtrmtFcltNm: string; + rmrk: string | null; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface ExcretaSlurgeProcessingStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + excretaSlurgeProcessingStatusList: ExcretaSlurgeProcessingStatusItem[]; +} + +export type ExcretaSlurgeProcessingStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110004 저장 — `saveList` 항목. + * `sggNm`·계산필드(`gnam`, `wholDspsQnt`)·검증결과는 저장 요청에서 제외. + */ +export type ExcretaSlurgeProcessingStatusSaveItem = Omit< + ExcretaSlurgeProcessingStatusItem, + "sggNm" | "verificationResult" | "gnam" | "wholDspsQnt" +>; + +/** ST00110004 저장 API 요청 */ +export interface ExcretaSlurgeProcessingStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: ExcretaSlurgeProcessingStatusSaveItem[]; +} + +export type ExcretaSlurgeProcessingStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type ExcretaSlurgeProcessingStatusSubmitItem = ExcretaSlurgeProcessingStatusSaveItem; + +/** ST00110004 제출 API 요청 */ +export interface ExcretaSlurgeProcessingStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: ExcretaSlurgeProcessingStatusSubmitItem[]; +} + +export interface ExcretaSlurgeProcessingStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** `public-sewage-facilities` 조회 요청 */ +export interface PublicSewageFacilitiesListRequest { + statsYr: string; + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string; +} + +/** `public-sewage-facilities` 조회 `data[]` 한 행 */ +export interface PublicSewageFacilityItem { + admdstCd: string; + admdstNm: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltSn: string; +} + +export type PublicSewageFacilitiesListApiResponse = ApiResponse & { + message: string; +}; + +export interface StatisticsInputSewageExcretaSlurgeService { + /** 찌꺼기(슬러지)처리시설현황 조회 — ST00110001 */ + getSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 찌꺼기(슬러지)처리시설현황 저장 — ST00110001 */ + saveSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSaveRequest, + ): Promise; + /** 찌꺼기(슬러지)처리시설현황 제출 — ST00110001 */ + submitSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSubmitRequest, + ): Promise; + /** 소화조 운영현황 조회 — ST00110002 */ + getDigestorOperatingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 소화조 운영현황 저장 — ST00110002 */ + saveDigestorOperatingStatus( + request: DigestorOperatingStatusSaveRequest, + ): Promise; + /** 소화조 운영현황 제출 — ST00110002 */ + submitDigestorOperatingStatus( + request: DigestorOperatingStatusSubmitRequest, + ): Promise; + /** 하수찌꺼기 처리시설 운영현황 조회 — ST00110003 */ + getSewageSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 하수찌꺼기 처리시설 운영현황 저장 — ST00110003 */ + saveSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSaveRequest, + ): Promise; + /** 하수찌꺼기 처리시설 운영현황 제출 — ST00110003 */ + submitSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSubmitRequest, + ): Promise; + /** 분뇨찌꺼기(슬러지)처리현황 조회 — ST00110004 */ + getExcretaSlurgeProcessingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 분뇨찌꺼기(슬러지)처리현황 저장 — ST00110004 */ + saveExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSaveRequest, + ): Promise; + /** 분뇨찌꺼기(슬러지)처리현황 제출 — ST00110004 */ + submitExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSubmitRequest, + ): Promise; + /** 공공하수처리시설 목록 조회 */ + getPublicSewageFacilitiesList( + params: PublicSewageFacilitiesListRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts index 36743925e..df35d2f76 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts @@ -1,4 +1,4 @@ -import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; export interface SewagePipeMaintainRepairVerificationResultItem { vrfcCd: string; @@ -13,7 +13,7 @@ export type SewagePipeMaintainRepairVerificationResult = Record< SewagePipeMaintainRepairVerificationResultItem[] >; -export interface SewagePipeMaintainRepairCommonInfo { +export interface SewagePipeMaintainRepairCommonInfo extends StatisticsInputCommonInfoBase { statsYr: string; ctpvNm: string; sewerBzmnCd: string; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts index fc85abb5f..eb734afca 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts @@ -1,4 +1,4 @@ -import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; /** 하수저류시설 Tab1 — `SewageReservoirInpColumn.tabKey` */ export const SEWAGE_RESERVOIR_STATS_CD = "ST00040001" as const; @@ -18,7 +18,7 @@ export type SewageReservoirVerificationResult = Record< >; /** 하수저류시설 조회 `data.commonInfo` */ -export interface SewageReservoirCommonInfo { +export interface SewageReservoirCommonInfo extends StatisticsInputCommonInfoBase { statsYr: string; ctpvNm: string; sewerBzmnCd: string; @@ -28,7 +28,7 @@ export interface SewageReservoirCommonInfo { /** * 하수저류시설 조회 `data.sewageReservoirList[]` 한 행. - * `SewageReservoirInpColumn` fieldName과 동일 (ctpvNm은 commonInfo에서 그리드에 합침). + * `SewageReservoirInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). */ export interface SewageReservoirItem { admdstCd: string; @@ -46,15 +46,15 @@ export interface SewageReservoirItem { wtrStrgZoneArea: number; impdFcltCpct: number; grssPpqty: number; - pumpFcltN1DmtrExpln: string; + pumpFcltN1DmtrExpln: number; pumpFcltN1PerPpqty: number; pumpFcltN1PumpCnt: number; pumpFcltN1RsrvPumpCnt: number; - pumpFcltN2DmtrExpln: string; + pumpFcltN2DmtrExpln: number; pumpFcltN2PerPpqty: number; pumpFcltN2PumpCnt: number; pumpFcltN2RsrvPumpCnt: number; - pumpFcltN3DmtrExpln: string; + pumpFcltN3DmtrExpln: number; pumpFcltN3PerPpqty: number; pumpFcltN3PumpCnt: number; pumpFcltN3RsrvPumpCnt: number; @@ -86,7 +86,7 @@ export type SewageReservoirApiResponse = ApiResponse & { + oprtngStrtYmd: string | null; +}; + +/** 하수저류시설(ST00040001) 저장 API 요청 */ +export interface SewageReservoirSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SewageReservoirSaveItem[]; + deleteList: SewageReservoirSaveItem[]; +} + +export interface SewageReservoirSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** 하수저류시설(ST00040001) 제출 API 요청 */ +export interface SewageReservoirSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + noApplicable?: boolean; + submitList: SewageReservoirSaveItem[]; + deleteList: SewageReservoirSaveItem[]; +} + +export interface SewageReservoirSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * 빗물펌프장(ST00040002) 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·검증결과는 저장 요청에서 제외. + */ +export type RainfallPumpStationSaveItem = Omit< + RainfallPumpStationItem, + "sggNm" | "verificationResult" | "oprtngStrtYmd" +> & { + oprtngStrtYmd: string | null; +}; + +/** + * 빗물펌프장(ST00040002) 저장 API 요청 + * (요청 shape은 예시 JSON과 동일) + */ +export interface RainfallPumpStationSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: RainfallPumpStationSaveItem[]; + deleteList: RainfallPumpStationSaveItem[]; +} + +/** 빗물펌프장(ST00040002) 저장 응답 */ +export type RainfallPumpStationSaveResponse = SewageReservoirSaveResponse; + +/** 빗물펌프장(ST00040002) 제출 API 요청 */ +export interface RainfallPumpStationSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + noApplicable?: boolean; + submitList: RainfallPumpStationSaveItem[]; + deleteList: RainfallPumpStationSaveItem[]; +} + +/** 빗물펌프장(ST00040002) 제출 응답 */ +export type RainfallPumpStationSubmitResponse = SewageReservoirSubmitResponse; + export interface StatisticsInputSewageReservoirPumpService { getSewageReservoirList(params: SewageReservoirRequestParams): Promise; getRainfallPumpStationList( params: SewageReservoirRequestParams, ): Promise; + saveSewageReservoir(request: SewageReservoirSaveRequest): Promise; + submitSewageReservoir( + request: SewageReservoirSubmitRequest, + ): Promise; + /** 빗물펌프장 저장 — ST00040002 */ + saveRainfallPumpStation( + request: RainfallPumpStationSaveRequest, + ): Promise; + /** 빗물펌프장 제출 — ST00040002 */ + submitRainfallPumpStation( + request: RainfallPumpStationSubmitRequest, + ): Promise; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts new file mode 100644 index 000000000..239d3c9d6 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts @@ -0,0 +1,555 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 하수도재정 결산(ST00090001) — `SewerageFinanceSettleInpColumn.tabKey` */ +export const SEWERAGE_FINANCE_SETTLE_STATS_CD = "ST00090001" as const; + +/** 검증 결과 한 건 */ +export interface SewerageFinanceVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewerageFinanceVerificationResult = Record< + string, + SewerageFinanceVerificationResultItem[] +>; + +/** ST00090001 조회 `data.commonInfo` */ +export interface SewerageFinanceSettleCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090001 조회 `data.sewerageFinanceSettleList[]` 한 행. + * `SewerageFinanceSettleInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageFinanceSettleItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fnnByWholPjtcoAmt: number; + cnterGovSprtAmt: number; + mctpvLcexpAmt: number; + wareaSggLclAmt: number; + sewerUseAmt: number; + orgprBrdnAmt: number; + etcIncmAmt: number; + crfwAmt: number; + lbltAmt: number; + usgByWholPjtcoAmt: number; + tmplntWholAmt: number; + tmplntFcltAmt: number; + tmplntMendngAmt: number; + tmplntOperKeepAmt: number; + tmplntEtcAmt: number; + wholSwagePplAmt: number; + swagePplFcltAmt: number; + swagePplMendngAmt: number; + swagePplOperKeepAmt: number; + swagePplEtcAmt: number; + wholSwageImpdFcltAmt: number; + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcWholAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface SewerageFinanceSettleResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageFinanceSettleCommonInfo; + sewerageFinanceSettleList: SewerageFinanceSettleItem[]; +} + +export type SewerageFinanceSettleApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090001 조회 요청 파라미터 */ +export interface SewerageFinanceSettleRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00090001 저장 API `saveList[]` 한 행. + * `SewerageFinanceSettleInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + */ +export interface SewerageFinanceSettleSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + cnterGovSprtAmt: number; + mctpvLcexpAmt: number; + wareaSggLclAmt: number; + sewerUseAmt: number; + orgprBrdnAmt: number; + etcIncmAmt: number; + crfwAmt: number; + lbltAmt: number; + tmplntFcltAmt: number; + tmplntMendngAmt: number; + tmplntOperKeepAmt: number; + tmplntEtcAmt: number; + swagePplFcltAmt: number; + swagePplMendngAmt: number; + swagePplOperKeepAmt: number; + swagePplEtcAmt: number; + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; + rmrk: string | null; + fnnByWholPjtcoAmt: number; + usgByWholPjtcoAmt: number; + tmplntWholAmt: number; + wholSwagePplAmt: number; + wholSwageImpdFcltAmt: number; + etcWholAmt: number; +} + +/** ST00090001 저장 API 요청 */ +export interface SewerageFinanceSettleSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageFinanceSettleSaveItem[]; +} + +export interface SewerageFinanceSettleSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00090001 제출 API 요청 */ +export interface SewerageFinanceSettleSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + submitList: SewerageFinanceSettleSaveItem[]; +} + +export interface SewerageFinanceSettleSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** 하수도재정 입력 제출 공통 본문 (`submitList` 제외) */ +export interface SewerageFinanceSubmitRequestBase { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; +} + +export interface SewerageFinanceSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** 하수도 요금(ST00090002) — `SewerageChargeInpColumn.tabKey` */ +export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const; + +/** ST00090002 조회 `data.commonInfo` */ +export interface SewerageChargeCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090002 조회 `data.sewerageChargeList[]` 한 행. + * `SewerageChargeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageChargeItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + anlLevyQnt: number; + levyAmt: number; + avgUntprcAmt: number; + grssPrmcoAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface SewerageChargeResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageChargeCommonInfo; + sewerageChargeList: SewerageChargeItem[]; +} + +export type SewerageChargeApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090002 조회 요청 파라미터 */ +export interface SewerageChargeRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00090002 저장·제출 API `saveList` / `submitList[]` 한 행. + * `SewerageChargeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + */ +export interface SewerageChargeSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + anlLevyQnt: number; + levyAmt: number; + grssPrmcoAmt: number; + rmrk: string | null; + avgUntprcAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; +} + +/** ST00090002 저장 API 요청 */ +export interface SewerageChargeSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageChargeSaveItem[]; +} + +export interface SewerageChargeSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00090002 제출 API 요청 (`submitList` 행은 `SewerageChargeSaveItem`과 동일) */ +export interface SewerageChargeSubmitRequest extends SewerageFinanceSubmitRequestBase { + submitList: SewerageChargeSaveItem[]; +} + +export type SewerageChargeSubmitItem = SewerageChargeSaveItem; + +/** 하수도 사용료(ST00090003) — `SewerageUsageFeeInpColumn.tabKey` */ +export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const; + +/** ST00090003 조회 `data.commonInfo` */ +export interface SewerageUsageFeeCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090003 조회 `data.sewerageUsageFeeList[]` 한 행. + * `SewerageUsageFeeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageUsageFeeItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + levyAmt: number; + levyQnt: number; + hshusLevyAmt: number; + hshusLevyQnt: number; + hshusUntprcAmt: number; + taskFrLevyAmt: number; + taskFrLevyQnt: number; + taskFrUntprcAmt: number; + fbuLevyAmt: number; + fbuLevyQnt: number; + fbuUntprcAmt: number; + bthhsFrSpcs1LevyAmt: number; + bthhsFrSpcs1LevyQnt: number; + bthhsFrSpcs1UntprcAmt: number; + bthhsFrSpcs2LevyAmt: number; + bthhsFrSpcs2LevyQnt: number; + bthhsFrSpcs2UntprcAmt: number; + induLevyAmt: number; + induLevyQnt: number; + induUntprcAmt: number; + etcLevyAmt: number; + etcLevyQnt: number; + etcLevyUntprcAmt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface SewerageUsageFeeResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageUsageFeeCommonInfo; + sewerageUsageFeeList: SewerageUsageFeeItem[]; +} + +export type SewerageUsageFeeApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090003 조회 요청 파라미터 */ +export interface SewerageUsageFeeRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00090003 저장·제출 API `saveList` / `submitList[]` 한 행. + * `SewerageUsageFeeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + */ +export interface SewerageUsageFeeSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + hshusLevyAmt: number; + hshusLevyQnt: number; + hshusUntprcAmt: number; + taskFrLevyAmt: number; + taskFrLevyQnt: number; + taskFrUntprcAmt: number; + fbuLevyAmt: number; + fbuLevyQnt: number; + fbuUntprcAmt: number; + bthhsFrSpcs1LevyAmt: number; + bthhsFrSpcs1LevyQnt: number; + bthhsFrSpcs1UntprcAmt: number; + bthhsFrSpcs2LevyAmt: number; + bthhsFrSpcs2LevyQnt: number; + bthhsFrSpcs2UntprcAmt: number; + induLevyAmt: number; + induLevyQnt: number; + induUntprcAmt: number; + etcLevyAmt: number; + etcLevyQnt: number; + etcLevyUntprcAmt: number; + rmrk: string | null; + levyAmt: number; + levyQnt: number; +} + +/** ST00090003 저장 API 요청 */ +export interface SewerageUsageFeeSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageUsageFeeSaveItem[]; +} + +export interface SewerageUsageFeeSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00090003 제출 API 요청 (`submitList` 행은 `SewerageUsageFeeSaveItem`과 동일) */ +export interface SewerageUsageFeeSubmitRequest extends SewerageFinanceSubmitRequestBase { + submitList: SewerageUsageFeeSaveItem[]; +} + +export type SewerageUsageFeeSubmitItem = SewerageUsageFeeSaveItem; + +/** 원인자 부담금(ST00090004) — `CauserBurdenInpColumn.tabKey` */ +export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const; + +/** ST00090004 조회 `data.commonInfo` */ +export interface CauserBurdenCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageFinanceVerificationResult; +} + +/** + * ST00090004 조회 `data.causerBurdenList[]` 한 행. + * `CauserBurdenInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface CauserBurdenItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + levyEvntcsCnt: number; + levyAmt: number; + trgtByWholClctnEvntcsCnt: number; + trgtByWholClctnAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; + rmrk: string | null; + verificationResult?: SewerageFinanceVerificationResult; +} + +export interface CauserBurdenResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: CauserBurdenCommonInfo; + causerBurdenList: CauserBurdenItem[]; +} + +export type CauserBurdenApiResponse = ApiResponse & { + message: string; +}; + +/** ST00090004 조회 요청 파라미터 */ +export interface CauserBurdenRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00090004 저장·제출 API `saveList` / `submitList[]` 한 행. + * `CauserBurdenInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + * `trgtByWholClctnEvntcsCnt`는 조회·그리드 전용이며 저장·제출 요청에는 포함하지 않음. + */ +export interface CauserBurdenSaveItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + levyEvntcsCnt: number; + levyAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; + rmrk: string | null; + trgtByWholClctnAmt: number; +} + +/** ST00090004 저장 API 요청 */ +export interface CauserBurdenSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: CauserBurdenSaveItem[]; +} + +export interface CauserBurdenSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00090004 제출 API 요청 (`submitList` 행은 `CauserBurdenSaveItem`과 동일) */ +export interface CauserBurdenSubmitRequest extends SewerageFinanceSubmitRequestBase { + submitList: CauserBurdenSaveItem[]; +} + +export type CauserBurdenSubmitItem = CauserBurdenSaveItem; + +/** 하수도재정 조회 공통 요청 파라미터 */ +export interface SewerageFinanceRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +export interface StatisticsInputSewerageFinanceService { + getSewerageFinanceSettleList( + params: SewerageFinanceSettleRequestParams, + ): Promise; + saveSewerageFinanceSettle( + request: SewerageFinanceSettleSaveRequest, + ): Promise; + submitSewerageFinanceSettle( + request: SewerageFinanceSettleSubmitRequest, + ): Promise; + getSewerageChargeList( + params: SewerageChargeRequestParams, + ): Promise; + saveSewerageCharge( + request: SewerageChargeSaveRequest, + ): Promise; + submitSewerageCharge( + request: SewerageChargeSubmitRequest, + ): Promise; + getSewerageUsageFeeList( + params: SewerageUsageFeeRequestParams, + ): Promise; + saveSewerageUsageFee( + request: SewerageUsageFeeSaveRequest, + ): Promise; + submitSewerageUsageFee( + request: SewerageUsageFeeSubmitRequest, + ): Promise; + getCauserBurdenList( + params: CauserBurdenRequestParams, + ): Promise; + saveCauserBurden( + request: CauserBurdenSaveRequest, + ): Promise; + submitCauserBurden( + request: CauserBurdenSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts new file mode 100644 index 000000000..b2e9f95df --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts @@ -0,0 +1,280 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 하수도 서비스 수준 — 민원(ST00130001) `ComplaintInpColumn.tabKey` */ +export const SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD = "ST00130001" as const; + +/** 검증 결과 한 건 */ +export interface SewerageServiceLevelVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewerageServiceLevelVerificationResult = Record< + string, + SewerageServiceLevelVerificationResultItem[] +>; + +/** ST00130001 조회 `data.commonInfo` */ +export interface SewerageServiceLevelComplaintCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +/** + * ST00130001 조회 `data.complaintList[]` 한 행. + * `ComplaintInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageServiceLevelComplaintItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + swtrmtPpltnCnt: number; + whol: number; + dschgWqCnt: number; + odorCnt: number; + ldsCnt: number; + plltnAcdntCnt: number; + swagePplCnt: number; + sewerCrgAmt: number; + etcCnt: number; + crtrPpltnCvlcptEvntcsCnt: number; + rmrk: string | null; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +export interface SewerageServiceLevelComplaintResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageServiceLevelComplaintCommonInfo; + complaintList: SewerageServiceLevelComplaintItem[]; +} + +export type SewerageServiceLevelComplaintApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00130001 조회 요청 파라미터 */ +export interface SewerageServiceLevelComplaintRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 하수도 서비스 수준 — 주민친화시설(ST00130002) `ResidentFriendlyFactInpColumn.tabKey` */ +export const SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD = "ST00130002" as const; + +/** ST00130002 조회 `data.commonInfo` */ +export interface SewerageServiceLevelResidentFriendlyFactCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +/** + * ST00130002 조회 `data.residentFriendlyFactList[]` 한 행. + * `ResidentFriendlyFactInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageServiceLevelResidentFriendlyFactItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: number; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltCpct: number; + tmplntArea: number; + whol: number; + brkFcltArea: number; + childFrshpSpacArea: number; + bdySportsFcltArea: number; + sportsFcltArea: number; + pkltArea: number; + eduMultiFcltArea: number; + wtlndMakeArea: number; + etcCnt: number; + rmrk: string | null; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +export interface SewerageServiceLevelResidentFriendlyFactResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageServiceLevelResidentFriendlyFactCommonInfo; + residentFriendlyFactList: SewerageServiceLevelResidentFriendlyFactItem[]; +} + +export type SewerageServiceLevelResidentFriendlyFactApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00130002 조회 요청 파라미터 */ +export interface SewerageServiceLevelResidentFriendlyFactRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** ST00130001 저장 `saveList[]` 한 행 */ +export interface SewerageServiceLevelComplaintSaveItem { + admdstCd: string; + admdstNm: string; + swtrmtPpltnCnt: number; + dschgWqCnt: number; + odorCnt: number; + ldsCnt: number; + plltnAcdntCnt: number; + swagePplCnt: number; + sewerCrgAmt: number; + etcCnt: number; + rmrk: string | null; + whol: number; + crtrPpltnCvlcptEvntcsCnt: number; +} + +/** ST00130001 저장 API 요청 */ +export interface SewerageServiceLevelComplaintSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageServiceLevelComplaintSaveItem[]; +} + +export interface SewerageServiceLevelComplaintSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00130002 저장 `saveList[]` 한 행 */ +export interface SewerageServiceLevelResidentFriendlyFactSaveItem { + admdstCd: string; + admdstNm: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltSn: string; + tmplntArea: number; + brkFcltArea: number; + childFrshpSpacArea: number; + bdySportsFcltArea: number; + sportsFcltArea: number; + pkltArea: number; + eduMultiFcltArea: number; + wtlndMakeArea: number; + etcCnt: number; + rmrk: string | null; +} + +/** ST00130002 저장 API 요청 */ +export interface SewerageServiceLevelResidentFriendlyFactSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable: boolean; + saveList: SewerageServiceLevelResidentFriendlyFactSaveItem[]; +} + +export interface SewerageServiceLevelResidentFriendlyFactSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00130001 제출 `submitList[]` 한 행 */ +export type SewerageServiceLevelComplaintSubmitItem = SewerageServiceLevelComplaintSaveItem; + +/** ST00130001 제출 API 요청 */ +export interface SewerageServiceLevelComplaintSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + submitList: SewerageServiceLevelComplaintSubmitItem[]; +} + +/** ST00130002 제출 `submitList[]` 한 행 */ +export type SewerageServiceLevelResidentFriendlyFactSubmitItem = + SewerageServiceLevelResidentFriendlyFactSaveItem; + +/** ST00130002 제출 API 요청 */ +export interface SewerageServiceLevelResidentFriendlyFactSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + noApplicable: boolean; + submitList: SewerageServiceLevelResidentFriendlyFactSubmitItem[]; +} + +export interface SewerageServiceLevelSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputSewerageServiceLevelService { + /** 민원 조회 — ST00130001 */ + getComplaintList( + params: SewerageServiceLevelComplaintRequestParams, + ): Promise; + /** 주민친화시설 조회 — ST00130002 */ + getResidentFriendlyFactList( + params: SewerageServiceLevelResidentFriendlyFactRequestParams, + ): Promise; + /** 민원 저장 — ST00130001 */ + saveComplaintList( + request: SewerageServiceLevelComplaintSaveRequest, + ): Promise; + /** 주민친화시설 저장 — ST00130002 */ + saveResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSaveRequest, + ): Promise; + /** 민원 제출 — ST00130001 */ + submitComplaintList( + request: SewerageServiceLevelComplaintSubmitRequest, + ): Promise; + /** 주민친화시설 제출 — ST00130002 */ + submitResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts new file mode 100644 index 000000000..b40a17da5 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts @@ -0,0 +1,611 @@ +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; + +/** 물 재이용 — 공공하수처리수 재이용(ST00100001) `PublicSewageTreatWaterReuseInpColumn.tabKey` */ +export const WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD = "ST00100001" as const; + +/** 물 재이용 — 중수도 현황(ST00100002) `RecycledWaterStatusInpColumn.tabKey` */ +export const WATER_REUSE_RECYCLED_WATER_STATS_CD = "ST00100002" as const; + +/** 물 재이용 — 빗물이용시설현황(ST00100003) `RainFallUtilFactStatusInpColumn.tabKey` */ +export const WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD = "ST00100003" as const; + +/** 물 재이용 — 하·폐수처리수 재이용시설 설계시공업 등록현황(ST00100004) */ +export const WATER_REUSE_DESIGN_CONST_STATS_CD = "ST00100004" as const; + +/** 물 재이용 — 하·폐수처리수 재이용사업 인가현황(ST00100005) */ +export const WATER_REUSE_BUSINESS_APPR_STATS_CD = "ST00100005" as const; + +/** 검증 결과 한 건 */ +export interface WaterReuseVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type WaterReuseVerificationResult = Record< + string, + WaterReuseVerificationResultItem[] +>; + +/** 조회 `data.commonInfo` */ +export interface WaterReuseCommonInfo extends StatisticsInputCommonInfoBase { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: WaterReuseVerificationResult; +} + +/** 물 재이용 조회 요청 파라미터 */ +export interface WaterReuseRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00100001 조회 `data.publicSewageTreatWaterReuseList[]` 한 행. + * `PublicSewageTreatWaterReuseInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PublicSewageTreatWaterReuseItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + sncnYmd: string; + prcsFcltCpct: number; + wholPrcsQnt: number; + lglYn: string; + instlYn: string; + oprtngStrtYmd: string; + instlAprvYmd: string; + rprcsFcltCpct: number; + mtcnsNm: string; + ruswtPplLen: number; + ruswtPumpLen: number; + wholUseQnt: number; + ithlWshwtrQnt: number; + ithlClnQty: number; + ithlFrzgQty: number; + ithlLdhcUswtrQnt: number; + ithlDltnUswtrQnt: number; + ithlRuswtrQnt: number; + ithlEtcQnt: number; + ithlUnsdQnt: number; + otcntrUswtrWhol: number; + otcntrClnRstrmQnt: number; + otcntrWshwtrQnt: number; + otcntrLdhcUswtrQnt: number; + otcntrHdrplUswtrQnt: number; + otcntrRvrKeepQnt: number; + agrcltUswtrPstnQnt: number; + ugwtrChargQnt: number; + mdsrUswtrQnt: number; + rtlztnRt: number; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface PublicSewageTreatWaterReuseResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + publicSewageTreatWaterReuseList: PublicSewageTreatWaterReuseItem[]; +} + +export type PublicSewageTreatWaterReuseApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100001 저장 — `saveList` 항목 */ +export interface PublicSewageTreatWaterReuseSaveItem { + admdstCd: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + wholPrcsQnt: number; + lglYn: string; + instlYn: string; + oprtngStrtYmd: string | null; + instlAprvYmd: string | null; + rprcsFcltCpct: number; + mtcnsNm: string; + ruswtPplLen: number; + ruswtPumpLen: number; + rmrk: string | null; + rtlztnRt: number; +} + +/** ST00100001 저장 API 요청 */ +export interface PublicSewageTreatWaterReuseSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: PublicSewageTreatWaterReuseSaveItem[]; +} + +export type PublicSewageTreatWaterReuseSaveResponse = ApiResponse & { + message?: string; +}; + +export type PublicSewageTreatWaterReuseSubmitItem = PublicSewageTreatWaterReuseSaveItem; + +/** ST00100001 제출 API 요청 (`submitList` 행은 저장과 동일) */ +export interface PublicSewageTreatWaterReuseSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: PublicSewageTreatWaterReuseSubmitItem[]; +} + +export interface PublicSewageTreatWaterReuseSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100002 조회 `data.recycledWaterList[]` 한 행. + * `RecycledWaterStatusInpColumn` fieldName과 동일. + */ +export interface RecycledWaterStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + bldgSeCn: string; + fcltPstnCn: string; + lglTrgtYn: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + archWholArea: number; + prcsCpct: number; + wtrUseQnt: number; + ruswtrUtztnQnt: number; + sbstCntUtztnQnt: number; + mnUsgCn: string; + ruswtrOperAmt: number; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface RecycledWaterStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + recycledWaterList: RecycledWaterStatusItem[]; +} + +export type RecycledWaterStatusApiResponse = ApiResponse & { + message: string; +}; + +/** ST00100002 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type RecycledWaterStatusSaveItem = Omit< + RecycledWaterStatusItem, + "sggNm" | "verificationResult" | "cmptnStrtYmd" | "oprtngStrtYmd" +> & { + cmptnStrtYmd: string | null; + oprtngStrtYmd: string | null; +}; + +/** ST00100002 저장 API 요청 */ +export interface RecycledWaterStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: RecycledWaterStatusSaveItem[]; + deleteList: RecycledWaterStatusSaveItem[]; +} + +export type RecycledWaterStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type RecycledWaterStatusSubmitItem = RecycledWaterStatusSaveItem; + +/** ST00100002 제출 API 요청 */ +export interface RecycledWaterStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: RecycledWaterStatusSubmitItem[]; + deleteList: RecycledWaterStatusSubmitItem[]; +} + +export interface RecycledWaterStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100003 조회 `data.rainFallUtilFactStatusList[]` 한 행. + * `RainFallUtilFactStatusInpColumn` fieldName과 동일. + */ +export interface RainFallUtilFactStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + bdstTypCn: string; + fcltPstnCn: string; + lglTrgtYn: string; + cmptYmd: string; + instlAmt: number; + rcptMynCn: string; + filtrPrcsFcltYnExpln: string; + impdCpct: number; + anlUseQnt: number; + exlnUtlzUsgExpln: string; + anlOperAmt: number; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface RainFallUtilFactStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + rainFallUtilFactStatusList: RainFallUtilFactStatusItem[]; +} + +export type RainFallUtilFactStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100003 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type RainFallUtilFactStatusSaveItem = Omit< + RainFallUtilFactStatusItem, + "sggNm" | "verificationResult" | "cmptYmd" +> & { + cmptYmd: string | null; +}; + +/** ST00100003 저장 API 요청 */ +export interface RainFallUtilFactStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: RainFallUtilFactStatusSaveItem[]; + deleteList: RainFallUtilFactStatusSaveItem[]; +} + +export type RainFallUtilFactStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type RainFallUtilFactStatusSubmitItem = RainFallUtilFactStatusSaveItem; + +/** ST00100003 제출 API 요청 */ +export interface RainFallUtilFactStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: RainFallUtilFactStatusSubmitItem[]; + deleteList: RainFallUtilFactStatusSubmitItem[]; +} + +export interface RainFallUtilFactStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100004 조회 `data.treatWaterReuseDesignConstStatusList[]` 한 행. + * `TreatWaterReuseDesignConstStatusInpColumn` fieldName과 동일. + */ +export interface TreatWaterReuseDesignConstStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + bplcSn: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + coRegYmd: string; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface TreatWaterReuseDesignConstStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + treatWaterReuseDesignConstStatusList: TreatWaterReuseDesignConstStatusItem[]; +} + +export type TreatWaterReuseDesignConstStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100004 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type TreatWaterReuseDesignConstStatusSaveItem = Omit< + TreatWaterReuseDesignConstStatusItem, + "sggNm" | "verificationResult" | "coRegYmd" +> & { + coRegYmd: string | null; +}; + +/** ST00100004 저장 API 요청 */ +export interface TreatWaterReuseDesignConstStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: TreatWaterReuseDesignConstStatusSaveItem[]; + deleteList: TreatWaterReuseDesignConstStatusSaveItem[]; +} + +export type TreatWaterReuseDesignConstStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type TreatWaterReuseDesignConstStatusSubmitItem = + TreatWaterReuseDesignConstStatusSaveItem; + +/** ST00100004 제출 API 요청 */ +export interface TreatWaterReuseDesignConstStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: TreatWaterReuseDesignConstStatusSubmitItem[]; + deleteList: TreatWaterReuseDesignConstStatusSubmitItem[]; +} + +export interface TreatWaterReuseDesignConstStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100005 조회 `data.treatWaterReuseBusinessApprStatusList[]` 한 행. + * `TreatWaterReuseBusinessApprStatusInpColumn` fieldName과 동일. + */ +export interface TreatWaterReuseBusinessApprStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + bplcSn: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + coRegYmd: string; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface TreatWaterReuseBusinessApprStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + treatWaterReuseBusinessApprStatusList: TreatWaterReuseBusinessApprStatusItem[]; +} + +export type TreatWaterReuseBusinessApprStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100005 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type TreatWaterReuseBusinessApprStatusSaveItem = Omit< + TreatWaterReuseBusinessApprStatusItem, + "sggNm" | "verificationResult" | "coRegYmd" +> & { + coRegYmd: string | null; +}; + +/** ST00100005 저장 API 요청 */ +export interface TreatWaterReuseBusinessApprStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: TreatWaterReuseBusinessApprStatusSaveItem[]; + deleteList: TreatWaterReuseBusinessApprStatusSaveItem[]; +} + +export type TreatWaterReuseBusinessApprStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type TreatWaterReuseBusinessApprStatusSubmitItem = + TreatWaterReuseBusinessApprStatusSaveItem; + +/** ST00100005 제출 API 요청 */ +export interface TreatWaterReuseBusinessApprStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: TreatWaterReuseBusinessApprStatusSubmitItem[]; + deleteList: TreatWaterReuseBusinessApprStatusSubmitItem[]; +} + +export interface TreatWaterReuseBusinessApprStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface WaterReuseOperationDataUpdateRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; +} + +export interface WaterReuseOperationDataUpdateResponse { + data?: string; + message?: string; + status: string; +} + +export interface StatisticsInputWaterReuseService { + /** 공공하수처리수 재이용 조회 — ST00100001 */ + getPublicSewageTreatWaterReuseList( + params: WaterReuseRequestParams, + ): Promise; + /** 공공하수처리수 재이용 저장 — ST00100001 */ + savePublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSaveRequest, + ): Promise; + /** 공공하수처리수 재이용 제출 — ST00100001 */ + submitPublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSubmitRequest, + ): Promise; + /** 중수도 현황 조회 — ST00100002 */ + getRecycledWaterStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 중수도 현황 저장 — ST00100002 */ + saveRecycledWaterStatus( + request: RecycledWaterStatusSaveRequest, + ): Promise; + /** 중수도 현황 제출 — ST00100002 */ + submitRecycledWaterStatus( + request: RecycledWaterStatusSubmitRequest, + ): Promise; + /** 빗물이용시설현황 조회 — ST00100003 */ + getRainFallUtilFactStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 빗물이용시설현황 저장 — ST00100003 */ + saveRainFallUtilFactStatus( + request: RainFallUtilFactStatusSaveRequest, + ): Promise; + /** 빗물이용시설현황 제출 — ST00100003 */ + submitRainFallUtilFactStatus( + request: RainFallUtilFactStatusSubmitRequest, + ): Promise; + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 조회 — ST00100004 */ + getTreatWaterReuseDesignConstStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 저장 — ST00100004 */ + saveTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSaveRequest, + ): Promise; + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 제출 — ST00100004 */ + submitTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSubmitRequest, + ): Promise; + /** 하·폐수처리수 재이용사업 인가현황 조회 — ST00100005 */ + getTreatWaterReuseBusinessApprStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 하·폐수처리수 재이용사업 인가현황 저장 — ST00100005 */ + saveTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSaveRequest, + ): Promise; + /** 하·폐수처리수 재이용사업 인가현황 제출 — ST00100005 */ + submitTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSubmitRequest, + ): Promise; + /** 공공하수처리수 재이용 운영 데이터 업데이트 — ST00100001 */ + updateWaterReuseOperationData( + request: WaterReuseOperationDataUpdateRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts index 093ea859b..7d685fdcb 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts @@ -1,4 +1,4 @@ -import type { SewerageStatusInfo } from "../statistics-interface"; +import type { SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; export interface PublicSewageFacilityRequestParams { ctpvNm?: string; @@ -22,7 +22,7 @@ export type PublicSewageFacilityVerificationResult = Record< PublicSewageFacilityVerifyResultItem[] >; -export interface PublicSewageFacilityCommonInfo { +export interface PublicSewageFacilityCommonInfo extends StatisticsInputCommonInfoBase { statsYr: string; ctpvNm: string; sewerBzmnCd: string; @@ -281,6 +281,20 @@ export interface PublicSewageFacilitySubmitResponse { status: string; } +export interface PublicSewageFacilityOperationDataUpdateRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; +} + +export interface PublicSewageFacilityOperationDataUpdateResponse { + data?: string; + message?: string; + status: string; +} + export interface StatisticsInputPublicSewageFacilityService { getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise; getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise; @@ -288,4 +302,7 @@ export interface StatisticsInputPublicSewageFacilityService { saveMainFacilityMapping(param: PublicSewageMainFacilityMappingSaveRequest): Promise; savePublicSewageFacility(param: PublicSewageFacilitySaveRequest): Promise; submitPublicSewageFacility(param: PublicSewageFacilitySubmitRequest): Promise; + updatePublicSewageFacilityOperationData( + param: PublicSewageFacilityOperationDataUpdateRequest, + ): Promise; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts index 4076e5f4f..454eb4446 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts @@ -1,4 +1,4 @@ -import type { SewerageStatusInfo } from "../statistics-interface"; +import type { SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; export interface SeweragePipeRequestParams { ctpvNm?: string; @@ -20,7 +20,7 @@ export interface VerifyResultItem { /** 필드명 -> 검증 결과 배열 */ export type SeweragePipeInputVerificationResult = Record; -export interface SeweragePipeInputCommonInfo { +export interface SeweragePipeInputCommonInfo extends StatisticsInputCommonInfoBase { statsYr: string; ctpvNm: string; sewerBzmnCd: string; diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts index 5744197f8..d58f3c41e 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts @@ -1,7 +1,7 @@ -import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; +import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface"; // 하수도 보급률 데이터 항목 -export interface SewerageSupplyRateCommonInfo { +export interface SewerageSupplyRateCommonInfo extends StatisticsInputCommonInfoBase { sewerBzmnArea: number; sewerBzmnCd: string; sewerBzmnFrgnrWholPpltnCnt: number; diff --git a/apps/st/src/entities/statistics/model/ppltn/statistics-domestic-population-interface.ts b/apps/st/src/entities/statistics/model/ppltn/statistics-domestic-population-interface.ts new file mode 100644 index 000000000..0adabf98b --- /dev/null +++ b/apps/st/src/entities/statistics/model/ppltn/statistics-domestic-population-interface.ts @@ -0,0 +1,70 @@ +import type { ApiResponse } from "@/entities/statistics/model/statistics-interface"; + +/* ══════════════════════════════════════════════════ + 16. 통계인구관리 > 내국인 인구 관련 인터페이스 +══════════════════════════════════════════════════ */ + +/** 내국인 인구 임시 적재 행 (좌측 전체 리스트 / 우측 미매핑 리스트 공통) */ +export interface DomesticPopulationItem { + statsYr: string; // 통계연도 + admdstCd: string; // 행정구역코드 + admdstNm: string; // 행정구역명 + domplWholPpltnCnt: number; // 내국인 합계 인구수 + domplMlPpltnCnt: number; // 내국인 남성 인구수 + domplFmlPpltnCnt: number; // 내국인 여성 인구수 + /** 미매핑 리스트 전용 - 매핑 대상 행정구역코드 (상위코드) */ + mpinCd: string; +} + +/** 엑셀 업로드 요청 파라미터 */ +export interface DomesticPopulationUploadParams { + file: File; + statsYr: string; +} + +/** 임시 적재 리스트 조회 요청 파라미터 */ +export interface DomesticPopulationListParams { + statsYr: string; + /** 1: 전체 리스트(좌측), 2: 미매핑 리스트(우측) */ + opt1: '1' | '2'; +} + +/** 수동 매핑 후 최종 반영 요청 파라미터 */ +export interface DomesticPopulationFinalParams { + YR: string; + /** 수동 매핑된 행 목록 (admdstCd → subAdmdstCd 매핑 정보) */ + regList: DomesticPopulationMappingItem[]; +} + +/** 수동 매핑 단건 아이템 */ +export interface DomesticPopulationMappingItem { + statsYr: string; + admdstCd: string; + admdstNm: string; + subAdmdstCd: string; +} + +/** 내국인/외국인 상위 코드 후보 조회 요청 */ +export interface UpperCodesByNameParams { + statsYr: string; + admdstNm: string; +} + +/** 내국인/외국인 상위 코드 후보 단건 아이템 */ +export interface UpperCodeCandidateItem { + admdstCd: string; // 행정동코드 + admdstNm: string; // 행정동명 + [key: string]: string; +} + +/** 행정동 갱신 법정동코드 후보 단건 아이템 */ +export interface LawCodeCandidateItem { + lawCd: string; // 법정동코드 + lawNm: string; // 법정동명 + [key: string]: string; + +} +/* ── API 응답 타입 ── */ +export type DomesticPopulationListResponse = ApiResponse; +export type DomesticPopulationFinalResponse = ApiResponse; +export type UpperCodesByNameResponse = ApiResponse; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-excreta-treatment-facility-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-excreta-treatment-facility-review-interface.ts new file mode 100644 index 000000000..ef4b618a8 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-excreta-treatment-facility-review-interface.ts @@ -0,0 +1,76 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface ExcretaTreatmentFacilityReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + fcltNm: string; + rowType: string; + fcltPstnCn: string | null; + telNo: string | null; + cmptnStrtYmd: string | null; + oprtngStrtYmd: string | null; + wholPjtcoAmt: number; + prcsFcltCpct: number; + cpct: number; + oprtngDayCnt: number; + prcsMthdExpln: string | null; + linkTmplntCn: string | null; + linkPrcsQnt: number; + inflwBodWqVl: number; + inflwTocWqVl: number; + inflwSsWqVl: number; + inflwTnWqVl: number; + inflwTpWqVl: number; + inflwClfgCnt: number; + dschgBodWqVl: number; + dschgTocWqVl: number; + dschgSsWqVl: number; + dschgTnWqVl: number; + dschgTpWqVl: number; + dschgClfgCnt: number; + wtsysNm: string | null; + trbtrNm: string | null; + msirbCn: string | null; + ssirbCn: string | null; + operMthdCdCn: string | null; + wholNope: number; + sftyAcdntEvntcsCnt: number; + rmrk: string | null; +} + +export interface ExcretaTreatmentFacilityReviewResponseData { + excretaTreatmentList: ExcretaTreatmentFacilityReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export interface ExcretaTreatmentFacilityReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + swtrmtFcltNm?: string; + sewerBzmnCd?: string; +} + +export type ExcretaTreatmentFacilityReviewApiResponse = ApiResponse< + ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData +>; + +export function extractExcretaTreatmentFacilityReviewList( + data: ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData | undefined, +): ExcretaTreatmentFacilityReviewItem[] { + if (!data) return []; + if (Array.isArray(data)) return data; + return data.excretaTreatmentList ?? []; +} + +export function extractExcretaTreatmentFacilityReviewStatsStatusInfo( + data: ExcretaTreatmentFacilityReviewItem[] | ExcretaTreatmentFacilityReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index 53b9c74e6..571835065 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -31,7 +31,7 @@ export interface cancelSubmitResponse { export interface ApiResponse { status: string; - data: T; + data?: T; message?: string; } @@ -49,6 +49,7 @@ export type StatisticsColumnGuideListResponse = ApiResponse; /** 통계 입력 기간 삭제 요청 */ export interface StatsInputPeriodDeleteRequest { + extSchdlId?: number; statsYr: string; seType: string; seCd: number; @@ -105,6 +109,11 @@ export interface StatsInputPeriodDeleteRequest { /** 통계 입력 기간 삭제 응답 */ export type StatsInputPeriodDeleteApiResponse = ApiResponse; +/** 입력 조회 `data.commonInfo` 공통 필드 */ +export interface StatisticsInputCommonInfoBase { + isVerificationFailed: boolean; +} + /** 하수 입력/보급률 등 조회 `data.statusInfo` 공통 (LAE 제출·담당자 정보) */ export interface SewerageStatusInfo { laePicNm: string | null; @@ -159,10 +168,13 @@ export interface VerificationResultListRequest { ctpvNm?: string; sggNm?: string; statsCd?: string; - /** 검증 방법 코드 (예: IMR) */ + /** 검증 방법 코드 (예: NV, IMR, HB) */ vrfcMthdCd?: string; - /** 처리여부 코드 목록, 콤마 구분 */ + /** 처리여부 코드 목록, 콤마 구분 (예: 0,2) */ prcsYnList?: string; + /** 반려상태 코드 목록, 콤마 구분 (예: 0,1,2) */ + rejectCodeList?: string; + /** @deprecated 신규 API에서는 미사용 */ pbcpAprvYn?: string; page?: number; size?: number; @@ -172,7 +184,6 @@ export interface VerificationResultListRequest { export interface VerificationResultReportItem { statsYr: string; statsCd: string; - statsNm: string; sewerBzmnCd: string; ctpvNm: string; sggNm: string; @@ -181,18 +192,22 @@ export interface VerificationResultReportItem { vrfcMthdCd: string; vrfcMthdNm: string; vrfcTrgtNm: string; - regDt: string; rsnExpln: string; nowVlCn: string; compVlCn: string | null; prcsYn: string; prcsYnNm: string; prcsOpnnExpln: string | null; - prcrId: string | null; + stVrfRjtComCd: string; + stVrfRjtComNm: string; rjctOpnnCn: string | null; - pbcpAprvYn: string; - mdfrId: string | null; + regDt: string; mdfcnDt: string; + /** 하위 호환: 일부 API/화면에서만 내려올 수 있음 */ + statsNm?: string; + prcrId?: string | null; + pbcpAprvYn?: string; + mdfrId?: string | null; } export interface VerificationResultListPageInfo { @@ -448,8 +463,6 @@ export interface StatiscicsManagementReviewHistoryItem { statsYr: string; ctpvNm: string; sggNm: string; - sewerBzmnCd: string; - statsCd: string; menuNm: string; wrterOpinionSm: string | null; chckerOpinionSm: string | null; diff --git a/apps/st/src/entities/statistics/model/statistics-private-sewage-facility-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-private-sewage-facility-review-interface.ts new file mode 100644 index 000000000..e75dcf3eb --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-private-sewage-facility-review-interface.ts @@ -0,0 +1,152 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface PrivateSewageFacilityReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; +} + +/** ST00080001 검토 `data.privateSewageList[]` 한 행 */ +export interface PrivateSewageTreatFactStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstCd: string; + admdstNm: string | null; + sewerBzmnCd: string; + displayYear: string; + rowType: string; + wholDrtwPrcsFcltCnt: number; + wholStaaCnt: number; + step1StaaPrcsQnt: number; + step2StaaPrcsQnt: number; + step3StaaPrcsQnt: number; + step4StaaPrcsQnt: number; + step5StaaPrcsQnt: number; + step6StaaPrcsQnt: number; + step7StaaPrcsQnt: number; + staaExcpWhol: number; + step1StaaExcpPrcsQnt: number; + step2StaaExcpPrcsQnt: number; + step3StaaExcpPrcsQnt: number; + step4StaaExcpPrcsQnt: number; + step5StaaExcpPrcsQnt: number; + step6StaaExcpPrcsQnt: number; + step7StaaExcpPrcsQnt: number; + rmrk: string | null; +} + +/** ST00080002 검토 `data.septicTankList[]` 한 행 */ +export interface SepticTankReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstCd: string; + admdstNm: string | null; + sewerBzmnCd: string; + displayYear: string; + rowType: string; + grssFcltCnt: number; + staaInsdWhol: number; + staaInsdSttkNprsflCnt: number; + step1StaaInsdSttkFrcCnt: number; + staaInsdSprktpFltrtbCnt: number; + staaInsdAertMthdCnt: number; + staaInsdCntcAertCnt: number; + staaInsdEtcCnt: number; + staaInsdPrcsCpctByWhol: number; + step1StaaInsdNope: number; + step2StaaInsdNope: number; + step3StaaInsdNope: number; + step4StaaInsdNope: number; + step5StaaInsdNope: number; + step6StaaInsdNope: number; + step7StaaInsdNope: number; + step8StaaInsdNope: number; + staaExcpWhol: number; + staaExcpSttkNprsflCnt: number; + step1StaaExcpSttkFrcCnt: number; + staaExcpSprktpFltrtbCnt: number; + staaExcpAertMthdCnt: number; + staaExcpCntcAertCnt: number; + staaExcpEtcCnt: number; + staaExcpPrcsCpctByWhol: number; + step1StaaExcpNope: number; + step2StaaExcpNope: number; + step3StaaExcpNope: number; + step4StaaExcpNope: number; + step5StaaExcpNope: number; + step6StaaExcpNope: number; + step7StaaExcpNope: number; + step8StaaExcpNope: number; + rmrk: string | null; +} + +/** ST00080003 검토 `data.mgmtAreaList[]` 한 행 */ +export interface MgmtAreaReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstCd: string; + admdstNm: string | null; + sewerBzmnCd: string; + displayYear: string; + rowType: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; + rmrk: string | null; +} + +export interface PrivateSewageFacilityReviewResponseData { + privateSewageList?: PrivateSewageTreatFactStatusReviewItem[]; + septicTankList?: SepticTankReviewItem[]; + mgmtAreaList?: MgmtAreaReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type PrivateSewageFacilityReviewApiResponse = ApiResponse< + unknown[] | PrivateSewageFacilityReviewResponseData +>; + +export function extractPrivateSewageFacilityReviewList( + tabKey: string, + data: unknown[] | PrivateSewageFacilityReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00080001") { + return data.privateSewageList ?? []; + } + if (tabKey === "ST00080002") { + return data.septicTankList ?? []; + } + if (tabKey === "ST00080003") { + return data.mgmtAreaList ?? []; + } + return []; +} + +export function extractPrivateSewageFacilityReviewStatsStatusInfo( + data: unknown[] | PrivateSewageFacilityReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +/** 검토 목록 null → 그리드 표시용 빈 문자열 */ +export function mapPrivateSewageFacilityReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts index 0f34edf86..4f4ad95d6 100644 --- a/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-pumping-station-review-interface.ts @@ -1,77 +1,82 @@ import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; /** * 05.중계펌프장 검토 아이템 인터페이스 */ export interface MediationPumpStationItem { - // 기본 정보 - ctpvNm: string; - sggNm: string; - admdstCd: string; - statsYr: string; - displayYear: string; - rowType: 'OLD' | 'CURR'; - pumpstNm: string; - impdFcltPstnCn: string; - oprtngStrtYmd: string; - pumpSeNm: string; - swtrmtFcltNm: string; - - // 제원 및 계획량 - planMltvArea: number; - sndyArea: number; - rainArea: number; - exlnQnt: number; - - // 오수펌프 (DRTW) - drtwPumpGrssPpqty: number; - drtwPumpN1DmtrArea: number; - drtwPumpN1Ppqty: number; - drtwPumpN1Cnt: number; - drtwPumpN1RsrvCnt: number; - + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string; + pumpSeNm: string | null; + swtrmtFcltNm: string | null; + fcltPstnCn: string | null; + oprtngStrtYmd: string | null; + planMltvArea: number; + sndyArea: number; + rainArea: number; + exlnQnt: number; + drtwPumpGrssPpqty: number; + drtwPumpN1DmtrArea: number; + drtwPumpN1Ppqty: number; + drtwPumpN1Cnt: number; + drtwPumpN1RsrvCnt: number; drtwPumpN2DmtrArea: number; drtwPumpN2Ppqty: number; drtwPumpN2Cnt: number; drtwPumpN2RsrvCnt: number; - drtwPumpN3DmtrArea: number; drtwPumpN3Ppqty: number; drtwPumpN3Cnt: number; drtwPumpN3RsrvCnt: number; - - // 우수펌프 (EXLN) - exlnPumpGrssPpqty: number; + exlnPumpGrssPpqty: number; exlnPumpN1DmtrArea: number; exlnPumpN1Ppqty: number; exlnPumpN1Cnt: number; - exlnPumpN4RsrvCnt: number; - + exlnPumpN1RsrvCnt: number; exlnPumpN2DmtrArea: number; exlnPumpN2Ppqty: number; exlnPumpN2Cnt: number; exlnPumpN2RsrvCnt: number; - exlnPumpN3DmtrArea: number; exlnPumpN3Ppqty: number; exlnPumpN3Cnt: number; exlnPumpN3RsrvCnt: number; - - // 기타 - anlElpwrUseQnt: number; - rmrk: string; + anlElpwrUseQnt: number; + rmrk: string | null; } - export interface MediationPumpStationResponseData { - pumpStationList: MediationPumpStationItem[]; + pumpingStationList?: MediationPumpStationItem[]; + pumpStationList?: MediationPumpStationItem[]; + statsStatusInfo?: StatsStatusInfo; } +export type MediationPumpStationApiResponse = ApiResponse< + MediationPumpStationItem[] | MediationPumpStationResponseData +>; -export type MediationPumpStationApiResponse = ApiResponse & { - message: string; -}; +export function extractMediationPumpStationList( + data: MediationPumpStationItem[] | MediationPumpStationResponseData | undefined, +): MediationPumpStationItem[] { + if (!data) return []; + if (Array.isArray(data)) return data; + const list = data.pumpingStationList ?? data.pumpStationList; + return Array.isArray(list) ? list : []; +} + +export function extractMediationPumpStationStatsStatusInfo( + data: MediationPumpStationItem[] | MediationPumpStationResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} export interface MediationPumpStationService { getMediationPumpStationList(params: { @@ -79,6 +84,6 @@ export interface MediationPumpStationService { sewerBzmnCd?: string; ctpvNm?: string; sggNm?: string; - pumpstNm?: string; + fcltNm?: string; }): Promise; -} \ No newline at end of file +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-excreta-related-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-related-review-interface.ts new file mode 100644 index 000000000..82d8ebb93 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-related-review-interface.ts @@ -0,0 +1,118 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewageExcretaRelatedReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; +} + +/** ST00120001·ST00120002 검토 `data.staffList[]` 한 행 */ +export interface StaffReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + displayYear: string; + admdstCd: string; + admdstNm: string | null; + rowType: string; + ognzNm: string | null; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; + rmrk: string | null; +} + +/** ST00120003 검토 `data.collectionList[]` 한 행 */ +export interface CollectionReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + bzentyCnt: number; + empNope: number; + fcltSecrPrstCnt: number; + n3TonUdrCnt: number; + n4TonUdrCnt: number; + n8TonUdrCnt: number; + etcCnt: number; + clctCnvncPrfmncQnt: number; + rmrk: string | null; +} + +/** ST00120004 검토 `data.businessList[]` 한 행 */ +export interface BusinessReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + designMdsrCnt: number; + designMdsrNope: number; + mfbizCnt: number; + mfbizMdsrNope: number; + mngCnt: number; + mngMdsrNope: number; + rmrk: string | null; +} + +export interface SewageExcretaRelatedReviewResponseData { + staffList?: StaffReviewItem[]; + collectionList?: CollectionReviewItem[]; + businessList?: BusinessReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewageExcretaRelatedReviewApiResponse = ApiResponse< + unknown[] | SewageExcretaRelatedReviewResponseData +>; + +export function extractSewageExcretaRelatedReviewList( + tabKey: string, + data: unknown[] | SewageExcretaRelatedReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00120001" || tabKey === "ST00120002") { + return data.staffList ?? []; + } + if (tabKey === "ST00120003") { + return data.collectionList ?? []; + } + if (tabKey === "ST00120004") { + return data.businessList ?? []; + } + return []; +} + +export function extractSewageExcretaRelatedReviewStatsStatusInfo( + data: unknown[] | SewageExcretaRelatedReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewageExcretaRelatedReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + ognzNm: row.ognzNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface.ts new file mode 100644 index 000000000..37b64f502 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface.ts @@ -0,0 +1,229 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewageExcretaSlurgeReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + swtrmtFcltNm?: string; +} + +/** ST00110001 검토 `data.sludgeProcessingList[]` 한 행 */ +export interface SludgeProcessingReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + swtrmtFcltNm: string | null; + gnamWhol: number; + itslfQnt: number; + otsdInflwQnt: number; + transfTmplntNm: string | null; + transfPrcsQnt: number; + srpFncRt: number; + wholSrpPrcsQnt: number; + itslfTs1Whol: number; + itslfTs1RclmQnt: number; + itslfTs1IncnrQnt: number; + incnrPrcsWhol: number; + incnrPrcsLfvlm: number; + incnrPrcsPrdctRmtrlQnt: number; + incnrPrcsFmlyrQnt: number; + incnrPrcsFcltNm: string | null; + itslfTs1AltdQnt: number; + stbznPrcsWhol: number; + stbznPrcsLfvlm: number; + stbznPrcsFmlyrQnt: number; + stbznPrcsFcltNm: string | null; + itslfTs1DryQnt: number; + dryPrcsWhol: number; + dryPrcsLfvlm: number; + dryPrcsIncnrQnt: number; + dryPrcsFuelQnt: number; + dryPrcsPrdctRmtrlQnt: number; + dryPrcsFmlyrQnt: number; + dryPrcsCmpstgQnt: number; + dryPrcsFcltNm: string | null; + itslfTs1FuelQnt: number; + fuelPrcsWhol: number; + fuelPrcsIncnrQnt: number; + fuelPrcsFuelQnt: number; + fuelPrcsFcltNm: string | null; + itslfTs1CmpstgQnt: number; + cmpsPrcsFcltBzentyNm: string | null; + otsdCnsgnWhol: number; + otsdCnsgnRclmQnt: number; + otsdCnsgnIncnrQnt: number; + otsdCnsgnFuelPwplQnt: number; + otsdCnsgnPrdctRmtrlQnt: number; + otsdCnsgnFmlyrQnt: number; + otsdCnsgnCmpstgQnt: number; + otsdCnsgnFcltNm: string | null; + cparTs2Whol: number; + rmrk: string | null; +} + +/** ST00110002 검토 `data.digestionTankOperList[]` 한 행 */ +export interface DigestionTankOperReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + swtrmtFcltNm: string | null; + digstnEnYn: string | null; + digstnCpct: number; + actlDrvnCpct: number; + digstnGasInputQnt: number; + digstnGasInputSldDnst: number; + digstnTnkOprtngDayCnt: number; + ocrnGasQnt: number; + digstnGasUtztnWholPrst: number; + digstnGasUtztnNtslQnt: number; + digstnGasUtztnEtgtQnt: number; + digstnGasUtztnItslfQnt: number; + digstnGasUtztnEtcQnt: number; + surplsGasQnt: number; + rmrk: string | null; +} + +/** ST00110003 검토 `data.sewageSludgeOperList[]` 한 행 */ +export interface SewageSludgeOperReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string | null; + prcsMthExpln: string | null; + fcltCpct: number | null; + anlPrcsQnt: number | null; + dayAvgOprtngHr: number | null; + anlOprtngDayCnt: number | null; + oprtngRt: number | null; + rmrk: string | null; +} + +/** ST00110004 검토 `data.humanWasteSludgeList[]` 한 행 */ +export interface HumanWasteSludgeReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string | null; + gnam: number | null; + wholDspsQnt: number | null; + rcycQnt: number | null; + olandLfvlm: number | null; + incnrQnt: number | null; + oceanInputQnt: number | null; + fuelQnt: number | null; + etcQnt: number | null; + nondspsQnt: number | null; + swtrmtFcltNm: string | null; + rmrk: string | null; +} + +export interface SewageExcretaSlurgeReviewResponseData { + sludgeProcessingList?: SludgeProcessingReviewItem[]; + digestionTankOperList?: DigestionTankOperReviewItem[]; + sewageSludgeOperList?: SewageSludgeOperReviewItem[]; + humanWasteSludgeList?: HumanWasteSludgeReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewageExcretaSlurgeReviewApiResponse = ApiResponse< + unknown[] | SewageExcretaSlurgeReviewResponseData +>; + +export function extractSewageExcretaSlurgeReviewList( + tabKey: string, + data: unknown[] | SewageExcretaSlurgeReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00110001") { + return data.sludgeProcessingList ?? []; + } + if (tabKey === "ST00110002") { + return data.digestionTankOperList ?? []; + } + if (tabKey === "ST00110003") { + return data.sewageSludgeOperList ?? []; + } + if (tabKey === "ST00110004") { + return data.humanWasteSludgeList ?? []; + } + return []; +} + +export function extractSewageExcretaSlurgeReviewStatsStatusInfo( + data: unknown[] | SewageExcretaSlurgeReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewageExcretaSlurgeReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + const isHumanWasteSludgeRow = "gnam" in row; + return { + ...row, + admdstNm: row.admdstNm ?? "", + fcltNm: isHumanWasteSludgeRow + ? (row.fcltNm ?? "") + : (row.fcltNm ?? row.swtrmtFcltNm ?? ""), + swtrmtFcltNm: row.swtrmtFcltNm ?? "", + transfTmplntNm: row.transfTmplntNm ?? "", + incnrPrcsFcltNm: row.incnrPrcsFcltNm ?? "", + stbznPrcsFcltNm: row.stbznPrcsFcltNm ?? "", + dryPrcsFcltNm: row.dryPrcsFcltNm ?? "", + fuelPrcsFcltNm: row.fuelPrcsFcltNm ?? "", + cmpsPrcsFcltBzentyNm: row.cmpsPrcsFcltBzentyNm ?? "", + otsdCnsgnFcltNm: row.otsdCnsgnFcltNm ?? "", + // ST00110002 — API 필드 → review 그리드 컬럼 fieldName + digesterYn: row.digesterYn ?? row.digstnEnYn ?? "", + dsgnCpct: row.dsgnCpct ?? row.digstnCpct ?? 0, + actlOperCpct: row.actlOperCpct ?? row.actlDrvnCpct ?? 0, + inputQnt: row.inputQnt ?? row.digstnGasInputQnt ?? 0, + tsConc: row.tsConc ?? row.digstnGasInputSldDnst ?? 0, + oprtngDays: row.oprtngDays ?? row.digstnTnkOprtngDayCnt ?? 0, + gnrtGasQnt: row.gnrtGasQnt ?? row.ocrnGasQnt ?? 0, + gasTotal: row.gasTotal ?? row.digstnGasUtztnWholPrst ?? 0, + gasSale: row.gasSale ?? row.digstnGasUtztnNtslQnt ?? 0, + gasGen: row.gasGen ?? row.digstnGasUtztnEtgtQnt ?? 0, + gasSelf: row.gasSelf ?? row.digstnGasUtztnItslfQnt ?? 0, + gasEtc: row.gasEtc ?? row.digstnGasUtztnEtcQnt ?? 0, + surplusGas: row.surplusGas ?? row.surplsGasQnt ?? 0, + prcsMthExpln: row.prcsMthExpln ?? "", + fcltCpct: row.fcltCpct ?? 0, + anlPrcsQnt: row.anlPrcsQnt ?? 0, + dayAvgOprtngHr: row.dayAvgOprtngHr ?? 0, + anlOprtngDayCnt: row.anlOprtngDayCnt ?? 0, + oprtngRt: row.oprtngRt ?? 0, + // ST00110004 + gnam: row.gnam ?? 0, + wholDspsQnt: row.wholDspsQnt ?? 0, + rcycQnt: row.rcycQnt ?? 0, + olandLfvlm: row.olandLfvlm ?? 0, + incnrQnt: row.incnrQnt ?? 0, + oceanInputQnt: row.oceanInputQnt ?? 0, + fuelQnt: row.fuelQnt ?? 0, + etcQnt: row.etcQnt ?? 0, + nondspsQnt: row.nondspsQnt ?? 0, + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewerage-finance-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewerage-finance-review-interface.ts new file mode 100644 index 000000000..7005dba1a --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewerage-finance-review-interface.ts @@ -0,0 +1,174 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewerageFinanceReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; +} + +/** ST00090001 검토 `data.financialSettlementList[]` 한 행 */ +export interface FinancialSettlementReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fnnByWholPjtcoAmt: number; + cnterGovSprtAmt: number; + mctpvLcexpAmt: number; + wareaSggLclAmt: number; + sewerUseAmt: number; + orgprBrdnAmt: number; + etcIncmAmt: number; + crfwAmt: number; + lbltAmt: number; + usgByWholPjtcoAmt: number; + tmplntWholAmt: number; + tmplntFcltAmt: number; + tmplntMendngAmt: number; + tmplntOperKeepAmt: number; + tmplntEtcAmt: number; + wholSwagePplAmt: number; + swagePplFcltAmt: number; + swagePplMendngAmt: number; + swagePplOperKeepAmt: number; + swagePplEtcAmt: number; + wholSwageImpdFcltAmt: number; + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcWholAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; + rmrk: string | null; +} + +/** ST00090002 검토 `data.sewerageFeeList[]` 한 행 */ +export interface SewerageFeeReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + anlLevyQnt: number; + levyAmt: number; + avgUntprcAmt: number; + grssPrmcoAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; + rmrk: string | null; +} + +/** ST00090003 검토 `data.usageFeeList[]` 한 행 */ +export interface UsageFeeReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + levyAmt: number; + levyQnt: number; + hshusLevyAmt: number; + hshusLevyQnt: number; + hshusUntprcAmt: number; + taskFrLevyAmt: number; + taskFrLevyQnt: number; + taskFrUntprcAmt: number; + bsnGnrlLevyAmt: number; + bsnGnrlLevyQnt: number; + bsnGnrlUntprcAmt: number; + massBthhsFrLevyAmt: number; + massBthhsFrLevyQnt: number; + massBthhsFrUntprcAmt: number; + bthhsFrSpcs2LevyAmt: number; + bthhsFrSpcs2LevyQnt: number; + bthhsFrSpcs2UntprcAmt: number; + induMdsrLevyAmt: number; + induMdsrLevyQnt: number; + induMdsrUntprcAmt: number; + etcLevyAmt: number; + etcLevyQnt: number; + etcLevyUntprcAmt: number; + rmrk: string | null; +} + +/** ST00090004 검토 `data.orgCauseList[]` 한 행 */ +export interface OrgCauseReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + levyEvntcsCnt: number; + levyAmt: number; + trgtByWholClctnEvntcsCnt: number; + trgtByWholClctnAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; + rmrk: string | null; +} + +export interface SewerageFinanceReviewResponseData { + financialSettlementList?: FinancialSettlementReviewItem[]; + sewerageFeeList?: SewerageFeeReviewItem[]; + usageFeeList?: UsageFeeReviewItem[]; + orgCauseList?: OrgCauseReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewerageFinanceReviewApiResponse = ApiResponse< + unknown[] | SewerageFinanceReviewResponseData +>; + +export function extractSewerageFinanceReviewList( + tabKey: string, + data: unknown[] | SewerageFinanceReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00090001") { + return data.financialSettlementList ?? []; + } + if (tabKey === "ST00090002") { + return data.sewerageFeeList ?? []; + } + if (tabKey === "ST00090003") { + return data.usageFeeList ?? []; + } + if (tabKey === "ST00090004") { + return data.orgCauseList ?? []; + } + return []; +} + +export function extractSewerageFinanceReviewStatsStatusInfo( + data: unknown[] | SewerageFinanceReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewerageFinanceReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-sewerage-service-level-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewerage-service-level-review-interface.ts new file mode 100644 index 000000000..2f3d416ce --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-sewerage-service-level-review-interface.ts @@ -0,0 +1,101 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface SewerageServiceLevelReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + swtrmtFcltNm?: string; +} + +/** ST00130001 검토 `data.complaintList[]` 한 행 */ +export interface ComplaintReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + swtrmtPpltnCnt: number; + whol: number; + odorCnt: number; + swagePplCnt: number; + dschgWqCnt: number; + ldsCnt: number; + plltnAcdntCnt: number; + sewerCrgAmt: number; + etcCnt: number; + crtrPpltnCvlcptEvntcsCnt: number; + rmrk: string | null; +} + +/** ST00130002 검토 `data.facilityList[]` 한 행 */ +export interface ResidentFriendlyFacilityReviewItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstCd: string; + admdstNm: string | null; + displayYear: string; + rowType: string; + swtrmtFcltNm: string | null; + fcltCpct: number; + tmplntArea: number; + whol: number; + brkFcltArea: number; + childFrshpSpacArea: number; + bdySportsFcltArea: number; + sportsFcltArea: number; + pkltArea: number; + eduMultiFcltArea: number; + wtlndMakeArea: number; + etcCnt: number; + rmrk: string | null; +} + +export interface SewerageServiceLevelReviewResponseData { + complaintList?: ComplaintReviewItem[]; + facilityList?: ResidentFriendlyFacilityReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type SewerageServiceLevelReviewApiResponse = ApiResponse< + unknown[] | SewerageServiceLevelReviewResponseData +>; + +export function extractSewerageServiceLevelReviewList( + tabKey: string, + data: unknown[] | SewerageServiceLevelReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00130001") { + return data.complaintList ?? []; + } + if (tabKey === "ST00130002") { + return data.facilityList ?? []; + } + return []; +} + +export function extractSewerageServiceLevelReviewStatsStatusInfo( + data: unknown[] | SewerageServiceLevelReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapSewerageServiceLevelReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + rowType: row.rowType ?? "", + admdstNm: row.admdstNm ?? "", + swtrmtFcltNm: row.swtrmtFcltNm ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface.ts new file mode 100644 index 000000000..dc9446b32 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface.ts @@ -0,0 +1,62 @@ +/** 07. 분뇨처리시설 현황 검색 조건 파라미터 DTO */ +export interface ExcretaTreatmentMaintainTotalRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + swtrmtFcltNm?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** 분뇨처리시설 목록 데이터 행 구조 */ +export interface ExcretaTreatmentStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + fcltNm: string; + rowType: string; + fcltPstnCn: string; + telNo: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + wholPjtcoAmt: number; + prcsFcltCpct: number; + cpct: number; + oprtngDayCnt: number; + prcsMthdExpln: string; + linkTmplntCn: string; + linkPrcsQnt: number; + // 유입수질 + inflwBodWqVl: number; + inflwTocWqVl: number; + inflwSsWqVl: number; + inflwTnWqVl: number; + inflwTpWqVl: number; + inflwClfgCnt: number; + // 방류수질 + dschgBodWqVl: number; + dschgTocWqVl: number; + dschgSsWqVl: number; + dschgTnWqVl: number; + dschgTpWqVl: number; + dschgClfgCnt: number; + // 방류수역 및 기타 정보 + wtsysNm: string; + trbtrNm: string; + msirbCn: string; + ssirbCn: string; + operMthdCdCn: string; + wholNope: number; + sftyAcdntEvntcsCnt: number; +} + +/** 분뇨처리시설 API 응답 인터페이스 */ +export interface ExcretaTreatmentStatusTotalResponse { + status: string; + message?: string; + data: ExcretaTreatmentStatusTotalItem[]; +} \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface.ts new file mode 100644 index 000000000..32c6cb5d3 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface.ts @@ -0,0 +1,67 @@ +/** 05. 중계펌프장 현황 검색 조건 파라미터 */ +export interface MediationPumpMaintainTotalRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + pumpstNm?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** 중계펌프장 현황 목록 데이터 단위 */ +export interface MediationPumpStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + pumpSeNm: string; + swtrmtFcltNm: string; + fcltPstnCn: string; + oprtngStrtYmd: string; + planMltvArea: number; + sndyArea: number; + rainArea: number; + exlnQnt: number; + // 오수펌프 + drtwPumpGrssPpqty: number; + drtwPumpN1DmtrArea: number; + drtwPumpN1Ppqty: number; + drtwPumpN1Cnt: number; + drtwPumpN1RsrvCnt: number; + drtwPumpN2DmtrArea: number; + drtwPumpN2Ppqty: number; + drtwPumpN2Cnt: number; + drtwPumpN2RsrvCnt: number; + drtwPumpN3DmtrArea: number; + drtwPumpN3Ppqty: number; + drtwPumpN3Cnt: number; + drtwPumpN3RsrvCnt: number; + // 우수펌프 + exlnPumpGrssPpqty: number; + exlnPumpN1DmtrArea: number; + exlnPumpN1Ppqty: number; + exlnPumpN1Cnt: number; + exlnPumpN1RsrvCnt: number; + exlnPumpN2DmtrArea: number; + exlnPumpN2Ppqty: number; + exlnPumpN2Cnt: number; + exlnPumpN2RsrvCnt: number; + exlnPumpN3DmtrArea: number; + exlnPumpN3Ppqty: number; + exlnPumpN3Cnt: number; + exlnPumpN3RsrvCnt: number; + // 전력 + anlElpwrUseQnt: number; +} + +/** 중계펌프장 API 최종 응답 규격 */ +export interface MediationPumpStatusTotalResponse { + status: string; + message?: string; + data: MediationPumpStatusTotalItem[]; +} \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface.ts new file mode 100644 index 000000000..43c4f4c9a --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface.ts @@ -0,0 +1,109 @@ +/** 08. 개인하수처리시설 공통 요청 검색 파라미터 */ +export interface PrivateSewageFacilitySummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + swtrmtFcltNm?: string; // Controller 수신 규격 + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] 오수처리시설 목록 항목 */ +export interface PrivateSewageTab1StatusItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstCd: string; + admdstNm: string; + sewerBzmnCd: string; + rowType: string; + wholDrtwPrcsFcltCnt: number; + // 하수처리구역 내 + wholStaaCnt: number; + step1StaaPrcsQnt: number; + step2StaaPrcsQnt: number; + step3StaaPrcsQnt: number; + step4StaaPrcsQnt: number; + step5StaaPrcsQnt: number; + step6StaaPrcsQnt: number; + step7StaaPrcsQnt: number; + // 하수처리구역 외 + staaExcpWhol: number; + step1StaaExcpPrcsQnt: number; + step2StaaExcpPrcsQnt: number; + step3StaaExcpPrcsQnt: number; + step4StaaExcpPrcsQnt: number; + step5StaaExcpPrcsQnt: number; + step6StaaExcpPrcsQnt: number; + step7StaaExcpPrcsQnt: number; +} + +/** [Tab2] 정화조 현황 목록 항목 */ +export interface PrivateSewageTab2StatusItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstCd: string; + admdstNm: string; + sewerBzmnCd: string; + rowType: string; + grssFcltCnt: number; + // 하수처리구역 내 + staaInsdWhol: number; + staaInsdSttkNprsflCnt: number; + step1StaaInsdSttkFrcCnt: number; + staaInsdSprktpFltrtbCnt: number; + staaInsdAertMthdCnt: number; + staaInsdCntcAertCnt: number; + staaInsdEtcCnt: number; + staaInsdPrcsCpctByWhol: number; + step1StaaInsdNope: number; + step2StaaInsdNope: number; + step3StaaInsdNope: number; + step4StaaInsdNope: number; + step5StaaInsdNope: number; + step6StaaInsdNope: number; + step7StaaInsdNope: number; + step8StaaInsdNope: number; + // 하수처리구역 외 + staaExcpWhol: number; + staaExcpSttkNprsflCnt: number; + step1StaaExcpSttkFrcCnt: number; + staaExcpSprktpFltrtbCnt: number; + staaExcpAertMthdCnt: number; + staaExcpCntcAertCnt: number; + staaExcpEtcCnt: number; + staaExcpPrcsCpctByWhol: number; + step1StaaExcpNope: number; + step2StaaExcpNope: number; + step3StaaExcpNope: number; + step4StaaExcpNope: number; + step5StaaExcpNope: number; + step6StaaExcpNope: number; + step7StaaExcpNope: number; + step8StaaExcpNope: number; +} + +/** [Tab3] 개인하수도 관리지역 지정공고 현황 목록 항목 */ +export interface PrivateSewageTab3StatusItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstCd: string; + admdstNm: string; + sewerBzmnCd: string; + rowType: string; + indvSwagePbancRgnCnt: number; + indvSwageDsgnScpLen: number; + spctnkGrssFcltCnt: number; + spctnkQnt: number; + drtwPrcsFcltGrssFcltCnt: number; + drtwPrcsFcltQnt: number; +} + +/** 3개 탭 개별 API 응답 규격 정의 */ +export interface PrivateSewageTab1Response { status: string; message?: string; data: PrivateSewageTab1StatusItem[]; } +export interface PrivateSewageTab2Response { status: string; message?: string; data: PrivateSewageTab2StatusItem[]; } +export interface PrivateSewageTab3Response { status: string; message?: string; data: PrivateSewageTab3StatusItem[]; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts index 49dd44a63..95a408ca4 100644 --- a/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-service-level-total-interface.ts @@ -4,14 +4,16 @@ export interface SewerageServiceLevelRequest { statsYr?: string; sewerBzmnCd?: string; rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; } -export interface SewerageServiceLevelComplaintTotalItem { +export interface SewageComplaintSummaryItem { statsYr: string; ctpvNm: string; - sggNm: string | null; - admdstCd: string; + sggNm: string; admdstNm: string; + admdstCd: string; rowType: string; swtrmtPpltnCnt: number; whol: number; @@ -23,24 +25,22 @@ export interface SewerageServiceLevelComplaintTotalItem { sewerCrgAmt: number; etcCnt: number; crtrPpltnCvlcptEvntcsCnt: number; - rmrk: string | null; } export interface SewerageServiceLevelComplaintResponse { status: string; message?: string; - data: SewerageServiceLevelComplaintTotalItem[]; + data: SewageComplaintSummaryItem[]; } export interface SewerageServiceLevelResidentFriendlyFactTotalItem { - statsYr: string; + statsYr: string; ctpvNm: string; sggNm: string; - admdstCd: string; admdstNm: string; + admdstCd: string; rowType: string; swtrmtFcltNm: string; - fcltCpct: number; tmplntArea: number; whol: number; brkFcltArea: number; @@ -51,7 +51,6 @@ export interface SewerageServiceLevelResidentFriendlyFactTotalItem { eduMultiFcltArea: number; wtlndMakeArea: number; etcCnt: number; - rmrk: string; } export interface SewerageServiceLevelResidentFriendlyFactResponse { diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts index 139320868..ca2c79b14 100644 --- a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface.ts @@ -4,6 +4,8 @@ export interface SewagePipeMaintainTotalRequest { statsYr?: string; sewerBzmnCd?: string; rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; } export interface SewagePipeMaintainRepairTotalItem { diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface.ts new file mode 100644 index 000000000..782875e34 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface.ts @@ -0,0 +1,114 @@ +export interface SewerReservoirMaintainTotalRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] `GET .../reservoir-status` 목록 항목 */ +export interface SewerReservoirStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + sewerBzmnCd: string | null; + fcltNm: string; + fcltPstnCn: string; + rowType: string; + oprtngStrtYmd: string; + planDesignFreqCnt: number; + fldngPrvtnYn: string; + wqPlltnMtgtYn: string; + rtlztnUsgYn: string; + fcltStrctCn: string; + wtrStrgZoneArea: number; + impdFcltCpct: number; + grssPpqty: number; + pumpFcltN1DmtrExpln: string; + pumpFcltN1PerPpqty: number; + pumpFcltN1PumpCnt: number; + pumpFcltN1RsrvPumpCnt: number; + pumpFcltN2DmtrExpln: string; + pumpFcltN2PerPpqty: number; + pumpFcltN2PumpCnt: number; + pumpFcltN2RsrvPumpCnt: number; + pumpFcltN3DmtrExpln: string; + pumpFcltN3PerPpqty: number; + pumpFcltN3PumpCnt: number; + pumpFcltN3RsrvPumpCnt: number; + prcsgFcltYn: string; + ddrztnEqpmYn: string; + oslfEtgtYn: string; + mltvMthCn: string; + impdMthCn: string; + impdPrcsMthdCn: string; + dschgWtrsExpln: string; + prcptnLastDspsQnt: number; + ctmnLastDspsQnt: number; + prcptnLastDspsMthdExpln: string; + ctmnLastDspsMthdExpln: string; + oprtngDayCnt: number; + grssMltvQnt: number; + rmrk: string | null; +} + +/** [Tab1] `GET .../reservoir-status` 응답 */ +export interface SewerReservoirStatusTotalResponse { + status: string; + message?: string; + data: SewerReservoirStatusTotalItem[]; +} + +/** [Tab2] `GET .../pump-station-status` 목록 항목 */ +export interface SewerPumpStationStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + sewerBzmnCd: string | null; + rowType: string; + fcltNm: string; + fcltPstnCn: string; + fcltyKndCn: string; + oprtngStrtYmd: string; + planDesignFreqCnt: number; + wtrStrgZoneArea: number; + rsvrYn: string; + rsvrImpndQnt: number; + abstnRgnImdQnt: number; + grssPpqty: number; + pumpFcltN1DmtrExpln: string; + pumpFcltN1PerPpqty: number; + pumpFcltN1PumpCnt: number; + pumpFcltN1RsrvPumpCnt: number; + pumpFcltN2DmtrExpln: string; + pumpFcltN2PerPpqty: number; + pumpFcltN2PumpCnt: number; + pumpFcltN2RsrvPumpCnt: number; + pumpFcltN3DmtrExpln: string; + pumpFcltN3PerPpqty: number; + pumpFcltN3PumpCnt: number; + pumpFcltN3RsrvPumpCnt: number; + prcsgFcltYn: string; + ddrztnEqpmYn: string; + oslfEtgtYn: string; + dschgWtrsExpln: string; + prcptnLastDspsQnt: number; + ctmnLastDspsQnt: number; + prcptnLastDspsMthdExpln: string; + ctmnLastDspsMthdExpln: string; + oprtngDayCnt: number; + grssMltvQnt: number; +} + +/** [Tab2] `GET .../pump-station-status` 응답 */ +export interface SewerPumpStationStatusTotalResponse { + status: string; + message?: string; + data: SewerPumpStationStatusTotalItem[]; +} \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface.ts new file mode 100644 index 000000000..798506ee1 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface.ts @@ -0,0 +1,38 @@ +/** 하수도 통계 총괄표 — 통계항목(탭) 목록 API `data[]` 한 행 */ +export interface SewerageAggregateSummaryTabItem { + statsCd: string; + tabNm: string; +} + +export interface SewerageAggregateSummaryTabListResponse { + status: string; + message: string; + data: SewerageAggregateSummaryTabItem[]; +} + +/** 하수도 통계 총괄표 — 엑셀 내려받기 API `Content-Type` */ +export const SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME = + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" as const; + +/** 하수도 통계 총괄표 — 엑셀 내려받기 API 응답 */ +export interface SewerageAggregateSummaryDownloadResult { + blob: Blob; + fileName?: string; +} + +/** 하수도 통계 총괄표 — 엑셀 내려받기 API 요청 */ +export interface SewerageAggregateSummaryDownloadRequest { + statsCdList: string[]; + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + summaryYearCnt: number; +} + +export interface StatisticsSewerageAggregateSummaryService { + getAggregateSummaryTabList(): Promise; + downloadAggregateSummary( + request: SewerageAggregateSummaryDownloadRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface.ts new file mode 100644 index 000000000..1a430a911 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface.ts @@ -0,0 +1,121 @@ +/** 09. 하수도 재정관리 검색 조건 공통 파라미터 */ +export interface SewerageFinanceSummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] 하수도재정 결산 목록 항목 */ +export interface SewerageFinanceSettlementTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fnnByWholPjtcoAmt: number; + cnterGovSprtAmt: number; + mctpvLcexpAmt: number; + wareaSggLclAmt: number; + sewerUseAmt: number; + orgprBrdnAmt: number; + etcIncmAmt: number; + crfwAmt: number; + lbltAmt: number; + usgByWholPjtcoAmt: number; + tmplntWholAmt: number; + tmplntFcltAmt: number; + tmplntMendngAmt: number; + tmplntOperKeepAmt: number; + tmplntEtcAmt: number; + wholSwagePplAmt: number; + swagePplFcltAmt: number; + swagePplMendngAmt: number; + swagePplOperKeepAmt: number; + swagePplEtcAmt: number; + wholSwageImdpFcltAmt: number; // 스펙 대조 자동 매핑 보정 (SwageImpdFclt) + swageImpdFcltAmt: number; + swageImpdMendngAmt: number; + swageImpdOperKeepAmt: number; + swageImpdFcltEtcAmt: number; + etcWholAmt: number; + etcAcdntCrfwAmt: number; + etcNrcpAmt: number; + etcAmt: number; +} + +/** [Tab2] 하수도 요금 목록 항목 */ +export interface SewerageFeeTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + anlLevyQnt: number; + levyAmt: number; + avgUntprcAmt: number; + grssPrmcoAmt: number; + grssUnitPrmcoAmt: number; + rlztnRt: number; +} + +/** [Tab3] 하수도 사용료 목록 항목 */ +export interface SewerageUsageFeeTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + levyAmt: number; + levyQnt: number; + hshusLevyAmt: number; + hshusLevyQnt: number; + hshusUntprcAmt: number; + taskFrLevyAmt: number; + taskFrLevyQnt: number; + taskFrUntprcAmt: number; + bsnGnrlLevyAmt: number; + bsnGnrlLevyQnt: number; + bsnGnrlUntprcAmt: number; + massBthhsFrLevyAmt: number; + massBthhsFrLevyQnt: number; + massBthhsFrUntprcAmt: number; + bthhsFrSpcs2LevyAmt: number; + bthhsFrSpcs2LevyQnt: number; + bthhsFrSpcs2UntprcAmt: number; + induMdsrLevyAmt: number; + induMdsrLevyQnt: number; + induMdsrUntprcAmt: number; + etcLevyAmt: number; + etcLevyQnt: number; + etcLevyUntprcAmt: number; +} + +/** [Tab4] 원인자 부담금 목록 항목 */ +export interface SewerageOrgCauseTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + levyEvntcsCnt: number; + levyAmt: number; + trgtByWholClctnEvntcsCnt: number; + trgtByWholClctnAmt: number; + mltvEqpmEvntcsCnt: number; + mltvEqpmAmt: number; + othrCstrnOthrActrCnt: number; + othrCstrnOthrActrAmt: number; +} + +export interface SewerageFinanceSettlementResponse { status: string; message?: string; data: SewerageFinanceSettlementTotalItem[]; } +export interface SewerageFeeResponse { status: string; message?: string; data: SewerageFeeTotalItem[]; } +export interface SewerageUsageFeeResponse { status: string; message?: string; data: SewerageUsageFeeTotalItem[]; } +export interface SewerageOrgCauseResponse { status: string; message?: string; data: SewerageOrgCauseTotalItem[]; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface.ts new file mode 100644 index 000000000..dd3803817 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface.ts @@ -0,0 +1,114 @@ + +/** 12. 하수및분뇨관리사업 공통 검색 파라미터 DTO 규격 */ +export interface SewerageIndustry { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [TAB1] 공공기관 하수분야 응답 DTO */ +export interface PublicSewageStaffTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + rowType: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; +} + +/** [TAB1] 공공기관 하수분야 응답 DTO */ +export interface PublicSewageStaffTab1Response { + status: string; + message?: string; + data: PublicSewageStaffTotalItem[]; +} + + +/** [TAB2] 공공기관 분뇨분야 응답 DTO */ +export interface PublicHumanWasteStaffTotalItem{ + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + rowType: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; +} + + +export interface PublicHumanWasteStaffTab2Response { + status: string; + message?: string; + data: PublicHumanWasteStaffTotalItem[]; +} + + +/** [TAB3] 분뇨수집 .운반업 응답 DTO */ +export interface HumanWasteCollectionOperItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + bzentyCnt: number; + empNope: number; + fcltSecrPrstCnt: number; + n3TonUdrCnt: number; + n4TonUdrCnt: number; + n8TonUdrCnt: number; + etcCnt: number; + clctCnvncPrfmncQnt: number; +} + +export interface HumanWasteCollectionTab3Response { + status: string; + message?: string; + data: HumanWasteCollectionOperItem[]; +} + +export interface IndividualSewageRelBusinessItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + designMdsrCnt: number; + designMdsrNope: number; + mfbizCnt: number; + mfbizMdsrNope: number; + mngCnt: number; + mngMdsrNope: number; +} + +export interface IndividualSewageTab4Response { + status: string; + message?: string; + data: IndividualSewageRelBusinessItem[]; +} \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface.ts new file mode 100644 index 000000000..6daaa773c --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface.ts @@ -0,0 +1,150 @@ +/** 10. 물재이용 검색 조건 공통 파라미터 DTO */ +export interface WaterReuseSummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [Tab1] 공공하수처리수 재이용 목록 항목 인터페이스 */ +export interface SewerageReuseStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + // 하수처리장 + swtrmtFcltNm: string; + sncnYmd: string; + prcsFcltCpct: string; + prcsQnt: number; + lglYn: string; + // 재이용시설 설치시 + instlYn: string; + oprtngStrtYmd: string; + instlAprvYmd: string; + rprcsFcltCpct: number; + mtcnsNm: string; + ruswtPplLen: number; + ruswtPumpLen: number; + // 하수처리수 재이용현황 - 장내용수 + wholUseQnt: number; + ithlWshwtrQnt: number; + ithlClnQty: number; + ithlFrzgQty: number; + ithlLdhcUswtrQnt: number; + ithlDltnUswtrQnt: number; + ithlRuswtrQnt: number; + ithlEtcQnt: number; + ithlUnsdQnt: number; + // 하수처리수 재이용현황 - 장외용수 + otcntrUswtrWhol: number; + otcntrClnRstrmQnt: number; + otcntrWshwtrQnt: number; + otcntrLdhcUswtrQnt: number; + otcntrHdrplUswtrQnt: number; + otcntrRvrKeepQnt: number; + agrcltUswtrPstnQnt: number; + ugwtrChargQnt: number; + mdsrUswtrQnt: number; + // 기타 + rtlztnRt: number; +} + +export interface SewerageReuseStatusTab1Response { + status: string; + message?: string; + data: SewerageReuseStatusTotalItem[]; +} + +// ============================================================================ +// ★ [TAB2] 중수도 현황 명세 (추가 취합 완료) +// ============================================================================ +export interface GrayWaterStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + fcltPstnCn: string; + bldgSeCn: string; + lglTrgtYn: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + archWholArea: number; + prcsCpct: number; + wtrUseQnt: number; + ruswtrUtztnQnt: number; + sbstCntUtztnQnt: number; + mnUsgCn: string; + ruswtrOperAmt: number; +} + +export interface GrayWaterStatusTab2Response { + status: string; + message?: string; + data: GrayWaterStatusTotalItem[]; +} + +// ============================================================================ +// ★ [TAB3] 빗물이용시설현황 명세 (신규 추가) +// ============================================================================ +export interface RainwaterUtilizationStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + bdstTypCn: string; + fcltPstnCn: string; + lglTrgtYn: string; + instlCmptnYmd: string; + instlAmt: number; + rcptMynCn: string; + filtrPrcsFcltYnExpln: string; + impdCpct: number; + anlUseQnt: number; + exlnUtlzUsgExpln: string; + anlOperAmt: number; +} +export interface RainwaterUtilizationStatusTab3Response { status: string; message?: string; data: RainwaterUtilizationStatusTotalItem[]; } + + +export interface SewageWwtRtlztnRegStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + regDt: string; +} +export interface SewageWwtRtlztnRegStatusTab4Response { status: string; message?: string; data: SewageWwtRtlztnRegStatusTotalItem[]; } + +// ============================================================================ +// ★ [TAB5] 하·폐수처리수 재이용시설 시공업 인가현황 명세 (신규 추가) +// ============================================================================ +export interface SewageWwtRtlztnSncnStatusTotalItem { + ctpvNm: string; + sggNm: string | null; + statsYr: string; + admdstNm: string; + admdstCd: string; + rowType: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + coRegYmd: string; +} +export interface SewageWwtRtlztnSncnStatusTab5Response { status: string; message?: string; data: SewageWwtRtlztnSncnStatusTotalItem[]; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sludge-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sludge-total-interface.ts new file mode 100644 index 000000000..0f5229385 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sludge-total-interface.ts @@ -0,0 +1,169 @@ +/** 11. 하수및분뇨찌꺼기 공통 검색 파라미터 DTO 규격 */ +export interface SludgeSummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +/** [TAB1] 찌꺼기(슬러지) 처리시설 현황 데이터 행 항목 인터페이스 */ +export interface SludgeProcessingTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string | null; + admdstNm: string; + admdstCd: string; + rowType: string; + + // 기본 정보 + fcltNm: string; + + // 찌꺼기 발생량(톤/년, 탈수기준) + gnamWhol: number; + itslfQnt: number; + otsdInflwQnt: number; + + // 이송 및 함수율 + transfTmplntNm: string; + transfPrcsQnt: number; + srpFncRt: number; + wholSrpPrcsQnt: number; + + // 자체 찌꺼기 처리량 소계 및 매립 + itslfTs1Whol: number; + itslfTs1RclmQnt: number; + + // 소각 후 처리(2차) + itslfTs1IncnrQnt: number; + incnrPrcsWhol: number; + incnrPrcsLfvlm: number; + incnrPrcsPrdctRmtrlQnt: number; + incnrPrcsFmlyrQnt: number; + incnrPrcsFcltNm: string; + + // 고화 후 2차 + itslfTs1AltdQnt: number; + stbznPrcsWhol: number; + stbznPrcsLfvlm: number; + stbznPrcsFmlyrQnt: number; + stbznPrcsFcltNm: string; + + // 건조 후 2차 + itslfTs1DryQnt: number; + dryPrcsWhol: number; + dryPrcsLfvlm: number; + dryPrcsIncnrQnt: number; + dryPrcsFuelQnt: number; + dryPrcsPrdctRmtrlQnt: number; + dryPrcsFmlyrQnt: number; + dryPrcsCmpstgQnt: number; + dryPrcsFcltNm: string; + + // 탄화 후 2차 + itslfTs1FuelQnt: number; + fuelPrcsWhol: number; + fuelPrcsIncnrQnt: number; + fuelPrcsFuelQnt: number; + fuelPrcsFcltNm: string; + + // 퇴비화 + itslfTs1CmpstgQnt: number; + cmpsPrcsFcltBzentyNm: string; + + // 외부(민간) 위탁 처리량 + otsdCnsgnWhol: number; + otsdCnsgnRclmQnt: number; + otsdCnsgnIncnrQnt: number; + otsdCnsgnFuelPwplQnt: number; + otsdCnsgnPrdctRmtrlQnt: number; + otsdCnsgnFmlyrQnt: number; + otsdCnsgnCmpstgQnt: number; + otsdCnsgnFcltNm: string; + + // 수도권광역 위탁 처리량 + cparTs2Whol: number; +} + +/** TAB1 서버 단일 응답 랩퍼 규격 */ +export interface SludgeProcessingTab1Response { + status: string; + message?: string; + data: SludgeProcessingTotalItem[]; +} +// ============================================================================ +// [TAB2] 소화조 운영현황 명세 +// ============================================================================ +export interface DigestionTankOperStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + swtrmtFcltNm: string; + digstnEnYn: string; + digstnCpct: number; + actlDrvnCpct: number; + digstnGasInputQnt: number; + digstnGasInputSldDnst: number; + digstnTnkOprtngDayCnt: number; + ocrnGasQnt: number; + digstnGasUtztnWholPrst: number; + digstnGasUtztnNtslQnt: number; + digstnGasUtztnEtgtQnt: number; + digstnGasUtztnItslfQnt: number; + digstnGasUtztnEtcQnt: number; + surplsGasQnt: number; +} +export interface DigestionTankOperTab2Response { status: string; message?: string; data: DigestionTankOperStatusTotalItem[]; } + +export interface SewageSludgeOperStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + prcsMthExpln: string; + fcltCpct: number; + anlPrcsQnt: number; + dayAvgOprtngHr: number; + anlOprtngDayCnt: number; + oprtngRt: number; +} + +export interface SewageSludgeOperTab3Response { + status: string; + message?: string; + data: SewageSludgeOperStatusTotalItem[]; +} + +export interface HumanWasteSludgeStatusTotalItem { + statsYr: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + admdstCd: string; + rowType: string; + fcltNm: string; + gnam: number; + wholDspsQnt: number; + rcycQnt: number; + olandLfvlm: number; + incnrQnt: number; + oceanInputQnt: number; + fuelQnt: number; + etcQnt: number; + nondspsQnt: number; + swtrmtFcltNm: string; +} + +export interface HumanWasteSludgeTab4Response { + status: string; + message?: string; + data: HumanWasteSludgeStatusTotalItem[]; +} \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-water-reuse-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-water-reuse-review-interface.ts new file mode 100644 index 000000000..d50c3470b --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-water-reuse-review-interface.ts @@ -0,0 +1,212 @@ +import type { ApiResponse } from "./statistics-interface"; +import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface"; + +export interface WaterReuseReviewRequestParams { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + sewerBzmnCd?: string; + swtrmtFcltNm?: string; +} + +/** ST00100001 검토 `data.reuseStatusList[]` 한 행 */ +export interface ReuseStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + swtrmtFcltNm: string; + sncnYmd: string | null; + prcsFcltCpct: number | string; + prcsQnt: number; + lglYn: string | null; + instlYn: string | null; + oprtngStrtYmd: string | null; + instlAprvYmd: string | null; + rprcsFcltCpct: number; + mtcnsNm: string | null; + ruswtPplLen: number; + ruswtPumpLen: number; + wholUseQnt: number; + ithlWshwtrQnt: number; + ithlClnQty: number; + ithlFrzgQty: number; + ithlLdhcUswtrQnt: number; + ithlDltnUswtrQnt: number; + ithlRuswtrQnt: number; + ithlEtcQnt: number; + ithlUnsdQnt: number; + otcntrUswtrWhol: number; + otcntrClnRstrmQnt: number; + otcntrWshwtrQnt: number; + otcntrLdhcUswtrQnt: number; + otcntrHdrplUswtrQnt: number; + otcntrRvrKeepQnt: number; + agrcltUswtrPstnQnt: number; + ugwtrChargQnt: number; + mdsrUswtrQnt: number; + rtlztnRt: number; + rmrk: string | null; +} + +/** ST00100002 검토 `data.grayWaterList[]` 한 행 */ +export interface GrayWaterReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string; + fcltPstnCn: string | null; + bldgSeCn: string | null; + lglTrgtYn: string | null; + cmptnStrtYmd: string | null; + oprtngStrtYmd: string | null; + archWholArea: number; + prcsCpct: number; + wtrUseQnt: number; + ruswtrUtztnQnt: number; + sbstCntUtztnQnt: number; + mnUsgCn: string | null; + ruswtrOperAmt: number; + rmrk: string | null; +} + +/** ST00100003 검토 `data.rainwaterList[]` 한 행 */ +export interface RainwaterReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + fcltNm: string; + bdstTypCn: string | null; + fcltPstnCn: string | null; + lglTrgtYn: string | null; + instlCmptnYmd: string | null; + instlAmt: number; + rcptMynCn: string | null; + filtrPrcsFcltYnExpln: string | null; + impdCpct: number; + anlUseQnt: number; + exlnUtlzUsgExpln: string | null; + anlOperAmt: number; + rmrk: string | null; +} + +/** ST00100004 검토 `data.regStatusList[]` 한 행 */ +export interface RegStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + bplcNm: string | null; + rprsvNm: string | null; + bplcPstnCn: string | null; + regDt: string | null; + rmrk: string | null; +} + +/** ST00100005 검토 `data.sncnStatusList[]` 한 행 */ +export interface SncnStatusReviewItem { + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstNm: string | null; + admdstCd: string; + displayYear: string; + rowType: string; + bplcNm: string | null; + rprsvNm: string | null; + bplcPstnCn: string | null; + coRegYmd: string | null; + rmrk: string | null; +} + +export interface WaterReuseReviewResponseData { + reuseStatusList?: ReuseStatusReviewItem[]; + grayWaterList?: GrayWaterReviewItem[]; + rainwaterList?: RainwaterReviewItem[]; + regStatusList?: RegStatusReviewItem[]; + sncnStatusList?: SncnStatusReviewItem[]; + statsStatusInfo?: StatsStatusInfo; +} + +export type WaterReuseReviewApiResponse = ApiResponse< + unknown[] | WaterReuseReviewResponseData +>; + +export function extractWaterReuseReviewList( + tabKey: string, + data: unknown[] | WaterReuseReviewResponseData | undefined, +): unknown[] { + if (!data) return []; + if (Array.isArray(data)) return data; + if (tabKey === "ST00100001") { + return data.reuseStatusList ?? []; + } + if (tabKey === "ST00100002") { + return data.grayWaterList ?? []; + } + if (tabKey === "ST00100003") { + return data.rainwaterList ?? []; + } + if (tabKey === "ST00100004") { + return data.regStatusList ?? []; + } + if (tabKey === "ST00100005") { + return data.sncnStatusList ?? []; + } + return []; +} + +export function extractWaterReuseReviewStatsStatusInfo( + data: unknown[] | WaterReuseReviewResponseData | undefined, +): StatsStatusInfo { + if (!data || Array.isArray(data)) return {}; + return data.statsStatusInfo ?? {}; +} + +export function mapWaterReuseReviewRows(list: unknown[]): object[] { + return list.map((item) => { + const row = item as Record; + return { + ...row, + admdstNm: row.admdstNm ?? "", + swtrmtFcltNm: row.swtrmtFcltNm ?? "", + sncnYmd: row.sncnYmd ?? "", + lglYn: row.lglYn ?? "", + instlYn: row.instlYn ?? "", + oprtngStrtYmd: row.oprtngStrtYmd ?? "", + instlAprvYmd: row.instlAprvYmd ?? "", + mtcnsNm: row.mtcnsNm ?? "", + fcltNm: row.fcltNm ?? "", + fcltPstnCn: row.fcltPstnCn ?? "", + bldgSeCn: row.bldgSeCn ?? "", + lglTrgtYn: row.lglTrgtYn ?? "", + cmptnStrtYmd: row.cmptnStrtYmd ?? "", + bdstTypCn: row.bdstTypCn ?? "", + instlCmptnYmd: row.instlCmptnYmd ?? "", + rcptMynCn: row.rcptMynCn ?? "", + filtrPrcsFcltYnExpln: row.filtrPrcsFcltYnExpln ?? "", + exlnUtlzUsgExpln: row.exlnUtlzUsgExpln ?? "", + bplcNm: row.bplcNm ?? "", + rprsvNm: row.rprsvNm ?? "", + bplcPstnCn: row.bplcPstnCn ?? "", + regDt: row.regDt ?? "", + coRegYmd: row.coRegYmd ?? "", + mnUsgCn: row.mnUsgCn ?? "", + rmrk: row.rmrk ?? "", + }; + }); +} diff --git a/apps/st/src/entities/statistics/model/verification-input-report-interface.ts b/apps/st/src/entities/statistics/model/verification-input-report-interface.ts new file mode 100644 index 000000000..0af515cbf --- /dev/null +++ b/apps/st/src/entities/statistics/model/verification-input-report-interface.ts @@ -0,0 +1,58 @@ +/** 이상치 선별 보고서 조회 요청 (`GET .../results/input/report`) */ +export interface VerificationInputReportRequest { + statsYr: string; + vrfcMthdCd: string; + statsCd?: string; + page?: number; + size?: number; +} + +export interface VerificationInputReportTypeCountingInfo { + nvCount: number; + imrCount: number; + hbCount: number; +} + +/** 이상치 선별 보고서 목록 한 행 */ +export interface VerificationInputReportItem { + statsYr: string; + statsCd: string; + /** 통계코드명 (타이틀 표시용) */ + statsNm?: string; + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string | null; + vrfcSn: number; + vrfcMthdCd: string; + vrfcMthdNm: string; + vrfcTrgtNm: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string | null; + vrfcStatusNm: string; +} + +export interface VerificationInputReportPageInfo { + totalElements: number; + totalPages: number; + currentPage: number; + size: number; +} + +export interface VerificationInputReportData { + verifyTypeCountingInfo: VerificationInputReportTypeCountingInfo; + verifyResultList: VerificationInputReportItem[]; + pageInfo: VerificationInputReportPageInfo; +} + +export interface VerificationInputReportResponse { + status: string; + message?: string; + data?: VerificationInputReportData; +} + +export interface VerificationInputReportService { + getVerificationInputReport( + param: VerificationInputReportRequest, + ): Promise; +} diff --git a/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx index 1bd281b28..bb3804b6c 100644 --- a/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx @@ -5,8 +5,9 @@ import { useRouter } from "next/navigation"; import { createColumnHelper, getCoreRowModel } from "@tanstack/react-table"; import dayjs from "dayjs"; +import { ST_DASHBOARD_BBS_ID } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; -import { type PstRecentListResponse, useRecentPostList } from "@/entities/post"; +import { type UserPost, useUserPostList } from "@/entities/post"; import { Button } from "@common/components"; import { useDragHandle } from "../sortable-panel"; @@ -15,25 +16,23 @@ export function NoticePanel() { const { dragHandleProps, isDragging } = useDragHandle(); const router = useRouter(); - const recentPostListQuery = useRecentPostList({ + const userPostListQuery = useUserPostList({ page: 0, size: 4, keywordType: "bbsTypeCd", keyword: "BBSNOTICE", }); - const handleRowClick = (post: PstRecentListResponse) => { - router.push( - `/cms/${post.bbsTypeCd.toLowerCase()}/${post.bbsId}/${post.pstNo}`, - ); + const handleRowClick = (post: UserPost) => { + router.push(`/cms/bbsnotice/${ST_DASHBOARD_BBS_ID}/${post.pstNo}`); }; const handleMoreClick = () => { - router.push("/cms/bbsnotice/7"); + router.push(`/cms/bbsnotice/${ST_DASHBOARD_BBS_ID}`); }; const { tableComponent } = useDataTable({ - query: recentPostListQuery, + query: userPostListQuery, tableOptions: { columns: noticeColumns, getCoreRowModel: getCoreRowModel(), @@ -52,8 +51,11 @@ export function NoticePanel() { title="헤더를 드래그하여 패널 순서 변경" >

공지사항

+
+
+ + + +
, + document.body, + )} + ); }; diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts index 6909ed26b..7e942879a 100644 --- a/apps/st/src/store/tab-store.ts +++ b/apps/st/src/store/tab-store.ts @@ -4,6 +4,8 @@ import { createJSONStorage, persist } from "zustand/middleware"; import type { MenuItem } from "@/entities/menu"; import { MenuAuthorityApi } from "@/entities/menu-authority"; import type { PageMenuAuthority } from "@/entities/menu-authority/model/menu-authority-interface"; +import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu"; +import { resolveStatisticsTabId } from "@/shared/lib/statistics-menu-url"; export interface Tab { id: string; // 고유 ID (urlPath) @@ -87,6 +89,8 @@ interface TabStore { setMenuList: (menuList: MenuItem[] | null) => void; closeAllTabs: () => void; closeOtherTabs: (tabId: string) => void; + closeTabsToLeft: (tabId: string) => void; + closeTabsToRight: (tabId: string) => void; } export const TAB_STORE_PERSIST_KEY = "st-tab-store"; @@ -101,11 +105,18 @@ function tabForPersist(tab: Tab): Tab { }; } +function normalizePersistedTab(tab: Tab): Tab { + const tabId = resolveStatisticsTabId(tab.urlPath || tab.id); + return tabId === tab.id ? tab : { ...tab, id: tabId }; +} + function sanitizePersistedTabsAndActive( tabs: Tab[], activeTabId: string | null, ): { tabs: Tab[]; activeTabId: string | null } { - let nextTabs = Array.isArray(tabs) ? tabs : []; + let nextTabs = Array.isArray(tabs) + ? tabs.filter((tab) => !isBbsNoticeMenu(tab.menuItem)).map(normalizePersistedTab) + : []; if (nextTabs.length > MAX_TABS) nextTabs = nextTabs.slice(0, MAX_TABS); if (nextTabs.length === 0) { @@ -130,8 +141,12 @@ const useTabStoreHook = create()( activeTabId: null, menuList: null, addTab: (menuItem: MenuItem) => { + if (isBbsNoticeMenu(menuItem)) { + return false; + } + const { tabs, menuList } = get(); - const tabId = menuItem.urlPath || ""; + const tabId = resolveStatisticsTabId(menuItem.urlPath || ""); const existingTab = tabs.find((tab) => tab.id === tabId); @@ -227,6 +242,46 @@ const useTabStoreHook = create()( }); } }, + closeTabsToLeft: (tabId: string) => { + const { tabs, activeTabId } = get(); + const index = tabs.findIndex((tab) => tab.id === tabId); + if (index <= 0) return; + + const newTabs = tabs.slice(index); + let newActiveTabId = activeTabId; + if (activeTabId && !newTabs.some((tab) => tab.id === activeTabId)) { + newActiveTabId = tabId; + } + + set({ + tabs: newTabs, + activeTabId: newActiveTabId, + }); + if (newActiveTabId) { + const nextActive = get().tabs.find((t) => t.id === newActiveTabId); + if (nextActive) ensureTabPageMenuAuthority(get, set, nextActive); + } + }, + closeTabsToRight: (tabId: string) => { + const { tabs, activeTabId } = get(); + const index = tabs.findIndex((tab) => tab.id === tabId); + if (index === -1 || index >= tabs.length - 1) return; + + const newTabs = tabs.slice(0, index + 1); + let newActiveTabId = activeTabId; + if (activeTabId && !newTabs.some((tab) => tab.id === activeTabId)) { + newActiveTabId = tabId; + } + + set({ + tabs: newTabs, + activeTabId: newActiveTabId, + }); + if (newActiveTabId) { + const nextActive = get().tabs.find((t) => t.id === newActiveTabId); + if (nextActive) ensureTabPageMenuAuthority(get, set, nextActive); + } + }, }), { name: TAB_STORE_PERSIST_KEY, diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 13798ff4f..10421b48b 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -22,7 +22,7 @@ import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay"; const Map = () => { const { code, comMap, rainMapDetile, rswtMapDetile, getRuseMaplListReturn - , pbnMapDetaile, ldrmntMapDetile, htdfMapDetile, coSttnMapDetail } = useWrStore((state) => state); + , pbnMapDetaile, ldrmntMapDetile, htdfMapDetile, coSttnMapDetail, initCommonCodes } = useWrStore((state) => state); const { isFeatured } = comMap; const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const gisMaps = useRef(null); @@ -73,6 +73,11 @@ const Map = () => { }; }, []); + useEffect(() => { + if (code.isInit) return; + initCommonCodes(); + }, [code.isInit, initCommonCodes]); + // 체크된 레이어 정보를 받는 핸들러 const handleCheckedLayersChange = ( checkedIds: string[], diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx index e20ce9f36..4c38b30b3 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/drgtLegend.tsx @@ -15,34 +15,34 @@ export function DrgtLegend() {
- {label} + {label}
); } return ( -
+
- +
-
+
가 뭄 정 보
- + {/* 관심 - 1회 미만 */} {/* 관심 - 1회 이상 */} - + {/* 주의 - 1회 미만 */} {/* 주의 - 1회 이상 */} - + {/* 관심+주의 - 1회 미만 */} {/* 관심+주의 - 1회 이상 */}
@@ -50,4 +50,4 @@ export function DrgtLegend() {
); -} \ No newline at end of file +} diff --git a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx index 75a938672..6514f5c06 100644 --- a/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/mapDrgtPopUp/searchBar.tsx @@ -172,12 +172,12 @@ export function SearchBar({ gisMaps, ctpvCode, sggCode }: CtpvSggPrcsFcltSelectB return ( -
+
- + - +
); -} \ No newline at end of file +} diff --git a/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx b/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx index 90b1cbc8a..5117229e1 100644 --- a/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx +++ b/apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx @@ -1,3 +1,6 @@ +/** + * 지도에서 feature 선택시 팝업 + */ "use client"; import { useWrStore } from "@/app/store"; import CoSttnDetailPopup from "@/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup"; @@ -20,7 +23,7 @@ export default function MapFeaturePopup() { coSttnMapDetail } = useWrStore((state) => state); const { pbnMapHeadList } = pbnMap; - const [isDetail, setIsDetail] = useState(undefined); + const [isDetail, setIsDetail] = useState(undefined); // isFeatureCd && layerNm const [layerNm, setLayerNm] = useState(); const searchParams = useSearchParams(); @@ -57,32 +60,32 @@ export default function MapFeaturePopup() { useEffect(() => { console.log(' layerNm ' , layerNm) if (isFeatureCd && layerNm) { - if (layerNm === "tb_wr_bdst_ruse_fclt") { + if (layerNm === "tb_wr_bdst_ruse_fclt") { // 하수재이용시설 setIsDetail(isFeatureCd); - } else if (layerNm === "tb_il_swage_prcs_fclt") { + } else if (layerNm === "tb_il_swage_prcs_fclt") { // 하수처리시설 pbnMapDetaile(isFeatureCd).then((res) => { console.log('res', res) setIsDetail([res]); }); - } else if (layerNm === "tb_wr_rnwtr_utztn_fclt") { + } else if (layerNm === "tb_wr_rnwtr_utztn_fclt") { // 빗물이용시설 rainMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); - } else if (layerNm === "tb_wr_rswtr_fclt") { + } else if (layerNm === "tb_wr_rswtr_fclt") { // 중수도시설 rswtMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); - } else if (layerNm === "tb_wr_htdfl_fclt") { + } else if (layerNm === "tb_wr_htdfl_fclt") { // 온배수시설 htdfMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); - }else if(layerNm ==='tb_wr_ruse_fclt_dmd_co'){ + }else if(layerNm ==='tb_wr_ruse_fclt_dmd_co'){ // 수요처 coSttnMapDetail(isFeatureCd).then((res) => { console.log('tb_wr_ruse_fclt_dmd_co ', res) setIsDetail(res); }); - } else if (layerNm === "tb_wr_tmpr_dmd_co") { + } else if (layerNm === "tb_wr_tmpr_dmd_co") { // 잠재수요처 ldrmntMapDetile(isFeatureCd).then((res) => { setIsDetail(res); }); @@ -94,14 +97,13 @@ export default function MapFeaturePopup() { return ( <> {isDetail ? ( -
+
+
{layerNm === "tb_wr_bdst_ruse_fclt" ? ( ) : layerNm === "tb_il_swage_prcs_fclt" ? (
-
- - </div> + <Title title="하수처리시설 상세정보" /> {/* 높이 조정 */} <UiTable tableData={isDetail || []} @@ -111,6 +113,7 @@ export default function MapFeaturePopup() { headName={"위치정보"} cellClick={() => {}} onPageChange={() => {}} + isHideCount={true} /> <UiTable tableData={isDetail || []} @@ -120,6 +123,7 @@ export default function MapFeaturePopup() { headName={"기본정보"} cellClick={() => {}} onPageChange={() => {}} + isHideCount={true} /> </div> ) : layerNm === "tb_wr_tmpr_dmd_co" ? ( @@ -149,6 +153,7 @@ export default function MapFeaturePopup() { isMap={true} /> )} + </div> </div> ) : ( <></> diff --git a/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx index 05f41d09b..e1e77e1c7 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx +++ b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx @@ -94,7 +94,7 @@ export default function SearchInputText({ value={textValue || ""} onChange={handleInputChange} onKeyDown={handleKeyDown} - className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="h-[3.2rem] w-full rounded-md border border-input pl-3 !text-xl text-foreground focus:outline-none focus:ring-2 focus:ring-ring" placeholder={`${label}을(를) 입력하세요`} onClick={() => { setTextValue(""); @@ -103,7 +103,7 @@ export default function SearchInputText({ }} /> {isOpen && ( - <div className="absolute top-full left-0 z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg"> + <div className="absolute left-0 top-full z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-border bg-popover text-popover-foreground shadow-lg"> <div className="max-h-72"> {searchList.length > 0 ? ( searchList.map((item, index) => ( @@ -111,8 +111,8 @@ export default function SearchInputText({ key={index} className={`w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl ${ highlightIndex === index - ? "bg-blue-100" - : "hover:bg-gray-100" + ? "bg-accent text-accent-foreground" + : "hover:bg-accent hover:text-accent-foreground" }`} onClick={() => handleSuggestionClick(item.bscFctNm)} role="button" @@ -127,7 +127,7 @@ export default function SearchInputText({ </div> )) ) : ( - <div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"> + <div className="w-full cursor-pointer overflow-auto px-5 py-3 !text-xl text-muted-foreground hover:bg-accent hover:text-accent-foreground"> 검색 결과가 없습니다. </div> )} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/page.tsx b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx index eac81032e..2947b2abf 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/page.tsx +++ b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx @@ -200,17 +200,17 @@ const Map = () => { // HTML 또는 React 오버레이 컴포넌트 구성 const overlayContent = ` - <div class="rounded-md border border-gray-300 bg-white shadow-md text-sm w-[120px]"> + <div class="w-[120px] rounded-md border border-border bg-popover text-sm text-popover-foreground shadow-md"> <!-- 헤더 --> - <div class="bg-cyan-700 text-white px-3 py-2 text-[11px] font-semibold text-center rounded-t-md"> + <div class="rounded-t-md bg-primary px-3 py-2 text-center text-[11px] font-semibold text-primary-foreground"> ${feature.get("swtrmt_fclt_nm") || "-"} </div> <!-- 테이블 --> - <table class="w-full text-sm text-gray-800 border-t border-gray-200"> + <table class="w-full border-t border-border text-sm text-foreground"> <tbody> - <tr class="border-b border-gray-200"> - <th class="bg-gray-100 text-gray-600 font-semibold text-[10px] text-left">용량(m³/일)</th> + <tr class="border-b border-border"> + <th class="bg-secondary text-secondary-foreground font-semibold text-[10px] text-left">용량(m³/일)</th> <td class="text-right text-[10px] font-bold">${formatNumber(feature.get("fclt_cpct"))}</td> </tr> </tbody> diff --git a/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx index 0f5430771..7abb3af50 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx +++ b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx @@ -60,12 +60,12 @@ export function SearchBar({ return ( <div className="flex justify-center w-full px-4 py-3"> - <div className="flex items-center w-full max-w-screen-lg rounded-xl bg-white shadow-md px-6 py-4 space-x-3"> + <div className="flex w-full max-w-screen-lg items-center space-x-3 rounded-xl bg-card px-6 py-4 text-card-foreground shadow-md"> <SelectBox labelClass="w-[40px]" className="w-[200px]" label="시도" - selectClass="rounded-lg bg-white" + selectClass="rounded-lg" selectArray={code.cityCd || []} onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")} value={sidoValue} @@ -74,12 +74,12 @@ export function SearchBar({ labelClass="w-[65px]" className="w-[200px]" label="시군구" - selectClass="rounded-lg bg-white" + selectClass="rounded-lg" selectArray={filteredSigunData || []} onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")} value={sigunValue} /> - <Label className="text-gray-600">하수재이용시설</Label> + <Label className="text-label">하수재이용시설</Label> <SearchInputText label="시설명" initValue={undefined} @@ -101,7 +101,7 @@ export function SearchBar({ /> <Button color="sky" - className="ml-2 text-white" + className="ml-2" size="sm" onClick={() => { onApply(); diff --git a/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx b/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx index deb58b906..29e9219df 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx +++ b/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx @@ -24,13 +24,13 @@ export default function SelectedFcltList({ }; return ( - <div className="fixed right-40 top-[8rem] w-[40rem] max-h-[130rem] overflow-auto bg-white shadow-lg rounded p-4 border border-gray-300 z-50"> - <h3 className="bg-cyan-700 text-white text-xl font-bold mb-2 p-2"> + <div className="fixed right-40 top-[8rem] z-50 max-h-[130rem] w-[40rem] overflow-auto rounded border border-border bg-popover p-4 text-popover-foreground shadow-lg"> + <h3 className="mb-2 bg-primary p-2 text-xl font-bold text-primary-foreground"> 선택된 시설 </h3> <div className="overflow-x-auto"> - <table className="w-full text-sm text-gray-800 border border-gray-200"> - <thead className="bg-gray-100 text-gray-700 text-[13px]"> + <table className="w-full border border-border text-sm text-foreground"> + <thead className="bg-secondary text-[13px] text-secondary-foreground"> <tr> <th className="px-3 py-2 text-left border-b">시설명</th> <th className="px-3 py-2 text-right border-b"> @@ -60,7 +60,7 @@ export default function SelectedFcltList({ <td className="px-3 py-2 text-center"> <button onClick={() => handleDelete(item.pbnstRuseFcltId)} - className="text-red-500 hover:text-red-700 text-sm font-bold" + className="text-sm font-bold text-destructive hover:opacity-80" aria-label="삭제" title="삭제" > @@ -71,7 +71,7 @@ export default function SelectedFcltList({ )) ) : ( <tr> - <td colSpan={4} className="px-3 py-6 text-center text-[12px] text-gray-500"> + <td colSpan={4} className="px-3 py-6 text-center text-[12px] text-muted-foreground"> 선택된 시설이 없습니다. </td> </tr> diff --git a/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts index 11d556468..7ceaa3892 100644 --- a/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts +++ b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts @@ -46,7 +46,8 @@ export const mapService = { return response.json(); }, getPbnstRuseFclts: async (ruseFcltPk:any) => { - const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}`, { + const year = new Date().getFullYear(); + const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}?year=${year}`, { method: 'GET', headers: { 'Accept': 'application/json', diff --git a/apps/wr/app/(popUp-layout)/util.ts b/apps/wr/app/(popUp-layout)/util.ts index e3428b300..f280aaab9 100644 --- a/apps/wr/app/(popUp-layout)/util.ts +++ b/apps/wr/app/(popUp-layout)/util.ts @@ -2,7 +2,8 @@ import { getFetch, postFectch, fetchType } from "@/components/lib/api"; const fetchpbnstDetail = async (featureCd: string, setTabData: any) => { try { - const response = await getFetch("wr", `/reuseFc/pubFc/${featureCd}`); + const year = new Date().getFullYear(); + const response = await getFetch("wr", `/reuseFc/pubFc/${featureCd}?year=${year}`); // 중복 데이터 추가 방지 setTabDataFunc(response, featureCd, setTabData); } catch (error) { @@ -55,4 +56,4 @@ const detainSttn = async (featureCd: string, layerNm: string) => { } } -export { fetchHasuDetail, fetchpbnstDetail, detainSttn }; \ No newline at end of file +export { fetchHasuDetail, fetchpbnstDetail, detainSttn }; diff --git a/apps/wr/app/(with-layout)/jwttest/page.tsx b/apps/wr/app/(with-layout)/jwttest/page.tsx index 40baa2034..a0d9735a0 100644 --- a/apps/wr/app/(with-layout)/jwttest/page.tsx +++ b/apps/wr/app/(with-layout)/jwttest/page.tsx @@ -26,6 +26,14 @@ interface JwtState { error: string | null; // 쿠키 없음, 파싱 실패 등 오류 메시지 } +const initialJwtState: JwtState = { + token: "", + payload: null, + exp: null, + expDateLocal: null, + remainingMs: null, + error: null, +}; function parseJwt(token: string): JwtPayload { const parts = token.split("."); @@ -112,7 +120,7 @@ function readJwtState(): JwtState { } export default function Page() { - const [jwtState, setJwtState] = useState<JwtState>(() => readJwtState()); + const [jwtState, setJwtState] = useState<JwtState>(initialJwtState); const isRefreshingRef = useRef(false); const handleSilentRefresh = async () => { diff --git a/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx b/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx index bf784b41a..2fba54fb6 100644 --- a/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMat/coMatRvw/page.tsx @@ -14,6 +14,7 @@ import { type DmdCoReqType, type DmdMatTableType, } from "@/app/store/matDmd/coMatRvw/type"; +import { DEFAULT_LIST_SIZE } from "@/src/shared/config"; import { formH1Class } from "@/components/lib/class/comClass"; import { formatPhoneNumber, @@ -46,9 +47,53 @@ import { useEffect, useRef, useState } from "react"; import { useForm, useWatch } from "react-hook-form"; import { z } from "zod"; +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 EMPTY_DETAIL_FORM: Partial<DmdCoReqType> = { + dmdInfoSn: 0, + dmdInfoId: "", + dmndQty: undefined, + wntUntprc: undefined, + useUsgCd: "", + dmndTmng: undefined, + grssClfg: undefined, + combResdch: undefined, + trbt: undefined, + blgcOxygDmndQnt: undefined, + smll: undefined, + crmty: undefined, + tn: undefined, + tph: undefined, + ph: undefined, + clIonQnt: undefined, + cdtnv: undefined, + rmrk: undefined, + ruseUtztnFcltNm: "", + splyCndtList: [], + totalConstCost: 0, + demandCst: { + usgPrcsProcs: "", + cstrnCst: 0, + fcltSbsdCstrnCst: 0, + prcsFcltCpctCstrnCst: 0, + }, + demandMatching: undefined, + demandCo: undefined, +}; export default function CoMatRvw() { - const readOnlyInputClass = "bg-slate-50 text-gray-700"; + const readOnlyInputClass = "bg-secondary text-secondary-foreground"; const { auth, code, coRvw, coMatRvwInit, coMatRvwSearch, coMatRvwInqua } = useWrStore((state) => state); const { coMatRvwRes, coMatRvwCount } = coRvw; @@ -75,6 +120,8 @@ export default function CoMatRvw() { const [toDate, setToDate] = useState<string>( String(new Date().getFullYear()) ); + const [selectedRowIndex, setSelectedRowIndex] = useState<number>(-1); + const [resetSelection, setResetSelection] = useState(false); const [selectData, setSelectData] = useState<DmdCoReqType | null>(null); // selectData를 null로 초기화 const [selectionVersion, setSelectionVersion] = useState(0); const bottomRef = useRef<HTMLDivElement | null>(null); @@ -100,14 +147,14 @@ export default function CoMatRvw() { if (dmdCoNm.length > 0) { coMatRvwSearch( pageNo, - 13, + DEFAULT_LIST_SIZE, String(startYear), String(endYear), sggCd, dmdCoNm ); } else { - coMatRvwInqua(pageNo, 13, String(startYear), String(endYear), sggCd); + coMatRvwInqua(pageNo, DEFAULT_LIST_SIZE, String(startYear), String(endYear), sggCd); } }; @@ -119,11 +166,22 @@ export default function CoMatRvw() { const TableCellClick = (index: number) => { if (coMatRvwRes && coMatRvwRes[index]) { + setSelectedRowIndex(index); + setResetSelection(false); setSelectData(coMatRvwRes[index]); setSelectionVersion((v) => v + 1); // 행을 클릭할 때마다 버전을 1씩 증가 bottomRef.current?.scrollIntoView({ behavior: "smooth" }); // 화면 스크롤 처리 } }; + + const resetDetailSections = () => { + setSelectedRowIndex(-1); + setResetSelection(true); + setSelectData(null); + form.reset(EMPTY_DETAIL_FORM); + setSelectionVersion((v) => v + 1); + }; + const useUsgCdWatch = useWatch({ control: form.control, name: "useUsgCd" }); const useUsgCdText = code.sewageOutFclttUse?.find((item) => item.val === useUsgCdWatch)?.text ?? @@ -166,6 +224,9 @@ export default function CoMatRvw() { }; }); setDmdMtaData(tableData(data, { code })); + } else { + setDmdMtaData([]); + setSelectData(null); } }, [coMatRvwRes]); @@ -185,7 +246,7 @@ export default function CoMatRvw() { useEffect(() => { if (!initRef.current) { - coMatRvwInit(1, 13, fromDate, toDate); + coMatRvwInit(1, DEFAULT_LIST_SIZE, fromDate, toDate); //setMounted(true); initRef.current = true; } @@ -244,7 +305,7 @@ export default function CoMatRvw() { /> <div className="col-span-7 w-full"> <div className="flex flex-row items-center"> - <Label className="text-xl text-gray-600 mx-6"> + <Label className="mx-6 text-xl text-label"> 수요신청년도 </Label> <SelectBox @@ -295,6 +356,7 @@ export default function CoMatRvw() { ); } else { const con = getCurrentRegionCode(); + resetDetailSections(); //setPageNatiion(1); getSearch(1, fromNum, toNum, con, dmdCoNm); } @@ -321,8 +383,10 @@ export default function CoMatRvw() { tableData={dmdMatData || []} headlist={coRvwHeadList} widthFixed={true} - pageSize={13} + pageSize={DEFAULT_LIST_SIZE} total={coMatRvwCount} + resetSelection={resetSelection} + index={selectedRowIndex} cellClick={(index: number | undefined) => { if (index !== undefined && index >= 0) { TableCellClick(index); @@ -330,6 +394,7 @@ export default function CoMatRvw() { }} onPageChange={(val) => { const con = getCurrentRegionCode(); + resetDetailSections(); //setPageNatiion(val); getSearch(val, Number(fromDate), Number(toDate), con, dmdCoNm); }} @@ -377,12 +442,11 @@ export default function CoMatRvw() { <FormUi label="요구수량(m³/일)"> <Input placeholder="" - type="number" - step="0.1" + type="text" onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly className={readOnlyInputClass} /> @@ -416,15 +480,14 @@ export default function CoMatRvw() { control={form.control} name="wntUntprc" render={({ field }) => ( - <FormUi label="희망단가(원/m3)"> + <FormUi label="희망단가(원/m³)"> <Input placeholder="" - type="number" - step="0.1" + type="text" onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly className={readOnlyInputClass} /> @@ -440,7 +503,7 @@ export default function CoMatRvw() { control={form.control} name="grssClfg" render={({ field }) => ( - <FormUi label={"총대장균군수(개/mg)"}> + <FormUi label={"총대장균군수(개/100㎖)"}> <div className="flex items-center gap-2"> <div className="relative flex-1 min-w-0"> <Input @@ -448,15 +511,18 @@ export default function CoMatRvw() { className={`w-full pr-16 border py-2 text-right rounded-md ${readOnlyInputClass}`} placeholder="" value={ - field.value != -1 ||field.value != undefined - ? `${field.value}` - : "불검출" + field.value === null || + field.value === undefined + ? "" + : field.value === -1 + ? "불검출" + : formatNumberWithComma(field.value) } readOnly /> {/* TODO: 총대장균군수 확인 필요*/} {field.value != -1 ? ( - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> ) : ( @@ -480,10 +546,10 @@ export default function CoMatRvw() { type="text" className={`w-full pr-16 border py-2 text-right rounded-md ${readOnlyInputClass}`} placeholder="" - value={field.value ?? ""} + value={formatNumberWithComma(field.value)} readOnly /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이상 </span> </div> @@ -504,10 +570,10 @@ export default function CoMatRvw() { placeholder="" className={`w-full pr-16 border py-2 text-right rounded-md ${readOnlyInputClass}`} type="text" - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly /> - <span className=" text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -532,10 +598,10 @@ export default function CoMatRvw() { onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? ""} + value={formatNumberWithComma(field.value)} readOnly /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -574,10 +640,10 @@ export default function CoMatRvw() { className={`w-full pr-16 border py-2 text-right rounded-md ${readOnlyInputClass}`} type="text" step="0.1" - value={field.value ?? ""} + value={formatNumberWithComma(field.value)} readOnly /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -601,10 +667,10 @@ export default function CoMatRvw() { onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -628,10 +694,10 @@ export default function CoMatRvw() { onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -655,7 +721,7 @@ export default function CoMatRvw() { onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly /> </div> @@ -679,10 +745,10 @@ export default function CoMatRvw() { onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -701,15 +767,15 @@ export default function CoMatRvw() { <div className="relative flex-1 min-w-0"> <Input placeholder="" - className="w-full pr-16 border py-2 text-right rounded-md" + className={`w-full pr-16 border py-2 text-right rounded-md ${readOnlyInputClass}`} type="text" onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={formatNumberWithComma(field.value)} readOnly /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> diff --git a/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx b/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx index 9fc636427..b8ca96ca1 100644 --- a/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx @@ -347,7 +347,7 @@ export default function RuseCaseMng() { }} resetPageNo={resetPage} /> - <span className="text-2xl font-bold text-gray-500"> + <span className="text-2xl font-bold text-muted-foreground"> ☞ 본 사례의 사업비는 재원협의 단계의 정보이므로, 최종확정된 비용이 아닙니다. </span> </div> diff --git a/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx b/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx index b142bff16..83f6fbe69 100644 --- a/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMat/ruseRvwHstry/page.tsx @@ -250,7 +250,7 @@ export default function RuseRvwHstry() { </SelectBox> <div className="col-span-4 w-full"> <div className="flex flex-row items-center"> - <Label className="text-xl text-gray-600 ml-4 mr-4"> + <Label className="ml-4 mr-4 text-xl text-label"> 수요신청년도 </Label> <div className="flex flex-row mt-2"> diff --git a/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx index 71ec60cf0..894a27c71 100644 --- a/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMng/coSttn/page.tsx @@ -363,7 +363,7 @@ export default function CoSttn() { getDemandCompList(searchText, pageCnt); }} rtnInputData={handleRtnInputData} - bodyClassNm={"w-full mx-auto bg-white rounded-lg shadow-md p-6"} + bodyClassNm={"w-full mx-auto bg-background rounded-lg shadow-md p-6"} updateList={updateData} openNetwork={true} ></SdmInsertForm> diff --git a/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx index 1aafb4318..00ce07183 100644 --- a/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx @@ -171,7 +171,7 @@ export default function DrgtSttn() { /> <div className="col-span-5 w-auto"> <div className="flex flex-row items-center"> - <Label className="text-xl text-gray-600 ml-8">조회기간</Label> + <Label className="ml-8 text-xl text-label">조회기간</Label> <div className="flex flex-row w-1/4"> <DateMonthPicker field={{ diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/htdflReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/htdflReg/page.tsx index cc892b6d1..84f6ad87e 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/htdflReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/htdflReg/page.tsx @@ -154,6 +154,7 @@ export default function Home() { onPageChange={setPageNation} index={htdfTableIndex} resetPageNo={resetPageNo} + widthFixed={true} ></UiTable> </ScrollArea> </ListDiv> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx index 75d718411..a93575f33 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx @@ -31,7 +31,6 @@ import { btnClass } from "@/app/store/common/code"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components"; import { useCallback, useEffect, useRef, useState } from "react"; import ListDiv from "@common/business_components/ui/list_div"; -import { tableData } from "@/components/lib/func/common"; import type { customPbnst, fcBizType, @@ -56,7 +55,6 @@ import { useAlertStore } from "@/src/shared/lib"; export default function PublicReuseFaciltyInsert() { const { auth, - code, pbnst, pubHasuInit, pbnstInsert, @@ -112,11 +110,11 @@ export default function PublicReuseFaciltyInsert() { useInitRef.current = true; }, []); - useEffect(() => { - if (useInitRef.current == true && laeSeCd !== undefined) { - pubHasuInit("1", laeSeCd); //page: string, sggOrCity: string - } - }, [useInitRef.current, laeSeCd]); + // useEffect(() => { + // if (useInitRef.current == true && laeSeCd !== undefined) { + // pubHasuInit(1, laeSeCd); + // } + // }, [useInitRef.current, laeSeCd]); const onSubmit = async (valuse: { items: PbnstRegArrrayType }) => { const data = writeRgtrAndMdfr(valuse.items); @@ -128,6 +126,13 @@ export default function PublicReuseFaciltyInsert() { const pbnst = data.map((fclt) => { return { pbnstRuseFcltId: fclt.pbnstRuseFcltId, + swtrmtFcltId: fclt.swtrmtFcltId, + swageTrtmfctId: fclt.swageTrtmfctId, + swageBscFctNm: fclt.swageBscFctNm, + swageRgnCd: fclt.swageRgnCd, + swageAdstDstrCd: fclt.swageAdstDstrCd, + swageRvbsnCd: fclt.swageRvbsnCd, + swageFctCpc: fclt.swageFctCpc, fcltCpct: fclt.fcltCpct, ruseCpct: fclt.ruseCpct, splyExtraCpct: fclt.splyExtraCpct, @@ -156,7 +161,7 @@ export default function PublicReuseFaciltyInsert() { message += "하수재이용 시설 사업 "; } alert(message + "오류 입니다 "); - pubHasuInit("1", laeSeCd); + pubHasuInit(1, laeSeCd); } }; if (await confirm(t("isUpdatetMsg"))) { @@ -233,10 +238,12 @@ export default function PublicReuseFaciltyInsert() { }, [items]); useEffect(() => { - if (code.sggCd.length && pbnstHasuRes.length > 0) { - setbdstTable(tableData(pbnstHasuRes, { code })); + if (pbnstHasuRes.length > 0) { + setbdstTable(pbnstHasuRes); + } else { + setbdstTable([]); } - }, [code, pbnstHasuRes]); + }, [pbnstHasuRes]); const cellClick = (val: number | undefined) => { if (val === undefined) return; @@ -288,7 +295,8 @@ export default function PublicReuseFaciltyInsert() { }, [pbnstRes, append, selectRow]); useEffect(() => { - pubHasuInit(String(pageNation), laeSeCd); // 해당 지자체의 하수처리시설 조회 + if (laeSeCd === undefined) return; + pubHasuInit(pageNation, laeSeCd); // 해당 지자체의 하수처리시설 조회 isResetPage(false); setSelectRow(undefined); setSelectedRowIndex(-1); @@ -316,9 +324,9 @@ export default function PublicReuseFaciltyInsert() { // size={"sm"} onClick={() => { if (searchTxt && searchTxt?.length > 0) { - pubHasuSearch(String(pageNation), laeSeCd, searchTxt); + pubHasuSearch(pageNation, laeSeCd, searchTxt); } else { - pubHasuInit(String(pageNation), laeSeCd); + pubHasuInit(pageNation, laeSeCd); } isResetPage(true); }} @@ -368,6 +376,7 @@ export default function PublicReuseFaciltyInsert() { resetSelection={resetSelection} index={selectedRowIndex} curPage={pageNation} + widthFixed={true} /> </ScrollArea> </ListDiv> @@ -423,7 +432,7 @@ export default function PublicReuseFaciltyInsert() { <div className="card-header text-2xl"> 등록된 재이용시설 정보가 없습니다. </div> - <div className="!mx-[1.6rem] !p-0r flex flex-col items-center justify-center h-64 border rounded-md"> + <div className="!mx-[1.6rem] !p-0 flex flex-col items-center justify-center h-64 border rounded-md"> <p className="text-3xl mb-4"> 신규등록 버튼을 클릭하여 재이용시설을 등록해주세요. </p> @@ -454,6 +463,12 @@ export default function PublicReuseFaciltyInsert() { void (async () => { const message = await pbnstInsert(data); alert(message); + + if (searchTxt && searchTxt.trim().length > 0) { + await pubHasuSearch(pageNation, laeSeCd, searchTxt); + } else { + await pubHasuInit(pageNation, laeSeCd); + } })(); setIsRuseReg({ isModal: false }); } diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/utilsZod.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/utilsZod.tsx index f61697e7e..3f64e42c6 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/utilsZod.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/utilsZod.tsx @@ -5,7 +5,13 @@ export function createDefaultPbnstItem(row: pubHasuType, index: number) { return JSON.parse(JSON.stringify({ pbnstRuseFcltId: pk, - swtrmtFcltId: "", + swtrmtFcltId: row.bscFctCd ||"", + swageTrtmfctId: row.bscFctCd || "", + swageBscFctNm: row.bscFctNm || "", + swageRgnCd: row.rgnCd || "", + swageAdstDstrCd: row.adstDstrCd || "", + swageRvbsnCd: row.rvbsnCd || "", + swageFctCpc: Number(row.fctCpc) || 0, ruseUtztnFcltNm: "", fcltCpct: 0, ruseCpct: 0, @@ -44,4 +50,4 @@ export function createDefaultPbnstItem(row: pubHasuType, index: number) { cycl, // 필요한 값 있으면 여기에 추가 } as fcBizType; - } \ No newline at end of file + } diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rainReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rainReg/page.tsx index 45fb636c6..cdbee61c3 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rainReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rainReg/page.tsx @@ -167,6 +167,7 @@ export default function Home() { onPageChange={setPageNation} index={rainTableIndex} resetPageNo={resetPageNo} + widthFixed={true} ></UiTable> </ScrollArea> </ListDiv> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rswtReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rswtReg/page.tsx index a4fa9b97b..b8e7e7727 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rswtReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/rswtReg/page.tsx @@ -171,6 +171,7 @@ export default function Home() { onPageChange={setPageNation} index={rswtTableIndex} resetPageNo={resetPageNo} + widthFixed={true} ></UiTable> </ScrollArea> </ListDiv> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx index 5705faabc..99f2645cf 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx @@ -40,11 +40,10 @@ import { useTranslation } from "react-i18next"; const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }]; export default function HtdflSttn() { - const { auth, code, htdflSttn, htdflSttnSearch, initHtdfl, getLaeSeCdCheck } = useWrStore((state) => state); + const { auth, code, htdflSttn, htdflSttnSearch, getLaeSeCdCheck } = useWrStore((state) => state); const { laeSeCd } = auth; // 지자체구분코드 const { htdflSttnRes, htdflSttnResCount } = htdflSttn; - const isDownloadTriggered = useRef(false); - const skipNextSearchRef = useRef(true); + const initializedSearchKeyRef = useRef<string | null>(null); const { t } = useTranslation('base'); const [pageNation, setPageNation] = useState(1); const [isChHead, setIsChHead] = useState(false); @@ -61,6 +60,10 @@ export default function HtdflSttn() { // chBuilding: chgBuilding[]; // }>({ isOpen: false, chBuilding: [] }); const [fcltNm, setFcltNm] = useState<string>(""); + const [appliedSearch, setAppliedSearch] = useState<{ + conValue: string; + fcltNm: string; + } | null>(null); const alert = useAlertStore((state) => state.setMessage); const isAllAreaUser = laeSeCd === "0"; const isLaeSeCdReady = @@ -89,7 +92,7 @@ export default function HtdflSttn() { setObjSearch({ ...objSearch, sgg: String(val) }) }; - const getConValue = (): string => { + const getConValue = useCallback((): string => { if (!objSearch.city || objSearch.city === "0") { return "0"; } @@ -97,7 +100,7 @@ export default function HtdflSttn() { return String(objSearch.sgg); } return String(objSearch.city); - }; + }, [objSearch]); const getFixedConValue = useCallback((): string => { if (laeSeCd === undefined || laeSeCd === "0") { @@ -105,7 +108,7 @@ export default function HtdflSttn() { } return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2); - }, [laeSeCd, objSearch]); + }, [laeSeCd, getConValue]); const cellClick = (index: number | undefined, headId: string) => { if ( @@ -117,14 +120,39 @@ export default function HtdflSttn() { } }; - const excelClick = useCallback(() => { - if (!isLaeSeCdReady) { + const excelClick = useCallback(async () => { + if (!isLaeSeCdReady || !appliedSearch) { return; } - isDownloadTriggered.current = true; - htdflSttnSearch(true, 1, htdflSttnResCount, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd); - }, [htdflSttnSearch, fcltNm, htdflSttnResCount, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]); + const excelData = await htdflSttnSearch( + true, + 1, + htdflSttnResCount, + appliedSearch.conValue, + appliedSearch.fcltNm, + code.cityCd, + code.sggCd + ); + if (excelData && excelData.length > 0) { + const sheetArray: SheetParameter[] = [{ + data: excelData, + headerNum: 3, + headerRange: "A1:R3", + sheetName: "sheet1", + startCol: "A", + decimalArray0: [6, 11, 13, 15], + }]; + excelDownloadNumStyle( + "온배수시설 현황", + "sample_htdflSttn.xlsx", + sheetArray + ); + return; + } + + alert(t("excelDownErrMsg")); + }, [htdflSttnSearch, htdflSttnResCount, appliedSearch, code.cityCd, code.sggCd, isLaeSeCdReady, alert, t]); useEffect(() => { getLaeSeCdCheck(); @@ -150,52 +178,39 @@ export default function HtdflSttn() { }); }, [laeSeCd, code.sggCd, isLaeSeCdReady]); - useEffect(() => { - if (!isDownloadTriggered.current) { - return; - } - if (htdflSttn.excelList.length > 0) { - const sheetArray: SheetParameter[] = [{ - data: htdflSttn.excelList, - headerNum: 3, - headerRange: "A1:R3", - sheetName: 'sheet1', - startCol: 'A', - decimalArray0: [6,11,13,15], // 0부터 시작 - }]; - excelDownloadNumStyle( - "온배수시설 현황", - "sample_htdflSttn.xlsx", - sheetArray - ); - } else { - alert(t('excelDownErrMsg')); - } - isDownloadTriggered.current = false; - }, [htdflSttn.excelList, code]); - - useEffect(() => { - if (!isLaeSeCdReady || htdflSttn.isInit) { - return; - } - - skipNextSearchRef.current = true; - initHtdfl(false, 1, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd); - }, [htdflSttn.isInit, initHtdfl, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]); - useEffect(() => { if (!isLaeSeCdReady) { return; } - if (skipNextSearchRef.current) { - skipNextSearchRef.current = false; + const initialSearchKey = laeSeCd ?? "0"; + if (initializedSearchKeyRef.current === initialSearchKey) { return; } - htdflSttnSearch(false, pageNation, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd); + const initialConValue = + laeSeCd === undefined || laeSeCd === "0" + ? "0" + : laeSeCd.length === 5 + ? laeSeCd + : laeSeCd.slice(0, 2); + + initializedSearchKeyRef.current = initialSearchKey; + setAppliedSearch({ + conValue: initialConValue, + fcltNm: "", + }); + setPageNation(1); + }, [isLaeSeCdReady, laeSeCd]); + + useEffect(() => { + if (!isLaeSeCdReady || !appliedSearch) { + return; + } + + htdflSttnSearch(false, pageNation, htdflSttnSize, appliedSearch.conValue, appliedSearch.fcltNm, code.cityCd, code.sggCd); isResetPage(false) - }, [pageNation, htdflSttnSearch, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]); + }, [pageNation, htdflSttnSearch, appliedSearch, code.cityCd, code.sggCd, isLaeSeCdReady]); useEffect(() => { const data = tableData(htdflSttnRes, { code }); @@ -215,6 +230,19 @@ export default function HtdflSttn() { setBdstTable(chData); }, [code, htdflSttnRes, htdflSttn.htdflSttHeadList]); + const handleSearch = useCallback(() => { + if (!isLaeSeCdReady) { + return; + } + + setAppliedSearch({ + conValue: String(getFixedConValue()), + fcltNm, + }); + isResetPage(true); + setPageNation(1); + }, [fcltNm, getFixedConValue, isLaeSeCdReady]); + return ( <> <ContentsDiv> @@ -266,13 +294,7 @@ export default function HtdflSttn() { <div className="flex flex-row space-x-2 items-center"> <Button className={btnClass} - onClick={() => { - if (!isLaeSeCdReady) { - return; - } - htdflSttnSearch(false, 1,htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd); - isResetPage(true) - }} + onClick={handleSearch} > 조회 </Button> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx index a481f66a1..cb03bd0ae 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx @@ -1,12 +1,17 @@ -/** - * 하수재이용 시설 현황 - * 재이용시설 현황 - 하수재이용시설 현황 +/** + * 하수재이용시설 현황 + * 재이용시설 현황 > 하수재이용시설 현황 + * + * 사용 API + * 하수재이용시설 현황 목록 조회 reuse-service /reuseFc/pbnstList + * 하수처리시설 기본정보 조회 operation-service /prcsFclt/ruseInfo + * 상세조회 모달 전달을 위한 하수재이용시설 상세 조회 reuse-service /reuseFc/pubFc/{pbnstRuseFcltId} + * * - layout.tsx에 의해 공통 레이아웃이 적용됨 * - with-layout 적용 - * 하수처리시설. 하수재이용시설 , 하수재이용시설 실적에 대한 정보를 확인한다. - * 모달 : 테이블의 헤더를 원하는대로 조절해서 볼수 있다. - * 클릭시 해당 재이용ㅇ - * */ + * - 하수처리시설, 하수재이용시설, 하수재이용시설 실적에 대한 정보를 확인한다. + * - 모달에서 테이블 헤더를 원하는대로 조절해서 볼 수 있다. + */ "use client"; import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; @@ -39,18 +44,15 @@ export default function PbnstSttn() { auth, code, pbnstSttn, - pbnstSttnSearch, - pbnSttnInit, + pbnstSttnList, getPbnStnDetail, - getFlcts, // 하수재이용시설(tb_wr_pbnst_ruse_fclt) 조회 resetPbnstDetail, getLaeSeCdCheck, } = useWrStore((state) => state); - const { pbnstSttnRes, fclts, pbnstSttnResCount } = pbnstSttn; + const { pbnstSttnRes, pbnstSttnResCount } = pbnstSttn; const { laeSeCd } = auth; // 지자체구분코드 - const [isReset, setIsReset] = useState(false); const ref = useRef<ReturnType>({} as ReturnType); const [searchYear, setSearchYear] = useState<string>( String(new Date().getFullYear()) @@ -66,7 +68,7 @@ export default function PbnstSttn() { const [pbnRuseNm, setPbnRuseNm] = useState<string>(""); const { t } = useTranslation("base"); const alert = useAlertStore((state) => state.setMessage); - const isAllAreaUser = laeSeCd === "0"; // 모든 지자체 권한이 있는 경우 + const isAllAreaUser = laeSeCd?.length != 5; // 모든 지자체 권한이 있는 경우 const fixedSidoValue = !isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : ""; const fixedSigunValue = @@ -110,25 +112,24 @@ export default function PbnstSttn() { const excelClick = useCallback(() => { isDownloadTriggered.current = true; const fixedSearchValue = getFixedSearchValue(ref.current); - pbnstSttnSearch( + pbnstSttnList( true, String(1), "100000", fixedSearchValue, pbnRuseNm, - searchYear, - fclts + searchYear ); - }, [pbnstSttnSearch, fclts, pbnRuseNm, searchYear, getFixedSearchValue]); + }, [pbnstSttnList, pbnRuseNm, searchYear, getFixedSearchValue]); const cellCLick = useCallback( (index: number | undefined) => { if (index !== undefined && pbnstSttnRes && pbnstSttnRes[index]) { const pbnstRuseFcltId = pbnstSttnRes[index].pbnstRuseFcltId; - getPbnStnDetail(pbnstRuseFcltId); + getPbnStnDetail(pbnstRuseFcltId, searchYear); } }, - [getPbnStnDetail, pbnstSttnRes] + [getPbnStnDetail, pbnstSttnRes, searchYear] ); useEffect(() => { @@ -137,16 +138,28 @@ export default function PbnstSttn() { }, []); useEffect(() => { - if (!pbnstSttn.isInit) { - getFlcts(); // 하수재이용시설 조회 + if ( + useInitRef.current === true && + laeSeCd !== undefined && + !pbnstSttn.isTableLoaded + ) { + pbnstSttnList( + false, + "1", + String(DEFAULT_LIST_SIZE), + getFixedSearchValue(ref.current), + pbnRuseNm, + searchYear + ); } - }, [pbnstSttn.isInit]); - - useEffect(() => { - if (useInitRef.current === true && laeSeCd !== undefined && fclts.length > 0) { - pbnSttnInit("1", String(DEFAULT_LIST_SIZE), getFixedSearchValue(ref.current), searchYear, fclts); - } - }, [fclts, laeSeCd, searchYear, pbnSttnInit, getFixedSearchValue]); + }, [ + laeSeCd, + pbnstSttn.isTableLoaded, + pbnRuseNm, + searchYear, + pbnstSttnList, + getFixedSearchValue, + ]); useEffect(() => { if (pbnstSttnRes && pbnstSttnRes.length > 0) { @@ -200,11 +213,9 @@ export default function PbnstSttn() { : code.cityCd } selectSigunData={ - !isAllAreaUser && laeSeCd?.length === 5 + laeSeCd?.length === 5 ? code.sggCd.filter((item) => item.val === laeSeCd) - : !isAllAreaUser && laeSeCd?.length === 2 - ? code.sggCd.filter((item) => item.val.slice(0, 2) === laeSeCd) - : code.sggCd + : code.sggCd } selectRegionData={code.rvbsnCd} selectUnitWaterShedData={code.lgbsnCd} @@ -236,7 +247,7 @@ export default function PbnstSttn() { setSearchYear(String(val)) } /> - <Label className="w-full ml-4 text-xl text-gray-600"> + <Label className="ml-4 w-full text-xl text-label"> {"하수처리시설명"} </Label> <SearchInput @@ -251,16 +262,14 @@ export default function PbnstSttn() { } queryClick={() => { const fixedSearchValue = getFixedSearchValue(ref.current); - pbnstSttnSearch( + pbnstSttnList( false, "1", String(DEFAULT_LIST_SIZE), fixedSearchValue, pbnRuseNm, - searchYear, - fclts + searchYear ); - setIsReset(true); }} excelClick={excelClick} initClick={function (): void { @@ -290,18 +299,15 @@ export default function PbnstSttn() { }} onPageChange={(page: number) => { const fixedSearchValue = getFixedSearchValue(ref.current); - pbnstSttnSearch( + pbnstSttnList( false, String(page), String(DEFAULT_LIST_SIZE), fixedSearchValue, pbnRuseNm, - searchYear, - fclts + searchYear ); - setIsReset(false); }} - resetPageNo={isReset} widthFixed={true} > <HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} /> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx index c1e9fb141..fae118ae3 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx @@ -1,5 +1,5 @@ /** - * 빗물이용시설 현황 + * 재이용시설 현황 > 빗물이용시설 현황 */ "use client"; import { useWrStore } from "@/app/store"; @@ -38,7 +38,7 @@ import { useTranslation } from "react-i18next"; const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }]; export default function RainSttn() { - const { auth, code, rainSttn, rainSttnSearch, rainSttnInit, getLaeSeCdCheck } = useWrStore( + const { auth, code, rainSttn, rainSttnSearch, getLaeSeCdCheck } = useWrStore( (state) => state ); const { rainSttnRes, rainSttnResCount } = rainSttn; @@ -49,7 +49,6 @@ export default function RainSttn() { const [bdstTable, setBdstTable] = useState<customRainBdst[]>(); const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions); const [resetPage, isResetPage] = useState(false); - const [isDownloadTriggered, setIsDownloadTriggered] = useState(false); const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "", @@ -60,9 +59,13 @@ export default function RainSttn() { }>({ data: undefined, isOpen: false }); const [fcltNm, setFcltNm] = useState<string>(""); + const [appliedSearch, setAppliedSearch] = useState<{ + conValue: string; + fcltNm: string; + } | null>(null); const { t } = useTranslation("base"); const alert = useAlertStore((state) => state.setMessage); - const skipNextSearchRef = useRef(true); + const initializedSearchKeyRef = useRef<string | null>(null); const isAllAreaUser = laeSeCd === "0"; const isLaeSeCdReady = laeSeCd === "0" || laeSeCd.length === 2 || laeSeCd.length === 5; @@ -95,7 +98,7 @@ export default function RainSttn() { setObjSearch({ ...objSearch, sgg: String(val) }); }; - const getConValue = (): string => { + const getConValue = useCallback((): string => { if (!objSearch.city || objSearch.city === "0") { return "0"; } @@ -103,7 +106,7 @@ export default function RainSttn() { return String(objSearch.sgg); } return String(objSearch.city); - }; + }, [objSearch]); const getFixedConValue = useCallback((): string => { if (laeSeCd === undefined || laeSeCd === "0") { @@ -111,7 +114,7 @@ export default function RainSttn() { } return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2); - }, [laeSeCd, objSearch]); + }, [laeSeCd, getConValue]); useEffect(() => { getLaeSeCdCheck(); @@ -137,29 +140,33 @@ export default function RainSttn() { }); }, [laeSeCd, code.sggCd, isLaeSeCdReady]); - useEffect(() => { - if (!isLaeSeCdReady || rainSttn.isInit) { - return; - } - - skipNextSearchRef.current = true; - rainSttnInit( - false, - 1, - rainSttnSize, - getFixedConValue(), - fcltNm, - code as unknown as Record<string, SelectDataType[]> - ); - }, [rainSttn.isInit, rainSttnInit, fcltNm, code, getFixedConValue, isLaeSeCdReady]); - useEffect(() => { if (!isLaeSeCdReady) { return; } - if (skipNextSearchRef.current) { - skipNextSearchRef.current = false; + const initialSearchKey = laeSeCd ?? "0"; + if (initializedSearchKeyRef.current === initialSearchKey) { + return; + } + + const initialConValue = + laeSeCd === undefined || laeSeCd === "0" + ? "0" + : laeSeCd.length === 5 + ? laeSeCd + : laeSeCd.slice(0, 2); + + initializedSearchKeyRef.current = initialSearchKey; + setAppliedSearch({ + conValue: initialConValue, + fcltNm: "", + }); + setPageNation(1); + }, [isLaeSeCdReady, laeSeCd]); + + useEffect(() => { + if (!isLaeSeCdReady || !appliedSearch) { return; } @@ -167,64 +174,75 @@ export default function RainSttn() { false, pageNation, rainSttnSize, - String(getFixedConValue()), - fcltNm, + appliedSearch.conValue, + appliedSearch.fcltNm, code as unknown as Record<string, SelectDataType[]> ); isResetPage(false); - }, [pageNation, rainSttnSearch, fcltNm, code, getFixedConValue, isLaeSeCdReady]); + }, [ + pageNation, + rainSttnSearch, + code, + appliedSearch, + isLaeSeCdReady, + ]); - useEffect(() => { - if (!isLaeSeCdReady) { + const excelClick = useCallback(async () => { + if (!isLaeSeCdReady || !appliedSearch) { return; } - if (!isDownloadTriggered) { - return; - } - if (rainSttn.excelList.length > 0) { - const sheetArray: SheetParameter[] = [{ - data: rainSttn.excelList, - headerNum: 3, - headerRange: "A1:W3", - sheetName: 'sheet1', - startCol: 'A', - decimalArray0: [6,10,12,14,16] // 0부터 시작 - }]; - excelDownloadNumStyle( - "빗물이용시설 현황", - "sample_rainSttn.xlsx", // public 폴더에 있는 template 파일 경로 - sheetArray - ); - } else { - alert(t("excelDownErrMsg")); - } - setIsDownloadTriggered(false); - }, [rainSttn.excelList, isDownloadTriggered, alert, t, isLaeSeCdReady]); - - const excelClick = useCallback(() => { - if (!isLaeSeCdReady) { - return; - } - - setIsDownloadTriggered(true); - rainSttnSearch( + const excelData = await rainSttnSearch( true, 1, rainSttnResCount, - String(getFixedConValue()), - fcltNm, + appliedSearch.conValue, + appliedSearch.fcltNm, code as unknown as Record<string, SelectDataType[]> ); + if (excelData && excelData.length > 0) { + const sheetArray: SheetParameter[] = [{ + data: excelData, + headerNum: 3, + headerRange: "A1:W3", + sheetName: "sheet1", + startCol: "A", + decimalArray0: [6, 10, 12, 14, 16], + }]; + excelDownloadNumStyle( + "빗물이용시설 현황", + "sample_rainSttn.xlsx", + sheetArray + ); + return; + } + + alert(t("excelDownErrMsg")); }, [ rainSttnSearch, - fcltNm, rainSttnResCount, code, - getFixedConValue, + appliedSearch, isLaeSeCdReady, + alert, + t, ]); + const handleSearch = useCallback(() => { + if (!isLaeSeCdReady) { + return; + } + + const nextSearch = { + conValue: String(getFixedConValue()), + fcltNm, + }; + + setAppliedSearch(nextSearch); + isResetPage(true); + setPageNation(1); + }, [fcltNm, getFixedConValue, isLaeSeCdReady]); + useEffect(() => { const data = tableData(rainSttnRes, { code }); const chData = data @@ -318,17 +336,7 @@ export default function RainSttn() { <div className="flex flex-row space-x-2 items-center"> <Button className={btnClass} - onClick={() => { - rainSttnSearch( - false, - 1, - rainSttnSize, - String(getFixedConValue()), - fcltNm, - code as unknown as Record<string, SelectDataType[]> - ); - isResetPage(true); - }} + onClick={handleSearch} > 조회 </Button> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx index da953bb43..ab5092f47 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx @@ -38,7 +38,7 @@ import { useTranslation } from "react-i18next"; const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }]; export default function RswtSttn() { - const { auth, code, rswtSttn, rswtSttnSearch, rswtSttnInit, getLaeSeCdCheck } = useWrStore( + const { auth, code, rswtSttn, rswtSttnSearch, getLaeSeCdCheck } = useWrStore( (state) => state ); @@ -49,12 +49,15 @@ export default function RswtSttn() { const [isChHead, setIsChHead] = useState(false); const [resetPage, isResetPage] = useState(false); const [bdstTable, setBdstTable] = useState<customRswtBdst[]>(); - const isDownloadTriggered = useRef(false); - const skipNextSearchRef = useRef(true); + const initializedSearchKeyRef = useRef<string | null>(null); const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions) const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" }); const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false }) const [fcltNm, setFcltNm] = useState<string>(""); + const [appliedSearch, setAppliedSearch] = useState<{ + conValue: string; + fcltNm: string; + } | null>(null); const { t } = useTranslation('base'); const alert = useAlertStore((state) => state.setMessage); const isAllAreaUser = laeSeCd === "0"; @@ -90,7 +93,7 @@ export default function RswtSttn() { setObjSearch({ ...objSearch, sgg: String(val) }) }; - const getConValue = (): string => { + const getConValue = useCallback((): string => { if (!objSearch.city || objSearch.city === "0") { return "0"; } @@ -98,7 +101,7 @@ export default function RswtSttn() { return String(objSearch.sgg); } return String(objSearch.city); - }; + }, [objSearch]); const getFixedConValue = useCallback((): string => { if (laeSeCd === undefined || laeSeCd === "0") { @@ -106,7 +109,7 @@ export default function RswtSttn() { } return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2); - }, [laeSeCd, objSearch]); + }, [laeSeCd, getConValue]); useEffect(() => { getLaeSeCdCheck(); @@ -133,20 +136,29 @@ export default function RswtSttn() { }, [laeSeCd, code.sggCd, isLaeSeCdReady]); useEffect(() => { - if (!isLaeSeCdReady || rswtSttn.isInit) { + if (!isLaeSeCdReady) { return; } - skipNextSearchRef.current = true; - rswtSttnInit( - false, - 1, - rswtSttnSize, - getFixedConValue(), - fcltNm, - code as unknown as Record<string, SelectDataType[]> - ); - }, [rswtSttn.isInit, rswtSttnInit, fcltNm, code, getFixedConValue, isLaeSeCdReady]); + const initialSearchKey = laeSeCd ?? "0"; + if (initializedSearchKeyRef.current === initialSearchKey) { + return; + } + + const initialConValue = + laeSeCd === undefined || laeSeCd === "0" + ? "0" + : laeSeCd.length === 5 + ? laeSeCd + : laeSeCd.slice(0, 2); + + initializedSearchKeyRef.current = initialSearchKey; + setAppliedSearch({ + conValue: initialConValue, + fcltNm: "", + }); + setPageNation(1); + }, [isLaeSeCdReady, laeSeCd]); useEffect(() => { const data = tableData(rswtSttnRes, { code }); @@ -178,52 +190,62 @@ export default function RswtSttn() { setBdstTable(chData); }, [code, rswtSttnRes]); - useEffect(() => { - if (!isDownloadTriggered.current) { + const excelClick = useCallback(async () => { + if (!isLaeSeCdReady || !appliedSearch) { return; } - if (rswtSttn.excelList.length > 0) { + + const excelData = await rswtSttnSearch( + true, + 1, + rswtSttnResCount, + appliedSearch.conValue, + appliedSearch.fcltNm, + code as unknown as Record<string, SelectDataType[]> + ); + if (excelData && excelData.length > 0) { const sheetArray: SheetParameter[] = [{ - data: rswtSttn.excelList, + data: excelData, headerNum: 3, headerRange: "A1:W3", - sheetName: 'sheet1', - startCol: 'A', - decimalArray0: [6,10,12,16], // 0부터 시작 + sheetName: "sheet1", + startCol: "A", + decimalArray0: [6, 10, 12, 16], }]; excelDownloadNumStyle( "중수도시설 현황", "sample_rswtSttn.xlsx", sheetArray ); - } else { - alert(t('excelDownErrMsg')); - } - isDownloadTriggered.current = false; - }, [rswtSttn.excelList]); - - const excelClick = useCallback(() => { - if (!isLaeSeCdReady) { return; } - isDownloadTriggered.current = true; - rswtSttnSearch(true, 1, rswtSttnResCount, String(getFixedConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>); - }, [rswtSttnSearch, fcltNm, rswtSttnResCount, code, getFixedConValue, isLaeSeCdReady]); + alert(t("excelDownErrMsg")); + }, [rswtSttnSearch, rswtSttnResCount, code, appliedSearch, isLaeSeCdReady, alert, t]); useEffect(() => { + if (!isLaeSeCdReady || !appliedSearch) { + return; + } + + rswtSttnSearch(false, pageNation, rswtSttnSize, appliedSearch.conValue, appliedSearch.fcltNm, code as unknown as Record<string, SelectDataType[]>); + isResetPage(false) + }, [pageNation, rswtSttnSearch, code, appliedSearch, isLaeSeCdReady]); + + const handleSearch = useCallback(() => { if (!isLaeSeCdReady) { return; } - if (skipNextSearchRef.current) { - skipNextSearchRef.current = false; - return; - } + const nextSearch = { + conValue: String(getFixedConValue()), + fcltNm, + }; - rswtSttnSearch(false, pageNation, rswtSttnSize, String(getFixedConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>); - isResetPage(false) - }, [pageNation, rswtSttnSearch, fcltNm, code, getFixedConValue, isLaeSeCdReady]); + setAppliedSearch(nextSearch); + isResetPage(true); + setPageNation(1); + }, [fcltNm, getFixedConValue, isLaeSeCdReady]); return ( <> @@ -279,13 +301,7 @@ export default function RswtSttn() { <div className="flex flex-row space-x-2 items-center"> <Button className={btnClass} - onClick={() => { - if (!isLaeSeCdReady) { - return; - } - rswtSttnSearch(false, 1, rswtSttnSize, String(getFixedConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>); - isResetPage(true) - }} + onClick={handleSearch} > 조회 </Button> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx index 72069f4d8..9fbcf716c 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/TableHead.tsx @@ -14,7 +14,7 @@ export const headMakeprfmnc = ( .map((head: TableUpperProps) => !head.upSequnce ? ( <TableHead - className={`border border-gray-600 px-2 py-1 `} + className={`border border-border px-2 py-1 `} rowSpan={2} key={head.id} style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }} @@ -23,7 +23,7 @@ export const headMakeprfmnc = ( </TableHead> ) : head.upSequnce === 1 ? ( <TableHead - className={`border border-gray-600 px-2 py-1 `} + className={`border border-border px-2 py-1 `} key={head.id} rowSpan={1} colSpan={ @@ -43,7 +43,7 @@ export const headMakeprfmnc = ( .map((head: TableUpperProps) => makeHeadNm?.find((ch) => ch.headNm === head.upName) ? ( <TableHead - className={`border border-gray-600 px-2 py-1 `} + className={`border border-border px-2 py-1 `} key={head.id} style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }} > diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx index d2fab71c2..6f3cb2d84 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/page.tsx @@ -250,7 +250,7 @@ export default function Page() { <SearchDiv> <SearchFormLeft> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-gray-600"> + <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-label"> {"지도점검년도"} </Label> <SelectBox @@ -265,7 +265,7 @@ export default function Page() { /> </div> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-gray-600 "> + <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-label "> {"시설구분"} </Label> <SelectBox @@ -281,7 +281,7 @@ export default function Page() { /> </div> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-gray-600 "> + <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-label "> {"시설명"} </Label> <SearchInput @@ -378,7 +378,7 @@ export default function Page() { readonly={mdfcnAuthrtYn === 'N'} /> )} - <span className="text-2xl font-bold text-gray-500"> + <span className="text-2xl font-bold text-muted-foreground"> ☞ 연도별 점검일 최종 데이터를 기준으로 통계 작성합니다. </span> </ListDiv> diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx index 4838624a2..fca1b511a 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx @@ -1,5 +1,9 @@ -/** +/** * 재이용실적등록 > 하수재이용실적등록 + * + * 🔗 사용 API + * 하수재이용시설 목록 조회 reuse-service /reuseFc/pbnstPrfRegList + * 하수재이용시설 실적 상세 조회 reuse-service /reuseFc/pbnstPrfRegDtl */ "use client"; import { useWrStore } from "@/app/store"; @@ -10,13 +14,11 @@ import { initValOutUsage, subTotalObj, totalObj, - type FcPrfType, + type PbnstPrfRegDtlType, + type PbnstPrfRegListType, type PrfType, } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; -import { - excelDownload, - yearOptions, -} from "@/components/lib/func/common"; +import { excelDownload, yearOptions } from "@/components/lib/func/common"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { pbnstTableBody } from "@/components/lib/func/TableData"; import PbnPrfExcelModal from "@/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal"; @@ -32,18 +34,104 @@ import { UiTable, } from "@common/business_components/ui"; import ListDiv from "@common/business_components/ui/list_div"; -import { - Button, - Label, - Table, - TableBody, - TableHeader, -} from "@common/components/ui"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { Button, Label, Table, TableBody, TableHeader } from "@common/components/ui"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useAlertStore } from "@/src/shared/lib"; import { useTranslation } from "react-i18next"; import { headMakeprfmnc } from "../chkgdnReg/TableHead"; +type PbnstPrfmncRow = { + sido: string; + gugun: string; + bscFctNm: string; + ruseUtztnFcltNm: string; + pbnstRuseFcltId: string; +}; + +const buildInputData = (detail: PbnstPrfRegDtlType | null, searchYear: string) => { + if (!detail?.prfList?.length) { + return []; + } + + const inUsageData = structuredClone(initValInUsage).map((row) => ({ + ...row, + 구분: "장내용수", + })); + const outUsageData = structuredClone(initValOutUsage).map((row) => ({ + ...row, + 구분: "장외용수", + })); + const inSubtotal = { + ...structuredClone(subTotalObj), + 구분: "장내용수", + }; + const outSubtotal = { + ...structuredClone(subTotalObj), + 구분: "장외용수", + }; + + detail.prfList.forEach((prfItem) => { + const yr = Number(prfItem.ym?.slice(0, 4)); + if (yr !== Number(searchYear)) { + return; + } + + const month = Number(prfItem?.ym?.slice(4)); + const monthKey = `${month}월`; + + inUsageData.forEach((inData: any) => { + const usageKey = inData.usage; + const value = (prfItem as any)[usageKey] ?? 0; + + inData[monthKey] = value; + inData.total += value; + + if (usageKey !== "subtotal") { + (inSubtotal as any)[monthKey] = + ((inSubtotal as any)[monthKey] ?? 0) + value; + inSubtotal.total += value; + } + }); + + outUsageData.forEach((outData: any) => { + const usageKey = outData.usage; + const value = (prfItem as any)[usageKey] ?? 0; + + outData[monthKey] = value; + outData.total += value; + + if (usageKey !== "subtotal") { + (outSubtotal as any)[monthKey] = + ((outSubtotal as any)[monthKey] ?? 0) + value; + outSubtotal.total += value; + } + }); + }); + + if (inSubtotal.total === 0 && outSubtotal.total === 0) { + return []; + } + + inUsageData.push(inSubtotal); + outUsageData.push(outSubtotal); + + const usageTotalObj: { [key: string]: any } = { + ...structuredClone(totalObj), + 구분: "총합계", + 용수명: "", + }; + for (const key in usageTotalObj) { + if (["usage", "gubun", "edit"].includes(key)) continue; + const val = + (Number((inSubtotal as any)[key]) || 0) + + (Number((outSubtotal as any)[key]) || 0); + + (usageTotalObj as any)[key] = val; + } + + return [...inUsageData, ...outUsageData, usageTotalObj]; +}; + export default function PrfmncPrfmncReg() { const { auth, @@ -51,59 +139,85 @@ export default function PrfmncPrfmncReg() { prfmncHasuInit, prfmnRegData, prfmncHasuNmSearch, - getPrfFmcRes, + getPbnstPrfRegDtl, getLaeSeCdCheck, + returnCodeText, } = useWrStore((state) => state); - const { - prfPbnFclts, - pbnstPrfmncHeadList, - monthHeadList, - prfmncRes, - prfmncResCount, - } = prfmnc; + const { pbnstPrfmncHeadList, monthHeadList, prfmncRes, prfmncResCount, prfmncDetail } = + prfmnc; const { regAuthrtYn, mdfcnAuthrtYn, laeSeCd } = auth; const [isReset, setIsReset] = useState(false); - const [bdstTable, setbdstTable] = useState<FcPrfType[]>(); - const [searchTxt, setSearchTxt] = useState<string>(); + const [searchTxt, setSearchTxt] = useState<string>(""); + const [selectedFacilityId, setSelectedFacilityId] = useState<string>(""); + const [selectedIndex, setSelectedIndex] = useState<number>(0); + const [inputData, setInputData] = useState<any[]>([]); const [isExcelOpen, setIsExcelOPen] = useState<{ isModal: boolean; - data: FcPrfType; - }>({ isModal: false, data: {} as FcPrfType }); - const [inputData, setInputData] = useState<any[]>(); - const [pbnstRuseFcltId, setpbnstRuseFcltId] = useState<string>(); - const [cellNum, setCellNum] = useState<number>(1); - const [searchYear, setSearchYear] = useState<string>( - String(new Date().getFullYear()) - ); + data: PbnstPrfRegDtlType; + }>({ isModal: false, data: {} as PbnstPrfRegDtlType }); + const [searchYear, setSearchYear] = useState<string>(String(new Date().getFullYear())); const { confirm } = useConfirm(); const alert = useAlertStore((state) => state.setMessage); + const { t } = useTranslation("base"); - const useInitRef = useRef(false); + const bdstTable = useMemo<PbnstPrfmncRow[]>( + () => + prfmncRes.map((item: PbnstPrfRegListType) => ({ + sido: returnCodeText(item.sidoCd, "cityCd"), + gugun: + item.sggCd && String(item.sggCd).trim().length === 5 + ? returnCodeText(String(item.sggCd), "sggCd") + : "", + bscFctNm: item.swageBscFctNm, + ruseUtztnFcltNm: item.ruseUtztnFcltNm, + pbnstRuseFcltId: item.pbnstRuseFcltId, + })), + [prfmncRes, returnCodeText] + ); + + const derivedInputData = useMemo( + () => buildInputData(prfmncDetail, searchYear), + [prfmncDetail, searchYear] + ); const excelClick = useCallback(() => { - excelDownload( - [[]], - "하수재이용실적", - "sample_pbnstPrfmncReg.xlsx", - "A1:O1" - ); - }, [prfmnc.excelList]); + excelDownload([[]], "하수재이용실적", "sample_pbnstPrfmncReg.xlsx", "A1:O1"); + }, []); + + const loadDetail = useCallback( + async (facilityId: string) => { + if (!facilityId) return; + + const detail = await getPbnstPrfRegDtl(searchYear, facilityId); + if (detail) { + setSelectedFacilityId(facilityId); + setIsExcelOPen((prev) => ({ + ...prev, + data: detail, + })); + } + }, + [getPbnstPrfRegDtl, searchYear] + ); + + const handleSearch = useCallback(() => { + if (!laeSeCd) return; + + setIsReset(true); + }, [laeSeCd]); - const { t } = useTranslation("base"); const onSave = useCallback(async () => { const handleConfirm = async () => { - if (!pbnstRuseFcltId) return; + if (!selectedFacilityId) return; const message = await prfmnRegData( - 1, - laeSeCd, excelToPbnPrfData( inputData as unknown as JSON[], searchYear, - pbnstRuseFcltId - ) as unknown as PrfType, + selectedFacilityId + ) as unknown as PrfType[], searchYear, - prfPbnFclts + selectedFacilityId ); alert(message); }; @@ -111,126 +225,47 @@ export default function PrfmncPrfmncReg() { if (await confirm(t("isUpdateMsg"))) { await handleConfirm(); } - }, [inputData, searchYear, pbnstRuseFcltId, cellNum, prfPbnFclts, confirm, t, laeSeCd, prfmnRegData]); - - const cellClick = useCallback( - (index: number | undefined) => { - if (index !== undefined) { - setIsExcelOPen({ - isModal: false, - data: prfmncRes[index] || ({} as FcPrfType), - }); - const inUsageData = structuredClone(initValInUsage).map((row) => ({ - ...row, - 구분: "장내용수", - })); - const outUsageData = structuredClone(initValOutUsage).map((row) => ({ - ...row, - 구분: "장외용수", - })); - const inSubtotal = { - ...structuredClone(subTotalObj), - 구분: "장내용수", - }; - const outSubtotal = { - ...structuredClone(subTotalObj), - 구분: "장외용수", - }; - if (prfmncRes[index]?.prfList) { - prfmncRes[index].prfList?.forEach((prfItem) => { - const yr = Number(prfItem.ym?.slice(0, 4)); // "YYYY" - - if (yr === Number(searchYear)) { - const month = Number(prfItem?.ym?.slice(4)); // "MM" - const monthKey = `${month}월`; - - inUsageData.forEach((inData: any) => { - const usageKey = inData.usage; - const value = (prfItem as any)[usageKey] ?? 0; - - inData[monthKey] = value; - inData.total += value; - - if (inSubtotal && usageKey !== "subtotal") { - (inSubtotal as any)[monthKey] = - ((inSubtotal as any)[monthKey] ?? 0) + value; - inSubtotal.total += value; - } - }); - - outUsageData.forEach((outData: any) => { - const usageKey = outData.usage; - const value = (prfItem as any)[usageKey] ?? 0; - - outData[monthKey] = value; - outData.total += value; - - if (usageKey !== "subtotal") { - (outSubtotal as any)[monthKey] = - ((outSubtotal as any)[monthKey] ?? 0) + value; - outSubtotal.total += value; - } - }); - } - }); - - if (inSubtotal.total !== 0 && outSubtotal.total !== 0) { - inUsageData.push(inSubtotal); - outUsageData.push(outSubtotal); - const usageTotalObj: { [key: string]: any } = { - ...structuredClone(totalObj), - 구분: "총합계", - 용수명: "", - }; - for (const key in usageTotalObj) { - if (["usage", "gubun", "edit"].includes(key)) continue; - const val = - (Number((inSubtotal as any)[key]) || 0) + - (Number((outSubtotal as any)[key]) || 0); - - (usageTotalObj as any)[key] = val; - } - const tableData = [...inUsageData, ...outUsageData, usageTotalObj]; - setpbnstRuseFcltId(prfmncRes[index].pbnstRuseFcltId); - setInputData(tableData); - } else { - setInputData([]); - } - } else { - setInputData([]); - } - } - }, - [prfmncRes, searchYear] - ); - useEffect(() => { - getLaeSeCdCheck(); - useInitRef.current = true; - }, []); - useEffect(() => { - if (useInitRef.current == true && laeSeCd !== undefined) { - getPrfFmcRes(); - useInitRef.current = true; - } - }, [useInitRef.current, laeSeCd]); + }, [alert, confirm, inputData, prfmnRegData, searchYear, selectedFacilityId, t]); useEffect(() => { - const year = new Date().getFullYear(); - - if(laeSeCd === undefined || laeSeCd ==='') return; - prfmncHasuInit(1, laeSeCd, String(year), prfPbnFclts); - }, [prfPbnFclts,laeSeCd]); + void getLaeSeCdCheck(); + }, [getLaeSeCdCheck]); useEffect(() => { - if (prfmncRes && prfmncRes.length > 0) { - setbdstTable(prfmncRes); + if (!laeSeCd) return; + void prfmncHasuInit(1, laeSeCd); + }, [laeSeCd, prfmncHasuInit]); + + useEffect(() => { + if (!prfmncRes.length) { + setSelectedFacilityId(""); + setSelectedIndex(0); + setInputData([]); + setIsExcelOPen((prev) => ({ ...prev, data: {} as PbnstPrfRegDtlType })); + return; } - // cellNum이 0일 경우(첫 번째 행)도 유효한 인덱스이므로 undefined 여부만 체크합니다. - if (cellNum !== undefined) { - cellClick(cellNum); + const nextIndex = Math.max( + prfmncRes.findIndex((item) => item.pbnstRuseFcltId === selectedFacilityId), + 0 + ); + const nextFacility = prfmncRes[nextIndex]; + if (!nextFacility) { + return; } - }, [prfmncRes, cellNum, cellClick]); + + setSelectedIndex(nextIndex); + void loadDetail(nextFacility.pbnstRuseFcltId); + }, [loadDetail, prfmncRes]); + + useEffect(() => { + if (!selectedFacilityId) return; + void loadDetail(selectedFacilityId); + }, [loadDetail, searchYear, selectedFacilityId]); + + useEffect(() => { + setInputData(derivedInputData); + }, [derivedInputData]); return ( <> @@ -240,7 +275,7 @@ export default function PrfmncPrfmncReg() { <SearchDiv> <SearchFormLeft> <div className="col-span-3 flex flex-row space-x-2"> - <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-gray-600 "> + <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-label "> {"실적등록년도"} </Label> <SelectBox @@ -249,19 +284,17 @@ export default function PrfmncPrfmncReg() { selectClass={""} selectArray={yearOptions} value={searchYear} - onSelectValue={(val: string | number) => - setSearchYear(String(val)) - } + onSelectValue={(val: string | number) => setSearchYear(String(val))} /> </div> <div className="col-span-3 flex flex-row space-x-2"> - <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-gray-600"> + <Label className="w-fit shrink-0 whitespace-nowrap self-center text-xl text-label"> {"시설명"} </Label> <SearchInput className={"w-full text-2xl"} label="" - textValue={searchTxt || ""} + textValue={searchTxt} setText={(e) => { setSearchTxt(e); }} @@ -271,12 +304,7 @@ export default function PrfmncPrfmncReg() { <SearchFormRight className="w-3/12 mx-5 flex flex-col items-center justify-center space-x-2 "> <div className="space-y"> <div className="flex flex-row space-x-2"> - <Button - className={btnClass} - onClick={() => { - setIsReset(true); - }} - > + <Button className={btnClass} onClick={handleSearch}> 조회 </Button> <Button @@ -291,12 +319,11 @@ export default function PrfmncPrfmncReg() { <Button className={btnClass} onClick={() => { - if (cellNum === undefined) { + if (!selectedFacilityId) { alert("하수처리재이용시설을 선택해주세요."); return; - } else { - setIsExcelOPen({ ...isExcelOpen, isModal: true }); } + setIsExcelOPen((prev) => ({ ...prev, isModal: true })); }} disabled={regAuthrtYn === "N"} > @@ -318,30 +345,29 @@ export default function PrfmncPrfmncReg() { <ScrollArea className="h-[60vh]"> <UiTable key={"leftTable"} - tableData={bdstTable || []} + tableData={bdstTable} headlist={pbnstPrfmncHeadList} pageSize={10} total={prfmncResCount} headName={"하수재이용시설 목록"} cellClick={(index: number | undefined) => { - cellClick(index); - setCellNum(index ?? 0); + if (index === undefined) return; + const selected = prfmncRes[index]; + if (!selected) return; + + setSelectedIndex(index); + void loadDetail(selected.pbnstRuseFcltId); }} onPageChange={(page: number) => { - if (searchTxt && searchTxt?.length > 0) { - prfmncHasuNmSearch( - page, - laeSeCd, - searchYear, - searchTxt, - prfPbnFclts - ); + if (searchTxt.trim().length > 0) { + void prfmncHasuNmSearch(page, laeSeCd, searchTxt); } else { - prfmncHasuInit(page, laeSeCd, searchYear, prfPbnFclts); + void prfmncHasuInit(page, laeSeCd); } setIsReset(false); }} resetPageNo={isReset} + index={selectedIndex} /> </ScrollArea> </ListDiv> @@ -353,19 +379,21 @@ export default function PrfmncPrfmncReg() { {"하수처리수재이용 실적(m³) 등록결과"} </h3> </div> - <Table variant="secondary"> - <TableHeader className="font-bold"> - {headMakeprfmnc(monthHeadList, [])} - </TableHeader> - <TableBody key={prfmnc.pageKey + "pageKey"}> - {pbnstTableBody( - inputData ?? [], - monthHeadList, - setInputData, - mdfcnAuthrtYn === "N" ? false : true - )} - </TableBody> - </Table> + <div className="card !m-[1.2rem]"> + <Table variant="secondary"> + <TableHeader className="font-bold"> + {headMakeprfmnc(monthHeadList, [])} + </TableHeader> + <TableBody key={prfmnc.pageKey + "pageKey"}> + {pbnstTableBody( + inputData, + monthHeadList, + setInputData, + mdfcnAuthrtYn === "N" ? false : true + )} + </TableBody> + </Table> + </div> </ScrollArea> </ListDiv> </div> @@ -376,22 +404,20 @@ export default function PrfmncPrfmncReg() { <PbnPrfExcelModal data={isExcelOpen.data} setIsExcelOPen={(open) => { - setIsExcelOPen({ ...isExcelOpen, isModal: open }); + setIsExcelOPen((prev) => ({ ...prev, isModal: open })); }} setModalExcelData={async (excelData) => { - const message = await prfmnRegData(1, laeSeCd, excelData, searchYear, prfPbnFclts); + if (!selectedFacilityId) return; + const message = await prfmnRegData(excelData, searchYear, selectedFacilityId); alert(message); - // 데이터 저장 후 UI 업데이트는 useEffect가 감지하여 처리하므로 여기서는 호출하지 않습니다. }} ></PbnPrfExcelModal> } open={isExcelOpen.isModal} onOpenChange={() => { - setIsExcelOPen({ ...isExcelOpen, isModal: false }); + setIsExcelOPen((prev) => ({ ...prev, isModal: false })); }} ></UiModal> </> ); } - - diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx index ee2e776ba..1750a2b6b 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/chkgdnSttn/page.tsx @@ -1,5 +1,8 @@ "use client"; +/** + * 재이용실적 현황 > 지도점검 현황 + */ import { useWrStore } from "@/app/store"; import { btnClass } from "@/app/store/common/code"; import { rainPageSize } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn"; @@ -298,7 +301,7 @@ export default function Page() { <SearchDiv> <SearchFormLeft> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-28 mt-2 text-xl text-gray-600"> + <Label className="mt-2 w-28 text-xl text-label"> {"시도"} </Label> <SelectBox @@ -315,7 +318,7 @@ export default function Page() { /> </div> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-28 mt-2 text-xl text-gray-600"> + <Label className="mt-2 w-28 text-xl text-label"> {"시군구"} </Label> <SelectBox @@ -334,7 +337,7 @@ export default function Page() { /> </div> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-48 mt-2 text-xl text-gray-600"> + <Label className="mt-2 w-48 text-xl text-label"> {"지도점검년도"} </Label> <SelectBox @@ -349,7 +352,7 @@ export default function Page() { /> </div> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-28 mt-2 text-xl text-gray-600"> + <Label className="mt-2 w-28 text-xl text-label"> {"시설구분"} </Label> <SelectBox @@ -365,7 +368,7 @@ export default function Page() { /> </div> <div className="col-span-2 flex flex-row space-x-2"> - <Label className="w-28 mt-2 text-xl text-gray-600"> + <Label className="mt-2 w-28 text-xl text-label"> {"시설명"} </Label> <SearchInput diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx index 34d4a4ffe..2238e8bdc 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/prfmncSttn/pbnstPrfmncSttn/page.tsx @@ -25,7 +25,6 @@ export default function PbnstPrfmncSttn() { pbnstPrfmncSearch, getFlctsSttn, prfmnc, - pbnNmSearch, getLaeSeCdCheck } = useWrStore((state) => state); const { laeSeCd } = auth; @@ -584,11 +583,6 @@ export default function PbnstPrfmncSttn() { if (prfmncRes?.[0]?.pbnstRuseFcltId) setChartId(prfmncRes[0].pbnstRuseFcltId) }, [prfmncRes]); - useEffect(() => { - if (pbnRuseNm.length > 1) - pbnNmSearch("1", "23", getFixedSearchValue(ref.current), pbnRuseNm, searchYear, []); - }, [pbnRuseNm, getFixedSearchValue, pbnNmSearch, searchYear]); - const cellClick = (index: number | undefined, headId: string) => { if (index !== undefined && headId !== undefined) { setChartId(prfmncRes[index].pbnstRuseFcltId) @@ -626,7 +620,7 @@ export default function PbnstPrfmncSttn() { {/* <div className="flex flex-row items-center"> */} <div className="flex flex-row items-center gap-x-4 flex-nowrap"> {/* <Label className="w-full text-xl text-gray-600"> */} - <Label className="flex-none whitespace-nowrap text-xl text-gray-600"> + <Label className="flex-none whitespace-nowrap text-xl text-label"> 하수 재이용년도</Label> <SelectBox // className="w-full h-full text-2xl mx-4" diff --git a/apps/wr/app/api/common-codes/route.ts b/apps/wr/app/api/common-codes/route.ts new file mode 100644 index 000000000..ae0c175cd --- /dev/null +++ b/apps/wr/app/api/common-codes/route.ts @@ -0,0 +1,219 @@ +export const dynamic = "force-dynamic"; +import { SITE_ID } from "@/src/shared/config"; +import { NextResponse } from "next/server"; + +export const revalidate = 86400; //60 * 60 * 24; + +type SelectDataType = { + text: string; + val: string; +}; + +type EnvPartSelectDataType = SelectDataType & { + adstDliStrCd: string; +}; + +const codeMapping = { + WR001: "bdstUsgCd", + WR002: "rswtBdstType", + WR003: "rainFcltCd", + WR004: "bdstGrntchClass", + WR005: "rswtPrscCdMthdCd", + WR006: "orgwtKndCd", + WR007: "ruseMngMtd", + WR008: "rusePrcsMthdCd", + WR009: "erctUswtrPrcsMeth", + WR010: "ruseFcltSpot", + WR011: "pplQulity", + WR012: "pplLenght", + WR013: "bdstFilterYn", + WR014: "rainRswtMng", + WR015: "rainFcltFlowRate", + WR016: "dmdUszCd", + WR017: "sewageInFcltUse", + WR018: "sewageOutFclttUse", + WR019: "htdPowerMthdCd", + WR020: "htdUtztnUsgCd", +} as const; + +const defaultHeaders = { + accept: "application/json", + "X-Site-Id": SITE_ID, +}; + +function getServerBaseUrl() { + const port = process.env.PORT ?? process.env.NEXT_PUBLIC_PORT ?? "3000"; + const proxyHost = process.env.NEXT_PUBLIC_PROXY_HOST; + + if (process.env.NODE_ENV === "production" && process.env.HOSTNAME) { + return `http://${process.env.HOSTNAME}:${port}`; + } + + return proxyHost || `http://localhost:${port}`; +} + +async function fetchJson(url: string, cookieHeader?: string) { + const response = await fetch(url, { + method: "GET", + headers: { + ...defaultHeaders, + ...(cookieHeader ? { cookie: cookieHeader } : {}), + }, + next: { revalidate }, + }); + + if (!response.ok) { + const responseText = await response.text(); + throw new Error( + `Failed to fetch ${url}: ${response.status} ${response.statusText}${ + responseText ? ` - ${responseText}` : "" + }` + ); + } + + return response.json(); +} + +async function fetchOperationData(path: string, cookieHeader?: string) { + const json = await fetchJson( + `${getServerBaseUrl()}/operation-service${path}`, + cookieHeader + ); + return json.data; +} + +async function fetchCmsData(path: string, cookieHeader?: string) { + return fetchJson(`${getServerBaseUrl()}${path}`, cookieHeader); +} + +async function fetchCommonCodesByKeys(keys: string[], cookieHeader?: string) { + const targets = [...new Set(keys)].filter( + (cd) => Boolean(codeMapping[cd as keyof typeof codeMapping]) + ); + + const entries = await Promise.all( + targets.map(async (cd) => { + const mapping = codeMapping[cd as keyof typeof codeMapping]; + const lwCd = await fetchCmsData( + `/cms-service/api/v1/lwpo-cds/up-cd/${cd}`, + cookieHeader + ); + const list = Array.isArray(lwCd) ? lwCd : (lwCd.content ?? []); + + return [ + mapping, + list.map((it: any) => ({ + text: it.lwpoCdNm, + val: it.lwpoCd, + })), + ] as const; + }) + ); + + return Object.fromEntries(entries) as Record<string, SelectDataType[]>; +} + +export async function GET(request: Request) { + try { + const cookieHeader = request.headers.get("cookie") ?? ""; + const [sgg, city, rvbsn, mdmbsn, lgbsn, lclgvrnmnt, upCd] = + await Promise.all([ + fetchOperationData("/addr/sgg", cookieHeader), + fetchOperationData("/addr/city", cookieHeader), + fetchOperationData("/addr/sarea", cookieHeader), + fetchOperationData("/addr/midArea", cookieHeader), + fetchOperationData("/addr/largeArea", cookieHeader), + fetchOperationData("/addr/govArea", cookieHeader), + fetchCmsData( + "/cms-service/api/v1/up-codes?keyword=&upCd=&cdSe=&useYn=Y&siteId=WR&page=0&size=100&sort=string", + cookieHeader + ), + ]); + + const upWrCode: string[] = (upCd.content ?? []) + .filter((item: any) => item.upCd.slice(0, 2) === "WR") + .map((cd: any) => cd.upCd); + + const lwCds = await fetchCommonCodesByKeys(upWrCode, cookieHeader); + + const payload = { + sggCd: [ + ...((sgg ?? []).map((item: any) => ({ + text: item.gugun || "", + val: String(item.adstDstrCd) || "", + })) as SelectDataType[]), + ], + cityCd: [ + { val: "0", text: "전체" }, + ...((city ?? []).map((item: any) => ({ + val: item.adstDstrCd, + text: item.sido, + })) as SelectDataType[]), + ], + rvbsnCd: [ + { val: "0", text: "전체" }, + ...((rvbsn ?? []).map((item: any) => ({ + val: item.rvbsnCd, + text: item.rvbsn, + })) as SelectDataType[]), + ], + mdmbsnCd: [ + { val: "0", text: "전체" }, + ...((mdmbsn ?? []).map((item: any) => ({ + val: item.rvbsnCd, + text: item.mdmbsn, + })) as SelectDataType[]), + ], + lgbsnCd: [ + { val: "0", text: "전체" }, + ...((lgbsn ?? []).map((item: any) => ({ + val: item.rvbsnCd, + text: item.lgbsn, + })) as SelectDataType[]), + ], + lclgvrnmntCd: [ + { val: "0", text: "전체", adstDliStrCd: "" }, + ...((lclgvrnmnt ?? []).map((item: any) => ({ + val: item.lclgvrnmntCd, + text: item.lclgvrnmntNm, + adstDliStrCd: item.adstDliStrCd || "", + })) as EnvPartSelectDataType[]), + ], + rainFcltCd: lwCds["rainFcltCd"] ?? [], + bdstUsgCd: lwCds["bdstUsgCd"] ?? [], + orgwtKndCd: lwCds["orgwtKndCd"] ?? [], + pplQulity: lwCds["pplQulity"] ?? [], + pplLenght: lwCds["pplLenght"] ?? [], + dmdUszCd: lwCds["dmdUszCd"] ?? [], + sewageOutFclttUse: lwCds["sewageOutFclttUse"] ?? [], + rainRswtMng: lwCds["rainRswtMng"] ?? [], + rainFcltFlowRate: lwCds["rainFcltFlowRate"] ?? [], + ruseMngMtd: lwCds["ruseMngMtd"] ?? [], + rusePrcsMthdCd: lwCds["rusePrcsMthdCd"] ?? [], + erctUswtrPrcsMeth: lwCds["erctUswtrPrcsMeth"] ?? [], + bdstGrntchClass: lwCds["bdstGrntchClass"] ?? [], + htdPowerMthdCd: lwCds["htdPowerMthdCd"] ?? [], + htdUtztnUsgCd: lwCds["htdUtztnUsgCd"] ?? [], + rswtBdstType: lwCds["rswtBdstType"] ?? [], + bdstTotalUsgType: (lwCds["bdstUsgCd"] ?? []).concat(lwCds["rswtBdstType"] ?? []), + }; + + return NextResponse.json(payload, { + headers: { + "Cache-Control": "s-maxage=86400, stale-while-revalidate=3600", + }, + }); + } catch (error) { + console.error("Failed to load common codes:", error); + const message = + error instanceof Error ? error.message : "Failed to load common codes"; + + return NextResponse.json( + { + message: "Failed to load common codes", + detail:message , + }, + { status: 500 } + ); + } +} diff --git a/apps/wr/app/api/proxy/[...url]/route.ts b/apps/wr/app/api/proxy/[...url]/route.ts index e847e8ad2..8fabc89dc 100644 --- a/apps/wr/app/api/proxy/[...url]/route.ts +++ b/apps/wr/app/api/proxy/[...url]/route.ts @@ -35,11 +35,11 @@ async function handler( // silent refresh if (isRefreshHandler) { - const refreshToken = cookieStore.get(REFRESH_TOKEN); + const refreshToken = cookieStore.get(REFRESH_TOKEN); // "refresh-token" console.log("refreshToken", refreshToken?.value); - headers[CLAIM_NAME] = refreshToken?.value ?? ""; + headers[CLAIM_NAME] = refreshToken?.value ?? ""; // "Authorization" - if (!headers[CLAIM_NAME]) { + if (!headers[CLAIM_NAME]) { // "Authorization" return NextResponse.json({ message: "Can't refresh" }, { status: 401 }); } } @@ -62,9 +62,9 @@ async function handler( ); } - const accessToken = result.headers.get(ACCESS_TOKEN); - const refreshToken = result.headers.get(REFRESH_TOKEN); - const userId = result.headers.get(AUTH_USER_ID); + const accessToken = result.headers.get(ACCESS_TOKEN); // "access-token" + const refreshToken = result.headers.get(REFRESH_TOKEN); // "refresh-token" 신규 받아온 token + const userId = result.headers.get(AUTH_USER_ID); // "token-id" if (accessToken && userId) { const response = NextResponse.json({ @@ -75,7 +75,7 @@ async function handler( if (refreshToken) { response.cookies.set(REFRESH_TOKEN, refreshToken, { httpOnly: true, - sameSite: "strict", // 다른 사이트에서 들어오는 모든 요청에 대해 쿠키를 보내지 않는 가장 강력한 설정 + sameSite: "lax", // 다른 사이트에서 들어오는 모든 요청에 대해 쿠키를 보내지 않는 가장 강력한 설정 domain: getCookieDomain(), path: "/", }); diff --git a/apps/wr/app/store/common/auth/index.tsx b/apps/wr/app/store/common/auth/index.tsx index feca57c80..fe99a02ae 100644 --- a/apps/wr/app/store/common/auth/index.tsx +++ b/apps/wr/app/store/common/auth/index.tsx @@ -32,7 +32,7 @@ export const authStateExport: authState = { export type authActions = { getMenuAuthrts: (menuCd: number) => void; - getLaeSeCdCheck: () => Promise<void>; + getLaeSeCdCheck: () => Promise<void>; // 로그인 사용자 지역코드 laeSeCd를 조회 후 상태 update getMyUUID: () => Promise<string |undefined>; }; export const authActionsExport: (set: any, get: any) => authActions = ( @@ -52,10 +52,10 @@ export const authActionsExport: (set: any, get: any) => authActions = ( auth: { ...state.auth, mngAuthrtYn: response[0].mngAuthrtYn, - inqAuthrtYn: response[0].mngAuthrtYn, - regAuthrtYn: response[0].mngAuthrtYn, - mdfcnAuthrtYn: response[0].mngAuthrtYn, - delAuthrtYn: response[0].mngAuthrtYn, + inqAuthrtYn: response[0].inqAuthrtYn, + regAuthrtYn: response[0].regAuthrtYn, + mdfcnAuthrtYn: response[0].mdfcnAuthrtYn, + delAuthrtYn: response[0].delAuthrtYn, }, }; }); diff --git a/apps/wr/app/store/common/code/index.ts b/apps/wr/app/store/common/code/index.ts index adcff57f2..66f66126a 100644 --- a/apps/wr/app/store/common/code/index.ts +++ b/apps/wr/app/store/common/code/index.ts @@ -11,7 +11,7 @@ import { ruseSeCd, yn, } from "./basicCode"; -export const btnClass = "p-4 m-2 text-white rounded-sm text-lg font-medium "; +export const btnClass = "p-4 m-2 rounded-sm text-lg font-medium text-primary-foreground "; const laeSecd = "41"; export const lwCds: Record<string, SelectDataType[]> = {}; @@ -21,12 +21,12 @@ const codeMApping = { WR002: "rswtBdstType", WR003: "rainFcltCd", WR004: "bdstGrntchClass", - WR005: "rswtPrscCdMthdCd", // 아직 디 못바꿈 - WR006: "orgwtKndCd", //작업 완ㅛ - WR007: "ruseMngMtd", // 작업완료 - WR008: "rusePrcsMthdCd", //w작업완료 - WR009: "erctUswtrPrcsMeth", //w작업완료 - WR010: "ruseFcltSpot", // inout은 yn 만 사용해야 함으로 해당 코드는 사용안함. + WR005: "rswtPrscCdMthdCd", + WR006: "orgwtKndCd", + WR007: "ruseMngMtd", + WR008: "rusePrcsMthdCd", + WR009: "erctUswtrPrcsMeth", + WR010: "ruseFcltSpot", WR011: "pplQulity", WR012: "pplLenght", WR013: "bdstFilterYn", // 사용 불가 yn만 사용해야함으로 해당코드 사용 안함 @@ -312,6 +312,7 @@ export type comCodeActions = { returnCodeText: (val: string, codeName: string) => string; returnTextCode: (text: string, codeName: string) => string; getCommonCodesByKeys: (keys: string[]) => Promise<void>; + initCommonCodes: () => Promise<void>; }; export const codeActionExport: (set: any, get:any) => comCodeActions = (set: any, get:any) => { @@ -368,5 +369,102 @@ export const codeActionExport: (set: any, get:any) => comCodeActions = (set: any const found = codeList.find((item) => item.text === text); return found ? found.val : text; }, + initCommonCodes: async () => { + if (get().code.isInit) return; + const response = await fetch("/api/common-codes", { + method: "GET", + headers: { + accept: "application/json", + }, + }); + + if (!response.ok) { + let errorMessage = `공통 코드 조회 실패: ${response.status}`; + + try { + const errorBody = await response.json(); + if (errorBody?.detail) { + errorMessage += ` - ${errorBody.detail}`; + } else if (errorBody?.message) { + errorMessage += ` - ${errorBody.message}`; + } + } catch { + const errorText = await response.text(); + if (errorText) { + errorMessage += ` - ${errorText}`; + } + } + + console.error("initCommonCodes failed:", errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + + lwCds["rainFcltCd"] = data.rainFcltCd ?? lwCds["rainFcltCd"] ?? []; + lwCds["bdstUsgCd"] = data.bdstUsgCd ?? lwCds["bdstUsgCd"] ?? []; + lwCds["orgwtKndCd"] = data.orgwtKndCd ?? lwCds["orgwtKndCd"] ?? []; + lwCds["pplQulity"] = data.pplQulity ?? lwCds["pplQulity"] ?? []; + lwCds["pplLenght"] = data.pplLenght ?? lwCds["pplLenght"] ?? []; + lwCds["dmdUszCd"] = data.dmdUszCd ?? lwCds["dmdUszCd"] ?? []; + lwCds["sewageOutFclttUse"] = + data.sewageOutFclttUse ?? lwCds["sewageOutFclttUse"] ?? []; + lwCds["rainRswtMng"] = data.rainRswtMng ?? lwCds["rainRswtMng"] ?? []; + lwCds["rainFcltFlowRate"] = + data.rainFcltFlowRate ?? lwCds["rainFcltFlowRate"] ?? []; + lwCds["ruseMngMtd"] = data.ruseMngMtd ?? lwCds["ruseMngMtd"] ?? []; + lwCds["rusePrcsMthdCd"] = + data.rusePrcsMthdCd ?? lwCds["rusePrcsMthdCd"] ?? []; + lwCds["erctUswtrPrcsMeth"] = + data.erctUswtrPrcsMeth ?? lwCds["erctUswtrPrcsMeth"] ?? []; + lwCds["bdstGrntchClass"] = + data.bdstGrntchClass ?? lwCds["bdstGrntchClass"] ?? []; + lwCds["htdPowerMthdCd"] = + data.htdPowerMthdCd ?? lwCds["htdPowerMthdCd"] ?? []; + lwCds["htdUtztnUsgCd"] = + data.htdUtztnUsgCd ?? lwCds["htdUtztnUsgCd"] ?? []; + lwCds["rswtBdstType"] = + data.rswtBdstType ?? lwCds["rswtBdstType"] ?? []; + lwCds["bdstTotalUsgType"] = + data.bdstTotalUsgType ?? lwCds["bdstTotalUsgType"] ?? []; + + set((state) => ({ + code: { + ...state.code, + sggCd: data.sggCd ?? state.code.sggCd, + cityCd: data.cityCd ?? state.code.cityCd, + rvbsnCd: data.rvbsnCd ?? state.code.rvbsnCd, + mdmbsnCd: data.mdmbsnCd ?? state.code.mdmbsnCd, + lgbsnCd: data.lgbsnCd ?? state.code.lgbsnCd, + lclgvrnmntCd: data.lclgvrnmntCd ?? state.code.lclgvrnmntCd, + rainFcltCd: data.rainFcltCd ?? state.code.rainFcltCd, + bdstUsgCd: data.bdstUsgCd ?? state.code.bdstUsgCd, + orgwtKndCd: data.orgwtKndCd ?? state.code.orgwtKndCd, + pplQulity: data.pplQulity ?? state.code.pplQulity, + pplLenght: data.pplLenght ?? state.code.pplLenght, + dmdUszCd: data.dmdUszCd ?? state.code.dmdUszCd, + sewageOutFclttUse: + data.sewageOutFclttUse ?? state.code.sewageOutFclttUse, + rainRswtMng: data.rainRswtMng ?? state.code.rainRswtMng, + rainFcltFlowRate: + data.rainFcltFlowRate ?? state.code.rainFcltFlowRate, + ruseMngMtd: data.ruseMngMtd ?? state.code.ruseMngMtd, + rusePrcsMthdCd: + data.rusePrcsMthdCd ?? state.code.rusePrcsMthdCd, + erctUswtrPrcsMeth: + data.erctUswtrPrcsMeth ?? state.code.erctUswtrPrcsMeth, + bdstGrntchClass: + data.bdstGrntchClass ?? state.code.bdstGrntchClass, + htdPowerMthdCd: + data.htdPowerMthdCd ?? state.code.htdPowerMthdCd, + htdUtztnUsgCd: + data.htdUtztnUsgCd ?? state.code.htdUtztnUsgCd, + rswtBdstType: data.rswtBdstType ?? state.code.rswtBdstType, + bdstTotalUsgType: + data.bdstTotalUsgType ?? state.code.bdstTotalUsgType, + isInit: true, + }, + })); + }, }; }; diff --git a/apps/wr/app/store/map/htdflMap/index.tsx b/apps/wr/app/store/map/htdflMap/index.tsx index a8b9eb7c1..7815bfa79 100644 --- a/apps/wr/app/store/map/htdflMap/index.tsx +++ b/apps/wr/app/store/map/htdflMap/index.tsx @@ -21,7 +21,7 @@ type htdfMapActions = { htdfMapInit: (page: number, total: number, cityOrSgg: string) => void; htdfMapSearch: (isExcel: boolean, page: number, total: string, con: string, txt: string, sidoCode: SelectDataType[], - sggCode: SelectDataType[]) => void; + sggCode: SelectDataType[]) => Promise<string[][] | undefined>; htdfMapDetile:(featureCd:string) => Promise<customHtdflBdst | undefined>; }; @@ -93,6 +93,7 @@ export const htdfMapActionsExport: (set: any) => htdfMapActions = ( }, }; }); + return htdflMapExcelList; }else{ set((state: htdfMapState) => { @@ -109,6 +110,7 @@ export const htdfMapActionsExport: (set: any) => htdfMapActions = ( } catch (e) { console.log(e); } + return undefined; }, htdfMapDetile: async (featureCd: string) => { try { diff --git a/apps/wr/app/store/map/pbnstMap/index.tsx b/apps/wr/app/store/map/pbnstMap/index.tsx index 18f9abfe2..f7a81a3a9 100644 --- a/apps/wr/app/store/map/pbnstMap/index.tsx +++ b/apps/wr/app/store/map/pbnstMap/index.tsx @@ -1,6 +1,5 @@ -import { fetchType, postFectch } from "@/components/lib/api"; +import { getFetchWithCount } from "@/components/lib/api"; import type { pubHasuType } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; -import { checkWrRegList } from "../../ruseFcltMng/fcltReg/pbnstReg"; import type { ReturnType} from "@common/business_components/ui"; import { pubFacHeadListExport } from "../../ruseFcltMng/fcltReg/pbnstReg/headList"; import type { TableUpperProps } from "../../common/type/header"; @@ -8,52 +7,68 @@ import { excelHasuMapParsing } from "./excel"; export const pbnMapTableSize = 20; +const buildPbnstRegListUrl = ( + page: number, + pageTotal: number, + reurnValue: ReturnType, + searchNm?: string, + featureCd?: string +) => { + let searchingCd = "A"; + let nameCd = "0"; + + if (reurnValue.partValue === "A") { + searchingCd = "A"; + nameCd = + reurnValue.sigunValue !== "0" ? reurnValue.sigunValue : reurnValue.sidoValue; + } else if (reurnValue.partValue === "R") { + searchingCd = "R"; + nameCd = reurnValue.regionValue + ? reurnValue.regionValue + : reurnValue.unitWaterShedValue + ? reurnValue.unitWaterShedValue + : reurnValue.middleRegionValue; + } else if (reurnValue.partValue === "G") { + searchingCd = reurnValue.envPartValue; + nameCd = + reurnValue.sigunValue !== "0" ? reurnValue.sigunValue : reurnValue.sidoValue; + } + + const url = `/reuseFc/pbnstRegList/${page}/${pageTotal}/${searchingCd}/${nameCd || "0"}/0/1/SW`; + const queryParams = new URLSearchParams(); + + if (searchNm && searchNm.length > 0) { + queryParams.set("swtrmtFcltNm", searchNm); + } + if (featureCd) { + queryParams.set("bscFctCd", featureCd); + } + + const queryString = queryParams.toString(); + return queryString ? `${url}?${queryString}` : url; +}; + const hasuListFetch = async ( + _get: any, + _set: any, page: number, pageTotal: number, reurnValue: ReturnType, searchNm?: string ) => { - let url = `/prcsFclt/ruseInfo/${page}/${pageTotal}/`; - const checkWrRegListRes = await checkWrRegList(); + const url = buildPbnstRegListUrl(page, pageTotal, reurnValue, searchNm); + const response = await getFetchWithCount("wr", url); - if (reurnValue.partValue === "A") { - url += `${reurnValue.partValue}/${ - // reurnValue.sidoValue ? reurnValue.sidoValue : reurnValue.sigunValue - reurnValue.sigunValue !== "0" - ? reurnValue.sigunValue - : reurnValue.sidoValue - }/0/1/SW`; - } else if (reurnValue.partValue === "R") { - url += `${reurnValue.partValue}/${ - reurnValue.regionValue - ? reurnValue.regionValue - : reurnValue.unitWaterShedValue - ? reurnValue.unitWaterShedValue - : reurnValue.middleRegionValue - }/0/1/SW`; - } else if (reurnValue.partValue === "G") { - url += `${reurnValue.envPartValue}/${reurnValue.sigunValue !== "0" ? reurnValue.sigunValue : reurnValue.sidoValue}/0/1/SW`; - } else { - url += `A/0/0/1/SW`; - } - if (searchNm && searchNm?.length > 0) { - url += "/" + searchNm; - } - - const data = checkWrRegListRes.filter((item: string) => item !== ""); - - const rainRes = await postFectch(url, data, fetchType.COMMON); - - const hasuList = rainRes.data.map((item: any) => { + const hasuList = response.data.map((item: any) => { return { ...item, fctCpc: Number(item.fctCpc) || 0, - ruse: item.ruse === false ? "N" : "Y", + ruse: item?.ruse ?? "N", + rswtrYn: item?.rswtrYn ?? "N", }; }); - return { data: hasuList, count: rainRes.count }; + return { data: hasuList, count: response.count }; }; export type pbnMapState = { @@ -74,7 +89,7 @@ type pbnMapActions = { pageTotal: number, reurnValue: ReturnType, txt: string, - ) => void; + ) => Promise<string[][] | undefined>; pbnMapDetaile: ( featureCd: string, ) => Promise<pubHasuType | undefined>; @@ -92,7 +107,7 @@ export const pbnMapStateExport: pbnMapState = { }, }; -export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { +export const pbnMapActionsExport: (set: any, get: any) => pbnMapActions = (set: any, get: any) => { return { pbnMapInit: async ( page: number, @@ -100,7 +115,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { reurnValue: ReturnType ) => { try { - const pubMapRes = await hasuListFetch(page, pageTotal, reurnValue); + const pubMapRes = await hasuListFetch(get, set, page, pageTotal, reurnValue); set((state: pbnMapState) => { return { ...state, @@ -118,16 +133,23 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { }, pbnMapDetaile: async ( featureCd: string) => { try { - const hasuRes = await postFectch( - `/prcsFclt/ruseInfo/1/1/A/0/0/1/SW/`, - [featureCd], - fetchType.COMMON - ); + const defaultReturnType = { + partValue: "A", + sidoValue: "0", + sigunValue: "0", + regionValue: "0", + unitWaterShedValue: "0", + middleRegionValue: "0", + envPartValue: "0", + } as ReturnType; + const url = buildPbnstRegListUrl(1, 1, defaultReturnType, undefined, featureCd); + const hasuRes = await getFetchWithCount("wr", url); const filteredData = hasuRes.data.map((item: any) => { return { ...item, fctCpc: Number(item.fctCpc) || 0, - ruse: item.ruse === false ? "N" : "Y", + ruse: item?.ruse ?? "N", + rswtrYn: item?.rswtrYn ?? "N", }; }); return filteredData[0]; @@ -144,7 +166,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { txt: string, ) => { try { - const hasuMapRes = await hasuListFetch(page, pageTotal, reurnValue, txt); + const hasuMapRes = await hasuListFetch(get, set, page, pageTotal, reurnValue, txt); if (isExcel) { const hasuMapExcelList = excelHasuMapParsing( hasuMapRes @@ -158,6 +180,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { }, }; }); + return hasuMapExcelList; } else { set((state: pbnMapState) => { return { @@ -174,6 +197,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => { } catch (e) { console.log(e); } + return undefined; }, }; }; diff --git a/apps/wr/app/store/map/rainMap/index.tsx b/apps/wr/app/store/map/rainMap/index.tsx index 050639096..1dba165bd 100644 --- a/apps/wr/app/store/map/rainMap/index.tsx +++ b/apps/wr/app/store/map/rainMap/index.tsx @@ -23,7 +23,7 @@ export type rainMapState = { type rainMapActions = { rainMapInit: (page: number, total: number, cityOrSgg:string) => void; - rainMapSearch: (isExcel: boolean, page: number, total: string, con: string, txt: string,code:Record<string , SelectDataType[]>) => void; + rainMapSearch: (isExcel: boolean, page: number, total: string, con: string, txt: string,code:Record<string , SelectDataType[]>) => Promise<string[][] | undefined>; rainMapDetile:(featureCd:string) => Promise<customRainBdst | undefined>; }; @@ -92,6 +92,7 @@ export const rainMapActionsExport: (set: any) => rainMapActions = ( }, }; }); + return rainMapExcelList; }else{ set((state: rainMapState) => { return { @@ -108,6 +109,7 @@ export const rainMapActionsExport: (set: any) => rainMapActions = ( } catch (e) { console.log(e); } + return undefined; }, rainMapDetile: async (featureCd: string) => { try { diff --git a/apps/wr/app/store/map/rswtMap/index.tsx b/apps/wr/app/store/map/rswtMap/index.tsx index 1c98a4dd2..94bd1d2c4 100644 --- a/apps/wr/app/store/map/rswtMap/index.tsx +++ b/apps/wr/app/store/map/rswtMap/index.tsx @@ -24,7 +24,7 @@ type rswtMapActions = { rswtMapInit: (page: number, total: number, cityOrSgg: string) => void; rswtMapSearch: ( isExcel: boolean, page: number, total: string, con: string, txt: string, code:Record<string, SelectDataType[]> - ) => void; + ) => Promise<string[][] | undefined>; rswtMapDetile: (featureCd: string) => Promise<customRswtBdst | undefined>; }; @@ -92,6 +92,7 @@ export const rswtMapActionsExport: (set: any) => rswtMapActions = ( }, }; }); + return rswtMapExcelList; }else{ set((state: rswtMapState) => { @@ -108,6 +109,7 @@ export const rswtMapActionsExport: (set: any) => rswtMapActions = ( } catch (e) { console.log(e); } + return undefined; }, rswtMapDetile: async (featureCd: string) => { try { diff --git a/apps/wr/app/store/map/ruseMap/index.tsx b/apps/wr/app/store/map/ruseMap/index.tsx index f68fb96b3..fa5ce06b9 100644 --- a/apps/wr/app/store/map/ruseMap/index.tsx +++ b/apps/wr/app/store/map/ruseMap/index.tsx @@ -1,4 +1,6 @@ import { fetchType, getFetch, postFectch } from "@/components/lib/api"; +import type { FcPrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; +import moment from "moment"; import type { CommonTableProps } from "../../common/type/header"; import type { WaterReuseFacility } from "../../ruseFcltMng/fcltSttn/pbnstSttn/type"; import { @@ -6,10 +8,39 @@ import { pbnSttHeadListExport, } from "../../ruseFcltMng/fcltSttn/pbnstSttn/headList"; import type { ReturnType } from "@common/business_components/ui"; -import { hasuListFetch } from "../../ruseFcltMng/fcltSttn/pbnstSttn"; -import type { customPbnst } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; +import type { customPbnst, pubHasuType } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; import { excelPbnstSttnParsing } from "../../ruseFcltMng/fcltSttn/pbnstSttn/excel"; +const getCurrentYear = () => String(new Date().getFullYear()); + +const sumPrfUsage = (prfList: FcPrfType["prfList"] = []) => { + return prfList.reduce( + (acc, cur) => { + acc.inUsage += + cur.wshwtr + + cur.coolUswtr + + cur.cleanUswtr + + cur.drwtUswtr + + cur.dltnUswtr + + cur.ithlRswtr + + cur.ithlEtcUswtr; + acc.outUsage += + cur.cleanToilUswtr + + cur.resn + + cur.lndwtr + + cur.hydUswtr + + cur.ugwtChr + + cur.farmUswtr + + cur.rvrKeepUswtr + + cur.indstUswtr; + acc.total = acc.inUsage + acc.outUsage; + + return acc; + }, + { total: 0, inUsage: 0, outUsage: 0 } + ); +}; + export const ruseMapSize = 15; export type ruseMapState = { ruseMap: { @@ -61,6 +92,141 @@ export const ruseMapStateExport: ruseMapState = { }, }; +const hasuListFetch = async ( + page: string, + total: string, + reurnValue: ReturnType, + searchNm: string | undefined, + year: string, + flcts: string[] +) => { + if (flcts.length === 0) { + return { data: [], count: 0 }; + } + + let url = ``; + let filteredFclt = flcts; + let nameCd = "0", + partStr = "A", + sidogunValue = "0"; + + switch (reurnValue.partValue) { + case "R": + if (reurnValue.regionValue !== "0") { + if (reurnValue.unitWaterShedValue === "0") nameCd = reurnValue.regionValue; + else if (reurnValue.middleRegionValue === "0") nameCd = reurnValue.unitWaterShedValue; + else nameCd = reurnValue.middleRegionValue; + } + partStr = reurnValue.partValue; + break; + default: + if (reurnValue.sidoValue && reurnValue.sidoValue !== "0") nameCd = reurnValue.sidoValue; + if (reurnValue.sigunValue && reurnValue.sigunValue !== "0") nameCd = reurnValue.sigunValue; + sidogunValue = nameCd; + partStr = reurnValue.envPartValue !== "0" ? reurnValue.envPartValue : "A"; + break; + } + + if (sidogunValue && sidogunValue !== "0") { + filteredFclt = flcts.filter((fclt: string) => { + return sidogunValue.length == 2 + ? fclt.slice(0, 2) === sidogunValue + : fclt.slice(0, 5) === sidogunValue; + }); + } + + url = `${partStr}/${nameCd}/0/1/SW`; + + if (searchNm && searchNm.length > 0) { + url += "/" + searchNm; + } + + if (filteredFclt.length === 0) { + return { data: [], count: 0 }; + } + + const pbnList = await postFectch( + `/prcsFclt/ruseInfo/1/${filteredFclt.length}/` + url, + filteredFclt, + fetchType.COMMON + ); + + const checkRuseFcltList = pbnList.data.map((item) => item.bscFctCd); + + const getRuseList = await postFectch( + `/reuseFc/pubDetailList/${year}/${sidogunValue}/${page}/${total}`, + checkRuseFcltList, + fetchType.WR + ); + const checkWrRegListRes = getRuseList.data; + + const data: WaterReuseFacility[] = checkWrRegListRes + .map((item: FcPrfType) => { + const findData = pbnList.data.find( + (fclt: pubHasuType) => + fclt.bscFctCd === item.pbnstRuseFcltId.substring(0, 11) + ); + + if (findData) { + const usage = item.prfList.reduce( + (acc, cur) => { + acc.inUsage += + cur.wshwtr + + cur.coolUswtr + + cur.cleanUswtr + + cur.drwtUswtr + + cur.dltnUswtr + + cur.ithlRswtr + + cur.ithlEtcUswtr; + acc.outUsage += + cur.cleanToilUswtr + + cur.resn + + cur.lndwtr + + cur.hydUswtr + + cur.ugwtChr + + cur.farmUswtr + + cur.rvrKeepUswtr + + cur.indstUswtr; + acc.total = acc.inUsage + acc.outUsage; + + return acc; + }, + { total: 0, inUsage: 0, outUsage: 0 } + ); + return { + rvbsn: findData?.rvbsn || "-", + lgbsn: findData?.lgbsn || "-", + mdmbsn: findData?.mdmbsn || "-", + rgnNm: findData?.rgnNm || "-", + sido: findData?.sido || "-", + gugun: findData?.gugun || "-", + bscFctNm: findData?.bscFctNm || "-", + efflWqltStndrdNm: findData?.efflWqltStndrdNm || "-", + fctCpc: Number(findData?.fctCpc) || 0, + tel: findData?.tel ?? "-", + ruseUtztnFcltNm: item?.ruseUtztnFcltNm || "-", + fcltCpct: item?.fcltCpct || "-", + splyExtraCpct: item?.splyExtraCpct || "-", + ruseCpct: item?.ruseCpct || "-", + pbnstRuseFcltId: item.pbnstRuseFcltId || "-", + usageTotal: usage?.total, + inUsage: usage?.inUsage, + outUsage: usage?.outUsage, + operMthd: item.operList[0]?.operMthd || "-", + rprtvBzenty: item.operList[0]?.rprtvBzenty, + bgngDt: moment(item.operList[0]?.bgngDt).format("YYYY-MM-DD") ?? "", + endDt: moment(item.operList[0]?.endDt).format("YYYY-MM-DD") ?? "", + searchNm: findData?.bscFctNm || "-", + searchPk: findData?.bscFctCd || "-", + prfList: item?.prfList ?? [], + }; + } + }) + .filter(Boolean); + + return { data: data, count: getRuseList.count }; +}; + export const ruseMapActionsExport: (set: any) => ruseMapActions = ( set: any ) => { @@ -111,22 +277,12 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = ( try { const ruseDetailList = await getFetch( fetchType.WR, - `/reuseFc/pubFc/${bscFctCd}` - ); - const year = new Date().getFullYear(); - const data = [bscFctCd]; - const rusePrfList = await postFectch( - `/reuseFc/pubDetailList/${year}/0/1/10`, - data, - fetchType.WR + `/reuseFc/pubFc/${bscFctCd}?year=${getCurrentYear()}` ); const dataList = ruseDetailList.map((ruse) => { - const matchRuse = rusePrfList?.data.find( - (prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId - ); return { ...ruse, - prfList: matchRuse.prfList, + prfList: ruse.prfList ?? [], }; }); console.log("/dataList ", dataList); @@ -195,51 +351,13 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = ( try { const ruseDetailList = await getFetch( fetchType.WR, - `/reuseFc/pubFc/${bscFctCd}` - ); - const year = new Date().getFullYear(); - const data = [bscFctCd]; - const rusePrfList = await postFectch( - `/reuseFc/pubDetailList/${year}/0/1/10`, - data, - fetchType.WR + `/reuseFc/pubFc/${bscFctCd}?year=${getCurrentYear()}` ); const dataList = ruseDetailList.map((ruse) => { - const matchRuse = rusePrfList?.data.find( - (prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId - ); - - - const usage = matchRuse.prfList.reduce( - (acc, cur) => { - acc.inUsage += - cur.wshwtr + - cur.coolUswtr + - cur.cleanUswtr + - cur.drwtUswtr + - cur.dltnUswtr + - cur.ithlRswtr + - cur.ithlEtcUswtr + - cur.cleanToilUswtr + - cur.resn; - acc.outUsage += - cur.lndwtr + - cur.hydUswtr + - cur.ugwtChr + - cur.farmUswtr + - cur.rvrKeepUswtr + - cur.indstUswtr; - acc.total = acc.inUsage + acc.outUsage; - - return acc; - }, - { total: 0, inUsage: 0, outUsage: 0 } - ); - - + const usage = sumPrfUsage(ruse.prfList ?? []); return { ...ruse, - prfList: matchRuse.prfList, + prfList: ruse.prfList ?? [], inUsage: usage?.inUsage, outUsage: usage?.outUsage, diff --git a/apps/wr/app/store/map/splyPop/index.tsx b/apps/wr/app/store/map/splyPop/index.tsx index 7ea586e63..b92df81df 100644 --- a/apps/wr/app/store/map/splyPop/index.tsx +++ b/apps/wr/app/store/map/splyPop/index.tsx @@ -1,3 +1,6 @@ +/** + * 표준공사비 산정 섹션에서 후보지 조회 + */ import { getFetch } from "@/components/lib/api"; import type { FacilityInfo, SubsInfo } from "./type"; import { DEV_URL_FO } from "@/components/lib/config"; diff --git a/apps/wr/app/store/matDmd/coMatRvw/index.tsx b/apps/wr/app/store/matDmd/coMatRvw/index.tsx index b347a1df8..afeb36fc1 100644 --- a/apps/wr/app/store/matDmd/coMatRvw/index.tsx +++ b/apps/wr/app/store/matDmd/coMatRvw/index.tsx @@ -1,4 +1,5 @@ import { fetchType, getFetchWithCount, putFectch } from "@/components/lib/api"; +import { DEFAULT_LIST_SIZE } from "@/src/shared/config"; import type { CommonTableProps } from "../../common/type/header"; import { constCostHeadListExport, @@ -12,6 +13,7 @@ const coRvwHeadList = coRvwHeadListExport; const coRvwNodeList = coRvwNodeListExport; const rateFeeHeadList = rateFeeHeadListExport; const constCostHeadList = constCostHeadListExport; + export type coRvwState = { coRvw: { isInit: boolean; @@ -118,7 +120,7 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => { const coMatRvwRes = await coMatInit( 1, - 10, + DEFAULT_LIST_SIZE, new Date().getFullYear().toString(), new Date().getFullYear().toString() ); diff --git a/apps/wr/app/store/matDmd/coMatRvw/type.tsx b/apps/wr/app/store/matDmd/coMatRvw/type.tsx index d195953cd..a505777ff 100644 --- a/apps/wr/app/store/matDmd/coMatRvw/type.tsx +++ b/apps/wr/app/store/matDmd/coMatRvw/type.tsx @@ -75,8 +75,8 @@ export const DmdCoReqSchema = z.object({ dmndTmng: z.coerce.date().nullish(), // 요구시기 dmndQty: z.number().nullish(), // 요구수량(m³/일) blgcOxygDmndQnt: z.number().nullish(), // 생물학적 산소요구량(BOD)(mg/L) - wntUntprc: z.number().nullish(), // 희망단가(원/m3) - grssClfg: z.number().nullish(), // 총대장균군수(개/mg) + wntUntprc: z.number().nullish(), // 희망단가(원/m³) + grssClfg: z.number().nullish(), // 총대장균군수(개/100㎖) combResdch: z.number().nullish(), // 결합잔류염소(mg/L) trbt: z.number().nullish(), // 탁도(NTU) smll: z.string().nullish(), // 냄새 diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx index 538135d0a..c70b821af 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList.tsx @@ -124,7 +124,7 @@ export const rainMainHeadNmExport: chHeadType[] = [ export const rainRuseHeadNmExport: chHeadType[] = [ { headNm: "useQuilty", text: "이용수질(필요시)", length: 3 }, { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, - { headNm: "ruseWater", text: "연간대체수 이용량(m³)", length: 13 }, + { headNm: "ruseWater", text: "연간빗물이용량(m³)", length: 13 }, { headNm: "detailQulity", text: "분기", length: 11 }, ]; @@ -133,7 +133,7 @@ export const rainSplHeadNmExport: chHeadType[] = [ { headNm: "useQuilty", text: "이용수질(필요시)", length: 3 }, { headNm: "feeHist", text: "과태료 부과내역", length: 3 }, { headNm: "detailQulity", text: "분기", length: 11 }, - { headNm: "splWater", text: "연간빗물 이용량(m³)", length: 13 }, + { headNm: "splWater", text: "연간대체수이용량(m³)", length: 13 }, ]; export const rainChkgdnHeadListExport: TableUpperProps[] = [ @@ -152,7 +152,7 @@ export const rainChkgdnHeadListExport: TableUpperProps[] = [ { id: "totalRuseFcltUtztnQnt", title: "연간빗물이용량(m³)", widthPercent: 7 }, { id: "ph", title: "수소이온농도(pH)", upName: "useQuilty", upSequnce: 1, type: "number", edit: true, widthPercent: 5 }, { id: "trbt", title: "탁도(NTU)", upName: "useQuilty", upSequnce: 2, type: "number", edit: true, widthPercent: 4 }, - { id: "grssClfg", title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 3, type: "number", edit: true, widthPercent: 5 }, + { id: "grssClfg", title: "총대장균군수(개/100㎖)", upName: "useQuilty", upSequnce: 3, type: "number", edit: true, widthPercent: 5 }, { id: "vltnDt", title: "부과일(위반일)", upName: "feeHist", upSequnce: 1, type: "text", edit: true, widthPercent: 5 }, { id: "vltnMttr", title: "위반사항", upName: "feeHist", upSequnce: 2, type: "text", edit: true, widthPercent: 6 }, { id: "levyAmt", title: "부과금액(천원)", upName: "feeHist", upSequnce: 3, type: "text", edit: true, widthPercent: 5 }, diff --git a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx index 9a8b65ced..7cba87e82 100644 --- a/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx +++ b/apps/wr/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/headList.tsx @@ -149,7 +149,7 @@ export const rswtChkHeadListExport: TableUpperProps[] = [ { id: "utztnUsgCdText", title: "이용용도", widthPercent: 7 }, { id: "splyUswtrUtztnQnt", title: "연간대체수이용량(m³)" ,type: "number", widthPercent: 5 }, { id: "ruseFcltUtztnQnt", title: "연간중수도이용량(m³)",type: "number", widthPercent: 5 }, - { id: "grssClfgUswtr", title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 1 ,type: "number", widthPercent: 6 }, + { id: "grssClfgUswtr", title: "총대장균군수(개/100㎖)", upName: "useQuilty", upSequnce: 1 ,type: "number", widthPercent: 6 }, { id: "combResdch", title: "결합잔류염소(mh/L)", upName: "useQuilty", upSequnce: 2 ,type: "number", widthPercent: 6 }, { id: "trbt", title: "탁도(NTU)", upName: "useQuilty", upSequnce: 3 ,type: "number", widthPercent: 4 }, { id: "bod", title: "BOD\n(mg/L)", upName: "useQuilty", upSequnce: 4 ,type: "number", widthPercent: 4 }, diff --git a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx index 83057114c..cadba69d4 100644 --- a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx +++ b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx @@ -1,229 +1,155 @@ -/** +/** * 재이용실적등록 > 하수재이용실적등록 - * - * 🔗 사용 API - * - 하수재이용시설 실적 조회: POST /api/v1/reuseFc/pubDetailList/{year}/{sggOrCity}/{page}/{pageSize} (reuse-service) - * - 하수재이용시설 실적 등록: POST /api/v1/reuseFc/pubFcPrf (reuse-service) - * - 공공재이용시설 목록 조회: POST /api/v1/prcsFclt/ruseInfo/{page}/{pageSize}/{type}/{sggOrCity}/{etc1}/{etc2}/{etc3} (server-service) - * - 공공재이용시설 목록 조회(이름검색): POST /api/v1/prcsFclt/ruseInfo/{page}/{pageSize}/{type}/{sggOrCity}/{searchTxt} (server-service) - * - 공공재이용시설 조회: GET /api/v1/reuseFc/pubFc (reuse-service) + * + * 🔗 사용 API + * - 좌측 목록 조회: GET /api/v1/reuseFc/pbnstPrfRegList/{page}/{pageSize}/{searchingCd}/{nameCd} + * - 우측 상세 조회: GET /api/v1/reuseFc/pbnstPrfRegDtl/{year}/{pbnstRuseFcltId} + * - 하수재이용시설 실적 등록: POST /api/v1/reuseFc/pubFcPrf **/ -import { fetchType, getFetch, postFectch } from "@/components/lib/api"; +import { fetchType, getFetch, getFetchWithCount, postFectch } from "@/components/lib/api"; import { pbnstPrfmncHeadListExport, monthHeadListExport } from "./headList"; -import type { pubHasuType } from "../../ruseFcltMng/fcltReg/pbnstReg/type"; -import type { FcPrfType, PrfType } from "./type"; +import type { PbnstPrfRegDtlType, PbnstPrfRegListType, PrfType } from "./type"; import type { TableUpperProps } from "../../common/type/header"; const pbnstPrfmncHeadList = pbnstPrfmncHeadListExport; const monthHeadList = monthHeadListExport; export const prfRegPageSize = 10; + export type pbnstPrfmncState = { prfmnc: { isInit: boolean; pbnstPrfmncHeadList: TableUpperProps[]; monthHeadList: TableUpperProps[]; - prfmncRes: FcPrfType[]; + prfmncRes: PbnstPrfRegListType[]; prfmncResCount: number; excelList: number[]; pageKey: number; - prfPbnFclts: string[]; + prfmncDetail: PbnstPrfRegDtlType | null; }; }; export const pbnstPrfmncStateExport: pbnstPrfmncState = { prfmnc: { isInit: false, - pbnstPrfmncHeadList: pbnstPrfmncHeadList, - monthHeadList: monthHeadList, + pbnstPrfmncHeadList, + monthHeadList, prfmncRes: [], - prfPbnFclts: [], prfmncResCount: 1, excelList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], pageKey: 1, + prfmncDetail: null, }, }; export type pbnstPrfmncType = pbnstPrfmncState & pbnstPrfmncActions; type pbnstPrfmncActions = { - prfmncHasuInit: ( - page: number, - sggOrCity: string, - year: string, - prfPbnFclts: string[] - ) => void; - prfmncHasuNmSearch: ( - page: number, - sggOrCity: string, - year: string, - searchTxt: string, - prfPbnFclts: string[] - ) => void; - prfmnRegData: ( - page: number, - sggOrCity: string, - data: PrfType, - year: string, - prfPbnFclts: string[] - ) => Promise<string>; - getPrfFmcRes: () => void; + prfmncHasuInit: (page: number, sggOrCity: string) => void; + prfmncHasuNmSearch: (page: number, sggOrCity: string, searchTxt: string) => void; + prfmnRegData: (data: PrfType[], year: string, pbnstRuseFcltId: string) => Promise<string>; + getPbnstPrfRegDtl: (year: string, pbnstRuseFcltId: string) => Promise<PbnstPrfRegDtlType | undefined>; }; -const hasuListFetch = async ( +const fetchPbnstPrfRegList = async ( page: number, sggOrCity: string, - year: string, - prfPbnFclts: string[], searchTxt?: string ) => { - let filteredFclt = prfPbnFclts; - if (sggOrCity !== "0") { - filteredFclt = prfPbnFclts?.filter((fclt: string) => { - return sggOrCity.length == 2 - ? fclt.slice(0, 2) === sggOrCity - : fclt.slice(0, 5) === sggOrCity; - }); + let url = `/reuseFc/pbnstPrfRegList/${page}/${prfRegPageSize}/A/${sggOrCity}`; + if (searchTxt && searchTxt.trim().length > 0) { + url += `?swtrmtFcltNm=${encodeURIComponent(searchTxt.trim())}`; } - //let url = `/prcsFclt/ruseInfo/1/${filteredFclt.length + 1}/A/${sggOrCity}/0/1/SW`; - let url = `/prcsFclt/ruseInfo/1/${filteredFclt.length + 1}`; - if (searchTxt && searchTxt?.length > 0) { - url += `/${searchTxt}`; - } - const rainRes = await postFectch(url, filteredFclt, fetchType.COMMON); - const checkRuseFcltList = - searchTxt && searchTxt?.length > 0 - ? rainRes.data.map((item) => item.bscFctCd) - : filteredFclt; - - const pubRuseList = await postFectch( - `/reuseFc/pubDetailList/${year}/${sggOrCity}/${page}/${prfRegPageSize}`, - checkRuseFcltList, - fetchType.WR - ); - const rusePrfList = pubRuseList.data - .map((item: FcPrfType) => { - const findData = rainRes.data?.find( - (fclt: pubHasuType) => - item.pbnstRuseFcltId.substring(0, 11) === fclt?.bscFctCd - ); - if (findData !== undefined) { - return { - sido: findData.sido, - gugun: findData.gugun, - bscFctNm: findData.bscFctNm, - bscFctCd: findData.bscFctCd, - ruseUtztnFcltNm: item.ruseUtztnFcltNm, - pbnstRuseFcltId: item.pbnstRuseFcltId, - prfList: item.prfList, - }; - } - }) - .filter(Boolean); // undefined 값을 제거합니다. - - return { data: rusePrfList, count: pubRuseList.count }; + const response = await getFetchWithCount(fetchType.WR, url); + return { + data: (response?.data ?? []) as PbnstPrfRegListType[], + count: response?.count ?? 0, + }; }; + export const pbnstPrfmncActionsExport: (set: any) => pbnstPrfmncActions = ( set: any ) => { return { - getPrfFmcRes: async () => { + prfmncHasuInit: async (page: number, sggOrCity: string) => { try { - const pbnPrfRes = await getFetch(fetchType.WR, "/reuseFc/pubFc"); - set((state: pbnstPrfmncState) => ({ + const prfmncRes = await fetchPbnstPrfRegList(page, sggOrCity); + return set((state: pbnstPrfmncState) => ({ ...state, prfmnc: { ...state.prfmnc, - prfPbnFclts: pbnPrfRes, + prfmncRes: prfmncRes.data, + prfmncResCount: prfmncRes.count, + prfmncDetail: null, + isInit: false, }, })); } catch (error) { console.log(error); } }, - prfmncHasuInit: async ( - page: number, - sggOrCity: string, - year: string, - prfPbnFclts: string[] - ) => { - try { - const prfmncRes = await hasuListFetch( - page, - sggOrCity, - year, - prfPbnFclts - ); - return set((state: pbnstPrfmncState) => ({ - ...state, - prfmnc: { - ...state.prfmnc, - prfmncRes: prfmncRes.data, - prfmncResCount: prfmncRes.count, - isInit: false, - }, - })); - } catch (errorHasu) { - console.log(errorHasu); - } - }, prfmncHasuNmSearch: async ( page: number, sggOrCity: string, - year: string, - searchTxt: string, - prfPbnFclts: string[] + searchTxt: string ) => { try { - const prfmncRes = await hasuListFetch( - page, - sggOrCity, - year, - prfPbnFclts, - searchTxt - ); + const prfmncRes = await fetchPbnstPrfRegList(page, sggOrCity, searchTxt); return set((state: pbnstPrfmncState) => ({ ...state, prfmnc: { ...state.prfmnc, prfmncRes: prfmncRes.data, prfmncResCount: prfmncRes.count, + prfmncDetail: null, isInit: false, }, })); - } catch (errorHasu) { - console.log(errorHasu); + } catch (error) { + console.log(error); } }, - prfmnRegData: async ( - page: number, - sggOrCity: string, - data: PrfType, - year: string, - prfPbnFclts: string[] - ) => { + getPbnstPrfRegDtl: async (year: string, pbnstRuseFcltId: string) => { + try { + const detail = (await getFetch( + fetchType.WR, + `/reuseFc/pbnstPrfRegDtl/${year}/${pbnstRuseFcltId}` + )) as PbnstPrfRegDtlType; + + set((state: pbnstPrfmncState) => ({ + ...state, + prfmnc: { + ...state.prfmnc, + prfmncDetail: detail, + }, + })); + + return detail; + } catch (error) { + console.log(error); + return undefined; + } + }, + prfmnRegData: async (data: PrfType[], year: string, pbnstRuseFcltId: string) => { const failMessage = "하수재이용 시설 실적 등록이 실패하였습니다."; try { - const ressponse = await postFectch( + const response = await postFectch( "/reuseFc/pubFcPrf", data, fetchType.WR ); - if (ressponse.status === 200) { - const prfmncRes = await hasuListFetch( - page, - sggOrCity, - year, - prfPbnFclts - ); + if (response.status === 200) { + const detail = (await getFetch( + fetchType.WR, + `/reuseFc/pbnstPrfRegDtl/${year}/${pbnstRuseFcltId}` + )) as PbnstPrfRegDtlType; set((state: pbnstPrfmncState) => ({ ...state, prfmnc: { ...state.prfmnc, - prfmncRes: prfmncRes.data, - prfmncResCount: prfmncRes.count, + prfmncDetail: detail, isInit: false, pageKey: state.prfmnc.pageKey + 1, }, diff --git a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx index 6723f3922..ff53a381f 100644 --- a/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx +++ b/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/type.tsx @@ -54,8 +54,26 @@ export const FcPrfSchema = z.object({ bscFctNm: z.string().nullish(), }); +export const PbnstPrfRegListSchema = z.object({ + swageBscFctNm: z.string().default(""), + ruseUtztnFcltNm: z.string().default(""), + pbnstRuseFcltId: z.string().default(""), + sidoCd: z.string().default(""), + sggCd: z.string().default(""), + totalCount: z.number().default(0), +}); + +export const PbnstPrfRegDtlSchema = z.object({ + pbnstRuseFcltId: z.string().default(""), + ruseUtztnFcltNm: z.string().default(""), + swageBscFctNm: z.string().default(""), + prfList: z.array(PrfSchema).default([]), +}); + export type FcPrfType = z.infer<typeof FcPrfSchema>; export type PrfType = z.infer<typeof PrfSchema>; +export type PbnstPrfRegListType = z.infer<typeof PbnstPrfRegListSchema>; +export type PbnstPrfRegDtlType = z.infer<typeof PbnstPrfRegDtlSchema>; export const initValInUsage:PrfUsageSummary[] = [ { diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx index c45a772e7..48909e20e 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rainChkgdnSttn/headList.tsx @@ -15,7 +15,7 @@ export const rainChkgdnHeadSttnExport: TableUpperProps[] = [ { id: "totalRuseFcltUtztnQnt", title: "연간빗물이용량(m³)", widthPercent: 7 }, { id: "ph", title: "수소이온농도(pH)", upName: "useQuilty", upSequnce: 1, type: "number", widthPercent: 5 }, { id: "trbt", title: "탁도(NTU)", upName: "useQuilty", upSequnce: 2, type: "number", widthPercent: 4 }, - { id: "grssClfg", title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 3, type: "number", widthPercent: 5 }, + { id: "grssClfg", title: "총대장균군수(개/100㎖)", upName: "useQuilty", upSequnce: 3, type: "number", widthPercent: 5 }, { id: "vltnDt", title: "부과일(위반일)", upName: "feeHist", upSequnce: 1, type: "text", widthPercent: 5 }, { id: "vltnMttr", title: "위반사항", upName: "feeHist", upSequnce: 2, type: "text", widthPercent: 6 }, diff --git a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx index 21a78b07d..6a0a925d2 100644 --- a/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx +++ b/apps/wr/app/store/prfmncSttn/chkgdnSttn/rswtChkgdnSttn/headList.tsx @@ -14,7 +14,7 @@ export const rswtChkSttnHeadListExport: TableUpperProps[] = [ { id: "splyUswtrUtztnQnt", title: "연간보급수이용량(m³)", type: "number", widthPercent: 5 }, { id: "ruseFcltUtztnQnt", title: "연간중수도이용량(m³)", type: "number", widthPercent: 5 }, - { id: "grssClfgUswtr", title: "총대장균군수(개/mg)", upName: "useQuilty", upSequnce: 1, type: "number", widthPercent: 6 }, + { id: "grssClfgUswtr", title: "총대장균군수(개/100㎖)", upName: "useQuilty", upSequnce: 1, type: "number", widthPercent: 6 }, { id: "combResdch", title: "결합잔류염소(mh/L)", upName: "useQuilty", upSequnce: 2, type: "number", widthPercent: 6 }, { id: "trbt", title: "탁도(NTU)", upName: "useQuilty", upSequnce: 3, type: "number", widthPercent: 4 }, { id: "bod", title: "BOD (mg/L)", upName: "useQuilty", upSequnce: 4, type: "number", widthPercent: 4 }, diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/headList.tsx index 4d0c228be..78b4f22cf 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/htdflReg/headList.tsx @@ -1,10 +1,10 @@ import type { TableUpperProps } from "@/app/store/common/type/header"; export const htdTableHeadList: TableUpperProps[] = [ - { id: "cityText", title: "시도" }, - { id: "sggText", title: "시군구" }, - { id: "bdstNm", title: "시설명" }, - { id: "daddr", title: "위치" }, - { id: "frstArchPrmsnDt", title: "준공년도" }, - { id: "archArea", title: "건축면적(㎡)" }, + { id: "cityText", title: "시도", widthPercent: 12 }, + { id: "sggText", title: "시군구", widthPercent: 10 }, + { id: "bdstNm", title: "시설명", widthPercent: 20 }, + { id: "daddr", title: "위치", widthPercent: 30 }, + { id: "frstArchPrmsnDt", title: "준공년도", widthPercent: 13 }, + { id: "archArea", title: "건축면적(㎡)", widthPercent: 15 }, ]; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx index 5d77c64ca..b5c6177b0 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/headList.tsx @@ -3,14 +3,14 @@ import type { TableUpperProps } from "@/app/store/common/type/header"; import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; export const pubFacHeadListExport: TableUpperProps[] = [ - { id: "sido", title: "시도" }, - { id: "gugun", title: "시군구" }, - { id: "bscFctNm", title: "시설명" }, - { id: "addr", title: "위치" }, - { id: "efflWqltStndrdNm", title: "지역구분" }, - { id: "fctCpc", title: "시설용량(m³/일)" }, - { id: "ruse", title: "재이용여부" }, - { id: "rswtrYn", title: "등록여부" }, + { id: "sido", title: "시도", widthPercent: 8 }, + { id: "gugun", title: "시군구", widthPercent: 8 }, + { id: "bscFctNm", title: "시설명", widthPercent: 18 }, + { id: "addr", title: "위치", widthPercent: 24 }, + { id: "efflWqltStndrdNm", title: "지역구분", widthPercent: 15 }, + { id: "fctCpc", title: "시설용량(m³/일)", widthPercent: 12 }, + { id: "ruse", title: "등록\n여부", widthPercent: 8 }, + { id: "rswtrYn", title: "재이용\n여부", widthPercent: 6 }, ]; export const pbnstMngUpHeadListExport: TableUpperProps[] = [ @@ -151,7 +151,7 @@ export const businessSttnInfo: formDetailType[] = [ export const ruseWaterQulityList: formDetailType[] = [ { id: "bod", lable: "BOD(mg/L)", type: "text", maxLenght:numSize }, - { id: "grssClfg", lable: "총대장균군수(개/mg)", type: "text" , maxLenght:numSize}, + { id: "grssClfg", lable: "총대장균군수(개/100㎖)", type: "text" , maxLenght:numSize}, { id: "combResdch", lable: "결합잔류염소(mh/L)", type: "text" , maxLenght:numSize}, { id: "trbt", lable: "탁도(NTU)", type: "text" , maxLenght:numSize}, { id: "smll", lable: "냄새", type: "text", maxLenght:numSize }, diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx index 6340f1082..1305c0126 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx @@ -5,6 +5,7 @@ import { getFetch, postFectch, putFectch, + getFetchWithCount } from "@/components/lib/api"; import { DEFAULT_LIST_SIZE, SITE_ID } from "@/src/shared/config"; import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList"; @@ -61,10 +62,11 @@ export type pubFacState = { dmdComList: any[]; apiMessage: string; pbnstHasuResCount: number; + fclts: any[] | null; }; }; type pubFacActions = { - pubHasuInit: (page: string, sggOrCity: string) => void; + pubHasuInit: (page: number, sggOrCity: string) => void; pbnstRegClick: (pubHasuRes: pubHasuType) => void; //pbnstReg: (txt: string | undefined) => void; pbnstInsert(pbnstRes: customPbnst): Promise<string>; @@ -73,7 +75,7 @@ type pubFacActions = { fileDownLoadName: (atflCd: string, fileName: string) => void; pbnstRuseFcltUpdate: (ruseRes: customPbnst[]) => void; pbnstRuseBizUpdate: (ruseRes: fcBizType[]) => void; - pubHasuSearch: (page: string, sggOrCity: string, searchTxt: string) => void; + pubHasuSearch: (page: number, sggOrCity: string, searchTxt: string) => void; getFcOrgwts: (fcltPkList: string[], size: number) => void; }; export type pubFacType = pubFacState & pubFacActions; @@ -98,42 +100,60 @@ export const pubFacStateExport: pubFacState = { dmdComList: [], apiMessage: "", pbnstHasuResCount: 1, + fclts: null, }, }; + // 재사용시설에 대한 정보 조회 -export const checkWrRegList = async () => { +export const checkWrRegList = async (get: any, set: any) => { + const { pbnst } = get(); + + if (Array.isArray(pbnst.fclts)) { + return pbnst.fclts; + } + const wrRegList = await getFetch("wr", `/reuseFc/pubFc`); - return wrRegList; + const fclts = Array.isArray(wrRegList) ? wrRegList : []; + + set((state: pubFacState) => ({ + ...state, + pbnst: { + ...state.pbnst, + fclts, + }, + })); + + return fclts; }; // 하수처리시설 정보 조회 const hasuListFetch = async ( - page: string, + _get: any, + _set: any, + page: number = 1, sggOrCity: string, // 해당 지자체의 하수처리시설 조회 searchTxt?: string ) => { - const checkWrRegListRes = await checkWrRegList(); // 재사용시설에 대한 정보 조회 - - let url = `/prcsFclt/ruseInfo/${page ? page : 1}/${DEFAULT_LIST_SIZE}/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`; - - const data = checkWrRegListRes.filter((item: string) => item !== ""); - if (searchTxt) { - url = url + `/${searchTxt}`; + let url = `/reuseFc/pbnstRegList/${page}/${DEFAULT_LIST_SIZE}/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`; + if (searchTxt?.trim()) { + url += `?swtrmtFcltNm=${encodeURIComponent(searchTxt.trim())}`; } - const rainRes = await postFectch(url, data, fetchType.COMMON); // fo + const fcltRes = await getFetchWithCount("wr", url); - const hasuList = rainRes.data.map((item: any) => { + const hasuList = fcltRes.data.map((item: any) => { return { ...item, fctCpc: Number(item.fctCpc) || 0, - ruse: item.ruse === false ? "N" : "Y", + ruse: item?.ruse ?? "N", + rswtrYn: item?.rswtrYn ?? "N", }; }); - return { data: hasuList, count: rainRes.count }; + return { data: hasuList, count: fcltRes.count }; }; const initFetch = async (ruseFcltPk: string | undefined) => { - const pbnstRes = await getFetch("wr", `/reuseFc/pubFc/${ruseFcltPk}`); + const year = new Date().getFullYear(); + const pbnstRes = await getFetch("wr", `/reuseFc/pubFc/${ruseFcltPk}?year=${year}`); return pbnstRes; }; @@ -172,12 +192,12 @@ export const writeRgtrAndMdfr = (data: customPbnst[]) => { return data; }; -export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { +export const pubFacActionsExport: (set: any, get: any) => pubFacActions = (set: any, get: any) => { return { // 하수처리시설을 조회한다(운영실태) - pubHasuInit: async (page: string, sggOrCity: string) => { + pubHasuInit: async (page: number, sggOrCity: string) => { try { - const pbnstHasuRes = await hasuListFetch(page, sggOrCity); // /prcsFclt/ruseInfo + const pbnstHasuRes = await hasuListFetch(get, set, page, sggOrCity); // /prcsFclt/ruseInfo return set((state: pubFacState) => ({ ...state, pbnst: { @@ -192,12 +212,12 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { }, // 하수처리시설을 조회한다(운영실태) (처리시설명 포함) pubHasuSearch: async ( - page: string, + page: number, sggOrCity: string, searchTxt: string ) => { try { - const pbnstHasuRes = await hasuListFetch(page, sggOrCity, searchTxt); + const pbnstHasuRes = await hasuListFetch(get, set, page, sggOrCity, searchTxt); return set((state: pubFacState) => ({ ...state, pbnst: { @@ -238,11 +258,14 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => { const newPbnstRes = await initFetch( pbnstRes.pbnstRuseFcltId.slice(0, 11) ); + const wrRegList = await getFetch("wr", `/reuseFc/pubFc`); + const fclts = Array.isArray(wrRegList) ? wrRegList : []; set((state: pubFacState) => ({ ...state, pbnst: { ...state.pbnst, pbnstRes: newPbnstRes, + fclts, }, })); return successMessage; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx index eafbc4477..3a9756693 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx @@ -62,12 +62,22 @@ export type PbnstDemandComType = { export const pubHasuZod = z.object({ choose: z.boolean().default(false), bscFctCd: z.string(), + rvbsnCd: z.string().default(""), // 권역코드 + rvbsn: z.string().default(""), + lgbsn: z.string().default(""), + mdmbsn: z.string().default(""), + rgnNm: z.string().default(""), + rgnCd: z.string().default(""), // 지역코드 + adstDstrCd: z.string().default(""), // 행정구역코드 sido: z.string().default(""), // 시도 gugun: z.string().default(""), // 구군 bscFctNm: z.string().default(""), // 시설명 efflWqltStndrdNm: z.string().default(""), // 지역구분 + tel: z.string().default(""), fctCpc: z.number().default(0), // 시설용량 addr: z.string().nullish(), // 위치 + ruse: z.string().default("N"), // 등록여부 + rswtrYn: z.string().default("N"), // 재이용여부 }); export type pubHasuType = z.infer<typeof pubHasuZod>; @@ -258,6 +268,15 @@ const PrfSchema = z.object({ export const pbnstRegZod = z.object({ pbnstRuseFcltId: z.string().default(""), swtrmtFcltId: z.string().default(""), + swageTrtmfctId: z.string().default(""), + swageBscFctNm: z.string().default(""), + swageRgnCd: z.string().default(""), + swageAdstDstrCd: z.string().default(""), + swageRvbsnCd: z.string().default(""), + swageFctCpc: z.preprocess( + (v) => (v !== null && v !== undefined ? Number(v) : v), + z.number().default(0) + ), ruseUtztnFcltNm: //z.string().default(''), z.preprocess( (v) => (v !== null && v !== undefined ? String(v).trim() : ""), // null/undefined → "", 공백 제거 @@ -283,7 +302,7 @@ export const pbnstRegZod = z.object({ fcOpers://z.array(fcOperSchema).default([]), z .array(fcOperSchema) .min(1, { message: "재이용시설의 운영정보를 최소 하나를 등록해주세요" }), - prfList: z.array(PrfSchema).nullish(), + prfList: z.array(PrfSchema).nullish(), }); diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx index ce6da6334..29c8e529d 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/rainReg/index.tsx @@ -69,15 +69,15 @@ export type chgBuilding = { export type customRainBdst = z.infer<typeof rainZod>; export const upHeadList: TableUpperProps[] = [ - { id: "cityText", title: "시도" }, - { id: "sggText", title: "시군구" }, - { id: "bdstNm", title: "시설명" }, - { id: "daddr", title: "위치" }, - { id: "bdsgUsgText", title: "건축물용도" }, - { id: "frstArchPrmsnDt", title: "최초건축허가일" }, - { id: "archArea", title: "건축면적(㎡)" }, - { id: "lawTrgtYn", title: "법적의무대상여부" }, - { id: "bdstChgYn", title: "건축물변경여부" }, + { id: "cityText", title: "시도", widthPercent: 10 }, + { id: "sggText", title: "시군구", widthPercent: 8 }, + { id: "bdstNm", title: "시설명", widthPercent: 15 }, + { id: "daddr", title: "위치", widthPercent: 22 }, + { id: "bdsgUsgText", title: "건축물용도", widthPercent: 12 }, + { id: "frstArchPrmsnDt", title: "최초\n건축허가일", widthPercent: 9 }, + { id: "archArea", title: "건축면적(㎡)", widthPercent: 9 }, + { id: "lawTrgtYn", title: "법적의무대상여부", widthPercent: 8 }, + { id: "bdstChgYn", title: "건축물변경여부", widthPercent: 7 }, ]; const rainRes: customRainBdst[] = []; diff --git a/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx index 816d6ab52..82915f4c7 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltReg/rswtReg/index.tsx @@ -1,5 +1,5 @@ import { deleteFectch, getFetchWithCount, postFectch, putFectch } from "@/components/lib/api"; -import { upHeadList, type chgBuilding } from "../rainReg"; +import { type chgBuilding } from "../rainReg"; import { DEFAULT_LIST_SIZE } from "@/src/shared/config"; import type { TableUpperProps } from "@/app/store/common/type/header"; @@ -57,6 +57,19 @@ export type customRswtBdst = z.infer<typeof rswtZod>; const rswtRes: customRswtBdst[] = []; +export const rswtUpHeadList: TableUpperProps[] = [ + { id: "cityText", title: "시도", widthPercent: 8 }, + { id: "sggText", title: "시군구", widthPercent: 7 }, + { id: "bdstNm", title: "건축물명", widthPercent: 14 }, + { id: "daddr", title: "위치", widthPercent: 20 }, + { id: "bdstUsgText", title: "건축물용도", widthPercent: 11 }, + { id: "frstArchPrmsnDt", title: "최초\n건축허가일", widthPercent: 9 }, + { id: "archArea", title: "건축면적(㎡)", widthPercent: 8 }, + { id: "lawTrgtYn", title: "법적의무대상여부", widthPercent: 8 }, + { id: "chBuildingYn", title: "증축/개축/재축여부", widthPercent: 8 }, + { id: "instlCmptnDt", title: "설치완료일시", widthPercent: 7 }, +]; + export type rswtFacState = { rswt: { upHeadList: TableUpperProps[]; @@ -83,7 +96,7 @@ export type rswtFacType = rswtFacState & rswtFacSActions; export const rswtFacStateExport: rswtFacState = { rswt: { - upHeadList: upHeadList, + upHeadList: rswtUpHeadList, rswtRes: rswtRes, rswtResCount: 1, }, diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx index 82e256a6e..e79398c18 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx @@ -1,5 +1,5 @@ import type { CommonTableProps } from "@/app/store/common/type/header"; -import { fetchType, getFetchWithCount } from "@/components/lib/api"; +import { getFetchWithCount } from "@/components/lib/api"; import type { SelectDataType } from "@common/business_components/ui"; import type { z } from "zod"; import type { customHtdflBdst } from "../../fcltReg/htdflReg"; @@ -26,10 +26,10 @@ export type htdflSttnState = { type htdflSttnActions = { initHtdfl: (isExcel: boolean, page: number, total: number, con: string, txt: string, sidoCode: SelectDataType[], - sggCode: SelectDataType[]) => void; + sggCode: SelectDataType[]) => Promise<string[][] | void>; htdflSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, sidoCode: SelectDataType[], - sggCode: SelectDataType[]) => void; + sggCode: SelectDataType[]) => Promise<string[][] | void>; htdflChSttnHead: ( headList: z.infer<typeof sttnHeadListType>, ) => void; @@ -63,36 +63,51 @@ const initFetch = async (page: number, total: number, con: string, txt: string) export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( set: any ) => { + const fetchHtdflSttn = async ( + isExcel: boolean, + page: number, + total: number, + con: string, + txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[] + ): Promise<string[][] | void> => { + const htdflSttnRes = await initFetch(page, total, con, txt); + if (isExcel) { + const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); + set((state: htdflSttnState) => { + return { + ...state, + htdflSttn: { + ...state.htdflSttn, + excelList: htdflExcel, + htdflSttnResCount: htdflSttnRes.count, + }, + }; + }); + return htdflExcel; + } + + set((state: htdflSttnState) => { + return { + ...state, + htdflSttn: { + ...state.htdflSttn, + htdflSttnRes: htdflSttnRes.data, + htdflSttnResCount: htdflSttnRes.count, + isInit: false, + }, + }; + }); + return; + }; + return { initHtdfl: async (isExcel: boolean, page: number, total: number, con: string, txt: string, sidoCode: SelectDataType[], sggCode: SelectDataType[]) => { try { - const htdflSttnRes = await initFetch(page, total, con, txt); - if (isExcel) { - const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - excelList: htdflExcel, - }, - }; - }); - } else { - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - htdflSttnRes: htdflSttnRes.data, - htdflSttnResCount: htdflSttnRes.count, - isInit: false, - }, - }; - }); - } + return await fetchHtdflSttn(isExcel, page, total, con, txt, sidoCode, sggCode); } catch (error) { console.log(error); } @@ -101,38 +116,7 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( sidoCode: SelectDataType[], sggCode: SelectDataType[]) => { try { - const htdflSttnRes = await getFetchWithCount( - fetchType.WR, - `/drain/facilityList` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/20") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - - if (isExcel) { - const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - excelList: htdflExcel, - }, - }; - }); - } else - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - htdflSttnRes: htdflSttnRes.data, - htdflSttnResCount: htdflSttnRes.count, - isInit: false, - }, - }; - }); + return await fetchHtdflSttn(isExcel, page, total, con, txt, sidoCode, sggCode); } catch (erTxt) { console.log("error", erTxt); } @@ -154,4 +138,4 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( } -} \ No newline at end of file +} diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx index 71c4c01d7..c6a50bf5c 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/headList.tsx @@ -63,6 +63,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [ { id: "endDt", title: "종료일", upName: "mngInfo", visible: true, row: 2, rowSpan: 1, colSpan: 1, }, ]; +// 하수처리수 재이용시설 현황 tab 상단 export const ruseFcltNodeListExport: TableUpperProps[] = [ { id: "cycl", title: "차수", visible: true, row: 1, rowSpan: 3, colSpan: 1 }, { id: "seCd", title: "구분", visible: true, row: 1, rowSpan: 3, colSpan: 1 }, @@ -84,6 +85,7 @@ export const ruseFcltNodeListExport: TableUpperProps[] = [ { id: "totalMoney", title: "합계", visible: true, row: 3, rowSpan: 1, colSpan: 1 }, ]; +// 재이용 원수 취수 현황 export const ruseSplyHeadListExport: TableUpperProps[] = [ { id: "cycl", title: "차수", visible: true }, { id: "orgwtWtritQnt", title: "원수취수량(m³/일)", visible: true }, @@ -102,7 +104,7 @@ export const rusePplHeadListExport: TableUpperProps[] = [ export const ruseGuranteeHeadListExport: TableUpperProps[] = [ { id: "cycl", title: "차수", visible: true }, { id: "bod", title: "BOD(mg/L)", visible: true }, - { id: "grssClfg", title: "총대장균군수(개/mg)", visible: true }, + { id: "grssClfg", title: "총대장균군수(개/100㎖)", visible: true }, { id: "combResdch", title: "결합잔류염소(mh/L)", visible: true }, { id: "trbt", title: "탁도(NTU)", visible: true }, { id: "smll", title: "냄새", visible: true }, @@ -125,6 +127,7 @@ export const ruseDmdHeadListExport: TableUpperProps[] = [ { id: "splyFmYon", title: "공급유량계 유무", visible: true }, ]; +// 하수처리수 재이용시설 현황 tab 하단 export const ruseFcltHeadListExport: CommonTableProps[] = [ { id: "cycl", title: "차수", visible: true}, { id: "seCd", title: "구분", visible: true}, diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx index 35a3fb3d7..0aca48f71 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx @@ -2,10 +2,8 @@ import type { CommonTableProps, TableUpperProps, } from "@/app/store/common/type/header"; -import type { FcPrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; -import { fetchType, getFetch, postFectch } from "@/components/lib/api"; +import { fetchType, getFetch, getFetchWithCount, postFectch } from "@/components/lib/api"; import type { ReturnType } from "@common/business_components/ui"; -import moment from "moment"; import { type z } from "zod"; import type { customPbnst, pubHasuType } from "../../fcltReg/pbnstReg/type"; import { controlHeadView } from "../rainSttn"; @@ -34,7 +32,6 @@ const pbnstSttNodeList = pbnstSttNodeListExport; export type pbnstSttnState = { pbnstSttn: { pbnstSttnRes: WaterReuseFacility[]; - pbnstSearchData: WaterReuseFacility[]; pbnstSttHeadList: CommonTableProps[]; pbnstSttNodeList: CommonTableProps[]; pbnstDetail: customPbnst; @@ -46,39 +43,21 @@ export type pbnstSttnState = { ruseFcltHeadList: TableUpperProps[]; pbnstSttnResCount: number; excelList: []; - fclts: string[]; - isInit: boolean; + isTableLoaded: boolean; // 현황 테이블 초기 조회 여부 }; }; type pbnstSttnActions = { pbnstChSttnHead: (headList: z.infer<typeof sttnHeadListType>) => void; - pbnstSttnSearch: ( + pbnstSttnList: ( isExcel: boolean, page: string, total: string, reurnValue: ReturnType, searchNm: string | undefined, - year: string, - fclts: string[] - ) => void; - getFlcts: () => void; - pbnSttnInit: ( - page: string, - total: string, - reurnValue: ReturnType, - year: string, - flcts: string[] - ) => void; - getPbnStnDetail: (pbnstRuseFcltId: string) => void; - pbnNmSearch: ( - page: string, - total: string, - reurnValue: ReturnType, - searchNm: string | undefined, - year: string, - flcts: string[] + year: string ) => void; + getPbnStnDetail: (pbnstRuseFcltId: string, year: string) => void; resetPbnstDetail: () => void; }; @@ -88,175 +67,177 @@ export const pbnstSttnStateExport: pbnstSttnState = { pbnstSttn: { pbnstSttHeadList: pbnstSttHeadList, pbnstSttNodeList: pbnstSttNodeList, - ruseFcltNodeList: ruseFcltNodeListExport, + ruseFcltNodeList: ruseFcltNodeListExport, // 하수재이용시설 상세정보 - 하수처리수 재이용시설 현황 tab 상단 ruseSplyHeadList: ruseSplyHeadListExport, rusePplHeadList: rusePplHeadListExport, ruseGuranteeHeadList: ruseGuranteeHeadListExport, ruseDmdHeadList: ruseDmdHeadListExport, - ruseFcltHeadList: ruseFcltHeadListExport, + ruseFcltHeadList: ruseFcltHeadListExport, // 하수재이용시설 상세정보 - 하수처리수 재이용시설 현황 tab 하단 pbnstSttnRes: [], pbnstSttnResCount: 1, pbnstDetail: {} as customPbnst, - pbnstSearchData: [], excelList: [], - fclts: [], - isInit: false, + isTableLoaded: false, }, }; -export const hasuListFetch = async ( +export const pbnstListFetch = async ( page: string, total: string, reurnValue: ReturnType, searchNm: string | undefined, - year: string, - flcts: string[] + year: string ) => { + let nameCd = "0"; + let partStr = "A"; - // 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환 - if (flcts.length === 0) { - return { data: [], count: 0 }; - } - - let url = ``; - let filteredFclt = flcts; - let nameCd="0", partStr="A", sidogunValue="0"; switch (reurnValue.partValue) { - case 'R': // 유역별 - if (reurnValue.regionValue !== '0') { - if(reurnValue.unitWaterShedValue==='0') nameCd = reurnValue.regionValue; - else if (reurnValue.middleRegionValue==='0') nameCd = reurnValue.unitWaterShedValue; + case "R": + if (reurnValue.regionValue !== "0") { + if (reurnValue.unitWaterShedValue === "0") nameCd = reurnValue.regionValue; + else if (reurnValue.middleRegionValue === "0") nameCd = reurnValue.unitWaterShedValue; else nameCd = reurnValue.middleRegionValue; } partStr = reurnValue.partValue; break; default: - if(reurnValue.sidoValue && reurnValue.sidoValue !== '0') nameCd = reurnValue.sidoValue; - if(reurnValue.sigunValue &&reurnValue.sigunValue !== '0') nameCd = reurnValue.sigunValue; - sidogunValue = nameCd; - partStr = reurnValue.envPartValue !== '0' ? reurnValue.envPartValue:'A'; + if (reurnValue.sidoValue && reurnValue.sidoValue !== "0") nameCd = reurnValue.sidoValue; + if (reurnValue.sigunValue && reurnValue.sigunValue !== "0") nameCd = reurnValue.sigunValue; + partStr = reurnValue.envPartValue !== "0" ? reurnValue.envPartValue : "A"; break; } - if (sidogunValue && sidogunValue !== "0") { // 시도, 시군구 조회 조건이 있다면 - filteredFclt = flcts.filter((fclt: string) => { - return sidogunValue.length == 2 - ? fclt.slice(0, 2) === sidogunValue - : fclt.slice(0, 5) === sidogunValue; - }); + let url = `/reuseFc/pbnstList/${year}/${page}/${total}/${partStr}/${nameCd}/0/1/SW`; + if (searchNm && searchNm.length > 0) { + url += `?swtrmtFcltNm=${searchNm}`; } - - url = `${partStr}/${nameCd}/0/1/SW`; - if (searchNm && searchNm?.length > 0) { - url += "/" + searchNm; + const response = await getFetchWithCount(fetchType.WR, url); + const wrList: WaterReuseFacility[] = Array.isArray(response.data) + ? response.data + : []; + + const swtrmtFcltIds = [ + ...new Set( + wrList + .map((item: WaterReuseFacility) => { + if (item.swtrmtFcltId && item.swtrmtFcltId.length > 0) { + return item.swtrmtFcltId; + } + + if (item.pbnstRuseFcltId && item.pbnstRuseFcltId.length >= 11) { + return item.pbnstRuseFcltId.substring(0, 11); + } + + return ""; + }) + .filter(Boolean) + ), + ]; + + let pbnListData: pubHasuType[] = []; + + if (swtrmtFcltIds.length > 0) { + let foUrl = `/prcsFclt/ruseInfo/1/${swtrmtFcltIds.length + 1}`; + if (searchNm && searchNm.length > 0) { + foUrl += `/${searchNm}`; + } + + const pbnList = await postFectch( + foUrl, + swtrmtFcltIds, + fetchType.COMMON + ); + + pbnListData = Array.isArray(pbnList?.data) ? pbnList.data : []; } - // console.log(url); - // 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환 - if (filteredFclt.length === 0) { - return { data: [], count: 0 }; - } - - // fo의 하수처리시설을 조회 - const pbnList = await postFectch( - `/prcsFclt/ruseInfo/1/${filteredFclt.length}/` + url, - filteredFclt, - fetchType.COMMON // FO - ); + const data: WaterReuseFacility[] = wrList.map((item: WaterReuseFacility) => { + const swtrmtFcltId = + item.swtrmtFcltId && item.swtrmtFcltId.length > 0 + ? item.swtrmtFcltId + : item.pbnstRuseFcltId?.substring(0, 11) ?? ""; - const checkRuseFcltList = pbnList.data.map((item) => item.bscFctCd); - // console.log(checkRuseFcltList); - + const findData = pbnListData.find( + (fclt: pubHasuType) => fclt.bscFctCd === swtrmtFcltId + ); - const getRuseList = await postFectch( - `/reuseFc/pubDetailList/${year}/${sidogunValue}/${page}/${total}`, - checkRuseFcltList, - fetchType.WR - ); - const checkWrRegListRes = getRuseList.data; - // console.log(getRuseList); + return { + ...item, + swtrmtFcltId, + rvbsn: findData?.rvbsn || item.rvbsn || "-", + lgbsn: findData?.lgbsn || item.lgbsn || "-", + mdmbsn: findData?.mdmbsn || item.mdmbsn || "-", + rgnNm: findData?.rgnNm || item.rgnNm || "-", + sido: findData?.sido || item.sido || "-", + gugun: findData?.gugun || item.gugun || "-", + bscFctNm: findData?.bscFctNm || item.bscFctNm || "-", + efflWqltStndrdNm: + findData?.efflWqltStndrdNm || item.efflWqltStndrdNm || "-", + tel: findData?.tel || item.tel || "-", + fctCpc: Number(findData?.fctCpc ?? item.fctCpc) || 0, + ruseUtztnFcltNm: item.ruseUtztnFcltNm || "-", + fcltCpct: Number(item.fcltCpct) || 0, + splyExtraCpct: Number(item.splyExtraCpct) || 0, + ruseCpct: Number(item.ruseCpct) || 0, + usageTotal: Number(item.usageTotal) || 0, + inUsage: Number(item.inUsage) || 0, + outUsage: Number(item.outUsage) || 0, + operMthd: item.operMthd || "-", + rprtvBzenty: item.rprtvBzenty || "-", + bgngDt: item.bgngDt || "", + endDt: item.endDt || "", + }; + }); - const data: WaterReuseFacility[] = checkWrRegListRes - .map((item: FcPrfType) => { - const findData = pbnList.data.find( - (fclt: pubHasuType) => - fclt.bscFctCd === item.pbnstRuseFcltId.substring(0, 11) - ); - - if (findData) { - const usage = item.prfList.reduce( - (acc, cur) => { - acc.inUsage += - cur.wshwtr + - cur.coolUswtr + - cur.cleanUswtr + - cur.drwtUswtr + - cur.dltnUswtr + - cur.ithlRswtr + - cur.ithlEtcUswtr + - cur.cleanToilUswtr + - cur.resn; - acc.outUsage += - cur.lndwtr + - cur.hydUswtr + - cur.ugwtChr + - cur.farmUswtr + - cur.rvrKeepUswtr + - cur.indstUswtr; - acc.total = acc.inUsage + acc.outUsage; - - return acc; - }, - { total: 0, inUsage: 0, outUsage: 0 } - ); - return { - rvbsn: findData?.rvbsn || "-", - lgbsn: findData?.lgbsn || "-", - mdmbsn: findData?.mdmbsn || "-", - rgnNm: findData?.rgnNm || "-", - sido: findData?.sido || "-", - gugun: findData?.gugun || "-", - bscFctNm: findData?.bscFctNm || "-", - efflWqltStndrdNm: findData?.efflWqltStndrdNm || "-", - fctCpc: Number(findData?.fctCpc) || 0, - tel: findData?.tel ?? "-", - ruseUtztnFcltNm: item?.ruseUtztnFcltNm || "-", - fcltCpct: item?.fcltCpct || "-", - splyExtraCpct: item?.splyExtraCpct || "-", - ruseCpct: item?.ruseCpct || "-", - pbnstRuseFcltId: item.pbnstRuseFcltId || "-", - usageTotal: usage?.total, - inUsage: usage?.inUsage, - outUsage: usage?.outUsage, - operMthd: item.operList[0]?.operMthd || "-", - rprtvBzenty: item.operList[0]?.rprtvBzenty, - bgngDt: moment(item.operList[0]?.bgngDt).format("YYYY-MM-DD") ?? "", - endDt: moment(item.operList[0]?.endDt).format("YYYY-MM-DD") ?? "", - searchNm: findData?.bscFctNm || "-", - searchPk: findData?.bscFctCd || "-", - prfList: item?.prfList ?? [], - }; - } - }) - .filter(Boolean); - - return { data: data, count: getRuseList.count }; + return { data, count: response.count }; }; export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( set: any ) => { return { - //등록된 하수재이용시설을 조회한다. - getFlcts: async () => { - const fclts = await getFetch(fetchType.WR, "/reuseFc/pubFc"); - return set((state: pbnstSttnState) => ({ - pbnstSttn: { - ...state.pbnstSttn, - fclts: fclts, - }, - })); + pbnstSttnList: async ( + isExcel: boolean, + page: string, + total: string, + reurnValue: ReturnType, + searchNm: string | undefined, + year: string + ) => { + try { + const pbnstSttnRes = await pbnstListFetch( + page, + total, + reurnValue, + searchNm, + year + ); + + if (isExcel) { + const pbnstExcel = excelPbnstSttnParsing(pbnstSttnRes.data); + set((state: pbnstSttnState) => ({ + ...state, + pbnstSttn: { + ...state.pbnstSttn, + excelList: pbnstExcel, + }, + })); + } else { + set((state: pbnstSttnState) => ({ + ...state, + pbnstSttn: { + ...state.pbnstSttn, + pbnstSttnRes: pbnstSttnRes.data, + pbnstSttnResCount: pbnstSttnRes.count, + pbnstDetail: {} as customPbnst, + isTableLoaded: true, + }, + })); + } + } catch (error) { + console.log("error", error); + } }, //테이블 헤드를 조정한다. pbnstChSttnHead: async ( @@ -272,90 +253,6 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( }, })); }, - //하수처리시설을 조회하여 하수재이용시설을 확인한다. - pbnstSttnSearch: async ( - isExcel: boolean, - page: string, - total: string, - reurnValue: ReturnType, // 검색조건 - searchNm: string | undefined, - year: string, - fclts: string[] - ) => { - try { - // console.log(reurnValue); - const pbnstSttnRes = await hasuListFetch( - page, - total, - reurnValue, - searchNm, - year, - fclts - ); - - // if (Number(total) > 9000) { - if (isExcel) { - const pbnstExcel = excelPbnstSttnParsing(pbnstSttnRes.data); - set((state: pbnstSttnState) => { - return { - ...state, - pbnstSttn: { - ...state.pbnstSttn, - excelList: pbnstExcel, - }, - }; - }); - } else { - set((state: pbnstSttnState) => { - return { - ...state, - pbnstSttn: { - ...state.pbnstSttn, - pbnstSttnRes: pbnstSttnRes.data, - pbnstSttnResCount: pbnstSttnRes.count, - // isInit: false, - }, - }; - }); - } - } catch (erTxt) { - console.log("error", erTxt); - } - }, - //하수처리시설을 조회하여 하수재이용시설을 조회한다. - pbnSttnInit: async ( - page: string, - total: string, - reurnValue: ReturnType, - year: string, - fclts: string[] - ) => { - try { - const pbnstSttnRes = await hasuListFetch( - page, - total, - reurnValue, - undefined, - year, - fclts - ); - - set((state: pbnstSttnState) => { - return { - ...state, - pbnstSttn: { - ...state.pbnstSttn, - pbnstSttnRes: pbnstSttnRes.data, - pbnstSttnResCount: pbnstSttnRes.count, - isInit: false, - pbnstDetail: {} as customPbnst, - }, - }; - }); - } catch (erTxt) { - console.log("error", erTxt); - } - }, // 팝업에 있는 데이터를 초기화한다. resetPbnstDetail: async () => { set((state: pbnstSttnState) => { @@ -369,17 +266,18 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( }); }, //하수처리시설을 조회하여 하수재이용시설을 조회한다. - getPbnStnDetail: async (pbnstRuseFcltId: string) => { + getPbnStnDetail: async (pbnstRuseFcltId: string, year: string) => { try { - let pbnstDetail = await getFetch( + const pbnstDetailList = await getFetch( "wr", - `/reuseFc/pubFc/${pbnstRuseFcltId}` + `/reuseFc/pubFc/${pbnstRuseFcltId}?year=${year}` ); - if (pbnstDetail.length >= 0) { - pbnstDetail = pbnstDetail.find( - (item: customPbnst) => item.pbnstRuseFcltId === pbnstRuseFcltId - ); - } + const pbnstDetail = Array.isArray(pbnstDetailList) + ? pbnstDetailList.find( + (item: customPbnst) => item.pbnstRuseFcltId === pbnstRuseFcltId + ) + : undefined; +console.log(pbnstDetail); set((state: pbnstSttnState) => { return { ...state, @@ -393,38 +291,5 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = ( console.log(errorHasu); } }, - //하수재이용시설 실적 현황 에서 사용하는 조회 - pbnNmSearch: async ( - page: string, - total: string, - reurnValue: ReturnType, - searchNm: string | undefined, - year: string, - flcts: string[] - ) => { - try { - const pbnstSttnRes = await hasuListFetch( - page, - total, - reurnValue, - searchNm, - year, - flcts - ); - - set((state: pbnstSttnState) => { - return { - ...state, - pbnstSttn: { - ...state.pbnstSttn, - pbnstSearchData: pbnstSttnRes.data, - pbnstSttnResCount: pbnstSttnRes.count, - }, - }; - }); - } catch (e) { - console.log(e); - } - }, }; }; diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx index 7d0f8a068..955ecd270 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type.tsx @@ -1,10 +1,9 @@ -import type { PrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; - export type WaterReuseFacility = { + swtrmtFcltId?: string; bscFctNm: string; // 기본시설명 (예: "난지") efflWqltStndrdNm: string; // 방류수 수질 기준명 (예: "500톤 이상(Ⅲ지역)") fcltCpct: number; // 시설 용량 (숫자형, 예: 1) - fctCpc: string; // 시설 처리 능력 (예: "2360000") + fctCpc: number; // 시설 처리 능력 (예: 2360000) gugun: string; // 구군 정보 (예: " ") inUsage: number; // 유입량 (예: 1387) lgbsn: string; // 대수계 (예: "한강본류") @@ -23,5 +22,4 @@ export type WaterReuseFacility = { rprtvBzenty: string; bgngDt: string; endDt: string; - prfList:PrfType[]; }; diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx index b278a9a22..ff06ea9d8 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx @@ -102,8 +102,8 @@ export type rainSttnState = { }; }; type rainSttnActions = { - rainSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]> ) => void - rainSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]>) => void; + rainSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]> ) => Promise<string[][] | void> + rainSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]>) => Promise<string[][] | void>; rainChSttnHead: ( headList: z.infer<typeof sttnHeadListType>, ) => void; @@ -129,79 +129,63 @@ export const rainSttnStateExport: rainSttnState = { export const rainSttnActionsExport: (set: any) => rainSttnActions = ( set: any ) => { + const fetchRainSttn = async ( + isExcel: boolean, + page: number, + total: number, + con: string, + txt: string, + code: Record<string, SelectDataType[]> + ): Promise<string[][] | void> => { + const rainSttnRes = await getFetchWithCount( + fetchType.WR, + `/reuseFc/rainyInfoList` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/15") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); + + if (isExcel) { + const rainExcel = excelRainSttnParsing(rainSttnRes, code); + set((state: rainSttnState) => { + return { + ...state, + rainSttn: { + ...state.rainSttn, + excelList: rainExcel, + rainSttnResCount: rainSttnRes.count, + }, + }; + }); + return rainExcel; + } + + set((state: rainSttnState) => { + return { + ...state, + rainSttn: { + ...state.rainSttn, + rainSttnRes: rainSttnRes.data, + rainSttnResCount: rainSttnRes.count, + isInit: false, + }, + }; + }); + return; + }; + return { rainSttnInit: async (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string, SelectDataType[]>) => { try { - - const rainSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/rainyInfoList` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/15") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rainExcel = excelRainSttnParsing(rainSttnRes, code); - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - excelList: rainExcel, - }, - }; - }); - } else { - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - rainSttnRes: rainSttnRes.data, - isInit: false, - }, - }; - }); - } + return await fetchRainSttn(isExcel, page, total, con, txt, code); } catch (e) { console.log(e) } }, rainSttnSearch: async (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]>) => { try { - const rainSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/rainyInfoList` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/15") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rainExcel = excelRainSttnParsing(rainSttnRes,code); - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - excelList: rainExcel, - rainSttnResCount: rainSttnRes.count, - }, - }; - }); - } else - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - rainSttnRes: rainSttnRes.data, - rainSttnResCount: rainSttnRes.count, - isInit: false, - }, - }; - }); + return await fetchRainSttn(isExcel, page, total, con, txt, code); } catch (erTxt) { console.log("error", erTxt); } diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx index 47444ce32..df5cd9735 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx @@ -30,8 +30,8 @@ export type rswtSttnState = { }; type rswtSttnActions = { - rswtSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => void; - rswtSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => void; + rswtSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => Promise<string[][] | void>; + rswtSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => Promise<string[][] | void>; rswtChSttnHead: ( headList: z.infer<typeof sttnHeadListType>, ) => void; @@ -55,79 +55,62 @@ export const rswtSttnStateExport: rswtSttnState = { export const rswtSttnActionsExport: (set: any) => rswtSttnActions = ( set: any ) => { + const fetchRswtSttn = async ( + isExcel: boolean, + page: number, + total: number, + con: string, + txt: string, + code: Record<string, SelectDataType[]> + ): Promise<string[][] | void> => { + const rswtSttnRes = await getFetchWithCount( + fetchType.WR, + `/reuseFc/waterreuseFc` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/10") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); + if (isExcel) { + const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code); + set((state: rswtSttnState) => { + return { + ...state, + rswtSttn: { + ...state.rswtSttn, + excelList: rswtExcel, + rswtSttnResCount: rswtSttnRes.count, + }, + }; + }); + return rswtExcel; + } + + set((state: rswtSttnState) => { + return { + ...state, + rswtSttn: { + ...state.rswtSttn, + rswtSttnRes: rswtSttnRes.data, + rswtSttnResCount: rswtSttnRes.count, + isInit: false, + }, + }; + }); + return; + }; + return { rswtSttnInit: async (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => { try { - const rswtSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/waterreuseFc` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/10") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code); - set((state: rswtSttnState) => { - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - excelList: rswtExcel, - }, - }; - }); - } - set((state: rswtSttnState) => { - - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - rswtSttnRes: rswtSttnRes.data, - rswtSttnResCount: rswtSttnRes.count, - isInit: false, - }, - }; - }); + return await fetchRswtSttn(isExcel, page, total, con, txt, code); } catch (e) { console.log(e) } }, rswtSttnSearch: async (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string,SelectDataType[]>) => { try { - const rswtSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/waterreuseFc` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/10") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code); - set((state: rswtSttnState) => { - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - excelList: rswtExcel, - }, - }; - }); - } else - set((state: rswtSttnState) => { - - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - rswtSttnRes: rswtSttnRes.data, - rswtSttnResCount: rswtSttnRes.count, - isInit: false, - }, - }; - }); + return await fetchRswtSttn(isExcel, page, total, con, txt, code); } catch (erTxt) { console.log("error", erTxt); } diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index fbb3434d9..268be198b 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -2,13 +2,6 @@ import { createStore } from "zustand/vanilla"; import { codeActionExport, comCodeStateExport, - getCityCd, - getCommonUpCd, - getEnvPartCd, - getMiddleRegionCd, - getRegionCd, - getSggCd, - getUnitWaterShedCd, type comCodeState, type comCodeType, } from "./common/code"; @@ -336,17 +329,17 @@ export const StoreZus = ( } ) => { return createStore<WrStore>()((set ,get) => { - getSggCd(set); - getCityCd(set); - getRegionCd(set); - getMiddleRegionCd(set); - getUnitWaterShedCd(set); - getEnvPartCd(set); - getCommonUpCd(set) + // getSggCd(set); + // getCityCd(set); + // getRegionCd(set); + // getMiddleRegionCd(set); + // getUnitWaterShedCd(set); + // getEnvPartCd(set); + // getCommonUpCd(set); return { ...initState, ...comFcltSearchAction(set), - ...pubFacActionsExport(set), + ...pubFacActionsExport(set, get), ...rainFacActionsExport(set), ...rswtFacActionsExport(set), ...matDmdActionsExport(set), @@ -373,7 +366,7 @@ export const StoreZus = ( ...rswtStatActionsExport(set), ...authActionsExport(set,get), ...commonMapActionsExport(set), - ...pbnMapActionsExport(set), + ...pbnMapActionsExport(set, get), ...rainMapActionsExport(set), ...rswtMapActionsExport(set), ...htdfMapActionsExport(set), diff --git a/apps/wr/components/lib/class/comClass.tsx b/apps/wr/components/lib/class/comClass.tsx index 654b006d7..603e79660 100644 --- a/apps/wr/components/lib/class/comClass.tsx +++ b/apps/wr/components/lib/class/comClass.tsx @@ -1,4 +1,4 @@ export const formTableName = 'text-xl my-4 font-bold' export const commonButonSize = "sm" -export const formH1Class = "bg-[#eaf3fb] text-xl text-center border-2 p-2 mt-4 font-medium text-gray-500" +export const formH1Class = "form-section-title" export const formH2Class = "text-xl my-4 underline font-bold" diff --git a/apps/wr/components/lib/func/TableData.tsx b/apps/wr/components/lib/func/TableData.tsx index b2411eea4..ad0526532 100644 --- a/apps/wr/components/lib/func/TableData.tsx +++ b/apps/wr/components/lib/func/TableData.tsx @@ -29,8 +29,8 @@ export const rainChkTableData = (chkData: rainChkZodType[]) => { totalRuseFcltUtztnQnt: 0, }; - acc[key].totalSplyUswtrUtztnQnt += cur.splyUswtrUtztnQnt; - acc[key].totalRuseFcltUtztnQnt += cur.ruseFcltUtztnQnt; + acc[key].totalSplyUswtrUtztnQnt += cur.splyUswtrUtztnQnt; // 연간대체수이용량(m³) + acc[key].totalRuseFcltUtztnQnt += cur.ruseFcltUtztnQnt; // 연간빗물이용량(m³) return acc; }, {}); @@ -252,7 +252,7 @@ export const chRowspanTable = ( "splyUswtrUtztnQnt", "ruseFcltUtztnQnt", ].includes(head.id) - ? "underline text-blue-600 cursor-pointer" + ? "cursor-pointer underline text-primary" : "" } > @@ -270,7 +270,7 @@ export const chRowspanTable = ( <TableRow> <TableCell colSpan={headList.length} - className="h-24 text-center text-gray-500" + className="h-24 text-center text-muted-foreground" > 조회된 결과가 없습니다. </TableCell> @@ -361,9 +361,9 @@ export const InputTableBody = ( const isTotal = data["구분"] === 0; const bgClass = isTotal - ? "bg-[#d5e1eb] font-bold" + ? "bg-accent text-accent-foreground font-bold" : isSubTotal - ? "bg-[#e6eef5] font-semibold" + ? "bg-secondary text-secondary-foreground font-semibold" : ""; return ( @@ -381,7 +381,7 @@ export const InputTableBody = ( <TableCell key={head.id + headIndex} colSpan={2} - className="font-bold text-center bg-[#d5e1eb]" + className="bg-accent text-accent-foreground font-bold text-center" > 총합계 </TableCell> @@ -397,7 +397,7 @@ export const InputTableBody = ( return ( <TableCell key={head.id + headIndex} - className="bg-[#d5e1eb] font-bold text-right" + className="bg-accent text-accent-foreground font-bold text-right" > {Number(data[head.id]) ? formatCommaNumber(data[head.id as keyof any]) @@ -482,7 +482,7 @@ export const InputTableBody = ( <TableRow> <TableCell colSpan={headList.length} - className="h-24 text-center text-gray-500" + className="h-24 text-center text-muted-foreground" > 조회된 결과가 없습니다. </TableCell> @@ -519,9 +519,9 @@ export const pbnstTableBody = ( const isTotal = data["구분"] === 0; const bgClass = isTotal - ? "bg-[#d5e1eb] font-bold" + ? "bg-accent text-accent-foreground font-bold" : isSubTotal - ? "bg-[#e6eef5] font-semibold" + ? "bg-secondary text-secondary-foreground font-semibold" : ""; return ( @@ -552,7 +552,7 @@ export const pbnstTableBody = ( <TableCell key={head.id + headIndex} colSpan={2} - className="font-bold text-center bg-[#d5e1eb]" + className="bg-accent text-accent-foreground font-bold text-center" > 총합계 </TableCell> @@ -571,7 +571,7 @@ export const pbnstTableBody = ( return ( <TableCell key={head.id + headIndex} - className="bg-[#d5e1eb] font-bold text-right" + className="bg-accent text-accent-foreground font-bold text-right" > {Number(data[head.id]) ? formatCommaNumber(data[head.id as keyof any]) @@ -645,7 +645,7 @@ export const pbnstTableBody = ( <TableRow> <TableCell colSpan={headList.length} - className="h-24 text-center text-gray-500" + className="h-24 text-center text-muted-foreground" > 조회된 결과가 없습니다. </TableCell> diff --git a/apps/wr/components/lib/func/common.tsx b/apps/wr/components/lib/func/common.tsx index 1d9d4a43e..cef465cf3 100644 --- a/apps/wr/components/lib/func/common.tsx +++ b/apps/wr/components/lib/func/common.tsx @@ -66,9 +66,9 @@ export const tableData = (data: any, code: comCodeState) => { ...item, sggText: sggText ?? "없음", cityText: cityText ?? "없음", - bdsgUsgText: bdsgUsgText, - lawTrgtYn: lawTrgtYn, - grntchBdstCertGrdText: grntchBdstCertGrdText, + bdsgUsgText: bdsgUsgText, // 건물용도명 + lawTrgtYn: lawTrgtYn, // 법정대상 여부 + grntchBdstCertGrdText: grntchBdstCertGrdText, // 녹색건축인증등급명 }; } else { returnData = { @@ -83,7 +83,7 @@ export const tableData = (data: any, code: comCodeState) => { }; export const excelDownload = ( - excelList: [][], + excelList: string[][], fileNm: string, sampleFile: string, headerRange = "A1:V3", @@ -291,16 +291,16 @@ export const showConfirmToast = (message: string) => { toast.custom( (t) => ( <div - className={`w-[500px] min-h-[140px] bg-white shadow-lg rounded-xl p-6 border border-gray-200 + className={`min-h-[140px] w-[500px] rounded-xl border border-border bg-card p-6 text-card-foreground shadow-lg ${t.visible ? "animate-enter" : "animate-leave"}`} > - <div className="text-3xl font-extrabold text-red-600 bg-yellow-100 px-4 py-2 rounded-md shadow-md"> + <div className="rounded-md bg-accent px-4 py-2 text-3xl font-extrabold text-destructive shadow-md"> {message} </div>{" "} <div className="flex justify-end gap-2"> <Button size={"sm"} - className="px-4 py-1.5 rounded bg-gray-200 text-sm hover:bg-gray-300" + className="rounded bg-secondary px-4 py-1.5 text-sm text-secondary-foreground hover:bg-accent" onClick={() => toast.dismiss(t.id)} > 확인완료 diff --git a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx index 09ed01987..2bc1861ce 100644 --- a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx +++ b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx @@ -1,4 +1,7 @@ "use client"; +/** + * 표준공사비 + */ import { useWrStore } from "@/app/store"; import { usageProcData } from "@/app/store/matDmd/coMatRvw/headList"; import type { @@ -28,6 +31,19 @@ import PopoverUi from "@common/business_components/ui/popover_ui"; import { Button, Checkbox, Input, Label } from "@common/components/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import { useForm, type UseFormReturn } from "react-hook-form"; + +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"); +}; /** * ConCostForm * 호출: (with-layout)/matMng/coMat/coMatRvw @@ -63,7 +79,7 @@ export default function ConCostForm({ const activeForm = form || localForm; const isSim = type === "simulation"; - const readOnlyInputClass = "bg-slate-50 text-gray-700"; + const readOnlyInputClass = "bg-secondary text-secondary-foreground"; const { constCostRes, // 표준사업비 rateFeeRes, // 부대경비요율 @@ -456,12 +472,16 @@ export default function ConCostForm({ <FormUi label="요구수량(m³/일)"> <Input placeholder="" - type="number" - step="0.1" + type={isSim ? "number" : "text"} + step={isSim ? "0.1" : undefined} onChange={(e) => { field.onChange(Number(e.target.value)); }} - value={field.value ?? 0} + value={ + isSim + ? field.value ?? 0 + : formatNumberWithComma(field.value) + } readOnly={!isSim} className={!isSim ? readOnlyInputClass : ""} /> @@ -524,14 +544,13 @@ export default function ConCostForm({ <FormUi label="처리시설용량(m³/일)"> <Input placeholder="" - type="number" - step="0.001" + type="text" onChange={(e) => { console.log("dmndQty ", e); //field.onChange(Number(e.target.value) * 365 ); }} // value={Number(field.value) * 365 || 0} - value={Number(field.value) || 0} + value={formatNumberWithComma(Number(field.value) || 0)} readOnly className={readOnlyInputClass} /> diff --git a/apps/wr/components/ui/form/fcltReg/bdstReg/bdstAdd.tsx b/apps/wr/components/ui/form/fcltReg/bdstReg/bdstAdd.tsx index b644112b3..847453ba0 100644 --- a/apps/wr/components/ui/form/fcltReg/bdstReg/bdstAdd.tsx +++ b/apps/wr/components/ui/form/fcltReg/bdstReg/bdstAdd.tsx @@ -59,14 +59,14 @@ export default function BdstAdd({ form, readonly }: { form: any; readonly?: bool <> <h1 className={formH1Class}>증축, 개축, 재축</h1> {!readonly && ( - <div className="flex justify-end gap-2 mt-4"> + <div className="rainreg_actions"> <Button type="button" className="" size={"sm"} onClick={plusHist}> 이력추가 </Button> <Button type="button" - className="!bg-red-500 text-white" + variant="destructive" size={"sm"} onClick={removeSelected} > @@ -151,7 +151,7 @@ export default function BdstAdd({ form, readonly }: { form: any; readonly?: bool </div> </div> ))} - <div ref={bottomRef} className="border-b border-gray-300 my-4"> </div> + <div ref={bottomRef} className="rainreg_divider"> </div> </> ) } diff --git a/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx b/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx index 0a147ac4b..15be1a64a 100644 --- a/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/htdflReg/htdflRegForm.tsx @@ -266,12 +266,12 @@ export default function HtdflRegForm({ }, [isPowerPlant, form]); return ( - <div className="!mx-[1.6rem] !p-0r"> + <div className="!mx-[1.6rem] !p-0"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> {/* <div className="flex justify-between items-center mx-3"> */} <div> - <h1 className="text-2xl font-bold text-gray-900 mt-3 ml-1"> + <h1 className="mt-3 ml-1 text-2xl font-bold text-foreground"> {readonly && `(시설명 : ${htdflForm?.bdstNm})`} </h1> <div className="grid grid-cols-12 flex items-center card-header"> @@ -289,7 +289,7 @@ export default function HtdflRegForm({ </Button> <Button type="button" - className="!bg-red-500 text-white" + variant="destructive" size={"sm"} onClick={onDelete} > @@ -619,7 +619,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -649,7 +649,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={6} />{" "} - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -704,7 +704,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -739,7 +739,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이상 </span> </div> @@ -768,7 +768,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={numSize} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이상 </span> </div> @@ -800,7 +800,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={numSize} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -830,7 +830,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={7} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> @@ -886,7 +886,7 @@ export default function HtdflRegForm({ readOnly={readonly} maxLength={numSize} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-placeholder"> 이하 </span> </div> diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.css b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.css new file mode 100644 index 000000000..2ecbf59ba --- /dev/null +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.css @@ -0,0 +1,27 @@ +@tailwind components; + +@layer components { + .pbnstbuse_sum_input { + @apply w-32 rounded-md border border-input pr-8; + } + + .pbnstbuse_demand_row { + @apply mt-3 grid grid-cols-12 gap-4 border-b border-border pb-3; + } + + .pbnstbuse_modal_body { + @apply mx-auto w-full rounded-lg border border-border bg-background p-6 shadow-md; + } + + .pbnstbuse_empty { + @apply !mx-[1.6rem] !p-0 flex h-64 flex-col items-center justify-center rounded-md border border-border text-label; + } + + .pbnstbuse_empty_title { + @apply text-lg text-foreground; + } + + .pbnstbuse_empty_desc { + @apply mb-4 text-sm text-label; + } +} diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx index 57f737a9d..411eebb95 100644 --- a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/bugRegForm.tsx @@ -1,4 +1,5 @@ "use client"; +import "./bugRegForm.css"; import { useWrStore } from "@/app/store"; import { @@ -325,7 +326,7 @@ export function PbnstBuseForm({ return ( <> {pbnstBiz && pbnstBiz?.length > 0 ? ( - <div className=" !mx-[1.6rem] !p-0r"> + <div className=" !mx-[1.6rem] !p-0"> {/* 차수별 등록 현황 폼 */} <div className="justify-between"> <Form {...form}> @@ -465,7 +466,7 @@ export function PbnstBuseForm({ type="text" value={formatCommaNumber(sumCpct)} readOnly={true} - className="w-32 border-gray-300 rounded pr-8 " + className="pbnstbuse_sum_input" /> {ppiYn === "Y" ? ( <> @@ -481,7 +482,8 @@ export function PbnstBuseForm({ <Button type="button" size={commonButonSize} - className="!text-xl !bg-red-500 !text-white ml-1" + variant="destructive" + className="!text-xl ml-1" onClick={removeSelected} disabled={readonly} > @@ -563,7 +565,7 @@ export function PbnstBuseForm({ {fcDemands.map((field, index) => ( <div key={field.id} // useFieldArray의 고유 id를 key로 사용 - className="grid grid-cols-12 gap-4 mt-3 border-b border-gray-400 pb-3" + className="pbnstbuse_demand_row" > <div key={index + "keySon1"} className="col-span-4"> <FormField @@ -787,7 +789,7 @@ export function PbnstBuseForm({ data: data as DemandCompType, }); }} - bodyClassNm={"w-full mx-auto bg-white rounded-lg shadow-md p-6"} + bodyClassNm={"pbnstbuse_modal_body"} openNetwork={true} ></SdmInsertForm> } @@ -795,9 +797,9 @@ export function PbnstBuseForm({ ></UiModal> </div> ) : ( - <div className="!mx-[1.6rem] !p-0r flex flex-col items-center justify-center h-64 text-gray-500 border rounded-md"> - <p className="text-lg">등록된 차수별 정보가 없습니다.</p> - <p className="text-sm mb-4"> + <div className="pbnstbuse_empty"> + <p className="pbnstbuse_empty_title">등록된 차수별 정보가 없습니다.</p> + <p className="pbnstbuse_empty_desc"> 아래 버튼을 눌러 신규 차수 정보를 등록해주세요. </p> <Button diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.css b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.css new file mode 100644 index 000000000..248e1cf4c --- /dev/null +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.css @@ -0,0 +1,19 @@ +@tailwind components; + +@layer components { + .busewq_input { + @apply w-full rounded-md border border-input py-2 pr-16 text-right; + } + + .busewq_suffix { + @apply pointer-events-none absolute inset-y-0 right-3 flex items-center text-xl text-label; + } + + .busewq_ph_input { + @apply w-28 !text-xl; + } + + .busewq_ph_range { + @apply text-xl text-foreground; + } +} diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.tsx b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.tsx index dd43658d1..06cfdf1ff 100644 --- a/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.tsx +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/buseReg/buseWaterQulity/buseWaterQulity.tsx @@ -1,3 +1,5 @@ +import "./buseWaterQulity.css"; + import type { PbnstRegArrrayType } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; import { formH1Class } from "@/components/lib/class/comClass"; import { FormField, FormUi, SelectBox } from "@common/business_components/ui"; @@ -119,7 +121,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -133,7 +135,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이하 </span> </div> @@ -151,12 +153,12 @@ export function BuseWaterQulity({ name={`items.${indexMng}.fcBizs.${indexBiz}.grssClfg`} key={`items.${indexMng}.fcBizs.${indexBiz}.grssClfg-key`} render={({ field }) => ( - <FormUi label="총대장균군수(개/mg)"> + <FormUi label="총대장균군수(개/100㎖)"> <div className="flex items-center gap-2"> <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -171,7 +173,7 @@ export function BuseWaterQulity({ maxLength={6} /> {/* TODO: 총대장균군수 확인 필요*/} - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이하 </span> </div> @@ -191,7 +193,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -205,7 +207,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이상 </span> </div> @@ -225,7 +227,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -239,7 +241,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이상 </span> </div> @@ -298,7 +300,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -312,7 +314,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이하 </span> </div> @@ -332,7 +334,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -346,7 +348,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이하 </span> </div> @@ -367,7 +369,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -381,7 +383,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={6} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이하 </span> </div> @@ -401,7 +403,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -415,7 +417,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={7} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이하 </span> </div> @@ -436,7 +438,7 @@ export function BuseWaterQulity({ <div className="relative flex-1 min-w-0"> <Input type="text" - className="w-full pr-16 border py-2 text-right rounded-md" + className="busewq_input" placeholder="" onChange={(e) => { const inputValue = e.target.value.replace(/[^0-9.]/g, ""); // 숫자만 저장 @@ -450,7 +452,7 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={7} /> - <span className="text-xl absolute inset-y-0 right-3 flex items-center text-gray-500 pointer-events-none"> + <span className="busewq_suffix"> 이하 </span> </div> @@ -463,7 +465,7 @@ export function BuseWaterQulity({ <Label className="text-xl">수소이온농도(pH)</Label> <div className="flex flex-row items-center gap-2"> <Input - className="!text-xl w-28" + className="busewq_ph_input" placeholder="" type="text" onChange={(e) => @@ -474,9 +476,9 @@ export function BuseWaterQulity({ readOnly={readonly} maxLength={3} /> - <span className="text-xl">~</span> + <span className="busewq_ph_range">~</span> <Input - className="!text-xl w-28" + className="busewq_ph_input" placeholder="" type="text" onChange={(e) => { diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.css b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.css new file mode 100644 index 000000000..d28e7bcc6 --- /dev/null +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.css @@ -0,0 +1,7 @@ +@tailwind components; + +@layer components { + .pbnstmng_file_link { + @apply block cursor-pointer text-2xl text-primary hover:underline; + } +} diff --git a/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx index 85364f0d1..9b592d3bc 100644 --- a/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/pbnstReg/mngReg/mngRegForm.tsx @@ -1,4 +1,5 @@ "use client"; +import "./mngRegForm.css"; import { useWrStore } from "@/app/store"; import { @@ -265,7 +266,7 @@ export function PbnstMngForm({ return ( - <div className=" !mx-[1.6rem] !p-0r"> + <div className=" !mx-[1.6rem] !p-0"> <div> <Form {...form}> {/* <form onSubmit={form.handleSubmit()}> */} @@ -332,13 +333,14 @@ export function PbnstMngForm({ > 추가등록 </Button> - <Button - type="button" - size={commonButonSize} - className="!text-xl !bg-red-500 !text-white ml-1" - onClick={removeSelected} - disabled={readonly} - > + <Button + type="button" + size={commonButonSize} + variant="destructive" + className="!text-xl ml-1" + onClick={removeSelected} + disabled={readonly} + > 삭제 </Button> </div> @@ -442,7 +444,7 @@ export function PbnstMngForm({ {relationfiles?.map((item, index) => ( <span key={index + "plan"} - className="text-2xl block cursor-pointer text-blue-600 hover:underline" + className="pbnstmng_file_link" onClick={() => fileDownLoadName(item.atflCd, item.file.name)} > {item.file.name} @@ -481,7 +483,7 @@ export function PbnstMngForm({ {planFiles?.map((item, index) => ( <span key={index + "plan"} - className="text-2xl block cursor-pointer text-blue-600 hover:underline" + className="pbnstmng_file_link" onClick={() => fileDownLoadName(item.atflCd, item.file.name)} > {item.file.name} diff --git a/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.css b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.css new file mode 100644 index 000000000..cf03d7f7f --- /dev/null +++ b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.css @@ -0,0 +1,23 @@ +@tailwind components; + +@layer components { + .rainreg_header_note { + @apply mt-3 ml-1 text-2xl font-bold text-foreground; + } + + .rainreg_header_title { + @apply col-span-10 text-2xl font-medium text-label; + } + + .rainreg_section_note { + @apply col-span-10 mt-4 pl-3 text-xl text-foreground; + } + + .rainreg_actions { + @apply mt-4 flex justify-end gap-2; + } + + .rainreg_divider { + @apply my-4 border-b border-border; + } +} diff --git a/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx index 27033d0e2..3619e95fc 100644 --- a/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/rainReg/rainRegForm.tsx @@ -1,5 +1,7 @@ "use client"; +import "./rainRegForm.css"; + import { useWrStore } from "@/app/store"; import { rainZod, @@ -180,19 +182,19 @@ export default function RainRegForm({ }, [rainForm]); return ( - <div className="!mx-[1.6rem] !p-0r"> + <div className="!mx-[1.6rem] !p-0"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div> - <h1 className="text-2xl font-bold text-gray-900 mt-3 ml-1"> + <h1 className="rainreg_header_note"> {readonly && `(시설명 : ${rainForm?.bdstNm})`} </h1> <div className="grid grid-cols-12 flex items-center card-header"> - <h1 className="text-2xl font-medium text-label col-span-10"> + <h1 className="rainreg_header_title"> {isModal ? "건축물 정보" : "빗물이용시설 정보"} </h1> {!readonly && ( - <div className="flex justify-end gap-2 mt-4"> + <div className="rainreg_actions"> <Button type="submit" className="col-start-12 col-span-1" @@ -202,7 +204,7 @@ export default function RainRegForm({ </Button> <Button type="button" - className="!bg-red-500 text-white" + variant="destructive" size={"sm"} onClick={onDelete} > @@ -215,7 +217,7 @@ export default function RainRegForm({ {/* <ScrollArea className={divStyle}> */} {isModal && rainForm != undefined ? <BdstReg form={form} /> : <></>} {isModal ? ( - <h1 className="pl-3 text-xl col-span-10 mt-4"> + <h1 className="rainreg_section_note"> 빗물이용시설 정보{" "} </h1> ) : ( @@ -455,7 +457,7 @@ export default function RainRegForm({ // <div className="flex items-center gap-2"> <FormUi label="처리공정"> <Input - className={" h-full !text-xl col-span-4"} + className={selectClass} placeholder="처리공정 입력" type="text" value={field.value ?? ""} diff --git a/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx b/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx index 61014d796..8784f87b2 100644 --- a/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx +++ b/apps/wr/components/ui/form/fcltReg/rswtrReg/rswtrRegForm.tsx @@ -174,12 +174,12 @@ export default function RswtrRegForm({ return ( - <div className="!mx-[1.6rem] !p-0r"> + <div className="!mx-[1.6rem] !p-0"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> {/* <div className="flex justify-between items-center mx-3"> */} <div> - <h1 className="text-2xl font-bold text-gray-900 mt-3 ml-1"> + <h1 className="mt-3 ml-1 text-2xl font-bold text-foreground"> {readonly && `(시설명 : ${rswtForm?.bdstNm})`} </h1> <div className="grid grid-cols-12 flex items-center card-header"> @@ -197,7 +197,7 @@ export default function RswtrRegForm({ </Button> <Button type="button" - className="!bg-red-500 text-white" + variant="destructive" size={"sm"} onClick={onDelete} > diff --git a/apps/wr/components/ui/map/LayerPopUp/HasuFacilty/HasuFacility.tsx b/apps/wr/components/ui/map/LayerPopUp/HasuFacilty/HasuFacility.tsx index beaf26b81..a3b686bfb 100644 --- a/apps/wr/components/ui/map/LayerPopUp/HasuFacilty/HasuFacility.tsx +++ b/apps/wr/components/ui/map/LayerPopUp/HasuFacilty/HasuFacility.tsx @@ -25,7 +25,7 @@ export function HasuFacility({rtnFeature}:{rtnFeature:(feature:any)=>void } ) { const [isReset, setIsReset] = useState(false); const [pbnRuseNm, setPbnRuseNm] = useState<string>(""); const ref = useRef<ReturnType>({} as ReturnType); - const { pbnMapHeadList, pbnMapRes, pbnMapCount ,hasuMapExcelList} = pbnMap; + const { pbnMapHeadList, pbnMapRes, pbnMapCount } = pbnMap; const { t } = useTranslation("base"); useEffect(() => { @@ -46,21 +46,6 @@ export function HasuFacility({rtnFeature}:{rtnFeature:(feature:any)=>void } ) { } }; - - useEffect(() => { - if (hasuMapExcelList.length > 0) { - excelDownload( - hasuMapExcelList, - "하수처리시설 현황", - "sample_hasuMap.xlsx", - "A1:H1", - 2 - ); - } else { - toast.error(t("excelDownErrMsg")); - } - }, [hasuMapExcelList]); - return ( <> <ContentsDiv> @@ -81,7 +66,7 @@ export function HasuFacility({rtnFeature}:{rtnFeature:(feature:any)=>void } ) { rightChildren={<></>} leftChildren={ <div className="col-span-3 flex items-center"> - <Label className="w-full mt-2 ml-4 text-xl text-gray-600"> + <Label className="mt-2 ml-4 w-full text-xl text-label"> {"하수처리시설명"} </Label> <SearchInput @@ -103,12 +88,28 @@ export function HasuFacility({rtnFeature}:{rtnFeature:(feature:any)=>void } ) { btnClass={btnClass} ref={ref} isAdmin={false} - excelClick={function (returnType: ReturnType): void { + excelClick={async function (returnType: ReturnType): Promise<void> { console.log("pbnstSttnSearch ", returnType); + const excelList = await pbnMapSearch( + true, + 1, + 100000, + returnType, + pbnRuseNm ?? "" + ); + if (excelList && excelList.length > 0) { + excelDownload( + excelList, + "하수처리시설 현황", + "sample_hasuMap.xlsx", + "A1:H1", + 2 + ); + return; + } - pbnMapSearch(true,1, 100000, returnType,pbnRuseNm??'' ) - + toast.error(t("excelDownErrMsg")); }} /> </div> diff --git a/apps/wr/components/ui/map/LayerPopUp/HedfFacilty/HtdfFacilty.tsx b/apps/wr/components/ui/map/LayerPopUp/HedfFacilty/HtdfFacilty.tsx index 86da242d7..3b72f21e5 100644 --- a/apps/wr/components/ui/map/LayerPopUp/HedfFacilty/HtdfFacilty.tsx +++ b/apps/wr/components/ui/map/LayerPopUp/HedfFacilty/HtdfFacilty.tsx @@ -40,7 +40,6 @@ export function HtdflFacility({ const { htdfMapCount, - htdflMapExcelList, htdfMapRes, htdfSttHeadList, htdfSttNodeList, @@ -105,19 +104,6 @@ export function HtdflFacility({ setHtdflTable(chData); }, [code, htdfMapRes]); - useEffect(() => { - if (htdflMapExcelList.length > 0) { - excelDownload( - htdflMapExcelList, - "온배수시설 현황", - "sample_htdflSttn.xlsx", - "A1:R3" - ); - } else { - toast.error(t("excelDownErrMsg")); - } - }, [htdflMapExcelList]); - return ( <> <ContentsDiv> @@ -166,13 +152,13 @@ export function HtdflFacility({ className="mt-2" size={"sm"} color="green" - onClick={() => { + onClick={async () => { const con = objSearch.sgg && objSearch.sgg !== "0" ? objSearch.sgg : objSearch.city; if (con) { - htdfMapSearch( + const excelList = await htdfMapSearch( true, 1, "1000000", @@ -181,6 +167,16 @@ export function HtdflFacility({ code.cityCd, code.sggCd ); + if (excelList && excelList.length > 0) { + excelDownload( + excelList, + "온배수시설 현황", + "sample_htdflSttn.xlsx", + "A1:R3" + ); + return; + } + toast.error(t("excelDownErrMsg")); } }} > diff --git a/apps/wr/components/ui/map/LayerPopUp/RainFacility/RainFacility.tsx b/apps/wr/components/ui/map/LayerPopUp/RainFacility/RainFacility.tsx index 74bf0aa55..154b8dadc 100644 --- a/apps/wr/components/ui/map/LayerPopUp/RainFacility/RainFacility.tsx +++ b/apps/wr/components/ui/map/LayerPopUp/RainFacility/RainFacility.tsx @@ -31,7 +31,7 @@ export function RainFacility({ const { code, rainMap, rainMapInit, rainMapSearch } = useWrStore( (state) => state ); - const { rainMapRes, rainMapExcelList,rainMapCount, rainSttHeadList, rainSttNodeList } = + const { rainMapRes, rainMapCount, rainSttHeadList, rainSttNodeList } = rainMap; const [rainTable, setRainTable] = useState<customRainBdst[]>(); const [isReset, setIsReset] = useState(false); @@ -122,18 +122,6 @@ export function RainFacility({ .filter((item: any) => item.rainyObj !== undefined); setRainTable(chData); }, [code, rainMapRes]); - useEffect(() => { - if (rainMapExcelList.length > 0) { - excelDownload( - rainMapExcelList, - "빗물이용시설 현황", - "sample_rainSttn.xlsx", - "A1:W3" - ); - } else { - toast.error(t("excelDownErrMsg")); - } - }, [rainMapExcelList]); return ( <> <ContentsDiv> @@ -182,13 +170,13 @@ export function RainFacility({ className="mt-2" size={"sm"} color="green" - onClick={() => { + onClick={async () => { const con = objSearch.sgg && objSearch.sgg !== "0" ? objSearch.sgg : objSearch.city; if (con) { - rainMapSearch( + const excelList = await rainMapSearch( true, 1, "1000000", @@ -196,6 +184,16 @@ export function RainFacility({ seartchTxt ?? "", code as unknown as Record<string, SelectDataType[]> ); + if (excelList && excelList.length > 0) { + excelDownload( + excelList, + "빗물이용시설 현황", + "sample_rainSttn.xlsx", + "A1:W3" + ); + return; + } + toast.error(t("excelDownErrMsg")); } }} > diff --git a/apps/wr/components/ui/map/LayerPopUp/RswtFacilty/RswtFacilty.tsx b/apps/wr/components/ui/map/LayerPopUp/RswtFacilty/RswtFacilty.tsx index 4fe818568..b7f8e6bed 100644 --- a/apps/wr/components/ui/map/LayerPopUp/RswtFacilty/RswtFacilty.tsx +++ b/apps/wr/components/ui/map/LayerPopUp/RswtFacilty/RswtFacilty.tsx @@ -31,7 +31,7 @@ export function RswtFacility({ const { code, rswtMap, rswtMapInit, rswtMapSearch } = useWrStore( (state) => state ); - const { rswtMapRes, rswtMapExcelList, rswtMapCount, rswtSttHeadList, rswtSttNodeList } = + const { rswtMapRes, rswtMapCount, rswtSttHeadList, rswtSttNodeList } = rswtMap; const [rswtrTable, setRswtrTable] = useState<customRswtBdst[]>(); const [isReset, setIsReset] = useState(false); @@ -118,18 +118,6 @@ export function RswtFacility({ setRswtrTable(chData); }, [code, rswtMapRes]); - useEffect(() => { - if (rswtMapExcelList.length > 0) { - excelDownload( - rswtMapExcelList, - "중수도시설 현황", - "sample_rswtSttn.xlsx", - "A1:W3", - ); - } else { - toast.error(t('excelDownErrMsg')); - } - }, [rswtMapExcelList]); return ( <> <ContentsDiv> @@ -178,13 +166,13 @@ export function RswtFacility({ className="mt-2" size={"sm"} color="green" - onClick={() => { + onClick={async () => { const con = objSearch.sgg && objSearch.sgg !== "0" ? objSearch.sgg : objSearch.city; if (con) { - rswtMapSearch( + const excelList = await rswtMapSearch( true, 1, "1000000", @@ -192,6 +180,16 @@ export function RswtFacility({ seartchTxt ?? "", code as unknown as Record<string, SelectDataType[]> ); + if (excelList && excelList.length > 0) { + excelDownload( + excelList, + "중수도시설 현황", + "sample_rswtSttn.xlsx", + "A1:W3", + ); + return; + } + toast.error(t('excelDownErrMsg')); } }} > diff --git a/apps/wr/components/ui/map/LayerPopUp/RuseFacilty/RuseFacility.tsx b/apps/wr/components/ui/map/LayerPopUp/RuseFacilty/RuseFacility.tsx index 8ed06293b..07e00ddf7 100644 --- a/apps/wr/components/ui/map/LayerPopUp/RuseFacilty/RuseFacility.tsx +++ b/apps/wr/components/ui/map/LayerPopUp/RuseFacilty/RuseFacility.tsx @@ -109,7 +109,7 @@ export function RuseFacility({ className={""} selectContentClass={''} /> - <Label className="w-full mt-2 ml-4 text-xl text-gray-600"> + <Label className="mt-2 ml-4 w-full text-xl text-label"> {"하수처리시설명"} </Label> <SearchInput diff --git a/apps/wr/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup.tsx b/apps/wr/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup.tsx index 9f8db7409..bb655f923 100644 --- a/apps/wr/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup.tsx +++ b/apps/wr/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup.tsx @@ -20,7 +20,7 @@ export default function CoSttnDetailPopup({isDetail} :{isDetail:DemandCompType}) rtnInputData={async (data) => { console.log(data) }} - bodyClassNm={"w-full mx-auto bg-white rounded-lg shadow-md p-6"} + bodyClassNm={"w-full mx-auto bg-background rounded-lg shadow-md p-6"} updateList={isDetail} readOnly={true} openNetwork={false} @@ -28,4 +28,4 @@ export default function CoSttnDetailPopup({isDetail} :{isDetail:DemandCompType}) </div> ) -} \ No newline at end of file +} diff --git a/apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx b/apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx index 7e4c3b603..dacb810f1 100644 --- a/apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx +++ b/apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx @@ -1,6 +1,7 @@ import { useWrStore } from "@/app/store"; import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type"; import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal"; +import { Title } from "@common/business_components/ui"; import { Tabs, TabsContent, @@ -39,6 +40,7 @@ export function RuseDetailPopup({ bscFctCd }: { bscFctCd: string }) { if (!(ruseDetailList && ruseDetailList[0])) return; return ( <div className=""> + <Title title="하수재이용시설 상세정보" /> <Tabs value={fcltTab} onValueChange={setFcltTab}> <TabsList className="mt-2 mb-3 gap-2 flex flex-wrap"> {ruseDetailList.map((item, index) => { diff --git a/apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx b/apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx index db992a507..3a036cb9d 100644 --- a/apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx +++ b/apps/wr/components/ui/map/wrOverLay/WrOverLayTable/WrOverLayTable.tsx @@ -11,14 +11,14 @@ export default function WrOverLayTable({overLayData}:{overLayData:any,}){ <TableBody> {Object.entries(overLayData).map(([key, value]) => ( <TableRow key={key}> - <TableCell className=" w-1/3 bg-slate-100 text-slate-800 font-medium text-sm align-middle border border-slate-300"> + <TableCell className="w-1/3 border border-border bg-secondary text-secondary-foreground font-medium text-sm align-middle"> {" "} {key}{" "} </TableCell> <TableCell className={ - "w-1/3 bg-white text-right pr-3 text-sm align-middle border border-slate-300" + "w-1/3 border border-border bg-background pr-3 text-right text-sm align-middle text-foreground" } > {" "} @@ -30,4 +30,4 @@ export default function WrOverLayTable({overLayData}:{overLayData:any,}){ </Table> </> ) -} \ No newline at end of file +} diff --git a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx index a6d04973d..36c733951 100644 --- a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx +++ b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx @@ -43,16 +43,16 @@ export default function WrOverLay({ onDrag={(e, data) => setPos({ x: data.x, y: data.y })} > <div ref={dragRef}> - <Card className="w-[320px] p-0 shadow-md rounded-xl overflow-hidden border border-slate-300 relative"> + <Card className="relative w-[320px] overflow-hidden rounded-xl border border-border bg-card p-0 text-card-foreground shadow-md"> {/* 헤더: 제목 바 + 우측 정렬 버튼 (절대배치 제거, 타이틀 중앙 정렬 유지) */} - <div className="bg-sky-600 text-white font-semibold py-2 text-xl"> + <div className="bg-primary py-2 text-xl font-semibold text-primary-foreground"> <div className="grid grid-cols-[auto_1fr_auto] items-center gap-2 px-2"> {/* 좌측 스페이서: 우측 버튼과 동일한 너비를 차지하도록 동일 버튼을 투명하게 복제 */} <div className="opacity-0 pointer-events-none"> <div className="flex gap-2"> <button onClick={() => setPos({ x: 0, y: 0 })} - className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition" + className="bg-secondary p-2 text-secondary-foreground shadow transition hover:bg-accent hover:text-accent-foreground hover:shadow-md" title="원위치" > ↺ @@ -62,7 +62,7 @@ export default function WrOverLay({ onClick={() => setOpen(false)} aria-label="축소" title="축소" - className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition" + className="bg-secondary p-2 text-secondary-foreground shadow transition hover:bg-accent hover:text-accent-foreground hover:shadow-md" > – </button> @@ -78,7 +78,7 @@ export default function WrOverLay({ <div className="flex gap-2 justify-end"> <button onClick={() => setPos({ x: 0, y: 0 })} - className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition" + className="bg-secondary p-2 text-secondary-foreground shadow transition hover:bg-accent hover:text-accent-foreground hover:shadow-md" title="원위치" > ↺ @@ -88,7 +88,7 @@ export default function WrOverLay({ onClick={() => setOpen(false)} aria-label="축소" title="축소" - className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition" + className="bg-secondary p-2 text-secondary-foreground shadow transition hover:bg-accent hover:text-accent-foreground hover:shadow-md" > – </button> @@ -110,7 +110,7 @@ export default function WrOverLay({ onClick={() => setOpen(true)} aria-label="확장" title={title || "정보 보기"} - className="w-[30px] h-[30px] bg-white font-bold shadow flex items-center justify-center border border-slate-300" + className="flex h-[30px] w-[30px] items-center justify-center border border-border bg-popover font-bold text-popover-foreground shadow" > {bubbleLabel} </button> diff --git a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx index 0e4152db6..6cdebc4c4 100644 --- a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx +++ b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx @@ -265,7 +265,7 @@ export default function ChkgdExcel({ >
-

+

{"점검정보 입력"}

diff --git a/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx b/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx index c5ba35e17..90bb0c91c 100644 --- a/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx +++ b/apps/wr/components/ui/modal/chkgd/chkgdModal/chkgdModal.tsx @@ -289,7 +289,7 @@ export default function ChkgdModal({ <> )}
-

+

{type === "RNB" ? `시설명 : (${obj.bdstNm})` : `시설명 : (${obj.building.bdstNm})`} diff --git a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx index 0857340e3..25d80794f 100644 --- a/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx +++ b/apps/wr/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal.tsx @@ -4,7 +4,7 @@ import { excelToPbnPrfData } from "@/app/store/prfmncReg/pbnstPrfmncReg/excel"; import { pbnPrfRegExcelZod } from "@/app/store/prfmncReg/pbnstPrfmncReg/headList"; import type { - FcPrfType, + PbnstPrfRegDtlType, PrfType, } from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; import { yearOptions } from "@/components/lib/func/common"; @@ -18,7 +18,7 @@ import { } from "@common/business_components/ui"; import { Button, Input } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; // import toast from "react-hot-toast"; import type { z } from "zod"; @@ -29,13 +29,14 @@ export default function PbnPrfExcelModal({ setIsExcelOPen, setModalExcelData, }: { - data: FcPrfType; + data: PbnstPrfRegDtlType; setIsExcelOPen: (open: boolean) => void; - setModalExcelData: (excelData: PrfType) => void; + setModalExcelData: (excelData: PrfType[]) => void; }) { const alert = useAlertStore((state) => state.setMessage); const [file, setFile] = useState(null); const [excelData, setExcelData] = useState(); + const fileInputRef = useRef(null); const form = useForm>({ resolver: zodResolver(pbnPrfRegExcelZod), defaultValues: { @@ -44,26 +45,32 @@ export default function PbnPrfExcelModal({ }); const handleFileChange = (e: React.ChangeEvent) => { + setExcelData(undefined); const selectedFile = e.target.files?.[0]; if (selectedFile) { setFile(selectedFile); } }; const onSubmit = (values: z.infer) => { - if (excelData) { - ////TODO: 궅이 서브밋을 써야하는 이유가 있는지 ? - console.log("values ", values); - // prfmnRegData(1,'41', excelData as unknown as PrfType); 활성화시 스토어 활성화 칠요 - - setModalExcelData(excelData as unknown as PrfType); - setIsExcelOPen(false); + if (!file) { + alert("엑셀 파일을 선택해 주세요."); + return; } + + if (!excelData || excelData.length === 0) { + alert("엑셀 파일을 먼저 확인해 주세요."); + return; + } + + ////TODO: 궅이 서브밋을 써야하는 이유가 있는지 ? + console.log("values ", values); + setModalExcelData(excelData); + setIsExcelOPen(false); }; useEffect(() => { - if (data && data.bscFctNm) - form.setValue("pbnstRuseFcltId", data.pbnstRuseFcltId); - form.setValue("bscFctNm", data.bscFctNm || ""); - form.setValue("pbnstRuseFcltNm", data.ruseUtztnFcltNm); + form.setValue("pbnstRuseFcltId", data.pbnstRuseFcltId || ""); + form.setValue("bscFctNm", data.swageBscFctNm || ""); + form.setValue("pbnstRuseFcltNm", data.ruseUtztnFcltNm || ""); }, [data]); useEffect(() => { @@ -76,20 +83,42 @@ export default function PbnPrfExcelModal({ return; } + if (!form.getValues("pbnstRuseFcltId")) { + alert("재이용시설을 다시 선택한 뒤 파일등록을 진행해 주세요."); + setFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + return; + } + if (isFile) { filePicked(isFile, 0).then((res) => { - if (res !== undefined) - setExcelData( - excelToPbnPrfData( - res as unknown as JSON[], - form.getValues("ym"), - form.getValues("pbnstRuseFcltId") - ) as PrfType[] - ); + if (res === undefined) { + alert("엑셀 파일을 읽지 못했습니다."); + return; + } + + const parsedData = excelToPbnPrfData( + res as unknown as JSON[], + form.getValues("ym"), + form.getValues("pbnstRuseFcltId") + ) as PrfType[]; + + if (!parsedData.length) { + alert("업로드 가능한 실적 데이터가 없습니다. 양식을 확인해 주세요."); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setFile(null); + return; + } + + setExcelData(parsedData); }); } } - }, [file]); + }, [alert, file, form]); return ( <> @@ -102,7 +131,7 @@ export default function PbnPrfExcelModal({ 저장

-

+

{"점검정보 입력"}

@@ -177,10 +206,11 @@ export default function PbnPrfExcelModal({
{ + onChange={(event) => { console.log(" field. valus", field.value); - if (file) handleFileChange(file); + handleFileChange(event); }} accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" /> diff --git a/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx b/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx index d3f02e278..f1d80a9be 100644 --- a/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx +++ b/apps/wr/components/ui/modal/coMat/constCostFeeTable.tsx @@ -106,7 +106,7 @@ export function ConstCostFeeTable() {
- +
- 단위: 백만원 + 단위: 백만원
{ }} /> -
+
시설용량 구간내 공사비는 직선보간법에 의하여 산출
diff --git a/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx b/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx index 5439d489c..40bac420c 100644 --- a/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx +++ b/apps/wr/components/ui/modal/coMat/rateFeeModal.tsx @@ -104,14 +104,14 @@ export function RateFeeTableModal() { return ( -
+

시설부대경비 요율(%)

-
+
- +
-
+
출처 : 하수도분야 보조금 편성 및 실무요령
시설부대경비 및 감리비 요율적용시 공사비가 요율표의 중간에 있을 때는 직선보간법에 의하여 산출 diff --git a/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx index 8f0445cd6..e85234c03 100644 --- a/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx +++ b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx @@ -185,7 +185,7 @@ export default function RuseCaseMngExcel({setIsExcelOpen}:{setIsExcelOpen: (isOp
-

+

{"물재이용 사례 파일"}

-

+

{"잠재수요처 파일"}

diff --git a/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx index 236797711..b3c5dcfcf 100644 --- a/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal.tsx @@ -177,7 +177,7 @@ export function BdstReg({ > {" "} -
-
+

건축물 목록 - + (유의 사항: 신규등록하려는 건축물이 목록에 없을 경우 하단 건축물 정보에 추가하여 주시기 바랍니다.) @@ -221,7 +221,7 @@ export function BdstReg({ >

-
+
{moType === "RNB" ? ( diff --git a/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx index 3be1806f8..0b5707b6e 100644 --- a/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/fcOrgwtsRegModal/fcOrgwtsRegModal.tsx @@ -105,14 +105,14 @@ export default function FcOrgwtsRegModal({ return (
-
+
<div className="flex items-center gap-2 mt-8"> - <Label className="text-xl text-gray-600">시설명</Label> + <Label className="text-xl text-label">시설명</Label> <Input className="w-48 text-xl" type="text" @@ -144,7 +144,7 @@ export default function FcOrgwtsRegModal({ {/* 왼쪽 테이블 */} <div className="w-5/12 min-w-[400px]"> - <Label className="text-2xl text-gray-600 mb-2 block text-right"> + <Label className="mb-2 block text-right text-2xl text-label"> 공공하수처리시설 목록 </Label> <RuseMngTable @@ -170,7 +170,7 @@ export default function FcOrgwtsRegModal({ } setbdstTable(unchecked ?? []); }} - className="w-8 h-8 text-gray-500 cursor-pointer" + className="h-8 w-8 cursor-pointer text-muted-foreground" /> <ArrowLeft onClick={() => { @@ -187,13 +187,13 @@ export default function FcOrgwtsRegModal({ } setRightData(unchecked ?? []); }} - className="w-8 h-8 text-gray-500 cursor-pointer" + className="h-8 w-8 cursor-pointer text-muted-foreground" /> </div> {/* 오른쪽 테이블 */} <div className="w-5/12 min-w-[400px]"> - <Label className="text-2xl text-gray-600 mb-2 block text-right"> + <Label className="mb-2 block text-right text-2xl text-label"> 취수하수처리장 목록 </Label> <RuseMngTable diff --git a/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx b/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx index fcb6fe392..c1dd95f95 100644 --- a/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx +++ b/apps/wr/components/ui/modal/fcltReg/pbnstRegModal/pbnstRegModal.tsx @@ -72,7 +72,7 @@ export function PbnstRegModal({ 하수재이용 시설 신규등록 </h1> - <div className="border-t border-gray-300 my-4"></div> + <div className="my-4 border-t border-border"></div> <Tabs defaultValue="mng" onValueChange={handleTabChange}> <div className="flex justify-between items-center"> diff --git a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx index 1a07720a7..fe69e0b4c 100644 --- a/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx +++ b/apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx @@ -1,3 +1,6 @@ +/** + * 하수재이용시설 현황 상세정보 modal + */ import { headMakeprfmnc } from "@/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/TableHead"; import { useWrStore } from "@/app/store"; import type { TableUpperProps } from "@/app/store/common/type/header"; @@ -34,7 +37,7 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; titleVisible?: boolean ,isMap?:boolean}) { +export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boolean }) { const { pbnstSttn, prfmnc, pbnstPrfmncSttn } = useWrStore((state) => state); const [fcltTab, setFcltTab] = useState<string>("fclt"); const [ruseTab, setRuseTab] = useState<string>("totalPrf"); @@ -57,7 +60,7 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; useEffect(() => { let tableData; - if (fcltTab === "fclt") { + if (fcltTab === "fclt") { // 하수처리수 재이용시설 현황 tableData = data.fcBizs?.map((item: fcBizType) => { return { cycl: item.cycl + "차", @@ -81,7 +84,7 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; mainSplyPumpCpct: item.mainSplyPumpCpct ?? "-", }; }); - } else if (fcltTab === "sply") { + } else if (fcltTab === "sply") { // 재이용 원수 취수 현황 tableData = data.fcBizs.flatMap((item: fcBizType) => { return item?.fcOrgwts?.map((fcOrgwt) => { return { @@ -93,7 +96,7 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; }; }); }); - } else if (fcltTab === "ppi") { + } else if (fcltTab === "ppi") { // 재이용 공급관로 현황 tableData = data.fcBizs?.flatMap((item: fcBizType) => { return item?.pplInstls?.map((ppl) => { return { @@ -104,7 +107,7 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; }; }); }); - } else if (fcltTab === "guarantee") { + } else if (fcltTab === "guarantee") { // 재이용 보증수질 현황 tableData = data.fcBizs?.map((item: fcBizType) => { return { cycl: item.cycl + "차", @@ -121,7 +124,7 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; cdtnv: item.cdtnv, }; }); - } else if (fcltTab === "dmdCo") { + } else if (fcltTab === "dmdCo") { // 수요처 tableData = data.fcBizs?.flatMap((item: fcBizType) => { return item?.fcDemands?.map((dmd) => { @@ -143,15 +146,15 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; } }, [fcltTab, data]); useEffect(() => { - if (fcltTab === "fclt") { - setRuseHeadList(pbnstSttn.ruseFcltHeadList); - } else if (fcltTab === "sply") { - setRuseHeadList(pbnstSttn.ruseSplyHeadList); - } else if (fcltTab === "ppi") { + if (fcltTab === "fclt") { // 하수처리수 재이용시설 현황 tab 하단 + setRuseHeadList(pbnstSttn.ruseFcltHeadList); + } else if (fcltTab === "sply") { // 재이용 원수 취수 현황 + setRuseHeadList(pbnstSttn.ruseSplyHeadList); + } else if (fcltTab === "ppi") { // 재이용 공급관로 현황 setRuseHeadList(pbnstSttn.rusePplHeadList); - } else if (fcltTab === "guarantee") { + } else if (fcltTab === "guarantee") { // 재이용 보증수질 현황 setRuseHeadList(pbnstSttn.ruseGuranteeHeadList); - } else if (fcltTab === "dmdCo") { + } else if (fcltTab === "dmdCo") { // 수요처 setRuseHeadList(pbnstSttn.ruseDmdHeadList); } }, [ @@ -225,26 +228,8 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; } }; useEffect(() => { - if(isMap){ - if(data.prfList &&data.prfList.length>0){ - getTablePrfData(data.prfList); - }else{ - getTablePrfData([]); - } - }else{ - - if (pbnstSttn.pbnstSttnRes && data) { - const prfList = pbnstSttn.pbnstSttnRes.find( - (item) => item.pbnstRuseFcltId === data.pbnstRuseFcltId - )?.prfList; - - if (prfList) { - - getTablePrfData(prfList); - } - } - } - }, [pbnstSttn.pbnstSttnRes, data,isMap]); + getTablePrfData(data.prfList ?? []); + }, [data]); useEffect(() => { @@ -254,17 +239,17 @@ export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; } }, [data, form]) - const Wrapper: React.ElementType = isMap ?"div": ScrollArea; + const Wrapper: React.ElementType = isMap ? "div" : ScrollArea; return ( <> <div className="min-h-0 px-4 md:px-6 py-4"> - {titleVisible && ( + {!isMap && ( <> - <Title title={"하수재이용 시설 상세정보"} /> + <Title title={"하수재이용시설 상세정보"} /> </> )} - <Wrapper className={ "h-[70vh]" }> + <Wrapper className={isMap ? "min-h-0" : "h-[70vh]"}> <PbnstMngForm form={form} formType={"sttn"} diff --git a/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx b/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx index 15fcdd1e2..ae233ed74 100644 --- a/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx +++ b/apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx @@ -4,7 +4,7 @@ import type { customLdrmnt } from "@/app/store/matDmd/ldrmntCoSttn"; import { ldrmntZod } from "@/app/store/matDmd/ldrmntCoSttn/headList"; import { selectClass } from "@/app/store/ruseFcltMng/fcltReg/rainReg"; import { formatCommaNumber, handleCommaNumberChange } from "@/components/lib/func/common"; -import { Form, FormField, FormUi, ScrollArea } from "@common/business_components/ui"; +import { Form, FormField, FormUi, ScrollArea, Title } from "@common/business_components/ui"; import { Button, Input } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect } from "react"; @@ -53,9 +53,9 @@ export default function CoDetailModal({ <Form {...form}> <form onSubmit={form.handleSubmit(onFormSubmit)}> <div className="grid grid-cols-12 items-center card-header"> - <h1 className="text-2xl font-medium text-label col-span-10"> - 잠재수요처 상세정보 - </h1> + <div className="col-span-10"> + <Title title="잠재수요처 상세정보" /> + </div> {!isMap ? ( <Button type="submit" diff --git a/apps/wr/components/ui/searchInput/searchInput.css b/apps/wr/components/ui/searchInput/searchInput.css new file mode 100644 index 000000000..e37d76e5e --- /dev/null +++ b/apps/wr/components/ui/searchInput/searchInput.css @@ -0,0 +1,19 @@ +@tailwind components; + +@layer components { + .wrsearchinput_field { + @apply h-[3.2rem] w-full rounded-md border border-input pl-3 !text-xl text-label focus:outline-none focus:ring-2 focus:ring-ring; + } + + .wrsearchinput_list { + @apply z-10 mt-2 h-[90px] w-full overflow-auto rounded-md border border-border bg-popover shadow-lg; + } + + .wrsearchinput_item { + @apply w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl text-foreground hover:bg-accent; + } + + .wrsearchinput_empty { + @apply w-full overflow-auto px-5 py-3 !text-xl text-label; + } +} diff --git a/apps/wr/components/ui/searchInput/searchInput.tsx b/apps/wr/components/ui/searchInput/searchInput.tsx index f512516b8..dfac70681 100644 --- a/apps/wr/components/ui/searchInput/searchInput.tsx +++ b/apps/wr/components/ui/searchInput/searchInput.tsx @@ -1,3 +1,5 @@ +import "./searchInput.css"; + import { Input } from "@common/components/ui"; import React, { useCallback, useEffect, useRef, useState } from "react"; @@ -73,18 +75,18 @@ export default function SearchInputText({ value={textValue} onChange={handleInputChange} /* onFocus={() => setIsOpen(true)} */ - className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + className="wrsearchinput_field" placeholder={`${label}을(를) 입력하세요`} readOnly={readonly} // readonly 상태 적용 /> {isOpen && ( - <div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto"> + <div className="wrsearchinput_list"> <div className="max-h-72"> {searchList && searchList.length > 0 ? ( searchList.map((item: SearchType, index: number) => ( <div key={index} - className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto" + className="wrsearchinput_item" onClick={() => handleSuggestionClick(item.searchNm)} role="button" tabIndex={0} @@ -99,7 +101,7 @@ export default function SearchInputText({ </div> )) ) : ( - <div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"> + <div className="wrsearchinput_empty"> 검색 결과가 없습니다. </div> )} diff --git a/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx b/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx index 35745077c..52a766262 100644 --- a/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx +++ b/apps/wr/components/ui/table/chkgdTable/guideDetailTable/guideDetailTable.tsx @@ -43,7 +43,7 @@ export function GuideDetailsTable({ return ( <> - <div className={`card !m-[1.2rem] !p-0r `}> + <div className={`card !m-[1.2rem] !p-0 `}> <Table variant="secondary" className="table-fixed"> <colgroup> {headList diff --git a/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx b/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx index a8664063e..76e32425e 100644 --- a/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx +++ b/apps/wr/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable.tsx @@ -63,8 +63,8 @@ export default function RainChkgdTable({ const handleCellClick = useCallback((index: number, headId: string) => { if ( - headId === "totalSplyUswtrUtztnQnt" || - headId === "totalRuseFcltUtztnQnt" + headId === "totalSplyUswtrUtztnQnt" || // 연간대체수이용량(m³) + headId === "totalRuseFcltUtztnQnt" // 연간빗물이용량(m³) ) { handleClick(makeTableData[index]) diff --git a/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx b/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx index 2e056a401..12e0061a3 100644 --- a/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx +++ b/apps/wr/components/ui/table/pbnstReg/ruseMngTable/ruseMngTable.tsx @@ -60,7 +60,7 @@ const RuseMngTable = React.forwardRef<any, RuseMngTableProps>( {headList.map((head) => ( <TableHead key={"table-head-row " + head.id} - className={`border border-gray-600 px-2 py-1 `} + className={`border border-border px-2 py-1 `} > {head.title} </TableHead> diff --git a/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx b/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx index 298c2bf5e..380cb9c88 100644 --- a/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx +++ b/apps/wr/components/ui/table/pbnstReg/ruseMngTableForm/ruseMngTableForm.tsx @@ -84,7 +84,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( {headList.map((head) => ( <TableHead key={"table-head-row " + head.id} - className={`border border-gray-600 px-2 py-1 `} + className={`border border-border px-2 py-1 `} > {head.title} </TableHead> @@ -118,7 +118,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( <FormUi label=""> {head.edit === true ? ( <Input - className={`${head.type === "number" ? "text-right" : "text-center"} text-gray-600`} + className={`${head.type === "number" ? "text-right" : "text-center"} text-foreground`} type="text" onChange={(txt) => { if (head.type === "number") { @@ -186,7 +186,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( value={ field.value? moment(field.value, "YYYYMMDD").format( "YYYY-MM-DD"): "" } - className={"w-full text-gray-600"} + className={"w-full text-foreground"} readOnly={readonly||head.readonly ? true : false} /> ) : head.type === "checkbox" ? ( @@ -202,7 +202,7 @@ const RuseMngTableForm = React.forwardRef<any, RuseMngTableProps>( }} ></Checkbox> ) : ( - <span className="text-black"> + <span className="text-foreground"> {head.type === "number" ? formatCommaNumber(field.value) : field.value || ""} diff --git a/apps/wr/components/ui/toast/toastFromUi.tsx b/apps/wr/components/ui/toast/toastFromUi.tsx index 5886c2e0b..4590d7b38 100644 --- a/apps/wr/components/ui/toast/toastFromUi.tsx +++ b/apps/wr/components/ui/toast/toastFromUi.tsx @@ -16,7 +16,7 @@ export function ToastFormUi({ body, title, url, buttonName }: toastFormType) { <div className={`${ t.visible ? "animate-enter" : "animate-leave" - } w-4/12 h-40 bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5`} + } h-40 w-4/12 pointer-events-auto flex rounded-lg border border-border bg-popover text-popover-foreground shadow-lg`} > <div className="w-full flex-1 py-10"> <div className="flex items-start"> diff --git a/apps/wr/docs/기준문서/하수재이용실적 등록 api 호출분석_20260630.md b/apps/wr/docs/기준문서/하수재이용실적 등록 api 호출분석_20260630.md new file mode 100644 index 000000000..81f46eb5a --- /dev/null +++ b/apps/wr/docs/기준문서/하수재이용실적 등록 api 호출분석_20260630.md @@ -0,0 +1,245 @@ +# 하수재이용실적 등록 API 호출 분석 + +작성일: 2026-06-30 +## 0. 공통코드 조회 +- 가장 메인 진입점은 [apps/wr/src/app/providers/app-provider.tsx (line 38)](/D:/iipbs/frontend/apps/wr/src/app/providers/app-provider.tsx:38) 의 CommonCodeInitializer +- 실제 공통코드 조회 액션은 [apps/wr/app/store/common/code/index.ts (line 372)](/D:/iipbs/frontend/apps/wr/app/store/common/code/index.ts:372) +- +## 1. 대상 화면 + +- 화면: `재이용실적등록 > 하수재이용실적등록` +- 페이지 파일: [apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:47) +- 스토어 파일: [apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:1) + +## 2. 분석 결론 요약 + +- 좌측 `하수재이용시설 목록`은 단일 API가 아니라 2개 API를 조합해 생성된다. +- 우측 `하수처리수재이용 실적(m³) 등록결과`는 별도 조회 API를 직접 호출하지 않는다. +- 우측 화면의 원본 데이터는 좌측 목록 조회 시 함께 내려오는 `prfList`이다. +- 우측 `저장`은 `POST /reuseFc/pubFcPrf`를 호출하고, 저장 성공 후 좌측 목록 조회 로직을 다시 호출해 화면을 갱신한다. + +## 3. 좌측 하수재이용시설 목록 API 호출 분석 + +### 3.1 화면 진입 시 호출 순서 + +1. 권한 코드 조회 + - `getLaeSeCdCheck()` + - 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:206) +2. WR에 등록된 하수재이용시설 기준 코드 목록 조회 + - `getPrfFmcRes()` + - 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:210) +3. 좌측 목록 초기 조회 + - `prfmncHasuInit(1, laeSeCd, year, prfPbnFclts)` + - 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:217) + +### 3.2 기준 시설코드 목록 조회 + +`getPrfFmcRes()`는 먼저 WR에서 등록된 시설코드 목록을 가져온다. + +- 함수 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:130) +- 호출 API: + +```text +GET /reuseFc/pubFc +``` + +- 목적: + - WR에 등록된 하수재이용시설의 기반 시설코드 목록 `prfPbnFclts`를 확보 + - 이후 좌측 목록 조회 시 FO/Common과 WR 조회의 기준 배열로 사용 + +### 3.3 좌측 목록 실제 조회 함수 + +좌측 목록은 `prfmncHasuInit()` 또는 `prfmncHasuNmSearch()`가 내부적으로 `hasuListFetch()`를 호출해 구성한다. + +- `prfmncHasuInit()` 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:144) +- `prfmncHasuNmSearch()` 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:170) +- 공통 실제 조회 함수 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:72) + +### 3.4 1차 호출: FO/Common 하수처리시설 기본정보 조회 + +`hasuListFetch()`는 먼저 `prfPbnFclts`를 `sggOrCity` 기준으로 필터링한다. + +- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:79) +- 필터 기준: + - `sggOrCity` 길이 2: 시도 코드 비교 + - 그 외: 앞 5자리 시군구 코드 비교 + +그 후 FO/Common API를 호출한다. + +```text +POST /prcsFclt/ruseInfo/1/{filteredFclt.length + 1} +``` + +- 호출 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:88) +- 실제 호출부: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:92) +- 요청 바디: + - `filteredFclt` + - WR 기준코드 목록 배열 + +이름 검색 시에는 URL 뒤에 검색어가 붙는다. + +```text +POST /prcsFclt/ruseInfo/1/{filteredFclt.length + 1}/{searchTxt} +``` + +- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:89) + +이 호출의 목적은 다음과 같다. + +- 하수처리시설의 기본정보 확보 + - `sido` + - `gugun` + - `bscFctNm` + - `bscFctCd` +- 검색어가 있을 경우 실제 검색 결과 시설코드 집합 재산정 + +### 3.5 2차 호출: WR 하수재이용시설 상세 및 실적 조회 + +1차 FO/Common 조회 결과를 바탕으로 WR API를 호출한다. + +```text +POST /reuseFc/pubDetailList/{year}/{sggOrCity}/{page}/{prfRegPageSize} +``` + +- 기본 페이지 크기: `prfRegPageSize = 10` +- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:19) +- 호출 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:99) +- 요청 바디: + - 검색어 없을 때: `filteredFclt` + - 검색어 있을 때: `rainRes.data.map((item) => item.bscFctCd)` + +즉 검색 시에는 FO/Common 검색 결과에서 살아남은 시설코드만 WR로 다시 전달한다. + +### 3.6 좌측 목록 최종 데이터 생성 방식 + +WR 응답 `pubRuseList.data`와 FO/Common 응답 `rainRes.data`를 다음 기준으로 매핑한다. + +- 매핑 키: + - `item.pbnstRuseFcltId.substring(0, 11)` + - `fclt.bscFctCd` + +- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:104) + +최종 좌측 목록 `prfmncRes`에는 다음 필드가 들어간다. + +- `sido` +- `gugun` +- `bscFctNm` +- `bscFctCd` +- `ruseUtztnFcltNm` +- `pbnstRuseFcltId` +- `prfList` + +여기서 중요한 점은 `prfList`가 좌측 목록 데이터에 이미 포함된다는 점이다. + +## 4. 우측 하수처리수재이용 실적(m³) 등록결과 조회 분석 + +### 4.1 결론 + +우측 `하수처리수재이용 실적(m³) 등록결과`는 별도 조회 API를 호출하지 않는다. + +우측 테이블의 데이터 원본은 좌측 목록 조회 결과 `prfmncRes[index].prfList`이다. + +### 4.2 데이터 생성 위치 + +좌측 행 클릭 시 `cellClick()`이 실행된다. + +- 함수 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:116) +- 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:326) + +`cellClick()` 내부 동작: + +1. 선택한 행의 `prfmncRes[index]`를 읽음 +2. 그 안의 `prfList`를 순회 +3. 현재 선택 연도 `searchYear`와 일치하는 월별 데이터만 추출 +4. `장내용수`, `장외용수`, `소계`, `총합계` 구조로 `inputData` 재구성 +5. 우측 테이블은 `inputData`를 그대로 렌더링 + +우측 렌더링 위치: + +- [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:356) + +따라서 우측 등록결과는 조회 API 기준으로 보면 다음 호출의 간접 결과다. + +```text +POST /reuseFc/pubDetailList/{year}/{sggOrCity}/{page}/{10} +``` + +## 5. 우측 저장 API 호출 분석 + +### 5.1 저장 버튼 호출 경로 + +우측 `저장` 버튼 클릭 시 `onSave()`가 실행된다. + +- 버튼 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:282) +- 저장 함수 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:93) + +`onSave()`는 화면 편집 데이터 `inputData`를 `excelToPbnPrfData()`로 변환한 뒤 `prfmnRegData()`에 전달한다. + +- 변환 코드: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:100) + +### 5.2 실제 저장 API + +`prfmnRegData()`에서 실제 저장 호출을 수행한다. + +- 함수 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:198) + +```text +POST /reuseFc/pubFcPrf +``` + +- 호출 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:207) +- 요청 바디: + - `PrfType` + - `excelToPbnPrfData(inputData, searchYear, pbnstRuseFcltId)` 변환 결과 + +### 5.3 저장 성공 후 후속 조회 + +저장 성공 시 `prfmnRegData()`는 종료하지 않고 다시 `hasuListFetch()`를 호출한다. + +- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:213) + +즉 저장 직후 다음 흐름으로 재조회가 일어난다. + +1. `POST /reuseFc/pubFcPrf` 저장 +2. `hasuListFetch()` 재실행 +3. 내부에서 다시 FO/Common + WR 조회 수행 +4. `prfmncRes`, `prfmncResCount` 갱신 +5. `pageKey` 증가 +6. 우측 테이블 `TableBody` 재렌더링 + +관련 코드: + +- 상태 갱신: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:221) +- 우측 `TableBody` key: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:360) + +## 6. 파일등록 엑셀 저장도 동일 API 사용 + +파일등록 모달에서도 저장 시 동일한 `prfmnRegData()`를 호출한다. + +- 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:381) + +즉 수기 입력 저장과 엑셀 등록 저장은 최종적으로 같은 API를 사용한다. + +```text +POST /reuseFc/pubFcPrf +``` + +## 7. 화면 이벤트별 API 요약 + +| 화면 이벤트 | 호출 함수 | 실제 API | +| --- | --- | --- | +| 화면 진입 | `getPrfFmcRes()` | `GET /reuseFc/pubFc` | +| 화면 진입 후 좌측 목록 초기조회 | `prfmncHasuInit()` | `POST /prcsFclt/ruseInfo/...` + `POST /reuseFc/pubDetailList/...` | +| 좌측 목록 이름검색 | `prfmncHasuNmSearch()` | `POST /prcsFclt/ruseInfo/.../{searchTxt}` + `POST /reuseFc/pubDetailList/...` | +| 좌측 행 클릭 | `cellClick()` | 별도 API 없음, `prfmncRes[index].prfList` 사용 | +| 우측 저장 | `prfmnRegData()` | `POST /reuseFc/pubFcPrf` 후 `POST /prcsFclt/ruseInfo/...` + `POST /reuseFc/pubDetailList/...` 재조회 | +| 엑셀 파일등록 저장 | `prfmnRegData()` | `POST /reuseFc/pubFcPrf` 후 재조회 | + +## 8. 확인이 필요한 사항 + +- `POST /prcsFclt/ruseInfo/1/{filteredFclt.length + 1}` 에서 `+ 1`을 주는 이유는 코드상 명확한 설명이 없다. +- 저장 후 재조회 시 현재 페이지를 유지하지 않고, 호출부에서 전달한 페이지 값에 의존한다. + - 현재 수기 저장과 엑셀 저장 모두 `1` 페이지로 호출하고 있다. +- 우측 화면은 독립 조회 API가 없으므로, `pubDetailList` 응답 구조가 바뀌면 좌우 화면이 함께 영향을 받는다. + diff --git a/apps/wr/docs/작업계획/지도팝업_20260626.md b/apps/wr/docs/작업계획/지도팝업_20260626.md new file mode 100644 index 000000000..3da41c87d --- /dev/null +++ b/apps/wr/docs/작업계획/지도팝업_20260626.md @@ -0,0 +1,300 @@ +# 지도 팝업 레이아웃 점검 작업계획 + +작성일: 2026-06-26 +대상 앱: `D:\iipbs\frontend\apps\wr` +대상 화면: +- `http://localhost:3001/mapFeaturePopup?layerNm=tb_wr_bdst_ruse_fclt&featureCd=43150SW001F` +- `http://localhost:3001/mapFeaturePopup?layerNm=tb_wr_tmpr_dmd_co&featureCd=미래산업` +- 선택 팝업 공통 검토 범위: + - `tb_wr_bdst_ruse_fclt` 하수재이용시설 + - `tb_wr_tmpr_dmd_co` 잠재수요처 + - `tb_wr_ruse_fclt_dmd_co` 수요처 + - `tb_wr_rnwtr_utztn_fclt` 빗물이용시설 + - `tb_wr_rswtr_fclt` 중수도시설 + - `tb_wr_htdfl_fclt` 온배수시설 + - `tb_il_swage_prcs_fclt` 하수처리시설 +- 공통 진입 파일: + - `apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx` + - `apps/wr/components/lib/func/mapUtil.ts` + +## 1. 작업 배경 + +- 지도 팝업에서 두 종류의 레이아웃 문제가 확인되었다. +- `하수재이용시설` 팝업은 컨텐츠 높이가 늘어나지 못해 하단 영역이 잘린다. +- `잠재수요처` 팝업은 컨텐츠 자체는 보이지만, 팝업 창 하단이 검은 배경으로 채워지지 않고 흰색 여백으로 남는다. +- 이 문제는 개별 폼 내부만이 아니라 지도 팝업 공통 래퍼, 팝업별 본문 높이 전략, 새 창 크기 전략을 함께 점검해야 재발을 막을 수 있다. + +## 2. 현재 구조 분석 + +### 2.1 지도 팝업 공통 진입 구조 + +- `apps/wr/app/(popUp-layout)/mapFeaturePopup/page.tsx` + - `layerNm` 값에 따라 서로 다른 상세 컴포넌트를 렌더링한다. + - 공통 본문 래퍼는 다음 클래스를 사용한다. + - `className="box-border m-0 bg-background p-4 pb-24 text-foreground md:p-6"` +- 현재 이 공통 래퍼에는 `min-h-screen`, `min-h-dvh`, `h-full` 같은 viewport 채움 조건이 없다. +- 따라서 컨텐츠 높이가 창 높이보다 작을 때는 문서 body의 기본 배경색이 하단에 그대로 노출될 수 있다. + +### 2.2 하수재이용시설 경로 + +- `mapFeaturePopup/page.tsx` + - `layerNm === "tb_wr_bdst_ruse_fclt"` 일 때 `RuseDetailPopup` 렌더링 +- `apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx` + - 시설별 탭을 만들고 각 탭에서 `PbnstSttnModal data={item} isMap={true}` 렌더링 +- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx:257` + - `const Wrapper: React.ElementType = isMap ? "div" : ScrollArea;` +- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx:267` + - 지도 팝업 여부와 무관하게 `<Wrapper className="h-[70vh]">` 적용 +- 지도 팝업에서는 `Wrapper`가 일반 `div`이므로: + - 내부 스크롤이 생기지 않고 + - 높이만 `70vh`로 고정되어 + - 아래쪽 `하수재이용시설 현황`, `하수재이용실적` 테이블이 박스 밖으로 밀린다. + +### 2.3 잠재수요처 경로 + +- `mapFeaturePopup/page.tsx` + - `layerNm === "tb_wr_tmpr_dmd_co"` 일 때 `CoDetailModal isMap={true}` 렌더링 +- `apps/wr/components/ui/modal/sttn/sttnDetailModal/coDatailModal.tsx` + - 내부 본문은 `<ScrollArea className="h-[79vh] mx-8">` 사용 +- 즉 잠재수요처는 내부 폼 스크롤은 이미 있으나, + - 공통 팝업 바깥 래퍼가 viewport 전체 높이를 채우지 못하면 + - 컨텐츠가 끝난 뒤 창 하단의 남는 공간이 `bg-background`가 아닌 흰색으로 보일 수 있다. +- 추가로 `pb-24` 같은 하단 padding이 공통 래퍼에 고정 적용되어 있어, + - 팝업별 실제 본문 높이와 맞지 않으면 시각적으로 불필요한 하단 여백처럼 보일 수 있다. + +### 2.4 다른 선택 팝업 경로 + +- `tb_wr_ruse_fclt_dmd_co` + - `apps/wr/components/ui/map/detailPopup/coSttnDetailPopup/coSttnDetailPopup.tsx` + - 내부 `bodyClassNm={"w-full mx-auto bg-background rounded-lg shadow-md p-6"}` 사용 +- `tb_wr_rnwtr_utztn_fclt`, `tb_wr_rswtr_fclt`, `tb_wr_htdfl_fclt` + - `apps/wr/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal.tsx` + - 별도 `ScrollArea`는 주석 처리되어 있고, 최상위 공통 팝업 래퍼 의존도가 높다. +- `tb_il_swage_prcs_fclt` + - `mapFeaturePopup/page.tsx` 안에서 `UiTable` 2개를 직접 렌더링 + - 역시 공통 래퍼 배경/높이 정책의 영향을 그대로 받는다. + +### 2.5 흰 여백이 없는 팝업과 있는 팝업의 구조 차이 + +- 흰 여백이 없다고 확인된 팝업: + - `tb_wr_ruse_fclt_dmd_co` + - `tb_wr_rnwtr_utztn_fclt` + - `tb_wr_rswtr_fclt` +- 흰 여백이 나타난다고 확인된 팝업: + - `tb_wr_tmpr_dmd_co` + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` + +차이점 1. 개별 본문 컴포넌트의 배경 책임 + +- 흰 여백이 없는 `tb_wr_ruse_fclt_dmd_co`는 + - `coSttnDetailPopup.tsx:23`에서 `bodyClassNm={"w-full mx-auto bg-background rounded-lg shadow-md p-6"}`를 직접 넘긴다. + - 즉 공통 팝업 래퍼 바깥이 아니라, 실제 본문 카드 자체가 `bg-background`를 명시적으로 가진다. +- 반대로 `tb_wr_tmpr_dmd_co`는 + - `coDatailModal.tsx:52`의 최상위가 단순 `<div>`이고, + - 내부는 `ScrollArea className="h-[79vh] mx-8"` 중심 구조다. + - 본문 전체 배경을 개별 카드로 강하게 감싸기보다 공통 팝업 래퍼 배경에 더 의존한다. +- `tb_il_swage_prcs_fclt`는 + - `mapFeaturePopup/page.tsx:103` 이하에서 `UiTable` 2개를 직접 렌더링하고, + - 별도 카드형 본문 래퍼 없이 공통 래퍼 위에 바로 쌓는다. + +차이점 2. 본문 높이 점유 방식 + +- 흰 여백이 없는 `tb_wr_rnwtr_utztn_fclt`, `tb_wr_rswtr_fclt`는 + - `sttnDetailModal.tsx:81` 이하에서 상세 폼 컴포넌트를 직접 렌더링한다. + - 현재 지도 팝업용 `ScrollArea`는 주석 처리되어 있어, 공통 래퍼 안에서 폼이 자연 높이로 이어진다. + - 실제 데이터량이 비교적 충분하면 본문이 창 하단 가까이까지 내려와 흰 여백이 드러나지 않을 수 있다. +- 반대로 `tb_wr_tmpr_dmd_co`는 + - `coDatailModal.tsx:67`의 `h-[79vh]` 스크롤 영역이 명시되어 있고, + - 바깥 공통 래퍼에는 `pb-24`가 남아 있어 하단 잔여 공간이 시각적으로 더 잘 드러날 가능성이 있다. +- `tb_wr_bdst_ruse_fclt`는 + - `pbnstSttnModal.tsx:267`에서 `h-[70vh]` 고정 높이를 사용한다. + - 이 경우 본문이 자연스럽게 창 끝까지 이어지지 못하고, 잘림 또는 하단 여백 문제가 함께 나타날 수 있다. + +차이점 3. 공통 래퍼 의존도 + +- 흰 여백이 없는 쪽은 + - 개별 컴포넌트가 자체 배경 카드나 충분한 컨텐츠 높이를 가져 공통 래퍼 결함이 눈에 덜 띈다. +- 흰 여백이 있는 쪽은 + - 개별 카드 배경이 약하거나 + - 본문 높이가 고정되거나 짧아 + - `mapFeaturePopup/page.tsx:100`의 공통 래퍼가 viewport 끝까지 채워지지 않는 문제가 바로 노출된다. + +### 2.6 팝업 창 자체 크기 + +- `apps/wr/components/lib/func/mapUtil.ts:146` + - 하수재이용시설: `window.innerHeight - 100` + - 잠재수요처: `window.innerHeight - 300` + - 수요처: `window.innerHeight - 430` +- 팝업 창 크기 자체 차이도 존재하지만, +- 이번 증상은 우선 새 창 크기보다 내부 공통 래퍼와 개별 팝업의 높이 전략 불일치 영향이 더 크다. + +## 3. 문제 유형 정리 + +### 3.1 유형 A: 내부 컨텐츠 잘림 + +- 대상: + - `tb_wr_bdst_ruse_fclt` +- 원인: + - `isMap=true`인데도 내부 래퍼가 `h-[70vh]`로 고정 + - `ScrollArea`가 아닌 일반 `div`라서 컨텐츠가 늘어나지 못함 + +### 3.2 유형 B: 팝업 하단 흰색 잔여 영역 + +- 우선 확인 대상: + - `tb_wr_tmpr_dmd_co` +- 함께 점검할 대상: + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` +- 원인 후보: + - 공통 래퍼가 viewport 전체 높이를 채우지 않음 + - 공통 래퍼의 하단 padding이 팝업별 컨텐츠 높이와 맞지 않음 + - 팝업별 내부 스크롤 높이와 새 창 높이 계산이 제각각임 + - 개별 팝업이 본문 배경을 자체적으로 채우지 못하고 공통 래퍼에 의존함 + +## 4. 수정 대상 + +### 4.1 공통 우선 수정 파일 + +- `D:\iipbs\frontend\apps\wr\app\(popUp-layout)\mapFeaturePopup\page.tsx` + +### 4.2 하수재이용시설 직접 수정 파일 + +- `D:\iipbs\frontend\apps\wr\components\ui\modal\sttn\pbnstSttnModal\pbnstSttnModal.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\map\detailPopup\ruseDetailPopup.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\form\fcltReg\pbnstReg\mngReg\mngRegForm.tsx` + +### 4.3 잠재수요처 및 타 팝업 점검 파일 + +- `D:\iipbs\frontend\apps\wr\components\ui\modal\sttn\sttnDetailModal\coDatailModal.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\map\detailPopup\coSttnDetailPopup\coSttnDetailPopup.tsx` +- `D:\iipbs\frontend\apps\wr\components\ui\modal\sttn\sttnDetailModal\sttnDetailModal.tsx` +- `D:\iipbs\frontend\apps\wr\components\lib\func\mapUtil.ts` + +## 5. 권장 구현 방향 + +### 5.1 공통 팝업 래퍼 정규화 + +- `mapFeaturePopup/page.tsx`의 최상위 래퍼를 지도 팝업 공통 레이아웃 기준점으로 삼는다. +- 다음 방향으로 정리하는 것이 우선이다. +- `bg-background`가 항상 팝업 창 하단까지 채워지도록 `min-h-screen` 또는 `min-h-dvh` 적용 +- 필요 시 `h-full`과 조합해 새 창 viewport를 안정적으로 덮도록 조정 +- 공통 `pb-24`는 팝업별로 과도한 여백을 만드는지 점검하고, 필요하면 축소 또는 분기 처리 + +### 5.2 하수재이용시설 전용 높이 분기 + +- `PbnstSttnModal`에서 `isMap`일 때 `h-[70vh]`를 제거하거나 `max-h` 기반으로 완화 +- 비지도 화면에서는 기존 `ScrollArea + h-[70vh]` 유지 +- 지도 팝업에서는: + - 본문 전체가 자연스럽게 늘어나게 하거나 + - 큰 테이블만 별도 스크롤 처리 + +### 5.3 잠재수요처 하단 흰 영역 대응 + +- `CoDetailModal` 자체의 `ScrollArea h-[79vh]`는 유지하되, +- 먼저 공통 팝업 래퍼의 배경/최소높이 문제를 해결한다. +- 그 다음에도 하얀 영역이 남으면 아래 순서로 추가 점검: + - `pb-24` 제거 또는 축소 시 시각적 개선 여부 + - `ScrollArea` 높이를 `79vh` 고정 대신 `calc(100vh - 헤더/패딩)` 기반으로 변경할지 검토 + - 팝업 창 자체 높이(`mapUtil.ts`)가 본문 높이 전략과 맞는지 재조정 + +### 5.4 선택된 타 팝업 공통 점검 + +- 잠재수요처 수정과 동일한 공통 래퍼 아래에서 동작하는 팝업들은 모두 한 번씩 확인한다. +- 특히 다음을 확인: + - 컨텐츠가 짧을 때 창 하단이 흰색으로 비는지 + - 컨텐츠가 길 때 내부 스크롤/전체 스크롤이 자연스러운지 + - 배경색이 본문 카드까지만 적용되고 바깥 여백은 흰색으로 남지 않는지 +- 이번 확인 결과상 우선순위는 두 그룹으로 나눈다. +- 흰 여백 발생 그룹: + - `tb_wr_tmpr_dmd_co` + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` +- 비교 기준 그룹: + - `tb_wr_ruse_fclt_dmd_co` + - `tb_wr_rnwtr_utztn_fclt` + - `tb_wr_rswtr_fclt` + +## 6. 상세 작업 단계 + +### 단계 1. 공통 래퍼 수정 + +- `mapFeaturePopup/page.tsx` + - 최상위 래퍼에 viewport 채움 클래스 추가 + - `pb-24`의 필요성 재검토 + - 팝업 종류별로 공통 패딩 분기 필요 여부 확인 + +### 단계 2. 하수재이용시설 잘림 수정 + +- `pbnstSttnModal.tsx` + - `isMap`일 때 `h-[70vh]` 제거 또는 `max-h`로 전환 + - 비지도 화면 동작은 유지 +- 필요 시: + - `RuseDetailPopup`의 `TabsContent` + - `PbnstMngForm` 내부 `h-[200px] overflow-y-auto` + - `하수재이용실적` 테이블 영역 + - 순서로 추가 보정 + +### 단계 3. 잠재수요처 하단 흰 영역 점검 + +- `coDatailModal.tsx` + - 공통 래퍼 수정 후에도 하얀 하단이 남는지 재확인 + - 남는 경우 `ScrollArea h-[79vh]`를 `calc()` 기반으로 조정 검토 +- 함께 비교: + - `coSttnDetailPopup.tsx`의 `bodyClassNm bg-background` 구조와 어떤 차이로 결과가 갈리는지 확인 +- `mapUtil.ts` + - 잠재수요처 팝업 높이(`window.innerHeight - 300`)가 현재 본문 구조와 맞는지 재검토 + +### 단계 4. 선택된 타 팝업 회귀 점검 + +- 발생 그룹 확인: + - `tb_wr_tmpr_dmd_co` + - `tb_wr_bdst_ruse_fclt` + - `tb_il_swage_prcs_fclt` +- 비교 그룹 확인: + - `tb_wr_ruse_fclt_dmd_co` + - `tb_wr_rnwtr_utztn_fclt` + - `tb_wr_rswtr_fclt` +- 두 그룹 비교 기준: + - 개별 본문이 `bg-background` 카드를 직접 가지는지 + - 본문이 자연 높이인지 고정 `vh` 높이인지 + - 공통 래퍼 `pb-24`와 결합될 때 하단 빈 공간이 드러나는지 +- 이후 추가 증상이 확인되면: + - 공통 래퍼로 해결 가능한지 우선 판단 + - 개별 컴포넌트 고정 높이/패딩이 있으면 2차 수정 대상으로 분리 + +## 7. 구현 시 주의사항 + +- 지도 팝업 컴포넌트들은 일반 화면/등록 화면과 공용인 경우가 많다. +- 따라서 수정은 반드시 `isMap` 또는 팝업 전용 경로 기준으로 제한해야 한다. +- `PbnstSttnModal`의 일반 화면 `ScrollArea + h-[70vh]` 동작은 회귀되지 않게 유지해야 한다. +- 잠재수요처의 경우 내부 폼이 아니라 바깥 공통 래퍼 문제일 가능성이 크므로, 개별 폼 높이부터 섣불리 줄이지 않는다. +- `UiTable`, `ScrollArea`, 카드형 래퍼가 혼합되어 있어, 공통 래퍼와 개별 본문 래퍼의 책임을 분리해서 봐야 한다. + +## 8. 검증 계획 + +### 8.1 하수재이용시설 검증 + +- `tb_wr_bdst_ruse_fclt` 클릭 시 팝업 본문 하단이 잘리지 않는다. +- `하수재이용시설 현황` 아래 `하수재이용실적` 영역이 같은 배경 안에서 정상적으로 이어진다. +- 데이터가 길어도 전체 스크롤 또는 의도한 내부 스크롤이 정상 동작한다. + +### 8.2 잠재수요처 검증 + +- `tb_wr_tmpr_dmd_co` 클릭 시 마지막 입력 필드 아래로 흰색 빈 영역이 남지 않는다. +- 팝업 창 하단까지 `bg-background`가 자연스럽게 이어진다. +- 내부 스크롤은 유지되되, 배경색이 창 하단에서 끊기지 않는다. + +### 8.3 선택 팝업 공통 검증 + +- 발생 그룹인 `tb_wr_tmpr_dmd_co`, `tb_wr_bdst_ruse_fclt`, `tb_il_swage_prcs_fclt`는 흰 여백이 제거되는지 확인한다. +- 비교 그룹인 `tb_wr_ruse_fclt_dmd_co`, `tb_wr_rnwtr_utztn_fclt`, `tb_wr_rswtr_fclt`는 기존처럼 흰 여백 없이 유지되는지 확인한다. +- 컨텐츠가 짧은 팝업에서 하단 흰색 잔여 영역이 남지 않는다. +- 컨텐츠가 긴 팝업에서 잘림 없이 스크롤이 가능하다. + +## 9. 결론 + +- 이번 이슈는 `하수재이용시설`의 고정 높이 문제와 `잠재수요처`·`하수처리시설`의 하단 배경 노출 문제로 나뉘지만, 비교 결과 공통 출발점은 `mapFeaturePopup/page.tsx` 공통 래퍼다. +- 1차 우선순위는 `mapFeaturePopup/page.tsx` 공통 래퍼를 viewport 기준으로 정규화하는 것이다. +- 2차 우선순위는 `PbnstSttnModal`의 지도 팝업 전용 `70vh` 고정 높이 제거다. +- 이후에는 "개별 본문 배경을 직접 가지는 팝업"과 "공통 래퍼 배경에 의존하는 팝업"의 차이를 기준으로, 하단 흰색 잔여 영역과 컨텐츠 잘림을 함께 정리하는 것이 맞다. diff --git a/apps/wr/docs/작업계획/테마적용 작업계획_20260623.md b/apps/wr/docs/작업계획/테마적용 작업계획_20260623.md new file mode 100644 index 000000000..cad73ed99 --- /dev/null +++ b/apps/wr/docs/작업계획/테마적용 작업계획_20260623.md @@ -0,0 +1,191 @@ +# apps/wr 테마 적용 작업계획 + +작성일: 2026-06-23 +대상 앱: `D:\iipbs\frontend\apps\wr` + +수정불가 component : `packages/common/components`와 `packages/common/assets` +수정가능 파일 : `packages/common/business_components/*`, `apps/wr/*` + +## 1. 분석 요약 + +- 현재 테마 구조는 이미 `data-theme` 기반으로 잡혀 있다. +- 실제 테마 키는 `default + black + blue + green + red` 기준으로 운영되고 있다. +- `apps/wr`는 `src/app/index.tsx`에서 `site-theme-key` 쿠키값을 읽어 `<html data-theme=...>`에 주입한다. +- `packages/common/components`와 `packages/common/assets`는 CSS 변수와 컴포넌트별 `.css` 파일을 통해 테마를 적용하는 구조다. +- 이번 작업의 전제는 `business_components`가 1차로 `assets`를 참조하고, 2차로 관련 `components`의 CSS 변경을 따라가야 한다는 점이다. +- 반면 현재 `packages/common/business_components`는 다수의 UI가 TSX 내부 Tailwind 클래스에 직접 의존하고 있어 위 전제를 충분히 만족하지 못한다. +- `apps/wr` 내부에서 `@common/business_components`를 참조하는 파일 수가 많아 영향 범위가 크다. + +## 2. 확인된 현재 구조 + +### 2.1 테마 진입점 + +- `D:\iipbs\frontend\apps\wr\src\app\index.tsx` + - `site-theme-key` 쿠키를 읽어 `data-theme`를 설정한다. +- `D:\iipbs\frontend\packages\common\components\src\ui\header\index.tsx` + - 사이트 정보 조회 후 `site-theme-key` 쿠키와 `document.documentElement[data-theme]`를 갱신한다. + +### 2.2 공통 테마 자산 + +- `D:\iipbs\frontend\packages\common\assets\src\styles\base.css` + - `:root`, `[data-theme="black"]`, `[data-theme="red"]`, `[data-theme="green"]`에서 CSS 변수를 정의한다. +- `D:\iipbs\frontend\packages\common\components\src\ui\*.css` + - `header.css`, `table.css`, `sidebar.css` 등에서 컴포넌트별 테마 오버라이드를 관리한다. +- `D:\iipbs\frontend\apps\wr\src\app\styles\components*.css` + - 앱 레벨의 blue/green/red/black 보정 CSS가 이미 존재한다. + +### 2.3 business_components 현황 + +- `packages/common/business_components/src/ui` 기준 소스 파일 수: 81 +- 같은 경로 기준 CSS 파일 수: 6 +- 이 중 일반 UI용 CSS는 사실상 `date_picker/date-picker.css` 중심이고, 나머지는 지도 legacy CSS 성격이 강하다. +- 하드코딩된 `text-gray-*`, `bg-white`, `border-gray-*`, `hover:bg-gray-*`, `dark:*` 패턴이 확인된 파일 수: 28 + +## 3. 핵심 판단 + +### 3.1 CSS 전환 가능 여부 + +- 가능하다. +- 다만 "모든 Tailwind를 제거"가 아니라 "테마 의존 스타일을 CSS로 추출하고, `assets` 및 `components` 참조 구조에 편입"하는 방향이 맞다. +- 즉 배경색, 글자색, 보더색, hover/active 색상, 선택 상태, 강조색은 CSS 변수 기반 클래스로 이동한다. +- 반대로 `flex`, `grid`, `gap`, `w-*`, `h-*`, `p-*` 같은 구조/간격 유틸리티까지 전부 CSS로 옮기는 것은 효율이 낮다. + +### 3.2 왜 이 방향이 맞는가 + +- 기존 `components`가 이미 같은 패턴을 사용한다. +- `assets/base.css`의 토큰(`--background`, `--foreground`, `--primary`, `--border`, `--label`, `--container`, `--title` 등)을 재사용할 수 있다. +- `business_components`는 `주 component가 바뀌면 따라 바뀌어야 한다`는 전제가 있으므로, 자체 색상 하드코딩보다 상위 스타일 자산 참조가 우선되어야 한다. +- 따라서 `business_components` 스타일은 독립 규격이 아니라 `assets -> components -> business_components` 흐름 안에서 정리되어야 한다. +- `apps/wr`는 `business_components` 사용 범위가 넓어, 테마 속성만 먼저 추출해도 화면 일관성이 크게 개선된다. +- 전부 TSX 내부 유틸리티로 유지하면 5개 테마 차이를 한 번에 통제하기 어렵다. + +### 3.3 참조 원칙 + +- 1차 기준: `@common/assets/styles/*` +- 2차 기준: 관련 `@common/components` 하위 CSS +- 3차 보완: 위 두 곳으로 흡수되지 않는 business 전용 표현만 `business_components` 내부 CSS에 작성 + +즉 `business_components`는 가능한 한 직접 색상을 소유하지 않고, 상위 레이어가 정의한 테마 결과를 따라가는 구조로 맞춘다. + +## 4. 우선 전환 대상 + +### 4.1 1순위: 색상 하드코딩이 직접 보이는 공통 UI + +- `packages/common/business_components/src/ui/file-upload/index.tsx` + - `text-gray-*`, `outline-slate-500`, `dark:text-gray-400` 사용 +- `packages/common/business_components/src/ui/tab/index.tsx` + - `text-blue-600`, `text-gray-500`, `border-gray-200`, `hover:border-gray-300` 사용 +- `packages/common/business_components/src/ui/search_input/index.tsx` +- `packages/common/business_components/src/ui/search_input_select/index.tsx` +- `packages/common/business_components/src/ui/select/index.tsx` +- `packages/common/business_components/src/ui/form_ui/index.tsx` +- `packages/common/business_components/src/ui/search_icon/index.tsx` + +이 영역은 `apps/wr`의 검색조건, 입력폼, 탭, 업로드 UI 전반에 재사용될 가능성이 높아서 우선순위가 높다. + +### 4.2 2순위: 테이블/검색/모달 계열 + +- `packages/common/business_components/src/ui/table/index.tsx` +- `packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx` +- `packages/common/business_components/src/ui/search_address/SearchAddressModal.tsx` +- `packages/common/business_components/src/ui/map/searchBar/SearchInputText.tsx` +- `packages/common/business_components/src/ui/map/searchBar/searchBar.tsx` +- `packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx` + +이 영역은 화면 체감도가 높고, 회색/흰색/파란색 하드코딩 비중이 높다. + +### 4.3 3순위: 차트 계열 + +- `packages/common/business_components/src/ui/chart/*.tsx` +- `packages/common/business_components/src/ui/chart/lib/chart.tsx` +- `packages/common/business_components/src/ui/chart/lib/card.tsx` + +차트는 레이아웃보다 tooltip, legend, card, axis text 색상이 중요하다. 우선 색상과 surface만 토큰화하고, 차트 팔레트는 기존 CSS 변수(`--chart-*`)를 재사용하는 방식이 적절하다. + +### 4.4 별도 트랙: 지도 legacy 영역 + +- `packages/common/business_components/src/ui/map/**` +- `packages/common/business_components/src/ui/map/lib/css/**` + +지도는 legacy JS/CSS, sprite/image, OpenLayers 연동이 섞여 있어 일반 UI와 분리해서 작업하는 것이 안전하다. + +## 5. 권장 작업 방식 + +### 단계 1. 테마 기준 정리 + +- `business_components`의 참조 순서를 먼저 고정한다. + - 1차: `assets` 토큰/공통 스타일 참조 + - 2차: 관련 `components` CSS 참조 + - 3차: business 전용 CSS 최소 추가 +- `components`에서 이미 쓰는 토큰과 클래스 패턴을 기준 문법으로 고정한다. +- `business_components`에서 새로 쓰는 색상 클래스는 직접 색상값을 쓰지 말고, 우선 상위 참조 자산으로 해결 가능한지 확인한 뒤 CSS 변수 기반으로 통일한다. +- 예시 기준 + - 텍스트: `text-foreground`, `text-label`, `text-title` + - 배경: `bg-background`, `bg-card`, `bg-container`, `bg-form` + - 보더: `border-border` + - 포커스/선택: `--primary`, `--primary-transparent` + +### 단계 2. 컴포넌트별 CSS 파일 추가 + +- 먼저 관련 `components`에 이미 존재하는 CSS가 있는지 확인한다. +- 같은 성격의 UI라면 `business_components`가 그 CSS 결과를 따라가도록 구조를 맞춘다. +- 상위 참조만으로 해결되지 않는 경우에만 `components`와 동일하게 각 컴포넌트 폴더에 `.css`를 둔다. +- CSS 파일명은 `packages/common/components`와 동일한 규칙으로 작성한다. +- 즉 CSS 파일명은 컴포넌트 폴더/컴포넌트명 기준의 kebab-case를 유지한다. +- CSS 클래스명은 너무 길게 만들지 않는다. +- 네이밍은 `__` 대신 `_`를 사용한다. +- 컴포넌트명도 필요 이상으로 길게 쓰지 않고 축약형을 허용한다. +- 예시 + - `file-upload/file-upload.css` + - `tab/tab.css` + - `search_input/search-input.css` + - `select/select.css` + - `search_address/search-address.css` +- TSX에서는 색상/상태 관련 className을 의미 있는 클래스명으로 치환한다. +- 이때 business 전용 CSS는 상위 CSS와 충돌하지 않도록 override 범위를 최소화한다. + +예시 방향 + +- 변경 전: `text-gray-500`, `bg-white`, `hover:bg-gray-50`, `data-[state=active]:text-blue-600` +- 변경 후: `fileup_hint`, `tab_trigger`, `searchaddr_suggestion`, `is_active` + +### 단계 3. 테마별 분기 작성 + +- 기본값은 `:root` 또는 기본 클래스에서 처리한다. +- 테마 차이가 필요한 경우만 `[data-theme="black"]`, `[data-theme="blue"]`, `[data-theme="green"]`, `[data-theme="red"]` 블록을 추가한다. +- 동일한 값은 중복 정의하지 않는다. +- 가능하면 테마 값은 `assets` 또는 `components`에서 정의하고, `business_components`는 해당 결과를 소비하는 쪽으로 유지한다. + +### 단계 4. apps/wr 화면 검증 + +- `apps/wr`에서 `business_components` 사용도가 높은 화면부터 확인한다. +- 우선 검증 화면 + - 통계 화면 + - 재이용시설 관리 등록/현황 화면 + - 자재관리 검색/등록 화면 + - 지도 검색바/상세팝업/레이어팝업 + +## 6. 실제 작업 순서 + +1. `business_components` 내 테마 하드코딩 파일 목록 확정 +2. 각 대상 UI별로 `assets` 참조와 `components` 참조 가능 여부를 먼저 판정 +3. 공통 토큰 매핑표 작성 +4. 1순위 UI에 상위 참조 우선 적용 후 부족한 부분만 business 전용 CSS 추가 +5. `apps/wr` 주요 화면에서 5개 테마 수동 점검 +6. 2순위 테이블/검색/모달 영역 확장 +7. 차트 계열 색상 토큰화 +8. 지도 legacy 영역은 별도 보수 계획으로 분리하며 배경지도는 수정하지 않는다. + +## 7. 검증 기준 + +- `assets` , `components`가 수정되면 안된다. +- 같은 화면에서 5개 테마 전환 시 텍스트, 보더, 배경, hover, 선택상태가 모두 읽혀야 한다. +- black 테마에서 회색/흰색 하드코딩이 남아 흐릿하거나 과도하게 튀는 영역이 없어야 한다. +- blue/green/red 테마에서 활성 탭, 버튼, 선택상태가 `primary` 계열로 일관되게 바뀌어야 한다. +- `apps/wr` 기존 기능 동작은 그대로 유지되어야 한다. + +## 8. 결론 + +- `business_components`의 Tailwind 스타일을 `assets` 1차 참조, `components` 2차 참조 구조에 맞춰 재편하는 것은 가능하다. +- 다만 전체 유틸리티를 전부 CSS로 옮기기보다, 테마에 영향을 주는 색상/상태 표현을 우선 상위 자산 참조 구조로 흡수하는 방식이 가장 현실적이다. +- 우선순위는 `공통 입력 UI -> 테이블/검색/모달 -> 차트 -> 지도 legacy` 순으로 잡는 것이 좋다. diff --git a/apps/wr/docs/작업계획/테마적용.md b/apps/wr/docs/작업계획/테마적용.md new file mode 100644 index 000000000..c5d8cbd9b --- /dev/null +++ b/apps/wr/docs/작업계획/테마적용.md @@ -0,0 +1,11 @@ +#frontend 5가지 테마(theme) css 적용 작업 +## 대상 dir: D:\iipbs\fr\frontend +## branch :iipbs/dm/dev +## 대상앱 : apps/wr +## 전제 : + - 주 component는 packages/common/components, packages/common/assets이다. 5가지 테마가 적용되어 있다. + - 부 component인 packages/common/business_components는 주 component가 바뀌면 따라 바뀌어야 한다. +## 지시 : + - business_components에 css로 안되어 있고 tailwind 코드가 삽입된 경우는 components와 같이 css로 넣어 줄 수 있는지 확인 부탁해. + - 어떤 방향으로 작업이 진행되어야 할지 프로젝트를 분석해 주고 작업계획_[오늘날짜].md 파일 만들어줘. + \ No newline at end of file diff --git a/apps/wr/docs/작업계획/하수재이용속도개선.md b/apps/wr/docs/작업계획/하수재이용속도개선.md new file mode 100644 index 000000000..1c5171d97 --- /dev/null +++ b/apps/wr/docs/작업계획/하수재이용속도개선.md @@ -0,0 +1,257 @@ +# 하수재이용시설 등록 목록 조회 속도개선 작업계획 + +## 1. 배경 + +- 대상 화면: `frontend/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx` +- 대상 스토어: `frontend/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx` +- 연계 백엔드: `D:\iipbs\wr_back_out` + +현재 `재이용시설 등록 > 하수재이용시설 등록` 화면의 목록은 아래 순서로 구성된다. + +1. WR 등록 시설 목록 조회 + - `GET /reuseFc/pubFc` +2. 하수처리시설 목록 조회 + - `GET/POST /prcsFclt/ruseInfo/reg/...` +3. 프론트에서 `bscFctCd` 기준으로 등록 여부를 합성 + +문제는 2번 조회가 운영 `ONSEMIRO_NEW` 의존 경로를 타면서 응답시간이 길어지고, 그 결과 등록 화면 진입과 검색 모두 느려진다는 점이다. + +이번 개선의 기준목록은 `ONSEMIRO_NEW.OM_MS_VW`의 하수처리시설 목록이다. 현재 이 기준정보가 WR(Postgres)에 적재된 테이블은 없으므로, `reuse-service`가 `ONSEMIRO_NEW.OM_MS_VW`를 직접 조회해야 한다. + +--- + +## 2. 현재 구조 확인 결과 + +### 2.1 프론트 + +- `checkWrRegList` + - `GET /reuseFc/pubFc` 호출로 WR 등록 시설 목록을 별도 조회 +- `hasuListFetch` + - `/prcsFclt/ruseInfo/reg/${page}/${DEFAULT_LIST_SIZE}/A/${sggOrCity}/0/1/SW` 호출 + - 검색어가 있으면 `swtrmtFcltNm` query 추가 + - FO 응답과 WR 목록을 프론트에서 합쳐 `ruse: "Y" | "N"` 계산 +- `pubHasuInit`, `pubHasuSearch` + - 페이지 이동, 최초 진입, 검색 시마다 위 로직 반복 + +즉, 현재 등록 화면은 "목록 원천 조회"와 "등록 여부 판정"을 프론트가 나눠서 처리하고 있다. + +### 2.2 백엔드 + +- `wr_back_out`에는 이미 WR 기준 조회용 `pbnstList` API가 존재한다. + - `PublicFcController.getPbnstList` + - `PublicFcService.getPbnstList` + - `public_fc_select.xml#getPbnstList` +- 다만 현재는 `ONSEMIRO_NEW.OM_MS_VW`가 WR(Postgres)에 적재되어 있지 않다. +- 따라서 `ONSEMIRO_NEW.OM_MS_VW`와 `sm_bs_wr.tb_wr_pbnst_ruse_fclt`를 DB 레벨에서 직접 조인하는 방식은 사용할 수 없다. + +하지만 등록 화면은 아직 이 WR 단일 조회 경로를 사용하지 않고, 여전히 `operation-service` 경유 목록 조회를 사용 중이다. + +--- + +## 3. 개선 방향 + +### 3.1 1차 목표 + +현 상황에서 가장 빠르게 체감속도를 높이는 방법은 아래 두 가지를 동시에 적용하는 것이다. + +1. 등록 화면 목록 조회를 `operation-service` 및 `ONSEMIRO_NEW` 실시간 호출 경로에서 분리 +2. 백엔드가 "`OM_MS_VW` 기준 하수처리시설 목록 + WR 등록여부"를 한 번에 내려주도록 변경 + +즉, 프론트의 `WR 목록 조회 + operation-service 목록 조회 + 프론트 merge` 구조를, +`reuse-service 단일 API 1회 호출` 구조로 바꾼다. + +### 3.2 기본 원칙 + +- 기준목록은 `ONSEMIRO_NEW.OM_MS_VW`의 하수처리시설이다. +- 기준정보는 WR 적재본이 아니라 `reuse-service`에서 `ONSEMIRO_NEW.OM_MS_VW`를 직접 조회한다. +- 다만 프론트는 `operation-service`를 호출하지 않고 `reuse-service`만 호출한다. +- 기존 `operation-service`의 하수처리시설 조회 로직은 `D:\iipbs\iipbs_fo`의 구현을 기준으로 `reuse-service`로 이관한다. +- 등록 화면에서 필요한 컬럼만 조회한다. +- 등록 여부 계산은 프론트가 아니라 백엔드 SQL에서 처리한다. +- `ruse`는 "재이용여부"가 아니라 "등록여부"로 사용한다. +- `rswtrYn`는 `reuse-service`에 신규로 만들어지는 API에서 조회되는 값을 재이용여부로 사용한다. +- 검색/페이지 이동 시 불필요한 중복 호출을 줄인다. + +--- + +## 4. 프론트 작업계획 + +### 4.1 대상 파일 + +- `frontend/apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/index.tsx` +- `frontend/apps/wr/app/(with-layout)/ruseFcltMng/fcltReg/pbnstReg/page.tsx` + +### 4.2 변경 계획 + +1. `hasuListFetch`를 `reuse-service` 단일 목록 API 호출 방식으로 교체 + - 기존 + - `GET /reuseFc/pubFc` + - `GET/POST /prcsFclt/ruseInfo/reg/...` (`operation-service`) + - 프론트 merge + - 변경 + - `GET /reuseFc/...` 신규 또는 확장 API 1회 + +2. `checkWrRegList` 제거 또는 비활성화 + - 등록 여부를 별도 조회하지 않고 백엔드 응답의 `ruse` 사용 + +3. `pubHasuInit` 삭제, `pubHasuSearch`로 통합. + - URL 파라미터만 조합하고 목록 바인딩만 수행 + +4. 검색 입력 최적화 + - 검색 버튼 클릭 기준 유지 시 즉시 반영 + - 필요 시 `swtrmtFcltNm` 입력에 debounce 적용 검토 + - 페이지 이동 시 같은 조건이면 재호출 최소화 + +5. 응답 필드 의미 정리 + - `ruse`: 등록여부 (`sm_bs_wr.tb_wr_pbnst_ruse_fclt` 등록 시 `Y`) + - `rswtrYn`: 기존 `operation-service` 목록 응답과 동일한 재이용여부 + +6. 수정/등록 후 재조회 범위 축소 + - 전체 목록 재초기화 대신 현재 페이지 재조회 + - 선택 행 유지 가능 여부 검토 + +### 4.3 프론트 기대효과 + +- 최초 진입 시 `operation-service` 외부 느린 경로 제거 +- 검색 시 응답시간 단축 +- 프론트 merge 로직 제거로 코드 단순화 +- 등록 여부 계산 일관성 확보 + +--- + +## 5. 백엔드 작업계획 + +### 5.1 대상 파일 + +- `D:\iipbs\wr_back_out\src\main\java\kr\co\mindone\wrout\controller\PublicFcController.java` +- `D:\iipbs\wr_back_out\src\main\java\kr\co\mindone\wrout\service\public_fc\PublicFcService.java` +- `D:\iipbs\wr_back_out\src\main\java\kr\co\mindone\wrout\mapper\wr\PublicFcMapper.java` +- `D:\iipbs\wr_back_out\src\main\resources\postgres\mapper\wr\public_fc_select.xml` + +### 5.2 구현 방향 + +1. 등록 화면 전용 목록 API 추가 + - 목적 + - `OM_MS_VW` 기준 하수처리시설 목록 + - WR 등록 여부(`ruse`) + - 재이용여부(`rswtrYn`) + - 등록 화면에 필요한 최소 컬럼 + - 페이지/검색/행정구역 조건 + - 후보 경로 예시 + - `GET /reuseFc/pbnstRegList/{pageNo}/{pageCnt}/{searchingCd}/{nameCd}` + - 구현 기준 + - `D:\iipbs\iipbs_fo`의 `ruseInfo/reg` 기존 조회 로직과 응답 컬럼을 최대한 그대로 가져온다. + - 프론트가 받는 목록 구조도 기존과 최대한 동일하게 유지한다. + + +2. 조회 원천을 `reuse-service`로 단일화 + - 기준목록은 `reuse-service`에서 `ONSEMIRO_NEW.OM_MS_VW` 조회 + - 등록정보는 `sm_bs_wr.tb_wr_pbnst_ruse_fclt` 조회 + - 두 DB는 다르므로 DB 조인 대신 `reuse-service` 서비스 로직에서 결과를 합성 + - 프론트의 `operation-service` 호출 제거 + +3. SQL을 등록 화면 전용으로 최소화 + - 상세/실적/운영정보까지 한 번에 끌고 오지 않음 + - 등록 화면 좌측 목록에 필요한 컬럼만 select + - 검색 조건 + - 행정구역 + - 시설명 + - 필요 시 시설용량 + +4. 등록여부/재이용여부 계산을 백엔드로 일원화 + - 등록여부 + - 기준: `OM_MS_VW`의 하수처리시설을 조회했을 때 `sm_bs_wr.tb_wr_pbnst_ruse_fclt`에도 등록되어 있으면 `ruse = 'Y'` + - 재이용여부 + - 기존 `operation-service`가 내려주던 `rswtrYn`를 그대로 반환 + - 즉, 현재 목록에서 보이던 재이용여부 표시는 유지하되, `ruse`의 의미만 등록여부로 수정 + - 구현 방식 + - `ONSEMIRO_NEW.OM_MS_VW` 결과에서 시설키 목록 추출 + - WR의 `tb_wr_pbnst_ruse_fclt` 등록정보를 별도 조회 + - `reuse-service`에서 키 기준으로 매핑 후 응답 생성 + - 프론트의 `Set` 비교 제거 + +5. 인덱스/실행계획 점검 + - `ONSEMIRO_NEW.OM_MS_VW` 조회 조건 컬럼 + - 시설코드 + - 행정구역 코드 + - 시설명 + - `tb_wr_pbnst_ruse_fclt`의 연계 키 컬럼 + - `swage_trtmfct_id` + - `pbnst_ruse_fclt_id` + - `LIKE '%검색어%'`가 병목이면 prefix 검색 또는 보조 검색전략 검토 + +6. API 호출 원칙 고정 + - 등록 화면 목록은 `reuse-service`만 호출한다. + - `operation-service`는 호출하지 않는다. + +7. 선택적 캐시 적용 검토 + - 같은 조건 반복 조회가 많으면 짧은 TTL 메모리 캐시 가능 + - 단, 1차는 SQL 단순화와 WR 내부조회 전환이 우선 + +### 5.3 백엔드 기대효과 + +- 등록 목록 조회 시 `operation-service` 및 `ONSEMIRO_NEW` 대기 제거 +- 목록 API 응답 구조 단순화 +- 등록여부/재이용여부 의미 분리 +- 프론트와 백엔드 간 호출 수 감소 + +--- + +## 6. 권장 작업 순서 + +### 단계 1. 백엔드 전용 목록 API 준비 + +- `D:\iipbs\iipbs_fo`의 `ruseInfo/reg` 기존 로직 분석 +- `ONSEMIRO_NEW.OM_MS_VW` 조회 SQL 작성 +- WR 등록정보 조회 SQL 작성 +- 두 결과를 `reuse-service`에서 합성하는 서비스 로직 작성 +- 등록여부(`ruse`) / 재이용여부(`rswtrYn`) 포함 응답 DTO 정의 +- Swagger 및 응답 count 포함 + +### 단계 2. 프론트 등록 화면 연결 + +- `hasuListFetch`를 `reuse-service` 신규 API로 교체 +- `checkWrRegList` 제거 +- 화면 목록 바인딩 점검 + +### 단계 3. 성능 검증 + +- 동일 조건 기준 기존/개선 응답시간 비교 +- 최초 진입, 검색, 페이지 이동 각각 측정 +- 운영데이터와 등록여부 정합성 확인 + +--- + +## 7. 검증 항목 + +1. `하수재이용시설 등록` 화면 최초 진입 시간이 개선되는지 +2. 시설명 검색 시 응답시간이 개선되는지 +3. `sm_bs_wr.tb_wr_pbnst_ruse_fclt` 등록 여부에 따라 `ruse = Y/N`가 정확히 표시되는지 +4. `rswtrYn`가 재이용여부 의미로 올바르게 내려오는지 +5. 특정 지자체 권한(`laeSeCd`) 조건에서 목록이 정상 필터링되는지 +6. 행 선택 후 상세 등록 폼 진입 동작에 영향이 없는지 + +--- + +## 8. 리스크 및 유의사항 + +- `ONSEMIRO_NEW.OM_MS_VW`와 WR 등록정보가 서로 다른 DB에 있으므로 DB 조인은 불가하다. +- 따라서 `reuse-service` 내부에서 2회 조회 후 합성하는 구조가 필요하다. +- `operation-service`는 제거하더라도 `reuse-service`의 `ONSEMIRO_NEW` 직접 조회 성능은 여전히 관리해야 한다. +- 기존 `pbnstList`는 현황 화면 성격이 강하므로, 등록 화면은 별도 API로 분리하는 편이 유지보수에 유리하다. +- 필드 의미 변경은 `ruse`에만 적용되므로, 기존 화면에서 `ruse`를 재이용여부로 해석하는 코드가 남아 있지 않은지 점검이 필요하다. +- `rswtrYn`는 기존 operation-service 응답으로 frontend에서 등록여부로 사용되었으나 재이용여부로 변경한다. +- 상세 진입 시 `pbnstRegClick -> /reuseFc/pubFc/{bscFctCd}` 경로는 별도 검토가 필요하다. 목록 최적화 후에도 상세 로딩 병목이 남을 수 있다. + +--- + +## 9. 결론 + +1차 개선안은 "`reuse-service`가 `OM_MS_VW` 기준 하수처리시설 목록과 WR 등록정보를 각각 조회해 서버에서 합성하고, 등록 화면은 `reuse-service` 단일 API만 호출"하도록 바꾸는 것이다. + +핵심은 다음 2건이다. + +1. 백엔드에서 `iipbs_fo`의 기존 `ruseInfo/reg` 로직을 `reuse-service`로 이관하고, `OM_MS_VW 기준정보 + 등록여부(ruse) + 재이용여부(rswtrYn)`를 한 번에 반환하는 등록 전용 API 추가 +2. 프론트에서 `operation-service` 경로와 merge 로직을 제거하고 `reuse-service`만 호출하도록 변경 + +이 방향이 현재 구조에서 가장 적은 변경으로 가장 큰 체감속도 개선을 낼 수 있다. diff --git a/apps/wr/docs/작업계획/하수재이용시설 현황 api 정리_20260701.md b/apps/wr/docs/작업계획/하수재이용시설 현황 api 정리_20260701.md new file mode 100644 index 000000000..b3a32c7d3 --- /dev/null +++ b/apps/wr/docs/작업계획/하수재이용시설 현황 api 정리_20260701.md @@ -0,0 +1,371 @@ +# 하수재이용시설 현황 API 정리 작업계획 + +작성일: 2026-07-01 + +## 1. 배경 + +현재 하수재이용시설 현황/상세 조회는 백엔드와 프런트 모두에서 응답 구조가 과도하게 분산되어 있다. + +- 상세 조회는 `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 호출 후, 다시 `POST /reuseFc/pubDetailList/...`를 추가 호출해서 실적을 붙이고 있다. +- 현황 목록은 `GET /reuseFc/pbnstList/...` 호출 시 월별 실적 내역 `prfList`까지 함께 조회한 뒤, 서비스 레이어에서 장내/장외/총합계를 계산하고 있다. +- 프런트도 위 구조에 맞춰 상세 모달과 현황 화면에서 중복 후처리를 수행한다. + +이번 작업의 목표는 아래 2가지다. + +1. `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 단일 호출만으로 상세 모달에 필요한 데이터가 모두 내려오도록 정리 +2. `GET /reuseFc/pbnstList/...`는 목록에 필요한 합계만 SQL에서 계산해 내려주고, 월별 실적 내역 `prfList`는 제외 + +추가 전제: + +- 본 작업은 backend 정리 후 이에 맞춰 `frontend`도 함께 수정한다. + +## 2. 현재 구조 분석 + +### 2.1 상세 조회 API + +현재 백엔드 상세 조회 엔드포인트: + +- `GET /reuseFc/pubFc/{pbnstRuseFcltId}` + +현재 SQL/매핑: + +- `public_fc_select.xml`의 `getFc` +- `PublicFcInsertResultMap` + +현재 `getFc`는 아래 성격의 데이터만 내려준다. + +- 기본 시설 정보 +- 파일 정보 +- 사업 정보 +- 운영 정보 +- 원수 취수 정보 +- 공급관로 정보 +- 수요처 정보 + +현재 `prfList`는 포함하지 않는다. + +### 2.2 상세 실적 추가 조회 API + +현재 추가 호출 엔드포인트: + +- `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}` + +현재 역할: + +- 시설 목록 기준으로 `prfList`를 포함한 실적 정보를 별도 조회 +- 프런트에서 `pubFc/{id}` 결과와 `pubDetailList` 결과를 다시 매핑 + +### 2.3 상세 모달 사용 위치 분석 + +실제 상세 모달 렌더링 위치는 아래 2곳이다. + +- `apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx` +- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx` + +추가 영향 지점은 아래와 같다. + +- `apps/wr/app/store/map/ruseMap/index.tsx` + - `getRuseMaplList()` + - `getRuseMaplListReturn()` + - 현재 `pubFc/{id}` 호출 후 `pubDetailList`를 다시 호출해 `prfList`를 수동 병합 + +- `apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx` + - `getPbnStnDetail()` + - 현재는 `pubFc/{id}`만 조회하지만, 모달 내부는 상황에 따라 목록의 `prfList`를 참조 + +정리하면, 상세 모달 렌더링 지점은 2곳이 맞고, 실제 수정 범위는 스토어까지 확장된다. + +### 2.4 현황 목록 API + +현재 목록 엔드포인트: + +- `GET /reuseFc/pbnstList/{year}/{pageNo}/{pageCnt}/{searchingCd}/{nameCd}/{fcltCpct}/{oprtngSttsCd}/{bscFctClsCd}` + +현재 구조: + +- SQL `getPbnstList`에서 `tb_wr_pbnst_ruse_fclt_prfmnc`를 조인 +- `PublicPrfListMap`으로 월별 `prfList`까지 전부 매핑 +- `PublicFcService.getPbnstList()`에서 `sumUsage()`로 장내/장외/총합 계산 +- 응답 객체 `PublicPbnstList`에 `prfList`도 포함 + +문제점: + +- 목록 조회인데 월별 실적 상세까지 매번 로딩 +- 페이지 목록 응답에 비해 payload가 과다 +- 서비스 레이어에서 반복 합계 계산 수행 +- 프런트는 목록 화면에서 합계만 사용하지만 상세 내역까지 수신 + +## 3. 목표 구조 + +### 3.1 상세 조회 목표 + +`GET /reuseFc/pubFc/{pbnstRuseFcltId}` 한 번으로 아래가 모두 내려오도록 변경한다. + +- 기본 시설 정보 +- 파일/사업/운영/원수취수/공급관로/수요처 정보 +- 해당 시설의 하수재이용실적 `prfList` +- 필요 시 장내/장외/총합도 함께 포함 여부 검토 + +프런트는 상세 모달 진입 시 더 이상 `POST /reuseFc/pubDetailList/...`를 호출하지 않도록 변경한다. + +### 3.2 목록 조회 목표 + +`GET /reuseFc/pbnstList/...`는 아래만 내려오도록 정리한다. + +- 시설 기본 정보 +- 운영정보 1건 +- 장내 합계 +- 장외 합계 +- 총합계 + +제외 대상: + +- 월별 실적 상세 `prfList` + +합계는 SQL에서 계산한다. + +## 4. 백엔드 작업계획 + +### 4.1 `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 응답 확장 + +대상 파일: + +- `wr_back_out/src/main/resources/postgres/mapper/wr/public_fc_select.xml` +- `wr_back_out/src/main/java/kr/co/mindone/wrout/object/publicFc/PublicFcInsert.java` 대신 PublicFcSelect.java 로 신규 object생성 +- `wr_back_out/src/main/java/kr/co/mindone/wrout/controller/PublicFcController.java` +- `wr_back_out/src/main/java/kr/co/mindone/wrout/service/public_fc/PublicFcService.java` + +계획: + +1. `getFc` SQL에 `tb_wr_pbnst_ruse_fclt_prfmnc` 조인 추가 +2. `year` query param을 추가하고, 해당 연도 실적만 조회하도록 수정 +3. frontend에서 상세 조회 시 현재년도를 세팅해서 호출 + +적용안: + +- `GET /reuseFc/pubFc/{pbnstRuseFcltId}?year=2026` +- frontend는 상세 모달 진입 시 `new Date().getFullYear()` 기준 현재년도를 세팅해서 호출 +- backend는 전달받은 `year` 기준으로 `prfList`를 조회 + +이유: + +- 상세 모달은 현재 연도 기준 실적을 보는 흐름이 명확하다 +- 전체 연도 실적을 무제한 포함하면 payload가 다시 커진다 +- 실적 조회 기준 연도가 API 계약에 명시되어야 응답이 예측 가능하다 + +추가 검토: + +- 현재 `getFc`는 `like #{fcltPk}||'%'`라서 같은 하수처리시설 하위 재이용시설 여러 건을 반환하는 구조다. +- 호출명이 `/{pbnstRuseFcltId}` 이므로 실제 의도가 + - 특정 재이용시설 1건 조회인지 + - 기반 시설코드(prefix) 하위 재이용시설 다건 조회인지 + 를 명확히 해야 한다. + +현 프런트 사용상 분석: + +- `ruseDetailPopup.tsx`는 탭으로 여러 재이용시설을 보여주므로 prefix 기반 다건 조회 구조를 활용 중이다. +- `getPbnStnDetail()`는 정확한 `pbnstRuseFcltId`로 단건 선택 후 화면에서 1건 사용한다. + +따라서 이번 변경에서는 기존 다건 반환 계약은 유지하되, 반환 각 항목에 `prfList`를 포함하는 쪽이 안전하다. + +### 4.2 `pubDetailList` 영향 검토 + +현재 `pubDetailList`는 아래에서 사용된다. + +- `apps/wr/app/store/map/ruseMap/index.tsx` + - `hasuListFetch()` + - `getRuseMaplList()` + - `getRuseMaplListReturn()` +- `packages/common/business_components/src/ui/map/searchBar/searchBar.tsx` + - 경로 문자열 참조 1건 +- 과거 실적등록 관련 코드/문서 + +계획: + +- 이번 작업에서 상세 모달 경로는 `pubFc/{id}` 단일 조회로 전환 +- `pubDetailList`는 잔여 런타임 사용처가 확인되었으므로 즉시 제거하지 않는다 +- 1차로 상세 모달 경로에서 제거하고, `ruseMap` 목록/지도 경로는 영향도 확인 후 후속 정리한다 +- 작업계획 기준으로는 "상세 모달에서는 더 이상 사용하지 않음"까지를 1차 목표로 둔다 + +### 4.3 `GET /reuseFc/pbnstList/...` SQL 합계화 + +대상 파일: + +- `public_fc_select.xml` +- `PublicPbnstList.java` +- `PublicPrfListMap` 및 관련 DTO +- `PublicFcService.getPbnstList()` + +계획: + +1. `getPbnstList` SQL에서 `prfList` 월별 내역 컬럼 제거 +2. 연도 조건이 반영된 실적 집계 서브쿼리 또는 집계 조인 추가 +3. SQL에서 아래 컬럼 직접 계산 + - `in_usage` + - `out_usage` + - `usage_total` +4. 서비스 레이어 `sumUsage()` 기반 계산 제거 +5. `PublicPbnstList`에서 `prfList` 제거 + +- `pbnstList`는 현황 목록 용도 +- `pubFc/{id}`는 상세 용도 +- 응답 목적이 달라 DTO를 분리하는 편이 유지보수에 유리 + +### 4.4 서비스 레이어 정리 + +`PublicFcService.getPbnstList()`에서는 아래를 제거할 수 있다. + +- `item.setPrfList(detail.getPrfList())` +- `sumUsage(...)` 연쇄 호출 + +남는 역할: + +- SQL 결과를 DTO로 변환 +- 운영정보 1건 정리 +- 기본 문자열 보정 + +## 5. 프런트 작업계획 + +### 5.1 상세 모달 경로 정리 + +대상 파일: + +- `apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx` +- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx` +- `apps/wr/app/store/map/ruseMap/index.tsx` +- `apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx` +- `apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx` + +계획: + +1. `ruseMap.getRuseMaplList()`에서 `pubDetailList` 추가 호출 제거 +2. `pubFc/{id}` 응답만으로 `ruseDetailList` 구성 +3. `PbnstSttnModal`이 `isMap` 여부에 따라 목록 `prfList`를 다시 참조하는 구조 단순화 +4. 상세 모달 공통 데이터 타입에 `prfList`가 항상 포함되도록 정리 +5. 상세 조회 호출 시 frontend에서 현재년도를 query param으로 전달 + +목표: + +- 상세 모달 렌더링 시 실적 데이터 원천을 `pubFc/{id}` 하나로 통일 + +### 5.2 현황 목록 경로 정리 + +대상 파일: + +- `apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx` +- 관련 화면: + - `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx` + +계획: + +1. `pbnstListFetch()`가 `pbnstList` 응답의 `inUsage/outUsage/usageTotal`만 사용하도록 정리 +2. 목록 응답에서 `prfList`를 기대하는 코드 제거 +3. 상세 버튼 클릭 시 목록 데이터가 아니라 상세 API 응답을 기준으로 모달 표시 + +## 6. 데이터 구조 변경 계획 + +### 6.1 상세 응답 + +현재 후보: + +- `PublicFcInsert` 확장 +- 또는 `PublicFcDetailWithPrf` 신규 DTO 추가 + +권장: + +- 신규 상세 DTO 분리 검토 + +이유: + +- 등록/수정용 DTO와 조회용 DTO 성격이 다름 +- 상세 조회에 `prfList`, 합계 필드, 화면 전용 값이 추가될 가능성이 높음 + +### 6.2 목록 응답 + +현재 `PublicPbnstList`는 `prfList`를 포함한다. + +변경 방향: + +- 목록 DTO에서는 `prfList` 제거 +- 합계 숫자 필드만 유지 + +프런트 타입도 이에 맞춰 변경: + +- `WaterReuseFacility` +- `customPbnst` +- `FcPrfType` 사용 위치 재점검 + +## 7. 구현 순서 + +1. `pubFc/{id}` 응답에 실적 포함 설계 확정 +2. 상세 모달 사용처 2곳 및 스토어를 `pubFc/{id}` 단일 호출 구조로 전환 +3. `pbnstList` SQL을 합계형 목록 조회로 변경 +4. 목록 DTO 및 프런트 타입 정리 +5. 잔여 `pubDetailList` 사용처 영향 확인 +6. 화면 수동 검증 + +## 8. 검증 계획 + +### 8.1 상세 모달 검증 + +- 지도 팝업 상세 + - `ruseDetailPopup.tsx` +- 현황 목록 상세 모달 + - `pbnstSttnModal.tsx` + +확인 항목: + +- `pubFc/{id}` 1회 호출만 발생하는지 +- 추가 `pubDetailList` 호출이 사라졌는지 +- 실적 탭의 장내/장외/합계 표가 기존과 동일하게 보이는지 + +### 8.2 목록 검증 + +- `pbnstList` 응답에서 `prfList`가 제거되었는지 +- `inUsage`, `outUsage`, `usageTotal` 합계가 기존과 동일한지 +- 검색/페이징/시도시군구 조건에서 정상 조회되는지 + +## 9. 리스크 및 확인 필요사항 + +### 9.1 `pubFc/{id}`의 조회 단위 + +현재 `getFc` SQL은 prefix 조회다. + +- `WHERE pf.pbnst_ruse_fclt_id like #{fcltPk} || '%'` + +이 계약을 유지할지, +정확한 ID 단건 조회로 바꿀지 먼저 결정이 필요하다. + +현 프런트와의 호환성을 고려하면 1차는 prefix 다건 조회 유지가 안전하다. + +### 9.2 실적 연도 파라미터 + +상세 응답에 실적을 포함할 때 연도 조건은 필요하다. + +이번 계획에서는 아래처럼 확정한다. + +- `getFc` 조회 시 `year` query param을 받는다 +- frontend는 상세 실적 조회 시 현재년도를 세팅해서 호출한다 +- backend는 해당 연도 실적만 `prfList`에 포함한다 + +### 9.3 `pubDetailList` 잔여 사용처 + +상세 모달 외에도 아래 스토어에서 여전히 사용 중이다. + +- `apps/wr/app/store/map/ruseMap/index.tsx` + +이번 작업에서 상세 경로는 정리하되, 다른 화면의 잔여 사용 여부는 후속 정리 대상으로 남을 수 있다. + +## 10. 결론 + +이번 작업은 단순 API 이름 정리가 아니라, + +- 상세 조회는 `pubFc/{id}` 중심 +- 목록 조회는 `pbnstList` 중심 + +으로 응답 책임을 다시 나누는 작업이다. + +핵심은 아래 두 줄로 요약된다. + +1. 상세 모달은 `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 한 번만 호출해도 되게 만든다. +2. 목록 API `GET /reuseFc/pbnstList/...`는 월별 내역을 빼고 SQL 합계만 내려주게 정리한다. diff --git a/apps/wr/docs/작업계획/하수처리수재이용 실적등록_20260630.md b/apps/wr/docs/작업계획/하수처리수재이용 실적등록_20260630.md new file mode 100644 index 000000000..def2d6d0b --- /dev/null +++ b/apps/wr/docs/작업계획/하수처리수재이용 실적등록_20260630.md @@ -0,0 +1,285 @@ +# 하수처리수재이용 실적등록 작업계획 + +작성일: 2026-06-30 + +## 1. 대상 + +- 화면 + - `frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx` +- 프론트 스토어 + - `frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx` +- 백엔드 + - `D:\iipbs\wr_back_out` +- 기준문서 + - `frontend/apps/wr/docs/기준문서/하수재이용실적 등록 api 호출분석_20260630.md` + +## 2. 요청사항 정리 + +- 좌측 목록은 `tb_wr_pbnst_ruse_fclt`만 기준으로 조회한다. +- 좌측 목록은 `reuse-service` 1회 호출로 조회한다. +- 시도/시군구 명칭은 공통코드 조회값을 사용해 화면에서 표시한다. +- 우측 `하수처리수재이용 실적(m³) 등록결과`는 좌측 시설 클릭 시 별도 API를 호출해 조회한다. +- 현재처럼 좌측 목록 조회 시 실적 `prfList`까지 함께 내려받는 구조는 제거한다. +- 좌측 신규 API명은 `reuseFc/pbnstPrfRegList`로 맞춘다. + +## 3. 현재 구조 확인 결과 + +### 3.1 프론트 + +- 화면 진입 시 현재 호출 흐름 + - `GET /reuseFc/pubFc` + - `POST /prcsFclt/ruseInfo/...` + - `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}` +- 좌측 목록은 FO/Common 응답과 WR 응답을 프론트에서 합성한다. +- 우측 실적은 별도 API를 호출하지 않고 좌측 목록의 `prfList`를 그대로 사용한다. +- 저장 후에도 다시 좌측 전체 조회를 수행한다. + +### 3.2 reuse-service + +- 이미 존재하는 API + - `GET /reuseFc/pubFc` + - 현재는 `pbnst_ruse_fclt_id`의 마지막 1자리를 제거한 기초시설코드 목록만 반환한다. + - 좌측 목록 API로는 재사용 불가하다. + - `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}` + - 시설 기본정보 + `prfList`를 함께 반환한다. + - 우측 클릭 조회용으로는 과조회이며 현재 구조의 결합 원인이다. + - `GET /reuseFc/pbnstList/...` + - WR 기준 목록이지만 `prfList`, 운영정보, 합계까지 같이 내려준다. + - 좌측 단순 목록 전용 API로는 과하다. +- 결론 + - 요청사항에 정확히 맞는 좌측 목록 API는 현재 없음 + - `reuseFc/pbnstPrfRegList` 신규 추가가 필요함 + +## 4. 목표 구조 + +### 4.1 좌측 목록 + +- 조회원천 + - `sm_bs_wr.tb_wr_pbnst_ruse_fclt` +- 호출 + - `reuse-service` 1회 +- 응답 범위 + - 시설명 + - 재사용시설명 + - 재사용시설코드 + - 시도코드 + - 시군구코드 + - 필요 시 페이징용 totalCount +- 화면 표시 + - 시도/시군구 명칭은 공통 코드 스토어에서 코드명으로 변환 + +### 4.2 우측 실적 + +- 조회시점 + - 좌측 행 클릭 시 +- 조회원천 + - `sm_bs_wr.tb_wr_pbnst_ruse_fclt_prfmnc` +- 응답범위 + - 선택 시설의 해당 연도 월별 실적만 반환 +- 저장 후 처리 + - 저장 성공 시 좌측 목록 전체 재조회 대신 우측 상세만 재조회 + - 좌측 목록은 변경 필요가 없으므로 불필요한 재호출 제거 + +## 5. 좌측 API 설계안 + +### 5.1 API 존재 여부 판단 + +- `GET /reuseFc/pubFc`: 부적합 + - 코드목록만 반환 +- `GET /reuseFc/pbnstList/...`: 부적합 + - `prfList`와 운영정보까지 포함 +- 따라서 신규 API 추가 + +### 5.2 API 경로 + +- `GET /reuseFc/pbnstPrfRegList/{pageNo}/{pageCnt}/{searchingCd}/{nameCd}` +- 이름검색이 필요하면 query parameter로 확장 + - 예: `?swtrmtFcltNm=...` + +### 5.3 조회조건 + +- `pageNo` +- `pageCnt` +- `searchingCd` + - 기본은 행정구역 기준 `A` +- `nameCd` + - `0`: 전체 + - `NN`: 시도코드 + - `NNNNN`: 시군구코드 +- `swtrmtFcltNm` + - 시설명 검색어 + +### 5.4 응답 컬럼 + +- `swageBscFctNm` + - 시설명 +- `ruseUtztnFcltNm` + - 재사용시설명 +- `pbnstRuseFcltId` + - 재사용시설코드 +- `sidoCd` + - `substring(swage_adst_dstr_cd, 1, 2)` +- `sggCd` + - `swage_adst_dstr_cd` +- `totalCount` + +### 5.5 DB 매핑 기준 + +- 시설명 + - `swage_bsc_fct_nm` +- 재사용시설명 + - `ruse_utztn_fclt_nm` +- 재사용시설코드 + - `pbnst_ruse_fclt_id` +- 시도코드 + - `swage_adst_dstr_cd` 앞 2자리 +- 시군구코드 + - `swage_adst_dstr_cd` + +### 5.6 SQL 방향 + +- `tb_wr_pbnst_ruse_fclt`만 조회 +- `prfList`, `operList`, 합계 컬럼 조인 제거 +- 정렬은 `pbnst_ruse_fclt_id` 기준 +- 검색은 `swage_bsc_fct_nm like '%' || #{swtrmtFcltNm} || '%'` + +## 6. 우측 API 설계안 + +### 6.1 기존 API 분석 + +- 현재 우측에 사용되는 데이터 원본은 `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}` 응답의 `prfList`다. +- 즉 기존 응답에서 실제 우측 화면이 쓰는 값은 아래뿐이다. + - `pbnstRuseFcltId` + - `ym` + - `wshwtr` + - `coolUswtr` + - `cleanUswtr` + - `drwtUswtr` + - `dltnUswtr` + - `ithlRswtr` + - `ithlEtcUswtr` + - `cleanToilUswtr` + - `resn` + - `lndwtr` + - `hydUswtr` + - `rvrKeepUswtr` + - `farmUswtr` + - `ugwtChr` + - `indstUswtr` + +### 6.2 신규 상세 API 방향 + +- 우측은 클릭 시 시설 1건만 조회하므로 시설 단건 + 연도 조건 API로 분리한다. +- 권장 경로 + - `GET /reuseFc/pbnstPrfRegDtl/{year}/{pbnstRuseFcltId}` +- 최종 명칭은 구현 시 팀 네이밍 규칙에 맞춰 확정한다. + +### 6.3 요청 파라미터 + +- `year` + - 조회연도 + - 예: `2026` +- `pbnstRuseFcltId` + - 선택한 재사용시설코드 + - 예: `11000SW001R1` + +### 6.4 응답값 + +- 시설식별 정보 + - `pbnstRuseFcltId` + - `ruseUtztnFcltNm` + - `swageBscFctNm` +- 실적목록 + - `prfList: PublicFcPrfmnc[]` +- `prfList` 원소 구조 + - `ym` + - `wshwtr` + - `coolUswtr` + - `cleanUswtr` + - `drwtUswtr` + - `dltnUswtr` + - `ithlRswtr` + - `ithlEtcUswtr` + - `cleanToilUswtr` + - `resn` + - `lndwtr` + - `hydUswtr` + - `rvrKeepUswtr` + - `farmUswtr` + - `ugwtChr` + - `indstUswtr` + +### 6.5 SQL 방향 + +- `tb_wr_pbnst_ruse_fclt_prfmnc`를 `pbnst_ruse_fclt_id`, `ym like #{year} || '%'` 조건으로 조회 +- 필요 시 시설명/재사용시설명 표시를 위해 `tb_wr_pbnst_ruse_fclt` 1회 join +- 반환 단위는 1개 시설 + 최대 12개월 실적 + +## 7. 프론트 변경계획 + +### 7.1 스토어 + +- `getPrfFmcRes()` 제거 + - 더 이상 `GET /reuseFc/pubFc` 선조회 불필요 +- `hasuListFetch()` 제거 또는 전면 교체 + - FO/Common + WR 합성 로직 삭제 +- 좌측 목록 조회 함수 신설 + - `GET /reuseFc/pbnstPrfRegList/...` +- 우측 상세 조회 함수 신설 + - 좌측 클릭 시 상세 API 호출 +- 저장 후 재조회 변경 + - 기존: 좌측 전체 목록 재조회 + - 변경: 우측 상세 재조회 + +### 7.2 페이지 + +- 최초 진입 시 좌측 목록만 조회 + 첫시설의 우측 상세 호출. 첫시설 선택 표시. +- 좌측 클릭 시 우측 상세 호출 +- `cellClick()` 내부의 `prfmncRes[index].prfList` 직접 사용 제거 +- 좌측 row 데이터는 코드 중심으로 유지 + - `sidoCd`, `sggCd`를 공통코드명으로 변환해 표시 + +## 8. 백엔드 변경계획 + +### 8.1 대상 파일 + +- `src/main/java/kr/co/mindone/wrout/controller/PublicFcController.java` +- `src/main/java/kr/co/mindone/wrout/service/public_fc/PublicFcService.java` +- `src/main/java/kr/co/mindone/wrout/mapper/wr/PublicFcMapper.java` +- `src/main/resources/postgres/mapper/wr/public_fc_select.xml` +- 필요 시 신규 DTO + - 좌측 목록 DTO + - 우측 상세 DTO + +### 8.2 구현 항목 + +1. 좌측 목록 DTO 추가 + - 실적 없는 가벼운 응답 모델 +2. `pbnstPrfRegList` 컨트롤러/서비스/매퍼 추가 +3. 우측 단건 실적 조회 API 추가 +4. `pubDetailList`는 기존 화면 영향 범위 확인 중요함. 그 후에 유지 혹은 신규 추가. +5. Swagger 설명 추가 + +## 9. 검증 항목 + +1. 화면 진입 시 좌측 목록 API 1회만 호출되는지 +2. 화면 진입 시 `POST /prcsFclt/ruseInfo/...`가 더 이상 호출되지 않는지 +3. 좌측 목록 응답에 `prfList`가 포함되지 않는지 +4. 좌측 클릭 시에만 우측 상세 API가 호출되는지 +5. 저장 후 좌측 목록 재조회 없이 우측 데이터만 갱신되는지 +6. 시도/시군구 명칭이 공통 코드 기준으로 정상 표시되는지 +7. 기존 저장 API `POST /reuseFc/pubFcPrf`와 정합성 문제가 없는지 + +## 10. 작업 순서 + +1. `reuse-service`에 좌측 목록 신규 API 추가 +2. `reuse-service`에 우측 상세 신규 API 추가 +3. 프론트 스토어를 신규 API 기준으로 분리 +4. 페이지 클릭/저장 후 갱신 흐름 수정 +5. 수동 테스트로 호출 수와 데이터 정합성 확인 + +## 11. 결론 + +- 좌측 목록 전용 API `reuseFc/pbnstPrfRegList`는 현재 `reuse-service`에 없으므로 신규 추가가 필요하다. +- 우측 실적도 현재는 `pubDetailList`에 묶여 있으므로, 시설 클릭 시 조회하는 단건 상세 API로 분리하는 것이 맞다. +- 이번 작업의 핵심은 "좌측은 시설목록만, 우측은 클릭 후 실적조회"로 API 책임을 분리해 화면 구조와 호출 구조를 단순화하는 것이다. diff --git a/apps/wr/docs/작업계획/호출병합2_20260625.md b/apps/wr/docs/작업계획/호출병합2_20260625.md new file mode 100644 index 000000000..e35d07fad --- /dev/null +++ b/apps/wr/docs/작업계획/호출병합2_20260625.md @@ -0,0 +1,201 @@ +# apps/wr 호출 병합 2차 작업계획 + +작성일: 2026-06-25 +대상 앱: `D:\iipbs\frontend\apps\wr` +대상 화면: +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx` +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx` + +## 1. 작업 배경 + +- `rainSttn`에서 정리한 호출 병합 방향을 `rswtSttn`, `htdflSttn`에도 확장할 필요가 있다. +- `rswtSttn`은 `searchTrigger`, `skipNextSearchRef`, `rswtSttnInit` 분리 구조가 그대로 남아 있어 `rainSttn`과 동일한 페이징 불량 가능성이 높다. +- `htdflSttn`은 `searchTrigger`는 없지만, 조회 버튼/페이징/엑셀 다운로드가 모두 `objSearch`, `fcltNm`을 직접 읽고 있어 “입력 중 값”과 “실제 적용된 값”이 섞일 수 있다. +- 두 화면 모두 엑셀 다운로드는 store의 `excelList` 반영 타이밍에 의존하고 있어, 첫 클릭 시 "다운로드할 데이터가 없습니다."가 발생할 수 있다. + +## 2. 현재 구조 분석 + +### 2.1 rswtSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `laeSeCd` 준비 후 `rswtSttnInit(false, 1, rswtSttnSize, ...)` 호출 +- 이후 페이징/조회: + - `pageNation`, `searchTrigger`, `appliedSearch`를 감시하는 effect에서 `rswtSttnSearch(...)` 호출 +- 추가 제어: + - `skipNextSearchRef`로 초기 effect 1회를 건너뜀 + - `searchTrigger === 0`이면 검색 effect를 중단 +- 엑셀: + - `isDownloadTriggered.current = true` 설정 + - `rswtSttnSearch(true, ...)` 호출 + - 이후 `rswtSttn.excelList` 변경 effect에서 다운로드 실행 + +### 2.2 htdflSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `initHtdfl(false, 1, htdflSttnSize, String(getFixedConValue()), fcltNm, ...)` 호출 +- 조회 버튼: + - 클릭 시 즉시 `htdflSttnSearch(false, 1, ..., String(getFixedConValue()), fcltNm, ...)` 호출 + - `pageNation`은 1로 돌리지 않고 `resetPageNo`만 켬 +- 페이징: + - `pageNation` 변경 effect에서 `String(getFixedConValue())`, `fcltNm`을 직접 읽어 `htdflSttnSearch(...)` 호출 +- 엑셀: + - `isDownloadTriggered.current = true` 설정 + - `htdflSttnSearch(true, ...)` 호출 + - 이후 `htdflSttn.excelList` 변경 effect에서 다운로드 실행 + +### 2.3 실제 문제 지점 + +- `rswtSttn` + - 사용자가 조회 버튼을 누르기 전에는 `searchTrigger === 0` 때문에 페이지 이동 시 API 호출이 막힌다. + - 초기 조회와 이후 조회가 `init/search`로 나뉘어 동일한 목록 조회인데도 상태 플래그에 의존한다. +- `htdflSttn` + - 페이지 이동 시 `objSearch`, `fcltNm`의 현재 입력값을 그대로 읽으므로, 마지막 조회 결과와 다른 조건으로 페이지가 바뀔 수 있다. + - 조회 버튼이 `pageNation`과 분리되어 있어 "1페이지로 되돌아간 뒤 같은 조건으로 재조회" 흐름이 일관되지 않다. +- 공통 + - 엑셀 다운로드는 클릭 직후 `excelList`를 검사할 수 있어 비동기 응답 전에 빈 배열로 판정될 수 있다. + - 첫 클릭 뒤 뒤늦게 store에 엑셀 데이터가 남으면, 다음 클릭에서 이전 응답 결과로 다운로드될 위험도 있다. + +## 3. 목표 구조 + +### 3.1 조회 호출 단일화 + +- 초기 진입도 `search` 계열 호출로 통일한다. +- 같은 파라미터라면 초기 진입, 조회 버튼, 페이지 이동, 엑셀 다운로드가 모두 동일한 조건 계산 경로를 사용한다. +- 조회 버튼은 “적용 조건 확정 + 페이지 1 초기화”만 담당하고, 실제 목록 조회는 하나의 경로로만 실행한다. + +### 3.2 적용 조건 분리 + +- 입력 중 값은 `objSearch`, `fcltNm`으로 유지한다. +- 실제 조회 기준 값은 `appliedSearch`로 분리한다. +- 페이지 이동, 재조회, 엑셀 다운로드는 반드시 `appliedSearch`만 사용한다. + +### 3.3 엑셀 다운로드 직접 처리 + +- `excelClick`에서 `await ...Search(true, ...)` 완료를 직접 기다린다. +- store의 `excelList` 변경 effect를 기다리지 않고, 반환받은 엑셀 데이터로 즉시 다운로드를 실행한다. +- store는 필요하면 기존 `excelList`도 유지할 수 있으나, 다운로드 성공 여부는 반환값 기준으로 판단한다. + +## 4. 수정 대상 + +### 4.1 화면 + +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\rswtSttn\page.tsx` +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\htdflSttn\page.tsx` + +### 4.2 store + +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\rswtSttn\index.tsx` +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\htdflSttn\index.tsx` + +## 5. 권장 구현 방향 + +### 5.1 rswtSttn 화면 정리 + +- `rswtSttnInit()` 직접 호출 제거 +- `searchTrigger` 제거 +- `skipNextSearchRef` 제거 또는 최소화 +- `appliedSearch`를 `null` 가능 상태로 두고, 초기 진입 시 1회 세팅 +- 목록 조회 effect는 `pageNation`, `appliedSearch`, `isLaeSeCdReady`만 감시 +- 조회 버튼은 `appliedSearch` 갱신 후 `setPageNation(1)`만 수행 +- 엑셀 다운로드는 `await rswtSttnSearch(true, ...)` 후 반환값으로 직접 다운로드 + +### 5.2 rswtSttn store 정리 + +- `rswtSttnInit`는 삭제 `rswtSttnSearch`으로 병합. +- `isExcel === true`일 때 파싱된 엑셀 행 데이터를 반환하도록 시그니처 확장 +- 목록 조회일 때는 기존처럼 `rswtSttnRes`, `rswtSttnResCount`를 갱신 + +### 5.3 htdflSttn 화면 정리 + +- `objSearch`, `fcltNm`와 별도로 `appliedSearch`를 추가 +- 초기 진입 시 `laeSeCd` 기준 초기 `appliedSearch`를 세팅하고, 목록 조회는 `htdflSttnSearch(false, 1, ...)` 경로로 통일 +- 조회 버튼은 `appliedSearch` 갱신 후 `setPageNation(1)`만 수행 +- 페이지 이동 effect는 반드시 `appliedSearch` 기준으로만 호출 +- 현재 `initHtdfl(false, ..., String(getFixedConValue()), fcltNm, ...)`처럼 입력 중 값에 직접 의존하는 구조를 제거 +- 엑셀 다운로드는 `await htdflSttnSearch(true, ...)` 후 반환값으로 직접 다운로드 + +### 5.4 htdflSttn store 정리 + +- `initHtdfl`는 삭제 `htdflSttnSearch` 으로 병합 +- `isExcel === true`일 때 파싱된 엑셀 행 데이터를 반환하도록 시그니처 확장 +- 목록 조회 시 `htdflSttnRes`, `htdflSttnResCount` 갱신 경로는 유지 + +## 6. 상세 작업 단계 + +### 단계 1. rswtSttn 호출 병합 + +- `page.tsx` + - `rswtSttnInit` 제거 + - `searchTrigger` 제거 + - `skipNextSearchRef` 제거 + - 초기 effect에서 `appliedSearch`만 세팅 + - 목록 조회 effect는 `appliedSearch` 준비 후 항상 `rswtSttnSearch(false, pageNation, ...)` 호출 +- `index.tsx` + - 공통 fetch 함수 도입 + - `rswtSttnSearch(true, ...)`가 `string[][]` 반환 가능하도록 수정 + +### 단계 2. rswtSttn 엑셀 다운로드 직접화 + +- `isDownloadTriggered` ref 제거 +- `rswtSttn.excelList` 감시 effect 제거 +- `excelClick`를 `async`로 변경 +- `const excelData = await rswtSttnSearch(true, ...)` 후: + - 데이터가 있으면 즉시 `excelDownloadNumStyle(...)` + - 없으면 `excelDownErrMsg` 노출 + +### 단계 3. htdflSttn 적용 조건 분리 + +- `appliedSearch` 상태 추가 +- 초기 진입 시 `laeSeCd` 기준 기본 조건을 `appliedSearch`에 반영 +- 조회 버튼은 입력값을 `appliedSearch`로 확정한 뒤 페이지를 1로 초기화 +- 페이지 이동 effect는 `appliedSearch`만 기준으로 목록 조회 +- `resetPageNo`는 UI 초기화용으로만 유지 + +### 단계 4. htdflSttn store 병합 및 엑셀 직접화 + +- `initHtdfl`는 삭제 `htdflSttnSearch` 으로 병합 +- `htdflSttnSearch(true, ...)`가 엑셀 데이터 반환 가능하도록 수정 +- 화면에서 `await htdflSttnSearch(true, ...)` 결과로 직접 다운로드 +- `isDownloadTriggered` ref 및 `htdflSttn.excelList` 감시 effect 제거 + +## 7. 구현 시 주의사항 + +- `objSearch`는 입력 UI 상태이고, API 호출 기준은 `appliedSearch`다. +- `setPageNation(1)` 직후 중복 조회가 생기지 않도록, 조회 버튼에서 직접 API를 다시 호출하지 말고 하나의 effect 경로만 사용한다. +- `htdflSttn`은 현재 조회 버튼이 `setPageNation(1)` 없이 직접 API를 호출하고 있으므로 이 부분을 반드시 통일해야 한다. +- 엑셀은 전체 건수 기준 조회를 유지하되, 조건 계산은 일반 목록 조회와 동일해야 한다. +- store에서 엑셀 데이터 반환을 추가하더라도 기존 목록 갱신 로직은 깨지지 않게 분기해야 한다. + +## 8. 검증 계획 + +### 8.1 rswtSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 초기 진입 직후 조회 버튼을 누르지 않고 2페이지로 이동하면 API가 호출된다. +- 조회 조건 변경 후 조회 버튼 클릭 시 1페이지로 이동하며 API가 1회만 호출된다. +- 페이지 2, 3으로 이동해도 같은 `appliedSearch` 조건이 유지된다. +- 첫 엑셀다운로드 클릭에서 즉시 파일이 내려오고, "다운로드할 데이터가 없습니다."가 잘못 뜨지 않는다. + +### 8.2 htdflSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 조회 버튼 클릭 시 항상 1페이지 기준으로 재조회된다. +- 조회 후 입력값만 바꾸고 조회 버튼을 누르지 않은 상태에서 페이지 이동해도, 마지막 조회 조건으로 페이지가 유지된다. +- 첫 엑셀다운로드 클릭에서도 현재 적용 조건 기준으로 정상 다운로드된다. + +### 8.3 회귀 검증 + +- 상세 모달 열기 동작 유지 +- 권한별 `laeSeCd` 초기 조건 유지 +- 시도/시군구 선택 제한 유지 +- 조회항목설정 모달 동작 유지 + +## 9. 결론 + +- `rswtSttn`은 `rainSttn`과 거의 동일한 구조적 문제를 갖고 있으므로 같은 방식으로 정리하는 것이 맞다. +- `htdflSttn`은 페이징 가드 문제는 없지만, 적용 조건 분리와 엑셀 다운로드 처리 방식은 동일하게 손봐야 한다. +- 이번 2차 작업의 핵심은 다음 두 가지다. +- “목록 조회는 항상 `appliedSearch` 기준의 단일 경로로 통일” +- “엑셀 다운로드는 store 반영 effect가 아니라 `await ...Search(true, ...)` 반환값으로 직접 처리” diff --git a/apps/wr/docs/작업계획/호출병합_20260625.md b/apps/wr/docs/작업계획/호출병합_20260625.md new file mode 100644 index 000000000..460519d8f --- /dev/null +++ b/apps/wr/docs/작업계획/호출병합_20260625.md @@ -0,0 +1,171 @@ +# apps/wr 호출 병합 및 페이징 정상화 작업계획 + +작성일: 2026-06-25 +대상 앱: `D:\iipbs\frontend\apps\wr` +대상 화면: +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx` +- 연관 검토 대상: `rswtSttn`, `htdflSttn` + +## 1. 작업 배경 + +- `rainSttn` 화면에서 초기 진입 시 목록은 조회되지만, 바로 페이지를 변경하면 API 호출이 발생하지 않아 페이징이 동작하지 않는다. +- 현재 구조는 초기 진입 시 `init` 계열 호출과 사용자 조회 이후 `search` 호출을 분리하고 있다. +- 이 분리 구조를 제어하기 위해 `searchTrigger`와 `skipNextSearchRef`가 함께 사용되고 있는데, 그 결과 초기 조회 이후의 페이지 이동까지 차단되는 상태가 발생했다. +- 요구사항은 초기 진입 시에도 `search` 호출로 통일하고, 페이징이 항상 같은 목록 조회 경로를 타도록 정리하는 것이다. + +## 2. 현재 구조 분석 + +### 2.1 rainSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `laeSeCd` 준비 후 `rainSttnInit(false, 1, rainSttnSize, ...)` 호출 +- 이후 페이징/조회: + - `pageNation`, `appliedSearch`, `searchTrigger`를 감시하는 effect에서 `rainSttnSearch(...)` 호출 +- 추가 제어: + - `skipNextSearchRef`로 초기 effect 1회를 건너뜀 + - `searchTrigger === 0`이면 검색 effect를 중단 + +### 2.2 실제 문제 지점 + +- `UiTable`의 `onPageChange={setPageNation}` 자체는 정상이다. +- 그러나 페이지 번호가 바뀐 뒤 실행되는 검색 effect에서 `searchTrigger === 0` 조건으로 return 한다. +- 따라서 사용자가 `조회` 버튼을 한 번도 누르지 않은 초기 상태에서는 페이지 변경만으로는 `rainSttnSearch()`가 호출되지 않는다. +- 결과적으로 첫 진입 후 페이징 클릭 시 API가 호출되지 않는다. + +### 2.3 구조적 문제 + +- 동일한 목록 조회인데 `init`과 `search`를 별도 액션으로 분리해 두었다. +- 화면은 “초기 조회”, “수동 조회”, “페이징 조회”를 서로 다른 상태 플래그로 구분한다. +- 이 구조는 중복 호출 방지에는 일부 도움이 되지만, 목록 조회의 단일 진입점을 없애고 상태 의존성을 높인다. +- 특히 페이징은 “현재 조건으로 특정 페이지를 조회한다”는 단순 동작이어야 하는데, 현재는 “사용자가 조회 버튼을 눌렀는지”까지 알아야 동작한다. + +## 3. 목표 구조 + +### 3.1 조회 호출 단일화 + +- 초기 진입 시에도 `search` 계열 호출을 사용한다. +- 화면에서 목록 조회는 하나의 함수만 사용한다. +- 같은 파라미터라면 초기 진입, 조회 버튼, 페이지 이동, 엑셀 다운로드가 모두 동일한 검색 조건 계산 방식을 사용한다. + +### 3.2 상태 제어 단순화 + +- `searchTrigger` 제거 +- 가능하면 `rainSttnInit` 사용 제거 또는 `rainSttnSearch`로 내부 위임 +- 초기 중복 호출 방지는 `init/search` 분리가 아니라 effect 설계 단순화로 해결 +- 페이지 이동은 항상 API 호출로 이어지도록 보장 + +### 3.3 기대 효과 + +- 초기 진입 직후에도 페이징 정상 동작 +- 조회 버튼을 누르기 전/후에 따라 동작이 달라지는 문제 제거 +- 목록 조회 흐름이 단일화되어 유지보수 용이성 개선 +- `rswtSttn` 같은 유사 화면에도 동일 패턴 적용 가능 + +## 4. 수정 대상 + +### 4.1 1차 수정 대상 + +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\rainSttn\page.tsx` +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\rainSttn\index.tsx` + +### 4.2 연계 검토 대상 + +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\rswtSttn\page.tsx` +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\rswtSttn\index.tsx` +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\htdflSttn\page.tsx` + +## 5. 권장 구현 방향 + +### 5.1 화면 레벨 정리 + +- `rainSttn/page.tsx`에서 다음을 정리한다. +- 초기 진입용 `rainSttnInit()` 호출을 `rainSttnSearch()` 호출로 대체한다. +- `searchTrigger` 상태 제거 +- `handleSearch()`는 검색 조건을 반영하고 `pageNation`을 1로 되돌린 뒤, 목록 조회를 직접 실행하거나 페이지 effect 하나로 일관되게 처리한다. +- 페이지 변경 시에는 항상 현재의 `appliedSearch` 기준으로 `rainSttnSearch()`가 호출되게 한다. +- 초기 1회 중복 호출만 필요하다면 `skipNextSearchRef` 유지 여부를 최소 범위에서 재판단한다. + +### 5.2 store 레벨 정리 + +- `rainSttn/index.tsx`에서 `rainSttnInit`와 `rainSttnSearch`의 구현이 사실상 동일하므로 병합 후보로 본다. +- 다음 방법으로 수정: + - `rainSttnInit` 제거 + - 화면에서는 `rainSttnSearch`만 사용 + +### 5.3 추천 방식 + +- 화면에서는 `search`만 사용하도록 통일 +- store에서는 당장 외부 참조 영향이 있으면 `init` 시그니처는 남기되 내부는 `search` 재사용으로 정리 +- 이후 호출처 정리까지 완료되면 `init` 제거. + +## 6. 상세 작업 단계 + +### 단계 1. rainSttn 화면 호출 흐름 단일화 + +- `searchTrigger` 제거 +- 초기 진입 effect에서 `rainSttnSearch(false, 1, rainSttnSize, initialConValue, "", code)` 호출 +- 조회 버튼 클릭 시: + - `appliedSearch` 갱신 + - 페이지를 1로 초기화 + - 조건에 따라 직접 `rainSttnSearch()` 호출하거나, page 1 변경 effect에서 1회만 호출되도록 정리 +- 페이지 이동 시: + - 현재 `appliedSearch`로 항상 `rainSttnSearch()` 호출 + +### 단계 2. 중복 호출 방지 로직 최소화 + +- 초기 진입 시점에 실제로 중복 호출이 발생하는지 확인 +- 불필요하면 `skipNextSearchRef`도 제거하고 `useEffect` 수를 축소 + +### 단계 3. store 액션 병합 + +- `rainSttnInit`는 제거 `rainSttnSearch`로 공통 URL 조합 로직 정리 +- 엑셀/일반 조회 분기만 남기고 목록 조회 경로를 단일화 + +### 단계 4. 유사 화면 점검 + +- `rswtSttn`은 `searchTrigger` 구조가 동일하므로 같은 페이징 불량 가능성이 높다. +- `rainSttn` 수정 후 동일 방식으로 `rswtSttn` 동시 정리 여부를 판단한다. +- `htdflSttn`은 이미 `searchTrigger` 없이 동작하므로 참조 구현으로 활용한다. + +## 7. 구현 시 주의사항 + +- 검색 조건 입력값(`objSearch`)과 실제 적용 조건(`appliedSearch`)을 구분하는 구조는 유지할 수 있다. +- 다만 API 호출은 반드시 `appliedSearch` 기준으로만 가야 한다. +- `setPageNation(1)` 직후 effect 호출 시점 때문에 중복 조회가 생기지 않도록, “직접 호출”과 “effect 기반 호출” 중 하나만 선택해야 한다. +- 엑셀 다운로드는 전체 건수 기준 조회를 유지하되, 검색 조건 계산 로직은 일반 목록 조회와 동일해야 한다. +- `resetPageNo` 플래그가 테이블 페이지 번호 UI 초기화용이라면, API 호출 조건과 분리해서 관리해야 한다. + +## 8. 검증 계획 + +### 8.1 rainSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 초기 진입 직후 조회 버튼을 누르지 않고 2페이지로 이동하면 API가 호출되고 목록이 바뀐다. +- 조회 조건 변경 후 조회 버튼 클릭 시 1페이지로 이동하며 API가 1회만 호출된다. +- 조회 후 2페이지, 3페이지 이동 시 동일 조건으로 API가 정상 호출된다. +- 엑셀 다운로드 시 현재 적용된 조건으로 전체 건수 조회가 수행된다. + +### 8.2 회귀 검증 + +- 상세 모달 열기 동작 유지 +- 권한별 `laeSeCd` 초기 조건 유지 +- 시도/시군구 선택 제한 유지 +- 검색어 없는 상태/있는 상태 모두 페이징 정상 동작 + +## 9. 후속 권장사항 + +- `rainSttn`, `rswtSttn`, `htdflSttn`의 목록 조회 패턴을 공통화하는 것이 바람직하다. +- 가능하면 다음 공통 원칙으로 정리한다. +- “초기 진입도 search” +- “페이지 이동도 search” +- “조회 버튼도 search” +- “엑셀만 isExcel=true search” +- 이 원칙으로 맞추면 `init/search` 이원화에서 반복적으로 발생하는 상태 꼬임을 줄일 수 있다. + +## 10. 결론 + +- 현재 문제의 직접 원인은 `searchTrigger` 가드이지만, 근본 원인은 `init`과 `search`를 분리한 화면 구조다. +- 이번 작업은 단순 버그 수정이 아니라 “목록 조회 진입점 단일화” 관점으로 진행하는 것이 맞다. +- 우선 `rainSttn`에서 초기 진입을 `search`로 통일하고 페이징이 항상 API를 타도록 수정한다. +- 이후 동일 패턴의 `rswtSttn`까지 함께 정리하면 재발 가능성을 크게 줄일 수 있다. diff --git a/apps/wr/global.d.ts b/apps/wr/global.d.ts new file mode 100644 index 000000000..cbe652dbe --- /dev/null +++ b/apps/wr/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/apps/wr/next-env.d.ts b/apps/wr/next-env.d.ts index 1b3be0840..830fb594c 100644 --- a/apps/wr/next-env.d.ts +++ b/apps/wr/next-env.d.ts @@ -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. diff --git a/apps/wr/public/images/help.png b/apps/wr/public/images/help.png index e872802e4..977ef9d38 100644 Binary files a/apps/wr/public/images/help.png and b/apps/wr/public/images/help.png differ diff --git a/apps/wr/src/app/index.tsx b/apps/wr/src/app/index.tsx index 692b55ef0..4cf2ad5fa 100644 --- a/apps/wr/src/app/index.tsx +++ b/apps/wr/src/app/index.tsx @@ -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"; diff --git a/apps/wr/src/app/providers/app-provider.tsx b/apps/wr/src/app/providers/app-provider.tsx index 63df8349d..81b8a5a69 100644 --- a/apps/wr/src/app/providers/app-provider.tsx +++ b/apps/wr/src/app/providers/app-provider.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect } from "react"; import { DndProvider } from "react-dnd"; import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -16,7 +17,11 @@ import i18next from "i18next"; import { I18nextProvider } from "react-i18next"; import { Toaster } from "react-hot-toast"; import koBase from "@/public/locales/ko/message.json"; -import { StoreProvider as WrStoreProvider } from "@/app/store"; +import { + StoreProvider as WrStoreProvider, + useWrStore, +} from "@/app/store"; +import { usePathname } from "next/navigation"; //import { LoginApi } from "@/src/entities/auth"; //import { useEffect } from "react"; import { setCookie } from "@/src/shared/lib/cookie"; @@ -34,6 +39,25 @@ i18n.init({ }, }); +function CommonCodeInitializer() { + const isInit = useWrStore((state) => state.code.isInit); + const initCommonCodes = useWrStore((state) => state.initCommonCodes); + const pathname = usePathname(); + const isLoginPage = + pathname === "/auth/login" || pathname?.startsWith("/auth/login/"); + + useEffect(() => { + if (isLoginPage) return; + if (isInit) return; + setTimeout(() => { + initCommonCodes(); + }, 3000); // 3초 후에 하는 것으로 왜냐하면 로그인 처리가 먼저 되어야 하기 때문에 + + }, [isInit, initCommonCodes, isLoginPage]); + + return null; +} + const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); return ( @@ -49,6 +73,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { > <I18nextProvider i18n={i18n}> <WrStoreProvider> + <CommonCodeInitializer /> {children} </WrStoreProvider> </I18nextProvider> diff --git a/apps/wr/src/app/styles/components-black.css b/apps/wr/src/app/styles/components-black.css new file mode 100644 index 000000000..e8c90d479 --- /dev/null +++ b/apps/wr/src/app/styles/components-black.css @@ -0,0 +1,139 @@ +@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 { + color: #fff; + background-color: #0a0a0a !important; + } + form { + color: rgb(255 255 255); + } + label { + color: rgb(241, 241, 241); + } + input { + height: 3.2rem; + background-color: #1a1a1a; + border: 1px solid #262626; + border-radius: 4px; + } + input.text-gray-600 { + color: #f1f1f1; + } + button[role="combobox"] { + color: #f1f1f1; + background-color: rgb(26, 26, 26); + } + div [role='tablist'] { + border: unset; + gap: 4px; + } + .tabs-trigger[data-state="inactive"] { + color: #a4b7bf; + background: #55595d; + } + .tabs-trigger[data-state="inactive"]:hover { + color: #6bbaff; + background: #4c4f52; + } + .card-border { + box-shadow: 0 0 10px -1px #0000000f; + border: 1px solid #393939; + } + a[aria-current="page"] { + background-color: rgb(32, 148, 250); + color: #000; + } + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + + .form-section-title { + @apply bg-[#5e7b9c] text-[#fff]; + } + + .gis-layer-panel { + background: #111214; + border-right-color: #2f3136; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03); + } + + .gis-layer-panel-title { + color: #f3f4f6; + } + + .gis-layer-panel hr { + border-color: #2f3136; + } + + .gis-layer-row { + color: #e5e7eb; + } + + .gis-layer-row:hover { + background: rgba(255, 255, 255, 0.08); + } + + .gis-layer-row span, + .gis-layer-row label { + color: #e5e7eb !important; + } + + .gis-layer-row input[type="checkbox"] { + accent-color: #c026d3; + } + + .gis-layer-action-button { + background: #1a1c20; + border-color: #3b82f6; + color: #cfe3ff; + } + + .gis-layer-action-button:hover { + background: #23262b; + } + + .gis-layer-setting { + background: #111214; + border-top: 1px solid #2f3136; + } + + .gis-layer-toggle { + background: #111214; + border-color: #2f3136; + color: #f3f4f6; + } + + .gis-layer-toggle:hover { + background: #1a1c20; + } + + input[type="file"] { + background-color: #1a1a1a; + border-color: #262626; + color: #f1f1f1; + } + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components-blue.css b/apps/wr/src/app/styles/components-blue.css new file mode 100644 index 000000000..1e49c377c --- /dev/null +++ b/apps/wr/src/app/styles/components-blue.css @@ -0,0 +1,35 @@ +@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]; + } + + .form-section-title { + @apply bg-[#eaf3fb] text-[#647784]; + } + + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components-green.css b/apps/wr/src/app/styles/components-green.css new file mode 100644 index 000000000..8803e7cb2 --- /dev/null +++ b/apps/wr/src/app/styles/components-green.css @@ -0,0 +1,35 @@ +@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]; + } + + .form-section-title { + @apply bg-[#ebf7f7] text-[#64908f]; + } + + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components-red.css b/apps/wr/src/app/styles/components-red.css new file mode 100644 index 000000000..9bfc1b792 --- /dev/null +++ b/apps/wr/src/app/styles/components-red.css @@ -0,0 +1,34 @@ +@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]; + } + + .form-section-title { + @apply bg-[#f6f6f9] text-[#808080]; + } + + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/app/styles/components.css b/apps/wr/src/app/styles/components.css index 4dc30bafd..60549b226 100644 --- a/apps/wr/src/app/styles/components.css +++ b/apps/wr/src/app/styles/components.css @@ -14,15 +14,12 @@ @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]; - } + .form-section-title { + @apply mt-4 border border-border bg-[#eaf3fb] p-2 text-center text-xl font-medium text-[#647784]; } + + input[type="file"] { @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; } diff --git a/apps/wr/src/entities/auth/api/login-service.ts b/apps/wr/src/entities/auth/api/login-service.ts index 1ad5f2cd0..92535f7b0 100644 --- a/apps/wr/src/entities/auth/api/login-service.ts +++ b/apps/wr/src/entities/auth/api/login-service.ts @@ -1,7 +1,7 @@ import type { LoginService, ILogin } from "../model/login-interface"; import { tokenManager } from "../lib/token-manager"; -import { ACCESS_TOKEN, AUTH_USER_ID, SITE_ID, CLAIM_NAME, ENDPOINT } from "../../../shared/config"; +import { ACCESS_TOKEN, AUTH_USER_ID, SITE_ID, CLAIM_NAME, ENDPOINT,JWT_EXPIRATION_TIME } from "../../../shared/config"; export class LoginApiService implements LoginService { private static instance: LoginApiService; @@ -44,7 +44,7 @@ export class LoginApiService implements LoginService { tokenManager.setToken(CLAIM_NAME, accessToken); tokenManager.setToken(AUTH_USER_ID, tokenId); tokenManager.setToken("site-id-key", SITE_ID); - tokenManager.setupRefreshTimer(() => this.silentRefresh()); + tokenManager.setupRefreshTimer(() => this.silentRefresh(),JWT_EXPIRATION_TIME ); } catch (error) { throw new Error(`Login failed: ${error}`); @@ -78,7 +78,7 @@ export class LoginApiService implements LoginService { tokenManager.setToken(CLAIM_NAME, accessToken); tokenManager.setToken(AUTH_USER_ID, tokenId); - tokenManager.setupRefreshTimer(() => this.silentRefresh()); + tokenManager.setupRefreshTimer(() => this.silentRefresh(),JWT_EXPIRATION_TIME); } catch (error) { tokenManager.clearAuthToken(); throw new Error(`Silent refresh failed: ${error}`); diff --git a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx index 91073c42c..796cd44a9 100644 --- a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx @@ -2,12 +2,10 @@ import { useEffect } from "react"; import { usePathname } from "next/navigation"; -import { LoginApi } from "../api/login-service"; -import { tokenManager } from "./token-manager"; import { client } from "../../../shared/api"; import type { IUserInfo } from "../../../shared/store"; import { useUserStore } from "../../../shared/lib"; -import { ENDPOINT, AUTH_USER_ID, CLAIM_NAME } from "../../../shared/config"; +import { ENDPOINT, AUTH_USER_ID } from "../../../shared/config"; import { isAtLoginRedirectUrl } from "@common/network"; import { getCookie } from "../../../shared/lib"; @@ -19,37 +17,22 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { - // 현재 페이지가 로그인 페이지이면 authCheck(쿠키점검) 안함. if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) { return; } const userId = getCookie(AUTH_USER_ID); - if (!userId) { // token_id 가 없으면 로그인 페이지로 이동 + if (!userId) { throw new Error("Missing required authentication tokens"); } - if (userInfo) return; - const response = await client.get<IUserInfo>( `${ENDPOINT.USER_SERVICE.USERS}/info/me`, ); - setUserInfo(response.data); + if (!userInfo) setUserInfo(response.data); } useEffect(() => { - const token = getCookie(CLAIM_NAME); - const userId = getCookie(AUTH_USER_ID); - - if (!token || !userId) { - return; - } - - tokenManager.setupRefreshTimer(() => LoginApi.silentRefresh()); - }, []); - - useEffect(() => { - const isJwtTestPage = pathname?.startsWith("/jwttest"); (async () => { try { await authCheck(); @@ -57,7 +40,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { console.error(error); setUserInfo(null); - if (isAtLoginRedirectUrl(LOGIN_PAGE_URL) || isJwtTestPage) return; + if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return; // const hostname = window.location.hostname; diff --git a/apps/wr/src/entities/auth/lib/token-manager.ts b/apps/wr/src/entities/auth/lib/token-manager.ts index af6ecb1c3..919386622 100644 --- a/apps/wr/src/entities/auth/lib/token-manager.ts +++ b/apps/wr/src/entities/auth/lib/token-manager.ts @@ -1,4 +1,4 @@ -import { AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/src/shared/config"; +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie"; type JwtPayload = { @@ -11,7 +11,6 @@ type JwtPayload = { export class TokenManager { private static instance: TokenManager; private refreshTimer: NodeJS.Timeout | null = null; - private static readonly REFRESH_BUFFER_MS = 60000; // 60초 private constructor() {} @@ -80,37 +79,20 @@ export class TokenManager { removeCookie(key); } - private handleRefreshFailure(error: unknown, message: string): void { - console.error(message, error); - this.clearAuthToken(); - } - - public setupRefreshTimer(callback: () => Promise<void>): void { + public setupRefreshTimer( + callback: () => Promise<void>, + expirationTime: number, + ): void { if (this.refreshTimer) { clearTimeout(this.refreshTimer); - this.refreshTimer = null; } - const remainingMs = this.getRemainingMsFromToken(CLAIM_NAME); - if (remainingMs === null) { - return; - } - - if (remainingMs <= 0) { - this.clearAuthToken(); - return; - } - - const refreshDelay = Math.max( - remainingMs - TokenManager.REFRESH_BUFFER_MS, - 0, - ); - this.refreshTimer = setTimeout(() => { callback().catch((error) => { - this.handleRefreshFailure(error, "Silent refresh failed:"); + console.error("Silent refresh failed:", error); + this.clearAuthToken(); }); - }, refreshDelay); + }, expirationTime - 60000); } public clearAuthToken(): void { @@ -120,7 +102,6 @@ export class TokenManager { } this.removeToken(CLAIM_NAME); - this.removeToken(REFRESH_TOKEN); this.removeToken(AUTH_USER_ID); } } diff --git a/apps/wr/src/shared/config/api.ts b/apps/wr/src/shared/config/api.ts index 73bbab9cf..eebb15c19 100644 --- a/apps/wr/src/shared/config/api.ts +++ b/apps/wr/src/shared/config/api.ts @@ -82,4 +82,4 @@ export const ENDPOINT = { }; -export const JWT_EXPIRATION_TIME = 3600000; +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/packages/common/auth/index.ts b/packages/common/auth/index.ts index 04fb01bb4..e069f7c6c 100644 --- a/packages/common/auth/index.ts +++ b/packages/common/auth/index.ts @@ -4,4 +4,9 @@ export type { TokenManager } from "./token-manager"; export { getCookie, setCookie, removeCookie } from "./cookie"; export { getJwtExpiry, getRemainingMs } from "./jwt-utils"; -export type { AuthConfig, CookieOptions } from "./types"; +export type { + AuthConfig, + AuthPayload, + AuthService, + CookieOptions, +} from "./types"; diff --git a/packages/common/auth/types.ts b/packages/common/auth/types.ts index 077eb821f..292c8edd7 100644 --- a/packages/common/auth/types.ts +++ b/packages/common/auth/types.ts @@ -1,3 +1,14 @@ +export interface AuthPayload { + userId: string; + password: string; +} + +export interface AuthService { + login(payload: AuthPayload): Promise<void>; + silentRefresh(): Promise<void>; + logout(): Promise<void>; +} + export interface AuthConfig { authUserId: string; claimName: string; diff --git a/packages/common/business_components/src/ui/chart/BarChart.tsx b/packages/common/business_components/src/ui/chart/BarChart.tsx index ec0647dff..486cc8a7c 100644 --- a/packages/common/business_components/src/ui/chart/BarChart.tsx +++ b/packages/common/business_components/src/ui/chart/BarChart.tsx @@ -53,13 +53,13 @@ const CustomTooltip = ({ }) => { if (active && payload && payload.length) { return ( - <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> - <p className="text-[12px] font-bold m-3">{label}</p> + <div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p> {payload.map((item, index) => ( <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> {/* 색상 박스 추가 (범례 색상과 동일) */} <div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div> - <span className="text-[11px] font-bold">{` ${item.name} : ${fncNumberFormet(item.value)}`}</span> + <span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${fncNumberFormet(item.value)}`}</span> </div> ))} </div> @@ -76,7 +76,7 @@ const CustomLegend = (props: any) => { {payload.map((entry: unknown, index: number) => ( <div key={`legend-${index}`} className="flex items-center gap-2"> <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: (entry as {color: string}).color }}></div> - <span className="text-[12px]">{(entry as {value: string}).value}</span> + <span className="text-[12px] text-card-foreground">{(entry as {value: string}).value}</span> </div> ))} </div> @@ -141,7 +141,7 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle=' radius={5} key={key} barSize={graphSize} - fill={colors[idx % colors.length]} + fill={colors[idx % colors.length] || "var(--foreground)"} /> )) )} @@ -151,4 +151,4 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle=' </div> </> ) -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/chart/DoubleBar.tsx b/packages/common/business_components/src/ui/chart/DoubleBar.tsx index be930f3af..598a5224f 100644 --- a/packages/common/business_components/src/ui/chart/DoubleBar.tsx +++ b/packages/common/business_components/src/ui/chart/DoubleBar.tsx @@ -44,13 +44,13 @@ const CustomTooltip = ({ }) => { if (active && payload && payload.length) { return ( - <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> - <p className="text-[12px] font-bold m-3">{label}</p> + <div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p> {payload.map((item, index) => ( <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> {/* 색상 박스 추가 (범례 색상과 동일) */} <div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div> - <span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span> + <span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value}`}</span> </div> ))} </div> @@ -67,7 +67,7 @@ const CustomLegend = (props: any) => { {payload.map((entry: unknown, index: number) => ( <div key={`legend-${index}`} className="flex items-center gap-2"> <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: (entry as {color: string}).color }}></div> - <span className="text-[12px]">{(entry as {value: string}).value}</span> + <span className="text-[12px] text-card-foreground">{(entry as {value: string}).value}</span> </div> ))} </div> @@ -123,7 +123,7 @@ export default function DoubleBarChart({chartConfig=undefined, chartData=[], cha key={key} stackId="a" barSize={graphSize} - fill={colors[idx]} + fill={colors[idx] || "var(--foreground)"} /> )) )} @@ -133,4 +133,4 @@ export default function DoubleBarChart({chartConfig=undefined, chartData=[], cha </div> </> ) -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx index ec8fc2fd9..1692f79fd 100644 --- a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx +++ b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx @@ -69,7 +69,7 @@ const renderCustomizedLabel = ({ <text x={x} y={y} - fill="#333" + fill="var(--muted-foreground)" fontSize={11} textAnchor={x > cx ? "start" : "end"} dominantBaseline="central" @@ -87,12 +87,12 @@ const CustomTooltip = ({ active, payload, label }: { }) => { if (active && payload && payload.length) { return ( - <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> - <p className="text-[12px] font-bold m-3">{label}</p> + <div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p> {payload.map((item, index) => ( <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> <div style={{ width: "10px", height: "10px", backgroundColor: item.payload.fill, borderRadius: "50%" }}></div> - <span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> + <span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> </div> ))} </div> @@ -126,7 +126,7 @@ const CustomLegend = (props: LegendProps) => { minHeight: "14px", }} ></div> - <span style={{ fontSize: "13px", whiteSpace: "nowrap" }}> + <span className="text-card-foreground" style={{ fontSize: "13px", whiteSpace: "nowrap" }}> {entry.payload?.label || entry.value} </span> </div> diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index 271629e6a..ae855dfb0 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -49,13 +49,13 @@ type LegendProps = { const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => { if (active && payload && payload.length) { return ( - <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> - <p className="text-[12px] font-bold m-3">{label}</p> + <div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p> {payload.map((item, index) => ( <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> {/* 색상 박스 추가 (범례 색상과 동일) */} <div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div> - <span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> + <span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> </div> ))} </div> @@ -74,7 +74,7 @@ const CustomLegend = (props: LegendProps) => { {payload.map((entry: LegendEntry, index: number) => ( <div key={`legend-${index}`} className="flex items-center gap-2"> <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div> - <span className="text-[12px]">{entry.value}</span> + <span className="text-[12px] text-card-foreground">{entry.value}</span> </div> ))} </div> @@ -172,9 +172,10 @@ export default function LineBarchart({ x={x - padding} // 맨 좌측 바깥 y={y + height / 2 - 12} // 수직 중앙 textAnchor="middle" - transform={`rotate(-90, ${x - padding}, ${y + height / 2})`} - className="text-[12px] font-bold" - > + transform={`rotate(-90, ${x - padding}, ${y + height / 2})`} + className="text-[12px] font-bold" + fill="var(--muted-foreground)" + > {fieldText} </text> ); @@ -184,9 +185,10 @@ export default function LineBarchart({ x={x + height / 2 } // 맨 좌측 바깥 y={y - width + padding } // 수직 중앙 textAnchor="middle" - transform={`rotate(90, ${x}, ${y})`} - className="text-[12px] font-bold" - > + transform={`rotate(90, ${x}, ${y})`} + className="text-[12px] font-bold" + fill="var(--muted-foreground)" + > {fieldText} </text> ); @@ -249,7 +251,7 @@ export default function LineBarchart({ key={`bar-${key}-${index}`} yAxisId="right" dataKey={key} - fill={config?.color || "black"} + fill={config?.color || "var(--foreground)"} name={String(config?.label || "Y축")} maxBarSize={80} /> @@ -267,7 +269,7 @@ export default function LineBarchart({ yAxisId="left" type="linear" dataKey={key} - stroke={config?.color || "black"} + stroke={config?.color || "var(--foreground)"} strokeWidth={2} dot={{r: 0}} name={String(config?.label || "X축")} @@ -281,4 +283,4 @@ export default function LineBarchart({ </div> </div> ); -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/chart/LineBarChart2.tsx b/packages/common/business_components/src/ui/chart/LineBarChart2.tsx index ff8e6b5cb..b0ae7a0c3 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart2.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart2.tsx @@ -46,13 +46,13 @@ type LegendProps = { const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => { if (active && payload && payload.length) { return ( - <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> - <p className="text-[12px] font-bold m-3">{label}</p> + <div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p> {payload.map((item, index) => ( <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> {/* 색상 박스 추가 (범례 색상과 동일) */} <div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div> - <span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> + <span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> </div> ))} </div> @@ -71,7 +71,7 @@ const CustomLegend = (props: LegendProps) => { {payload.map((entry: LegendEntry, index: number) => ( <div key={`legend-${index}`} className="flex items-center gap-2"> <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div> - <span className="text-[12px]">{entry.value}</span> + <span className="text-[12px] text-card-foreground">{entry.value}</span> </div> ))} </div> @@ -151,7 +151,7 @@ export default function LineBarchart2({ key={`bar-${key}-${index}`} yAxisId="right" dataKey={key} - fill={config?.color || "black"} + fill={config?.color || "var(--foreground)"} name={String(config?.label || "Y축")} maxBarSize={80} /> @@ -169,7 +169,7 @@ export default function LineBarchart2({ yAxisId="left" type="linear" dataKey={key} - stroke={config?.color || "black"} + stroke={config?.color || "var(--foreground)"} strokeWidth={2} dot={{r: 0}} name={String(config?.label || "X축")} @@ -183,4 +183,4 @@ export default function LineBarchart2({ </div> </div> ); -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/chart/LineChart.tsx b/packages/common/business_components/src/ui/chart/LineChart.tsx index b9ba58ff6..036dbfae4 100644 --- a/packages/common/business_components/src/ui/chart/LineChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineChart.tsx @@ -63,13 +63,13 @@ type LegendProps = { const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => { if (active && payload && payload.length) { return ( - <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> - <p className="text-[12px] font-bold m-3">{label}</p> + <div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p> {payload.filter(item => item.value != null && String(item.value) != 'NaN' && item.value != undefined).map((item, index) => ( <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> {/* 색상 박스 추가 (범례 색상과 동일) */} <div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div> - <span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(',','')}`}</span> + <span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(',','')}`}</span> </div> ))} </div> @@ -122,7 +122,7 @@ const CustomLegend = (props: LegendProps) => { minHeight: "14px", }} ></div> - <span style={{ fontSize: "13px", whiteSpace: "nowrap" }}>{entry.value}</span> + <span className="text-card-foreground" style={{ fontSize: "13px", whiteSpace: "nowrap" }}>{entry.value}</span> </div> ))} </div> @@ -141,6 +141,7 @@ const CustomAxisText : React.FC<{ viewBox: any, fieldText: string, position:stri textAnchor="middle" transform={`rotate(-90, ${x - padding}, ${y + height / 2})`} className="text-[12px] font-bold" + fill="var(--muted-foreground)" > {fieldText} </text> @@ -153,6 +154,7 @@ const CustomAxisText : React.FC<{ viewBox: any, fieldText: string, position:stri textAnchor="middle" transform={`rotate(90, ${x}, ${y})`} className="text-[12px] font-bold" + fill="var(--muted-foreground)" > {fieldText} </text> @@ -277,4 +279,4 @@ export default function Linechart({ </div> </div> ); -} \ No newline at end of file +} diff --git a/packages/common/business_components/src/ui/excel/index.tsx b/packages/common/business_components/src/ui/excel/index.tsx index ef753f976..12fda86c9 100644 --- a/packages/common/business_components/src/ui/excel/index.tsx +++ b/packages/common/business_components/src/ui/excel/index.tsx @@ -269,7 +269,7 @@ export const filePickedArray=(oFile: File, headerNum:number=0):Promise<JSON[] | export const filePickedBlob = ( oFile: ArrayBuffer, - data:[][],fileNm:string, + data:string[][],fileNm:string, headerNum:number=0, headerRange='A1:V3', startCol?:number diff --git a/packages/common/business_components/src/ui/file-upload/file-upload.css b/packages/common/business_components/src/ui/file-upload/file-upload.css new file mode 100644 index 000000000..9a8737063 --- /dev/null +++ b/packages/common/business_components/src/ui/file-upload/file-upload.css @@ -0,0 +1,67 @@ +@tailwind components; + +@layer components { + .fileup_root { + @apply relative rounded-lg bg-background p-2; + } + + .fileup_dropzone { + @apply border border-border outline-dashed outline-1 outline-border; + } + + .fileup_panel { + @apply flex w-full flex-col items-center justify-center p-1; + } + + .fileup_icon { + @apply h-4 w-5 text-label; + } + + .fileup_hint { + @apply mb-1 text-sm text-label; + } + + .fileup_hint_strong { + @apply font-semibold text-foreground; + } + + .fileup_meta { + @apply text-xs text-label; + } + + .fileup_item { + @apply text-foreground; + } + + .fileup_item_active { + @apply bg-muted; + } + + .fileup_item_name { + @apply flex h-full w-4/5 items-center gap-1.5 overflow-hidden font-medium leading-normal tracking-tight; + } + + .fileup_item_remove { + @apply text-label; + } + + .fileup_item_remove_icon { + @apply h-4 w-4 transition-colors duration-200 ease-in-out hover:stroke-destructive; + } + + .fileup_input { + @apply border; + } + + .fileup_input_idle { + @apply border-border; + } + + .fileup_input_accept { + @apply border-primary; + } + + .fileup_input_reject { + @apply border-destructive; + } +} diff --git a/packages/common/business_components/src/ui/file-upload/index.tsx b/packages/common/business_components/src/ui/file-upload/index.tsx index 6836691e2..ae6e8cc55 100644 --- a/packages/common/business_components/src/ui/file-upload/index.tsx +++ b/packages/common/business_components/src/ui/file-upload/index.tsx @@ -1,4 +1,6 @@ "use client"; +import "./file-upload.css"; + import { CloudUpload, Paperclip } from "lucide-react"; import { FileInput, FileUploader, FileUploaderContent, FileUploaderItem } from "./lib/file_upload"; @@ -36,19 +38,19 @@ export const FileUpload = onValueChange={setFiles} dropzoneOptions={dropZoneConfig} acceptName={fileTypes} - className="relative bg-background rounded-lg p-2" + className="fileup_root" > <FileInput id="fileInput" - className="outline-dashed outline-1 outline-slate-500" + className="fileup_dropzone" > - <div className="flex items-center justify-center flex-col p-1 w-full "> - <CloudUpload className='text-gray-500 w-5 h-4' /> - <p className="mb-1 text-sm text-gray-500 dark:text-gray-400"> - <span className="font-semibold">클릭 후 업로드 </span> + <div className="fileup_panel"> + <CloudUpload className='fileup_icon' /> + <p className="fileup_hint"> + <span className="fileup_hint_strong">클릭 후 업로드 </span>   혹은 파일을 드래그 해주세요 </p> - <p className="text-xs text-gray-500 dark:text-gray-400"> + <p className="fileup_meta"> {!fileTypes?"SVG, PNG, JPG, GIF":fileTypes} </p> </div> diff --git a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx index 9e47d296e..f0071748e 100644 --- a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx +++ b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx @@ -344,14 +344,14 @@ export const FileUploaderItem = forwardRef< ref={ref} className={cn( buttonVariants({ variant: "ghost" }), - "h-9 justify-between cursor-pointer relative w-full", + "fileup_item h-9 justify-between cursor-pointer relative w-full", className, - isSelected ? "bg-muted" : "", + isSelected ? "fileup_item_active" : "", )} {...props} > - <div className="font-medium leading-normal tracking-tight flex items-center gap-1.5 h-full !w-4/5 overflow-hidden " + <div className="fileup_item_name" onClick={() =>{onFileClick?.(index) }}> {children} @@ -359,7 +359,7 @@ export const FileUploaderItem = forwardRef< <button type="button" className={cn( - "absolute", + "fileup_item_remove absolute", "!w-1/5 z-50", direction === "rtl" ? "left-1 top-1/2 -translate-y-1/2" @@ -372,7 +372,7 @@ export const FileUploaderItem = forwardRef< removeFileFromSet(index)}} > <span className="sr-only">remove item {index}</span> - <RemoveIcon className="w-4 h-4 hover:stroke-destructive duration-200 ease-in-out" /> + <RemoveIcon className="fileup_item_remove_icon" /> </button> </div> ); @@ -396,14 +396,12 @@ export const FileInput = forwardRef< > <div className={cn( - `w-full rounded-lg duration-300 ease-in-out - ${ - dropzoneState.isDragAccept - ? "border-green-500" - : dropzoneState.isDragReject || isFileTooBig - ? "border-red-500" - : "border-gray-300" - }`, + "fileup_input w-full rounded-lg duration-300 ease-in-out", + dropzoneState.isDragAccept && "fileup_input_accept", + (dropzoneState.isDragReject || isFileTooBig) && "fileup_input_reject", + !dropzoneState.isDragAccept && + !(dropzoneState.isDragReject || isFileTooBig) && + "fileup_input_idle", className, )} {...rootProps} diff --git a/packages/common/business_components/src/ui/form_ui/form-ui.css b/packages/common/business_components/src/ui/form_ui/form-ui.css new file mode 100644 index 000000000..95059a81c --- /dev/null +++ b/packages/common/business_components/src/ui/form_ui/form-ui.css @@ -0,0 +1,19 @@ +@tailwind components; + +@layer components { + .formui_label { + @apply text-xl; + } + + .formui_option_wrap { + @apply float-right; + } + + .formui_option { + @apply flex items-center gap-2; + } + + .formui_option_label { + @apply w-28 text-xl text-label; + } +} diff --git a/packages/common/business_components/src/ui/form_ui/index.tsx b/packages/common/business_components/src/ui/form_ui/index.tsx index 27b74bd97..39aa83b5d 100644 --- a/packages/common/business_components/src/ui/form_ui/index.tsx +++ b/packages/common/business_components/src/ui/form_ui/index.tsx @@ -1,9 +1,11 @@ +import "./form-ui.css"; + import React, { useCallback, useEffect, useState } from "react"; import { FormControl, FormItem, FormLabel, FormMessage } from "../form"; -import { Button, Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components/ui"; -export const BZNM_TOT_NO = '종사업자등록번호'; -export const CANDIDATE = '공급처 후보지 선택(필수아님)'; +export const BZNM_TOT_NO = "종사업자등록번호"; +export const CANDIDATE = "공급처 후보지 선택(필수아님)"; interface FormUiProps extends React.HTMLAttributes<HTMLDivElement> { label?: string; @@ -29,17 +31,19 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>( }, [onCheckboxChange]); return ( <FormItem ref={ref}> - {label && <FormLabel className="text-xl">{label}</FormLabel>} + {label && <FormLabel className="formui_label">{label}</FormLabel>} {label === BZNM_TOT_NO && ( <> - <div className="float-right"> - <div className="flex items-center gap-2"> - <Checkbox - id="check" + <div className="formui_option_wrap"> + <div className="formui_option"> + <Checkbox + id="check" checked={isChecked} onCheckedChange={handleCheckboxChange} /> - <label className="w-28 text-xl text-gray-600" htmlFor="check">해당없음</label> + <label className="formui_option_label" htmlFor="check"> + 해당없음 + </label> </div> </div> {children} @@ -48,14 +52,14 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>( {label === CANDIDATE && ( <FormControl> {children} - </FormControl> + </FormControl> )} {label !== BZNM_TOT_NO && label !== CANDIDATE && ( <FormControl> {React.isValidElement(children) && isDisabled - ? React.cloneElement(children, { + ? React.cloneElement(children, { disabled: isDisabled, - ...(children.props as FormElementProps) + ...(children.props as FormElementProps), }) : children} </FormControl> @@ -66,5 +70,5 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>( } ); -FormUi.displayName = 'FormUi'; -export { FormUi }; \ No newline at end of file +FormUi.displayName = "FormUi"; +export { FormUi }; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 4b903d087..2eea996df 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -49,12 +49,14 @@ function Gis({ searchView: SearchView, detailView: DetailView, onFeatureSelect, + canSelectFeature, setIsSearchModalOpen, onCheckedLayersChange, searchBar: SearchBar, waterDepth: WaterDepth = null, layerSettingView: LayerSettingView = null, inlineLayerSettingView, + treeSectionClassName = "", layerIconVisible, isPlaying = false, setIsPlaying = () => { }, @@ -73,6 +75,7 @@ function Gis({ zoomGuide, showFloatingSearchButton = true, treeTogglePosition = "center", + detachedTreeToggleButton = false, showZoomScaleInfo = false, }) { const mapRef = useRef(); @@ -111,7 +114,7 @@ function Gis({ (data) => { onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 }, - [mapInstanceRef], + [onFeatureSelect], ); // 지도 생성 및 초기화 @@ -315,7 +318,9 @@ function Gis({ onProgressChange={handleProgressChange} layerSettingView={LayerSettingView} inlineLayerSettingView={inlineLayerSettingView} + treeSectionClassName={treeSectionClassName} toggleButtonPosition={treeTogglePosition} + detachedToggleButton={detachedTreeToggleButton} /> {typeof WaterDepth === "function" && ( @@ -354,7 +359,7 @@ function Gis({ <GisZoomLevel zoomGuide={zoomGuide} showZoomScaleInfo={showZoomScaleInfo} /> )} <GisMapToggle /> - <GisSelect onFeatureSelect={handleFeatureSelect} /> + <GisSelect onFeatureSelect={handleFeatureSelect} canSelectFeature={canSelectFeature} /> </div> </div> diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index 38729b2b2..ac862c2f1 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -1,25 +1,43 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useRef } from "react"; import { GisContext } from "./Gis"; import GisApp from "./lib/gis.core"; import GisOlSelect from "./lib/gis.select"; -export default React.memo(function GisSelect({ onFeatureSelect }) { +export default React.memo(function GisSelect({ onFeatureSelect, canSelectFeature }) { const { gisMap } = useContext(GisContext); + const onFeatureSelectRef = useRef(onFeatureSelect); + const canSelectFeatureRef = useRef(canSelectFeature); + + useEffect(() => { + onFeatureSelectRef.current = onFeatureSelect; + }, [onFeatureSelect]); + + useEffect(() => { + canSelectFeatureRef.current = canSelectFeature; + }, [canSelectFeature]); useEffect(() => { if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 - // 이미 select가 있으면 재생성하지 않음 const select = new GisOlSelect(gisMap); GisApp.Module.select = select; // 피처 선택 시 콜백 등록 select.getSelectedInfo(function (features) { if (features?.[0]) { - onFeatureSelect(features[0]); + const isSelectable = canSelectFeatureRef.current?.(features[0]); + if (isSelectable === false) { + select.resetSelectedFeature?.(); + return; + } + onFeatureSelectRef.current?.(features[0]); } }); - }, [gisMap, onFeatureSelect]); + + return () => { + select.removeInteration?.(); + }; + }, [gisMap]); // const select = new GisOlSelect(gisMap); // GisApp.Module.select = select; diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 75f523cd4..cdf67af25 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -29,6 +29,7 @@ import GisApp from "./lib/gis.core"; * - onProgressChange(total, current): 애니메이션 진행상태 콜백 * - layerSettingView: (선택) 좌측 레이어 트리 하단에 표시할 설정 영역 * - layerIconVisible: (선택) 레이어 행 우측 액션 아이콘 노출 여부를 결정하는 함수 + * - detachedToggleButton: (선택) 접기/펼치기 버튼을 패널 밖 고정 핸들로 노출할지 여부 */ export default React.memo(function GisTree({ checkedNodes = [], @@ -46,7 +47,10 @@ export default React.memo(function GisTree({ layerSettingView: LayerSettingView = null, layerIconVisible, inlineLayerSettingView, + treeSectionClassName = "", toggleButtonPosition = "center", + showLayerPanelTitle = true, + detachedToggleButton = false, }) { const { gisCore, treeData } = useGisContext(); @@ -442,10 +446,11 @@ export default React.memo(function GisTree({ * 레이어 정보로부터 범례 이미지 경로 또는 동적 생성 이미지를 반환합니다. */ const makeLegendSrc = (layerinfo) => { - if (!layerinfo.gobjTy) return null; if (layerinfo.iconNm) { return GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm; - } else { + } + if (!layerinfo.gobjTy) return null; + else { return makeLegendImage(layerinfo).src; } }; @@ -512,25 +517,33 @@ export default React.memo(function GisTree({ if (!treeData || treeData.length === 0) return null; const toggleButtonPositionClass = - toggleButtonPosition === "top" ? "top-[5.5rem]" : "top-1/2 -translate-y-1/2"; + toggleButtonPosition === "top" ? "top-4" : "top-1/2 -translate-y-1/2"; + const thinScrollbarClass = + "[scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]"; + const treeSectionClassNames = [treeSectionClassName, thinScrollbarClass].filter(Boolean).join(" "); return ( <> {/* 사이드 패널 */} <div - className={`absolute left-0 top-0 z-40 flex h-full w-[250px] flex-col overflow-hidden border-r border-gray-300 bg-white transition-all duration-300 ${visible + className={`gis-layer-panel absolute left-0 top-0 z-40 flex h-full w-[250px] flex-col overflow-hidden border-r border-gray-300 bg-white transition-all duration-300 ${visible ? "translate-x-0 opacity-100" : "pointer-events-none -translate-x-full opacity-0" }`} > <div - className={`grow overflow-y-auto border-none p-2 pt-0 shadow-none ${activeTab === "layers" ? "block" : "hidden" + className={`grow overflow-y-auto border-none p-2 ${showLayerPanelTitle ? "pt-0" : "pt-2"} shadow-none ${activeTab === "layers" ? "block" : "hidden" }`} > - <h3 className="mx-2 my-4 text-3xl font-bold text-gray-600 text-center">시설물</h3> - <hr className="mb-4" /> + {showLayerPanelTitle && ( + <> + <h3 className="gis-layer-panel-title mx-2 my-4 text-3xl font-bold text-gray-600 text-center">시설물</h3> + <hr className="mb-4" /> + </> + )} {/* DnD Tree */} - <DndProvider backend={MultiBackend} options={getBackendOptions()}> + <div className={treeSectionClassNames || undefined}> + <DndProvider backend={MultiBackend} options={getBackendOptions()}> <Tree tree={nodes} rootId={null} @@ -542,6 +555,13 @@ export default React.memo(function GisTree({ (child) => child.parent === node.id && child.data.exprsYn === "Y" ); + const nodeLabel = String(node?.data?.lyrNm ?? "").trim(); + const isSinkholeRootTreeNode = + String(node.id) === "SN_SINKHOLE" || + nodeLabel === "지반침하정보"; + const isFloodHistoryRootTreeNode = nodeLabel === "침수흔적도"; + const hideTreeLegendIcon = + isSinkholeRootTreeNode || isFloodHistoryRootTreeNode; const showLayerActionButton = layerIconVisible?.(node); const showLayerActionIcon = depth > 0 && imageVisible; @@ -550,9 +570,10 @@ export default React.memo(function GisTree({ return ( <div style={{ display: isVisible ? "block" : "none" }}> <div - className="flex items-center gap-2 rounded px-2 py-1 hover:bg-gray-100" + className="gis-layer-row flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-gray-100" style={{ marginInlineStart: depth * 16, + width: `calc(100% - ${depth * 16}px)`, }} > {hasVisibleChildren ? ( @@ -563,7 +584,7 @@ export default React.memo(function GisTree({ title={isOpen ? "닫기" : "열기"} /> ) : ( - <span style={{ display: "inline-block", width: 16 }} /> + <span className="shrink-0" style={{ display: "inline-block", width: 16 }} /> )} <input type="checkbox" @@ -571,14 +592,17 @@ export default React.memo(function GisTree({ onChange={() => handleCheck(node.id)} className="cursor-pointer" /> - {node.icon && <span>{node.icon}</span>} - <span className="text-[1.3rem] font-medium text-label"> + {!hideTreeLegendIcon && node.icon && <span>{node.icon}</span>} + <span + className="min-w-0 flex-1 truncate whitespace-nowrap text-[1.3rem] font-medium text-label" + title={String(node.text ?? "")} + > {node.text} </span> {showLayerActionButton && ( <button type="button" - className="ml-1 shrink-0 rounded-sm border border-[#9cc6f0] bg-white px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-[#2c6fb7] transition-colors hover:bg-[#eef6ff]" + className="gis-layer-action-button ml-auto shrink-0 rounded-sm border border-[#9cc6f0] bg-white px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-[#2c6fb7] transition-colors hover:bg-[#eef6ff]" onClick={() => handleLayerImgClick( node.data.lyrTyNm, @@ -624,26 +648,41 @@ export default React.memo(function GisTree({ return aOrder - bOrder; }} /> - </DndProvider> + </DndProvider> + </div> </div> {LayerSettingView && ( - <div className="shrink-0 bg-white px-2 pb-3"> + <div className="gis-layer-setting shrink-0 bg-white px-2 pb-3"> <LayerSettingView /> </div> )} + {!detachedToggleButton && ( + <button + onClick={() => setVisible(false)} + className={`gis-layer-toggle absolute right-[-3px] z-50 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`} + > + <ChevronLeft size={16} /> + </button> + )} + </div> + {detachedToggleButton && visible && ( <button onClick={() => setVisible(false)} - className={`absolute right-[-3px] z-50 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`} + className={`gis-layer-toggle absolute left-[250px] z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] text-[#1e88e5] shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-[#eaf4ff] ${toggleButtonPositionClass}`} + title="접기" > - <ChevronLeft size={16} /> + <ChevronLeft className="h-4 w-4" /> </button> - </div> + )} {!visible && ( <button onClick={() => setVisible(true)} - className={`absolute left-[-3px] z-50 transform rounded-r border border-r border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`} + className={detachedToggleButton + ? `gis-layer-toggle absolute left-0 z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] text-[#1e88e5] shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-[#eaf4ff] ${toggleButtonPositionClass}` + : `gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-gray-300 bg-white px-1 py-2 shadow-sm ${toggleButtonPositionClass}`} + title="열기" > - <ChevronRight size={16} /> + {detachedToggleButton ? <ChevronRight className="h-4 w-4" /> : <ChevronRight size={16} />} </button> )} </> diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index b136cdcee..e9e652183 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -45,7 +45,7 @@ GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${G // NGII 정보(배경지도) //GisApp.NGII_WMTS_URL = process.env.NEXT_PUBLIC_NGII_URL; -GisApp.NGII_WMTS_URL = `/ngii-server/openapi/Gettile.do?apikey=${process.env.NEXT_PUBLIC_NGII_PARAM}`; +GisApp.NGII_WMTS_PARAM = process.env.NEXT_PUBLIC_NGII_PARAM ?? ''; GisApp.EMAP_URL = '/emap/'; GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; @@ -79,6 +79,17 @@ GisApp.setLayerInfos = function (layerInfos) { GisApp.layerInfos = layerInfos; }; +GisApp.getNgiiWmtsUrl = function (layerName, matrix, x, y) { + const layerAwareParams = /(^|&)layer=[^&]*/i.test(GisApp.NGII_WMTS_PARAM) + ? GisApp.NGII_WMTS_PARAM.replace(/(^|&)layer=[^&]*/i, `$1layer=${layerName}`) + : `${GisApp.NGII_WMTS_PARAM}&layer=${layerName}`; + + return `/ngii-server/openapi/Gettile.do?apikey=${layerAwareParams}` + .replace("{z}", matrix) + .replace("{x}", x) + .replace("{y}", y); +}; + /** * GisApp core * @requires gis.layer.js @@ -161,7 +172,7 @@ GisApp.core.prototype = { }); const wmtsSource = new TileWMTS({ - //url: GisApp.NGII_WMTS_URL, + // url은 setTileUrlFunction에서 layer별로 동적으로 만든다. projection: projection, tileGrid: tileGrid, crossOrigin: "anonymous", @@ -175,11 +186,7 @@ GisApp.core.prototype = { wmtsSource.setTileUrlFunction((tileCoord) => { const [z, x, y] = tileCoord; const matrix = matrixIds[z]; - // console.log(GisApp.NGII_WMTS_URL.replace("{z}", matrix).replace("{x}", x).replace("{y}", y)); - return GisApp.NGII_WMTS_URL - .replace("{z}", matrix) - .replace("{x}", x) - .replace("{y}", y); + return GisApp.getNgiiWmtsUrl(_layer, matrix, x, y); }); return new TileLayer({ diff --git a/packages/common/business_components/src/ui/map/lib/gis.layer.js b/packages/common/business_components/src/ui/map/lib/gis.layer.js index 314b93338..97d3eb6db 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.layer.js +++ b/packages/common/business_components/src/ui/map/lib/gis.layer.js @@ -742,10 +742,13 @@ GisOlLayer.prototype = { const extent = this.combineExtents(geometries.map(g => g?.getExtent())); let map = GisApp.Module.core.map; + const targetZoom = 16; + const currentZoom = Number(map.getView().getZoom()); + const maxZoom = Number.isFinite(currentZoom) && currentZoom > targetZoom ? currentZoom : targetZoom; map.getView().fit(extent, { duration: 300, padding: [50, 50, 50, 50], - maxZoom: 16 + maxZoom: maxZoom }); } } else { diff --git a/packages/common/business_components/src/ui/map/lib/gis.select.js b/packages/common/business_components/src/ui/map/lib/gis.select.js index a1dab024e..e05edb9af 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.select.js +++ b/packages/common/business_components/src/ui/map/lib/gis.select.js @@ -132,13 +132,15 @@ GisOlSelect.prototype = { const selectedLayer = e.target?.getLayer(feature) || feature.getLayer?.(); const clckZoom = Number(selectedLayer?.get('clckZoom')); const targetZoom = Number.isFinite(clckZoom) && clckZoom > 0 ? clckZoom : 16; + const currentZoom = Number(map.getView().getZoom()); + const maxZoom = Number.isFinite(currentZoom) && currentZoom > targetZoom ? currentZoom : targetZoom; const geometry = feature.getGeometry(); if (geometry) { const extent = geometry.getExtent(); map.getView().fit(extent, { duration: 300, padding: [50, 50, 50, 50], - maxZoom: targetZoom + maxZoom: maxZoom }); } } diff --git a/packages/common/business_components/src/ui/map/lib/gis.style.js b/packages/common/business_components/src/ui/map/lib/gis.style.js index 4ad75c386..767244e73 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.style.js +++ b/packages/common/business_components/src/ui/map/lib/gis.style.js @@ -343,12 +343,13 @@ GisOlStyle.prototype = { //레이어 스타일 반환 getLayerStyle: function (layer, options) { let gobjTy = layer.get('gobjTy'); - if (!gobjTy) return; + if (!gobjTy || typeof this[gobjTy] !== 'function') return; let style = this[gobjTy](layer, options); return style; }, getLayerStyleFunction: function (layer, options) { let gobjTy = layer.get('gobjTy'); + if (!gobjTy || typeof this[gobjTy] !== 'function') return; let styleFunction = function (feature, resolution) { return this[gobjTy](layer, options, feature, resolution); }.bind(this); diff --git a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx index a663feabf..e13965768 100644 --- a/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx +++ b/packages/common/business_components/src/ui/map/searchBar/searchBar.tsx @@ -4,7 +4,7 @@ import { Button } from "@common/components/ui"; import { useCallback, useEffect, useState } from "react"; import SearchInputText from "./SearchInputText"; import GisApp from "../lib/gis.core"; -import { moveMapToCenter, sidoCenter, sigunguCenter } from "./utils"; +import { getSigunguCenter, moveMapToCenter, sidoCenter } from "./utils"; const FACILITY_TYPE_URL_MAP: Record<string, string> = { tb_il_swage_prcs_fclt: @@ -112,9 +112,9 @@ export const SearchBar = ({ .map((item: any) => ({ val: item.lwpoCd, text: item.lwpoCdNm, - center: sigunguCenter[item.lwpoCd?.trim()], - })) - .sort((a: any, b: any) => a.text.localeCompare(b.text, 'ko')); + center: getSigunguCenter(item.lwpoCd), + })); + //.sort((a: any, b: any) => a.text.localeCompare(b.text, 'ko')); setSidoData([{ text: "시도", val: "0" }, ...sidoList]); setSigunData(sigunList); }; @@ -168,7 +168,10 @@ export const SearchBar = ({ const selectedSido = sidoData.find((item) => item.val === sidoValue); if (!selectedSido?.center) return; + // console.log("시도 : " +sidoValue); + // console.log("시군구 : " + sigunValue); if (sigunValue !== "0") { + console.log(sigunData.filter((item) => item.val == sigunValue)); const selectedSigun = filteredSigunData.find((item) => item.val === sigunValue) ?? sigunData.find((item) => item.val === sigunValue); diff --git a/packages/common/business_components/src/ui/map/searchBar/utils.ts b/packages/common/business_components/src/ui/map/searchBar/utils.ts index 57528d6b9..5c9aef350 100644 --- a/packages/common/business_components/src/ui/map/searchBar/utils.ts +++ b/packages/common/business_components/src/ui/map/searchBar/utils.ts @@ -2,6 +2,11 @@ import type { RefObject } from "react"; import { transform } from "ol/proj"; import GisApp from "../lib/gis.core"; +/** + * + * @param geometry + * @returns + */ export const getGeometryCenter = (geometry: any): [number, number] | null => { if (!geometry?.coordinates) return null; @@ -42,22 +47,34 @@ export const getGeometryCenter = (geometry: any): [number, number] | null => { return [(minX + maxX) / 2, (minY + maxY) / 2]; }; +/** + * 이미 5179좌표계이면 변환하지 않음. + * @param centerCoord + * @returns + */ export const getMapCenterCoordinate = (centerCoord: [number, number]): [number, number] => { const [x, y] = centerCoord; - const looksLikeLonLat = Math.abs(x) <= 180 && Math.abs(y) <= 90; - const looksLikeLatLonSwapped = Math.abs(x) <= 90 && Math.abs(y) <= 180; + const looksLikeLonLat = Math.abs(x) <= 180 && Math.abs(y) <= 90; // 경위도 + const looksLikeLatLonSwapped = Math.abs(x) <= 90 && Math.abs(y) <= 180; // 위경도 - if (looksLikeLonLat) { - return transform(centerCoord, GisApp.FROM_PROJ, GisApp.TO_PROJ) as [number, number]; + if (looksLikeLonLat) { + return transform(centerCoord, GisApp.FROM_PROJ, GisApp.TO_PROJ) as [number, number]; // 4326 -> 5179 } if (looksLikeLatLonSwapped) { - return transform([y, x], GisApp.FROM_PROJ, GisApp.TO_PROJ) as [number, number]; + return transform([y, x], GisApp.FROM_PROJ, GisApp.TO_PROJ) as [number, number]; // 4326 -> 5179 } return centerCoord; }; +/** + * + * @param gisMaps + * @param centerCoord + * @param zoom + * @returns + */ export const moveMapToCenter = ( gisMaps: RefObject<any>, centerCoord?: [number, number], @@ -69,7 +86,7 @@ export const moveMapToCenter = ( if (!map?.getView) return; const view = map.getView(); - const center = getMapCenterCoordinate(centerCoord); + const center = getMapCenterCoordinate(centerCoord); // 4326 -> 5179 if (!Number.isFinite(center[0]) || !Number.isFinite(center[1])) return; const projection = view.getProjection?.(); @@ -89,7 +106,28 @@ export const moveMapToCenter = ( }); }; +/** + * 시군구 코드에 해당하는 중심 좌표를 반환한다. + * 먼저 전체 코드로 정확히 조회하고, 값이 없으면 앞 4자리가 같은 다른 시군구 코드의 좌표를 대체로 사용한다. + * + * @param lwpoCd 행정구역 코드. 예: "41110" + * @returns 시군구 중심 좌표. 조회 결과가 없으면 undefined + */ +export const getSigunguCenter = (lwpoCd?: string | null): [number, number] | undefined => { + const code = lwpoCd?.trim(); + if (!code) return undefined; + const exactCenter = sigunguCenter[code]; + if (exactCenter) return exactCenter; + + const prefix = code.slice(0, 4); + if (prefix.length < 4) return undefined; + + const fallbackCode = Object.keys(sigunguCenter).find((key) => key.startsWith(prefix)); + return fallbackCode ? sigunguCenter[fallbackCode] : undefined; +}; + +// EPSG:5179 기준 export const sidoCenter: Record<string, [number, number]> = { "11": [953551.4119216551, 1951826.357686395], // 서울특별시 "26": [1139616.39795744, 1683535.70569214], // 부산광역시 @@ -98,23 +136,25 @@ export const sidoCenter: Record<string, [number, number]> = { "29": [939294.465052485, 1684630.0528689548], // 광주광역시 "30": [991301.069516585, 1816039.953510165], // 대전광역시 "31": [1155621.6870651902, 1727062.69578941], // 울산광역시 - "36": [979381.304388835, 1841430.83666584], // 세종특별자치시 + // "36": [979381.304388835, 1841430.83666584], // 세종특별자치시 "41": [965621.8990488851, 1954345.5683645], // 경기도 - "51": [1065114.938094025, 1980661.8453088552], // 강원특별자치도 + "42": [1065114.938094025, 1980661.8453088552], // 강원특별자치도 "43": [1041183.7558293699, 1848671.9260201901], // 충청북도 "44": [918689.7963483699, 1836216.9375309348], // 충청남도 - "52": [949568.330647045, 1748060.215177075], // 전북특별자치도 + "45": [949568.330647045, 1748060.215177075], // 전북특별자치도 "46": [906934.38266853, 1637218.959990745], // 전라남도 "47": [1207219.543276005, 1843195.136606685], // 경상북도 "48": [1081504.77257488, 1690185.644486565], // 경상남도 - "50": [912295.3285630699, 1508716.724020285], // 제주특별자치도 + "49": [909678.884, 1485988.314], // [912295.3285630699, 1508716.724020285], // 제주특별자치도 + "50": [979381.304388835, 1841430.83666584], // 세종특별자치시 }; +// EPSG:5179 기준, 구코드 기준임. 즉, 하수도정보시스템 기준, tb_ms_sgg는 신코드 기준임. export const sigunguCenter: Record<string, [number, number]> = { "41820": [994886.197272545, 1979052.53325589], // 가평군 "11680": [961673.31210415, 1944170.63969123], // 강남구 "11740": [968769.7482373801, 1950018.61637764], // 강동구 - "51150": [1117008.10469879, 1968660.393087245], // 강릉시 + "42150": [1117008.10469879, 1968660.393087245], // 강릉시 "11305": [957184.89551525, 1960948.17897649], // 강북구 "11500": [940169.1006571399, 1952038.261591505], // 강서구 "26440": [1125722.34144724, 1680630.4026719849], // 강서구 @@ -128,11 +168,11 @@ export const sigunguCenter: Record<string, [number, number]> = { "28245": [932663.7165225251, 1951314.751646345], // 계양구 "47830": [1074241.6005863552, 1748726.26930297], // 고령군 "48820": [1073144.623548525, 1668111.92215727], // 고성군 - "51820": [1079906.976889925, 2044841.96337935], // 고성군 + "42820": [1079906.976889925, 2044841.96337935], // 고성군 "41281": [945034.57886003, 1962467.607140625], // 고양시 덕양구 "41285": [937759.4023643399, 1964877.5403761598], // 고양시 일산동구 "41287": [931767.42700348, 1964473.38259711], // 고양시 일산서구 - "52790": [918393.62804468, 1716132.826828825], // 고창군 + "45790": [918393.62804468, 1716132.826828825], // 고창군 "46770": [982955.40283695, 1619845.06015117], // 고흥군 "46720": [976956.4637679299, 1689787.68075054], // 곡성군 "44150": [962642.898486655, 1831283.240475945], // 공주시 @@ -148,14 +188,14 @@ export const sigunguCenter: Record<string, [number, number]> = { "11530": [943255.85156361, 1944224.7496726601], // 구로구 "41310": [967836.346287095, 1955821.725581055], // 구리시 "47190": [1076297.5737083051, 1801900.2253747801], // 구미시 - "52130": [904674.42544751, 1767715.183037305], // 군산시 + "45130": [904674.42544751, 1767715.183037305], // 군산시 "27720": [1103849.611897755, 1797204.1944948], // 군위군 "41410": [948411.5776788951, 1927135.757437655], // 군포시 "44710": [998110.87580872, 1792026.97835628], // 금산군 "26410": [1144696.85330015, 1697028.92074261], // 금정구 "11545": [946955.492372005, 1940271.753176735], // 금천구 "26710": [1155182.296211175, 1700204.308554315], // 기장군 - "52210": [935224.890489175, 1755205.56953502], // 김제시 + "45210": [935224.890489175, 1755205.56953502], // 김제시 "47150": [1052988.6733146599, 1783129.650228145], // 김천시 "41570": [925936.0119416551, 1965389.1203246699], // 김포시 "48250": [1123054.361467805, 1698359.11561172], // 김해시 @@ -166,7 +206,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "31140": [1164388.02782853, 1725008.616189565], // 남구 "28200": [931330.60305263, 1937119.742216935], // 남동구 "41360": [976250.43203106, 1960416.01281481], // 남양주시 - "52190": [993534.846345315, 1715101.9621615452], // 남원시 + "45190": [993534.846345315, 1715101.9621615452], // 남원시 "48840": [1042145.28987352, 1636788.3540021451], // 남해군 "11350": [962686.53614943, 1961831.238915105], // 노원구 "44230": [969310.08745166, 1800569.86826501], // 논산시 @@ -187,20 +227,20 @@ export const sigunguCenter: Record<string, [number, number]> = { "41250": [963148.54470934, 1991284.1699954001], // 동두천시 "26260": [1143854.4548997101, 1691107.4825817398], // 동래구 "11590": [950875.3949149849, 1944274.8505625948], // 동작구 - "51170": [1137915.778823135, 1947378.462972425], // 동해시 + "42170": [1137915.778823135, 1947378.462972425], // 동해시 "11440": [947783.55349907, 1951598.7669664049], // 마포구 "46110": [897040.76889998, 1645038.46904772], // 목포시 "46840": [899587.661355305, 1664175.9893810651], // 무안군 - "52730": [1019648.0651991949, 1770211.22681309], // 무주군 + "45730": [1019648.0651991949, 1770211.22681309], // 무주군 "47280": [1055620.11152262, 1855147.380883595], // 문경시 - "28177": [926226.3594373399, 1940093.5607894901], // 미추홀구 + "28170": [926226.3594373399, 1940093.5607894901], // 미추홀구 "48270": [1117632.749546145, 1722736.830743895], // 밀양시 "44180": [896412.984839845, 1817364.412693855], // 보령시 "46780": [974939.082074685, 1646364.3864357], // 보성군 "43720": [1018843.696119875, 1832681.50774474], // 보은군 "47920": [1125627.975921425, 1881676.019098885], // 봉화군 "26230": [1140857.032737495, 1686905.10585393], // 부산진구 - "52800": [906904.98605649, 1743711.597374285], // 부안군 + "45800": [906904.98605649, 1743711.597374285], // 부안군 "44760": [943835.889385045, 1803731.764881715], // 부여군 "41194": [937043.513148805, 1941917.36078241], // 부천시 소사구 "41196": [937651.62357278, 1948088.650724335], // 부천시 오정구 @@ -214,14 +254,14 @@ export const sigunguCenter: Record<string, [number, number]> = { "48240": [1048464.90586407, 1669923.18795966], // 사천시 "26380": [1134089.12930976, 1669378.70600097], // 사하구 "48860": [1036496.214687915, 1711373.1111203502], // 산청군 - "51230": [1142435.56217199, 1918978.17539709], // 삼척시 + "42230": [1142435.56217199, 1918978.17539709], // 삼척시 "47250": [1050861.110185015, 1827830.141259105], // 상주시 "26140": [1137931.5983933099, 1678744.3981297952], // 서구 "27170": [1094695.438967345, 1764784.74637237], // 서구 "28260": [922809.601799805, 1950975.3525035651], // 서구 "29140": [941246.057711575, 1682370.2693891348], // 서구 "30170": [985795.240808525, 1808929.24401602], // 서구 - "50130": [912245.46105533, 1478972.2801259952], // 서귀포시 + "49130": [912245.46105533, 1478972.2801259952], // 서귀포시 "11410": [950162.9458121, 1953864.48715979], // 서대문구 "44210": [909519.454637895, 1868915.7895147852], // 서산시 "44770": [925279.862903135, 1788678.070235445], // 서천군 @@ -233,7 +273,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "11290": [957924.54027983, 1956512.99273417], // 성북구 "47840": [1065227.20912238, 1769071.96163128], // 성주군 "36110": [979381.304388835, 1841430.83666584], // 세종특별자치시 - "51210": [1089137.1194603802, 2019900.279034885], // 속초시 + "42210": [1089137.1194603802, 2019900.279034885], // 속초시 "11710": [965928.83189988, 1945132.495701395], // 송파구 "27260": [1104712.551021275, 1760363.086093105], // 수성구 "26500": [1146933.8448469099, 1686288.07716536], // 수영구 @@ -241,7 +281,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "41117": [961096.623048695, 1919787.236461525], // 수원시 영통구 "41111": [955699.294199125, 1924459.3724800851], // 수원시 장안구 "41115": [956709.41575029, 1919672.72074137], // 수원시 팔달구 - "52770": [962532.77503817, 1714757.02288094], // 순창군 + "45770": [962532.77503817, 1714757.02288094], // 순창군 "46150": [989441.17839757, 1668074.09581569], // 순천시 "41390": [934025.6249972349, 1932609.450570995], // 시흥시 "46910": [837068.838454425, 1628081.7615658198], // 신안군 @@ -252,14 +292,14 @@ export const sigunguCenter: Record<string, [number, number]> = { "41550": [983047.68986298, 1891835.77152603], // 안성시 "41173": [951774.092625505, 1933701.863194715], // 안양시 동안구 "41171": [947757.35105343, 1934091.7197804102], // 안양시 만안구 - "51800": [1044799.8868826301, 2018930.475621245], // 양구군 + "42800": [1044799.8868826301, 2018930.475621245], // 양구군 "48330": [1140452.0009971252, 1713003.89300298], // 양산시 - "51830": [1097181.1822994999, 2001875.93591869], // 양양군 - "41630": [957059.65493005, 1978556.8707086649], // 양주시 + "42830": [1097181.1822994999, 2001875.93591869], // 양양군 + "41710": [957059.65493005, 1978556.8707086649], // 양주시 "11470": [943115.85871501, 1947726.50098438], // 양천구 "41830": [1006799.81121338, 1946640.3454016], // 양평군 "46130": [1005710.6789662449, 1606473.622557845], // 여수시 - "41670": [1007389.488139985, 1920986.5590416798], // 여주시 + "41730": [1007389.488139985, 1920986.5590416798], // 여주시 "28185": [924301.1973107599, 1932716.5874987599], // 연수구 "26470": [1143967.4104618952, 1688206.29479672], // 연제구 "41800": [955142.1788103101, 2011772.731874695], // 연천군 @@ -270,7 +310,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "11560": [948221.9355719599, 1946989.433783595], // 영등포구 "46830": [918568.42419672, 1645644.03685685], // 영암군 "47760": [1147157.8720550952, 1855483.55416634], // 영양군 - "51750": [1089601.7085692, 1913792.65877573], // 영월군 + "42750": [1089601.7085692, 1913792.65877573], // 영월군 "47210": [1096747.798418135, 1876468.75553605], // 영주시 "47230": [1128168.955619915, 1779896.3669964802], // 영천시 "44810": [935536.49511435, 1852175.0178411552], // 예산군 @@ -279,7 +319,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "43730": [1016925.1630849349, 1812306.15335931], // 옥천군 "28720": [831017.06725137, 1941763.7671776651], // 옹진군 "46890": [934681.072970785, 1580260.96434381], // 완도군 - "52710": [971714.974980355, 1764297.860421155], // 완주군 + "45710": [971714.974980355, 1764297.860421155], // 완주군 "11170": [954152.265892775, 1948105.11473188], // 용산구 "41463": [966362.167328945, 1919086.059435395], // 용인시 기흥구 "41465": [962751.309054115, 1926067.760215145], // 용인시 수지구 @@ -287,7 +327,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "47940": [1339459.86503077, 1939972.251018405], // 울릉군 "31710": [1152103.383022245, 1727062.6953479801], // 울주군 "47930": [1158508.6557213152, 1879083.142978875], // 울진군 - "51130": [1042675.604280725, 1925192.470561375], // 원주시 + "42130": [1042675.604280725, 1925192.470561375], // 원주시 "30200": [985050.9636310451, 1820539.200680445], // 유성구 "11380": [949045.72456328, 1957730.2124330248], // 은평구 "43770": [1010863.03644352, 1888460.954926385], // 음성군 @@ -296,17 +336,18 @@ export const sigunguCenter: Record<string, [number, number]> = { "41430": [954677.940289845, 1928890.756176705], // 의왕시 "41150": [962517.29720437, 1970466.861837395], // 의정부시 "41500": [998578.52141335, 1911408.9081171248], // 이천시 - "52140": [954517.27659099, 1780482.64843965], // 익산시 - "51810": [1065461.7186508449, 2010839.9914423148], // 인제군 - "52750": [978229.1338949, 1735882.14962747], // 임실군 + "45140": [954517.27659099, 1780482.64843965], // 익산시 + "42810": [1065461.7186508449, 2010839.9914423148], // 인제군 + "45750": [978229.1338949, 1735882.14962747], // 임실군 "46880": [932255.0392670301, 1705063.362106785], // 장성군 - "52740": [1002766.6532682499, 1739693.270420835], // 장수군 + "45740": [1002766.6532682499, 1739693.270420835], // 장수군 "46800": [946864.73876017, 1628580.0165673052], // 장흥군 + "45110": [965336.86328101, 1761811.9640769851], // 전주시 "52113": [965336.86328101, 1761811.9640769851], // 전주시 덕진구 "52111": [966893.605588815, 1754468.0213283598], // 전주시 완산구 - "51770": [1110360.32014753, 1931013.538670365], // 정선군 - "52180": [947098.239651715, 1735070.37728938], // 정읍시 - "50110": [912295.3285630699, 1517557.88079664], // 제주시 + "42770": [1110360.32014753, 1931013.538670365], // 정선군 + "45180": [947098.239651715, 1735070.37728938], // 정읍시 + "49110": [910293.463, 1500596.952], //[912295.3285630699, 1517557.88079664], // 제주시 "43150": [1055671.53255738, 1893115.04197246], // 제천시 "11110": [954639.31671291, 1955652.65017071], // 종로구 "11140": [955326.326625775, 1951065.0090038702], // 중구 @@ -318,7 +359,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "11260": [964157.58830932, 1955182.665424475], // 중랑구 "43745": [1009013.34585424, 1864734.46162539], // 증평군 "46900": [870540.7578711801, 1597916.46618356], // 진도군 - "52720": [995028.62264954, 1758625.23076353], // 진안군 + "45720": [995028.62264954, 1758625.23076353], // 진안군 "48170": [1057167.869451315, 1689727.04548209], // 진주시 "43750": [994342.7517215649, 1875793.7535851048], // 진천군 "48740": [1090841.266427235, 1726085.44764784], // 창녕군 @@ -329,7 +370,7 @@ export const sigunguCenter: Record<string, [number, number]> = { "48129": [1111617.62626141, 1680996.6599733299], // 창원시 진해구 "44131": [974478.67761805, 1862026.08139811], // 천안시 동남구 "44133": [971707.02854063, 1875704.530427825], // 천안시 서북구 - "51780": [997447.306165415, 2024301.05104793], // 철원군 + "42780": [997447.306165415, 2024301.05104793], // 철원군 "47820": [1116479.97088183, 1746481.442573125], // 청도군 "47750": [1139213.58132652, 1821067.804006115], // 청송군 "44790": [941752.03871163, 1827462.3607307798], // 청양군 @@ -337,16 +378,16 @@ export const sigunguCenter: Record<string, [number, number]> = { "43112": [994258.23710022, 1838908.2251619152], // 청주시 서원구 "43114": [997183.756680205, 1857171.4242312452], // 청주시 청원구 "43113": [989161.30366111, 1849501.26145491], // 청주시 흥덕구 - "51110": [1023515.451806125, 1986727.7291947599], // 춘천시 + "42110": [1023515.451806125, 1986727.7291947599], // 춘천시 "43130": [1034982.8699803599, 1889790.67989893], // 충주시 "47850": [1086736.697665845, 1779277.075473485], // 칠곡군 - "51190": [1131634.14282853, 1912631.8063723398], // 태백시 + "42190": [1131634.14282853, 1912631.8063723398], // 태백시 "44825": [864678.84145598, 1854929.12571281], // 태안군 "48220": [1085316.421521755, 1640596.827168765], // 통영시 "41480": [941812.744809255, 1983528.9263305948], // 파주시 - "51760": [1088652.567431635, 1949959.4658569149], // 평창군 + "42760": [1088652.567431635, 1949959.4658569149], // 평창군 "41220": [952557.5035727851, 1891740.1058186102], // 평택시 - "41650": [979667.7288193901, 1996457.8400075398], // 포천시 + "41810": [979667.7288193901, 1996457.8400075398], // 포천시 "47111": [1173941.15980316, 1776164.6804896551], // 포항시 남구 "47113": [1155413.511896325, 1798939.39325854], // 포항시 북구 "41450": [974619.56368595, 1947933.69704679], // 하남시 @@ -358,10 +399,10 @@ export const sigunguCenter: Record<string, [number, number]> = { "46820": [907157.24936254, 1615346.232287635], // 해남군 "26350": [1151062.0301465848, 1690808.34175552], // 해운대구 "44800": [919404.43702764, 1841255.1935457601], // 홍성군 - "51720": [1049442.09422194, 1972233.65191397], // 홍천군 + "42720": [1049442.09422194, 1972233.65191397], // 홍천군 "41590": [941810.22622111, 1906896.59289364], // 화성시 "46790": [955520.512544285, 1669429.020528105], // 화순군 - "51790": [1015098.9945956401, 2015706.532260655], // 화천군 - "51730": [1046904.40402372, 1943541.6685614302], // 횡성군 + "42790": [1015098.9945956401, 2015706.532260655], // 화천군 + "42730": [1046904.40402372, 1943541.6685614302], // 횡성군 }; diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index c5c9ef809..9da949bee 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from "react"; import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; import { FormField } from "../form"; import { FormUi } from "../form_ui"; -import { SelectBox } from "../select"; import { SdmSearchForm } from "./sdmNameBox"; import { SearchAddressModal } from "../search_address/SearchAddressModal"; @@ -80,6 +79,11 @@ type FieldComponent = ( field: ControllerRenderProps<FormSchema, keyof FormSchema>, ) => React.ReactElement; +const getReadonlyInputClassName = (isReadonly?: boolean) => + isReadonly + ? "!text-xl text-gray-600 bg-gray-100 cursor-default" + : "!text-xl text-gray-600"; + export function SdmRegForm({ form, onCheckboxChange, @@ -92,11 +96,6 @@ export function SdmRegForm({ readOnly, openNetwork, }: SdmRegFormProps) { - const [sidoValue, setSidoValue] = useState<string>(""); - const [sigunValue, setSigunValue] = useState<string>(""); - const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([ - { text: "전체", val: "0" }, - ]); const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); const [orgBznmNo, setOrgBznmNo] = useState<string>(""); const [updateCoNm, setUpdateNm] = useState<string>(""); @@ -134,46 +133,12 @@ export function SdmRegForm({ const initializeSigunData = useCallback( (sidoCd: string) => { if (sidoCd === "0") { - setSigunValue("0"); - setFilteredSigunData([{ text: "전체", val: "0" }]); + form.setValue("sggCd", ""); } else { - const newFilteredData = [ - { text: "전체", val: "0" }, - ...selectSigunData.filter( - (item: SelectDataType) => item.val.substring(0, 2) === sidoCd, - ), - ]; - setSigunValue("0"); - setFilteredSigunData(newFilteredData); + form.setValue("sggCd", ""); } }, - [selectSigunData], - ); - - const onSelectValue = useCallback( - ( - val: string, - type: string, - field: ControllerRenderProps<FormSchema, keyof FormSchema> | undefined, - ) => { - if (!updateList?.dmdInfoId) - switch (type) { - case "ctpvCd": - initializeSigunData(val); - setSidoValue(val); - field?.onChange(val); - form.setValue("dmdInfoId", ""); - form.setValue("bznmTotNo", ""); - break; - case "sggCd": - setSigunValue(val); - field?.onChange(val); - form.setValue("dmdInfoId", ""); - form.setValue("bznmTotNo", ""); - break; - } - }, - [initializeSigunData, form, updateList], + [form], ); const handleCheckboxChange = useCallback( @@ -207,13 +172,11 @@ export function SdmRegForm({ // 수요처 불러온 후 사업자번호를 바꿀경우를 대비함 setOrgBznmNo(rtnCompData?.dmdInfoId); const sidoCd = rtnCompData?.ctpvCd.trim() || ""; - setSidoValue(sidoCd); form.setValue("ctpvCd", sidoCd); initializeSigunData(sidoCd); setTimeout(() => { const sigunCd = rtnCompData?.sggCd.trim() || ""; - setSigunValue(sigunCd); form.setValue("sggCd", sigunCd); }, 100); @@ -258,7 +221,6 @@ export function SdmRegForm({ ); if (!matchedSido) return; - setSidoValue(matchedSido.val); form.setValue("ctpvCd", matchedSido.val, { shouldValidate: true }); initializeSigunData(matchedSido.val); @@ -279,7 +241,6 @@ export function SdmRegForm({ if (!matchedSigun) return; - setSigunValue(matchedSigun.val); form.setValue("sggCd", matchedSigun.val, { shouldValidate: true }); }, [form, initializeSigunData, selectSidoData, selectSigunData], @@ -364,68 +325,6 @@ export function SdmRegForm({ label: string; component: FieldComponent; }> = [ - { - name: "ctpvCd", - label: "시도", - component: (field) => - updateList?.dmdInfoId ? ( - <Input - placeholder="시도" - type="text" - className="!text-xl text-gray-600" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={ - selectSidoData.find((item) => item.val === field.value?.trim()) - ?.text ?? "" - } - readOnly={!!updateList?.dmdInfoId} - /> - ) : ( - <SelectBox - selectClass={undefined} - label="" - selectArray={selectSidoData} - onSelectValue={(val) => - onSelectValue(val.toString(), "ctpvCd", field ?? "") - } - className="" - value={sidoValue} - /> - ), - }, - { - name: "sggCd", - label: "시군구", - component: (field) => - updateList?.dmdInfoId ? ( - <Input - placeholder="시도" - type="text" - className="!text-xl text-gray-600" - onChange={(e) => { - field.onChange(e.target.value); - }} - value={ - selectSigunData.find((item) => item.val === field.value?.trim()) - ?.text ?? "" - } - readOnly={!!updateList?.dmdInfoId} - /> - ) : ( - <SelectBox - selectClass={"!text-xl text-gray-600"} - label="" - selectArray={filteredSigunData} - onSelectValue={(val) => - onSelectValue(val.toString(), "sggCd", field) - } - className="" - value={sigunValue} - /> - ), - }, { name: "dmdCoNm", label: "회사명", @@ -449,7 +348,7 @@ export function SdmRegForm({ <Input placeholder="사업자등록번호를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(!!updateList?.dmdInfoId)} onChange={(e) => bznNoChange(e, "dmdInfoId", field)} value={formatBusinessNumber(field.value ?? "")} readOnly={!!updateList?.dmdInfoId} @@ -468,7 +367,7 @@ export function SdmRegForm({ <Input placeholder="여러사업장이 있을 경우 입력해주세요" type="text" - className="!mt-5 !text-xl text-gray-600" + className={`${getReadonlyInputClassName(!!updateList?.dmdInfoId)} !mt-5`} disabled={isBznmTotNoDisabled} onChange={(e) => bznNoChange(e, "bznmTotNo", field)} // {...field} @@ -488,7 +387,7 @@ export function SdmRegForm({ {/* 우편번호 */} <Input type="text" - className="!text-xl text-gray-600 w-64" + className={`${getReadonlyInputClassName(true)} w-64`} readOnly={true} value={form.watch("zip") ?? ""} placeholder="우편번호" @@ -498,7 +397,7 @@ export function SdmRegForm({ {/* 주소 */} <Input type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(true)} readOnly={true} value={address ?? ""} placeholder="주소" @@ -507,7 +406,7 @@ export function SdmRegForm({ <Input placeholder="상세 주소를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} onChange={(e) => { const value = e.target.value; @@ -521,13 +420,51 @@ export function SdmRegForm({ <Input placeholder="주소를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} {...field} value={field.value ?? ""} /> ), }, + { + name: "ctpvCd", + label: "시도", + component: (field) => ( + <Input + placeholder="시도" + type="text" + className={getReadonlyInputClassName(true)} + onChange={(e) => { + field.onChange(e.target.value); + }} + value={ + selectSidoData.find((item) => item.val === field.value?.trim()) + ?.text ?? "" + } + readOnly={true} + /> + ), + }, + { + name: "sggCd", + label: "시군구", + component: (field) => ( + <Input + placeholder="시군구" + type="text" + className={getReadonlyInputClassName(true)} + onChange={(e) => { + field.onChange(e.target.value); + }} + value={ + selectSigunData.find((item) => item.val === field.value?.trim()) + ?.text ?? "" + } + readOnly={true} + /> + ), + }, { name: "telno", label: "전화번호", @@ -538,7 +475,7 @@ export function SdmRegForm({ <Input placeholder="전화번호를 입력하세요" type="text" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} value={formattedValue} readOnly={readOnly} onChange={(e) => { @@ -557,7 +494,7 @@ export function SdmRegForm({ <Input placeholder="패스워드를 입력하세요" type="password" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} {...field} value={field.value ?? ""} @@ -571,7 +508,7 @@ export function SdmRegForm({ <Input placeholder="패스워드를 입력하세요" type="password" - className="!text-xl text-gray-600" + className={getReadonlyInputClassName(readOnly)} readOnly={readOnly} {...field} value={field.value ?? ""} @@ -595,8 +532,6 @@ export function SdmRegForm({ const inputId = `${name}-input`; const errorId = `${name}-error`; const isRequired = [ - "ctpvCd", - "sggCd", "dmdCoNm", "dmdInfoId", "daddr", diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 09c357c41..372cc553e 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -9,7 +9,6 @@ import * as z from "zod"; import type { SelectDataType } from "../search_input_select"; import { getCookie } from "@common/components/lib"; - type thisProps = { selectSidoData: SelectDataType[]; selectSigunData: SelectDataType[]; @@ -19,7 +18,7 @@ type thisProps = { bodyClassNm?: string; updateList?:DemandCompType; readOnly?:boolean; - openNetwork:boolean + openNetwork:boolean; } const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,updateList?:DemandCompType) => z.object({ @@ -161,10 +160,10 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList return ( <> - <div className="text-center mb-8"> - <h1 className="text-4xl font-bold text-gray-900 underline"> - {!updateList? '수요처 등록' :readOnly==true? '수요처 정보':'수요처 수정'} - </h1> + <div className="mb-8"> + <h2 className="px-[1.8rem] pt-[2rem] text-[2.4rem] font-semibold text-title"> + {!updateList ? '수요처 등록' : readOnly == true ? '수요처 상세정보' : '수요처 수정'} + </h2> </div> <div className= {bodyClassNm?bodyClassNm:"!max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"}> diff --git a/packages/common/business_components/src/ui/search_icon/index.tsx b/packages/common/business_components/src/ui/search_icon/index.tsx index 0ef0e17e3..5c608354f 100644 --- a/packages/common/business_components/src/ui/search_icon/index.tsx +++ b/packages/common/business_components/src/ui/search_icon/index.tsx @@ -1,3 +1,5 @@ +import "./search-icon.css"; + export default function SearchIcon() { return ( <svg @@ -6,7 +8,7 @@ export default function SearchIcon() { viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" - className="w-6 h-6 text-gray-700" + className="searchicon_svg" > <path strokeLinecap="round" @@ -15,4 +17,4 @@ export default function SearchIcon() { /> </svg> ); - } \ No newline at end of file + } diff --git a/packages/common/business_components/src/ui/search_icon/search-icon.css b/packages/common/business_components/src/ui/search_icon/search-icon.css new file mode 100644 index 000000000..b0ebb1f0a --- /dev/null +++ b/packages/common/business_components/src/ui/search_icon/search-icon.css @@ -0,0 +1,7 @@ +@tailwind components; + +@layer components { + .searchicon_svg { + @apply h-6 w-6 text-label; + } +} diff --git a/packages/common/business_components/src/ui/search_input/index.tsx b/packages/common/business_components/src/ui/search_input/index.tsx index 503322559..e7be9f75d 100644 --- a/packages/common/business_components/src/ui/search_input/index.tsx +++ b/packages/common/business_components/src/ui/search_input/index.tsx @@ -1,5 +1,8 @@ +import "./search-input.css"; + import { Input, Label } from "@common/components"; import React from "react"; +import { cn } from "../../lib/utils"; const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label:string, textValue:string, setText:(val:string)=>void}>(({className, label, textValue, setText}, ref) => { const changeText=(e:React.ChangeEvent<HTMLInputElement>)=>{ @@ -20,9 +23,9 @@ const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label: } return ( - <div className={className+" flex items-center"} > - {label && <Label className="w-20 text-xl text-gray-600">{label}</Label>} - <Input className="!text-lg flex-1 w-3/4 h-13 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input> + <div className={cn("searchinput_root", className)} > + {label && <Label className="searchinput_label">{label}</Label>} + <Input ref={ref} className="searchinput_field" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input> </div> ) }); diff --git a/packages/common/business_components/src/ui/search_input/search-input.css b/packages/common/business_components/src/ui/search_input/search-input.css new file mode 100644 index 000000000..f272abc7e --- /dev/null +++ b/packages/common/business_components/src/ui/search_input/search-input.css @@ -0,0 +1,15 @@ +@tailwind components; + +@layer components { + .searchinput_root { + @apply flex items-center; + } + + .searchinput_label { + @apply w-20 text-xl; + } + + .searchinput_field { + @apply h-14 w-3/4 flex-1 rounded-md border border-input text-lg; + } +} diff --git a/packages/common/business_components/src/ui/search_input_select/index.tsx b/packages/common/business_components/src/ui/search_input_select/index.tsx index bf8787356..8632eac78 100644 --- a/packages/common/business_components/src/ui/search_input_select/index.tsx +++ b/packages/common/business_components/src/ui/search_input_select/index.tsx @@ -1,6 +1,9 @@ +import "./search-input-select.css"; + import { Input, Label } from "@common/components"; import React from "react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../select/lib/selectBox"; +import { cn } from "../../lib/utils"; export type SelectDataType = { text: string; val: string; @@ -23,12 +26,12 @@ const SearchInputSelect = React.forwardRef<HTMLInputElement, {className:string, } return ( - <div className={className+" flex items-center"} > - {label && <Label className="w-20 text-xl text-gray-600">{label}</Label>} - <div className="flex w-full items-center"> - <Input className="w-2/3 text-xl h-14 p-2 border rounded" value={textValue} onChange={changeText} /> + <div className={cn("searchinputselect_root", className)} > + {label && <Label className="searchinputselect_label">{label}</Label>} + <div className="searchinputselect_controls"> + <Input ref={ref} className="searchinputselect_input" value={textValue} onChange={changeText} /> <Select onValueChange={onSelectValue} value={selectValue}> - <SelectTrigger className="ml-2 w-1/3" > + <SelectTrigger className="searchinputselect_select" > <SelectValue placeholder={selectData?.[0]?.text ?? ""} /> </SelectTrigger> {/* <SelectContent ref={refs ? (refs as unknown as RefType)?.selectValue : null}> */} diff --git a/packages/common/business_components/src/ui/search_input_select/search-input-select.css b/packages/common/business_components/src/ui/search_input_select/search-input-select.css new file mode 100644 index 000000000..e37add037 --- /dev/null +++ b/packages/common/business_components/src/ui/search_input_select/search-input-select.css @@ -0,0 +1,23 @@ +@tailwind components; + +@layer components { + .searchinputselect_root { + @apply flex items-center; + } + + .searchinputselect_label { + @apply w-20 text-xl; + } + + .searchinputselect_controls { + @apply flex w-full items-center; + } + + .searchinputselect_input { + @apply h-14 w-2/3 rounded-md border border-input p-2 text-xl; + } + + .searchinputselect_select { + @apply ml-2 w-1/3; + } +} diff --git a/packages/common/business_components/src/ui/select/index.tsx b/packages/common/business_components/src/ui/select/index.tsx index 32510eb18..851d0539d 100644 --- a/packages/common/business_components/src/ui/select/index.tsx +++ b/packages/common/business_components/src/ui/select/index.tsx @@ -1,3 +1,5 @@ +import "./select.css"; + // selectBox import { Label } from "@common/components"; @@ -10,6 +12,7 @@ import { SelectTrigger, SelectValue, } from "./lib/selectBox"; +import { cn } from "../../lib/utils"; /** 차트 기본 스타일 */ interface SelectBox1 { @@ -43,7 +46,7 @@ const SelectBox = React.forwardRef< value, readOnly = false, selectContentClass, - labelClass = "w-20 whitespace-nowrap text-xl text-gray-600", + labelClass = "w-20 whitespace-nowrap text-xl", }, ref, ) => { @@ -72,9 +75,9 @@ const SelectBox = React.forwardRef< : undefined; return ( - <div className={className + " flex items-center"} ref={ref}> + <div className={cn("selectbox_root", className)} ref={ref}> {label && ( - <Label className={labelClass}> + <Label className={cn("selectbox_label", labelClass)}> {label} </Label> )} @@ -92,9 +95,7 @@ const SelectBox = React.forwardRef< value={currentValue} // disabled={readOnly} //물재이용 감리 지적사항으로 일단 주석처리 > - <SelectTrigger - className={selectClass ? selectClass : "flex-1 rounded border p-2"} - > + <SelectTrigger className={cn("selectbox_trigger", selectClass)}> <SelectValue placeholder={placeHolder} /> </SelectTrigger> <SelectContent className={selectContentClass}> diff --git a/packages/common/business_components/src/ui/select/select.css b/packages/common/business_components/src/ui/select/select.css new file mode 100644 index 000000000..8aa16f386 --- /dev/null +++ b/packages/common/business_components/src/ui/select/select.css @@ -0,0 +1,15 @@ +@tailwind components; + +@layer components { + .selectbox_root { + @apply flex items-center; + } + + .selectbox_label { + @apply text-label; + } + + .selectbox_trigger { + @apply flex-1 rounded-md border border-input bg-form px-3 py-2; + } +} diff --git a/packages/common/business_components/src/ui/tab/index.tsx b/packages/common/business_components/src/ui/tab/index.tsx index 273a4b907..949884df9 100644 --- a/packages/common/business_components/src/ui/tab/index.tsx +++ b/packages/common/business_components/src/ui/tab/index.tsx @@ -1,46 +1,61 @@ -import { Tabs, TabsList, TabsTrigger } from '@common/components/ui'; -import React from 'react'; +import "./tab.css"; + +import { Tabs, TabsList, TabsTrigger } from "@common/components/ui"; +import React from "react"; +import { cn } from "../../lib/utils"; interface TabItem { value: string; label: string; } -interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'dir'> { +interface UITabProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "dir"> { defaultValue?: string; tabList: TabItem[]; children?: React.ReactNode; onValueChange?: (value: string) => void; + variant?: "default" | "underline" | "outline"; } -const UITab = React.forwardRef< - HTMLDivElement, - UITabProps ->(({defaultValue, tabList, children, onValueChange, ...props}, ref) => { - return ( - <Tabs - defaultValue={defaultValue || tabList[0]?.value} - className={"w-full space-x-1 border-gray-200"} - onValueChange={onValueChange} - ref={ref} - {...props} - > - <TabsList className="flex space-x-1"> - {tabList.map((tab) => ( - <TabsTrigger - key={tab.value} - value={tab.value} - className="py-2 px-4 text-sm font-medium border-b-2 focus:outline-none data-[state=active]:border-blue-500 data-[state=active]:text-blue-600 data-[state=inactive]:border-transparent data-[state=inactive]:text-gray-500 hover:text-gray-700 hover:border-gray-300" - > - {tab.label} - </TabsTrigger> - ))} - </TabsList> - {children} - </Tabs> - ); -}); +const UITab = React.forwardRef<HTMLDivElement, UITabProps>( + ( + { + defaultValue, + tabList, + children, + onValueChange, + className, + variant = "default", + ...props + }, + ref, + ) => { + return ( + <Tabs + variant={variant} + defaultValue={defaultValue || tabList[0]?.value} + className={cn("w-full", variant === "underline" && "btab_root", className)} + onValueChange={onValueChange} + ref={ref} + {...props} + > + <TabsList className={cn(variant === "underline" && "btab_list")}> + {tabList.map((tab) => ( + <TabsTrigger + key={tab.value} + value={tab.value} + className={cn(variant === "underline" && "btab_trigger")} + > + {tab.label} + </TabsTrigger> + ))} + </TabsList> + {children} + </Tabs> + ); + }, +); -UITab.displayName = 'UITab'; +UITab.displayName = "UITab"; export { UITab }; diff --git a/packages/common/business_components/src/ui/tab/tab.css b/packages/common/business_components/src/ui/tab/tab.css new file mode 100644 index 000000000..5e6fbcd29 --- /dev/null +++ b/packages/common/business_components/src/ui/tab/tab.css @@ -0,0 +1,27 @@ +@tailwind components; + +@layer components { + .btab_root { + @apply w-full; + } + + .btab_list { + @apply flex space-x-1 border-b border-border bg-transparent p-0; + } + + .btab_trigger { + @apply rounded-none border-b-2 border-transparent bg-transparent px-4 py-2 text-sm font-medium text-label; + } + + .btab_root .btab_trigger[data-state="active"] { + @apply border-primary text-primary; + } + + .btab_root .btab_trigger[data-state="inactive"] { + @apply text-label; + } + + .btab_root .btab_trigger:hover { + @apply border-border text-foreground; + } +} diff --git a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx index b3d3fdc40..3ad4c8e23 100644 --- a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx +++ b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx @@ -29,7 +29,7 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { colSpan={head.colSpan || 1} rowSpan={head.rowSpan || 1} style={{ whiteSpace: 'pre-line' }} - className="bg-gray-50" + className="" > {head.title} </TableHead> diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index ed05a7f19..351f08dff 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -151,7 +151,7 @@ export function UiTable<T extends object = Record<string, unknown>>({ ) : ( <></> )} - <div className={`card !p-0r !m-[1.2rem] ${className}`}> + <div className={`card !m-[1.2rem] ${className}`}> <Table variant="secondary" className={widthFixed ? "table-fixed" : ""}> {widthFixed ? <colgroup> diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 4f6c61c56..b303b5e9f 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -3,7 +3,7 @@ import "./header.css"; import { useEffect, useCallback, useState, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { images } from "@common/assets"; -import type { TokenManager } from "@common/auth"; +import type { AuthService, TokenManager } from "@common/auth"; import { getConfig } from "@common/config"; import { ENDPOINT } from "../../config"; import { setCookie, getCookie } from "../../lib"; @@ -29,7 +29,84 @@ const userId = getCookie(appConfig.AUTH_USER_ID ?? ""); const siteId = getCookie("site-id-key") ?? ""; const isLoggedIn = !!userId; -export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager; viewOnly?: boolean }) { +function getRootDomain(hostname: string): string { + if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { + return hostname; + } + + if (hostname.includes("hasuon.or.kr")) { + return ".hasuon.or.kr"; + } + + const parts = hostname.split("."); + if (parts.length >= 2) { + return `.${parts.slice(-2).join(".")}`; + } + + return hostname; +} + +function clearClientCookies(tokenManager?: TokenManager) { + const actualHost = window.location.hostname; + const domain = getRootDomain(actualHost); + + const cookiesToDelete = [ + appConfig.AUTH_USER_ID, + appConfig.REFRESH_TOKEN, + appConfig.ACCESS_TOKEN, + appConfig.CLAIM_NAME, + "site-id-key", + "site-theme-key", + ].filter(Boolean); + + cookiesToDelete.forEach((cookieName) => { + if (!cookieName) return; + + tokenManager?.removeToken(cookieName); + + if (domain !== "localhost") { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`; + } + + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + + if (window.location.protocol === "https:") { + if (domain !== "localhost") { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; secure`; + } + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure`; + } + }); +} + +async function legacyLogout(tokenManager?: TokenManager) { + try { + const response = await fetch("/api/auth/logout", { + method: "POST", + credentials: "include", + }); + + if (!response.ok) { + console.error("Logout API failed"); + } + } catch (error) { + console.error("Logout error:", error); + } finally { + clearClientCookies(tokenManager); + window.location.href = "/auth/login"; + } +} + +export function Header({ + authService, + tokenManager, + viewOnly, +}: { + authService?: AuthService; + /** @deprecated authService를 사용하세요. */ + tokenManager?: TokenManager; + viewOnly?: boolean; +}) { const [mounted, setMounted] = useState(false); const [logoWidth, setLogoWidth] = useState(0); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); @@ -66,93 +143,13 @@ export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager queryFn: () => getSiteList(), enabled: isLoggedIn && !viewOnly, }); - const logout = async () => { - try { - // 1. 백엔드 API 로그아웃 호출 - const response = await fetch("/api/auth/logout", { - method: "POST", - credentials: "include", // ⭐ 쿠키 포함 중요! - }); - - if (!response.ok) { - console.error("Logout API failed"); - // API 실패해도 계속 진행 - } - } catch (error) { - console.error("Logout error:", error); - // 에러가 나도 계속 진행 - } finally { - // 2. 클라이언트 쿠키 삭제 (백업) - clearClientCookies(); - - // 3. 로컬/세션 스토리지 정리 - // if (typeof window !== "undefined") { - // localStorage.clear(); - // sessionStorage.clear(); - // } - - // 4. 로그인 페이지로 리다이렉트 - window.location.href = "/auth/login"; + const handleLogout = () => { + if (authService) { + void authService.logout(); + return; } - }; - // 쿠키 삭제 헬퍼 함수 - const clearClientCookies = () => { - const actualHost = window.location.hostname; - - // 루트 도메인 추출 - const getRootDomain = (hostname: string): string => { - if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { - return hostname; - } - - if (hostname.includes("hasuon.or.kr")) { - return ".hasuon.or.kr"; - } - - const parts = hostname.split("."); - if (parts.length >= 2) { - return `.${parts.slice(-2).join(".")}`; - } - - return hostname; - }; - - const domain = getRootDomain(actualHost); - - // 삭제할 쿠키 목록 - const cookiesToDelete = [ - appConfig.AUTH_USER_ID, - appConfig.REFRESH_TOKEN, - appConfig.ACCESS_TOKEN, - appConfig.CLAIM_NAME, - "site-id-key", - "site-theme-key", - ].filter(Boolean); // undefined 제거 - - // 여러 방법으로 쿠키 삭제 시도 - cookiesToDelete.forEach((cookieName) => { - if (!cookieName) return; - - // 방법 1: removeToken 함수 사용 - tokenManager?.removeToken(cookieName); - - // 방법 2: 직접 document.cookie 설정 (도메인 포함) - if (domain !== "localhost") { - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`; - } - - // 방법 3: 도메인 없이 삭제 (현재 도메인만) - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; - - // 방법 4: Secure 속성 포함 삭제 - if (window.location.protocol === "https:") { - if (domain !== "localhost") { - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; secure`; - } - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure`; - } - }); + void legacyLogout(tokenManager); }; const openPopup = useCallback(async (popup: Popup) => { @@ -270,7 +267,7 @@ export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager <NoticePopover /> </li> <li - onClick={logout} + onClick={handleLogout} className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent" > 로그아웃 @@ -346,7 +343,7 @@ export function Header({ tokenManager, viewOnly }: { tokenManager?: TokenManager <NoticePopover /> </li> <li - onClick={logout} + onClick={handleLogout} className="cursor-pointer rounded-sm p-3 text-xl hover:bg-accent" > 로그아웃 diff --git a/packages/common/components/src/ui/layout/base.tsx b/packages/common/components/src/ui/layout/base.tsx index e1f0c1329..58b678b25 100644 --- a/packages/common/components/src/ui/layout/base.tsx +++ b/packages/common/components/src/ui/layout/base.tsx @@ -1,4 +1,4 @@ -import type { TokenManager } from "@common/auth"; +import type { AuthService, TokenManager } from "@common/auth"; import type { QueryClient } from "@tanstack/react-query"; import { QueryProvider } from "../../provider/query-provider"; import { PopupProvider } from "../../provider/popup-provider"; @@ -14,12 +14,15 @@ import "./common.css"; export function BaseLayout({ children, queryClient, + authService, tokenManager, currentPath, handleMenuClick, }: { children: React.ReactNode; queryClient?: QueryClient; + authService?: AuthService; + /** @deprecated authService를 사용하세요. authService가 없을 때만 Header legacy logout에 사용됩니다. */ tokenManager?: TokenManager; currentPath?: string; handleMenuClick?: (item: MenuItem) => void; @@ -31,7 +34,10 @@ export function BaseLayout({ <HelpDialogProvider> <SidebarProvider> <div className="flex-1"> - <Header tokenManager={tokenManager} /> + <Header + authService={authService} + tokenManager={authService ? undefined : tokenManager} + /> <div className="flex"> <LNB currentPath={currentPath} diff --git a/packages/common/components/src/ui/layout/nolnb.tsx b/packages/common/components/src/ui/layout/nolnb.tsx index c43941793..47717c99b 100644 --- a/packages/common/components/src/ui/layout/nolnb.tsx +++ b/packages/common/components/src/ui/layout/nolnb.tsx @@ -1,3 +1,4 @@ +import type { AuthService, TokenManager } from "@common/auth"; import type { QueryClient } from "@tanstack/react-query"; import { QueryProvider } from "../../provider/query-provider"; import { PopupProvider } from "../../provider/popup-provider"; @@ -13,11 +14,16 @@ import "./common.css"; export function NoLNBLayout({ children, queryClient, + authService, + tokenManager, currentPath, handleMenuClick, }: { children: React.ReactNode; queryClient?: QueryClient; + authService?: AuthService; + /** @deprecated authService를 사용하세요. authService가 없을 때만 Header legacy logout에 사용됩니다. */ + tokenManager?: TokenManager; currentPath?: string; handleMenuClick?: (item: MenuItem) => void; }) { @@ -28,7 +34,10 @@ export function NoLNBLayout({ <HelpDialogProvider> <SidebarProvider> <div className="flex-1"> - <Header /> + <Header + authService={authService} + tokenManager={authService ? undefined : tokenManager} + /> <div className="flex"> <div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto"> <div className="flex-1">{children}</div> diff --git a/packages/common/components/src/ui/tabs/tabs.css b/packages/common/components/src/ui/tabs/tabs.css index b1227b0ce..bb097be0e 100644 --- a/packages/common/components/src/ui/tabs/tabs.css +++ b/packages/common/components/src/ui/tabs/tabs.css @@ -22,4 +22,6 @@ .tabs[data-variant="outline"] .tabs-trigger { @apply relative rounded-none rounded-t-sm border-border bg-background text-muted-foreground after:absolute after:bottom-0 after:left-0 after:h-[0.1rem] after:w-full after:bg-border after:content-[''] data-[state=active]:border data-[state=active]:border-b-0 data-[state=active]:border-border data-[state=active]:text-foreground data-[state=active]:after:bg-transparent; } + + }