Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import {
|
||||
@@ -11,6 +11,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
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";
|
||||
@@ -20,6 +21,8 @@ import { getConfig } from "@common/config";
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport";
|
||||
|
||||
const appConfig = getConfig();
|
||||
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
|
||||
const appSiteId = getCookie("site-id-key") ?? "";
|
||||
@@ -33,6 +36,10 @@ export default function WithBaseLayout({
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
/** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */
|
||||
const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] =
|
||||
useState(false);
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore(
|
||||
(state) => state.setCurrentMenuInfo,
|
||||
);
|
||||
@@ -145,6 +152,12 @@ export default function WithBaseLayout({
|
||||
const handleMenuClick = useCallback(
|
||||
(item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
|
||||
if (isVerificationSelectionReportMenu(item)) {
|
||||
setReportPanelOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
@@ -153,11 +166,11 @@ export default function WithBaseLayout({
|
||||
}
|
||||
|
||||
if (!addTabResult) {
|
||||
alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다.
|
||||
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
}
|
||||
},
|
||||
[setCurrentMenuInfo, addTab, addTabForGrid, alert],
|
||||
[setCurrentMenuInfo, addTab, addTabForGrid, alert, setReportPanelOpen],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -167,7 +180,14 @@ export default function WithBaseLayout({
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
<div className="flex flex-1 h-full">
|
||||
{isVerificationSelectionReportPanelOpen && (
|
||||
<VerificationSelectionReport
|
||||
onClose={() => setReportPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,6 +250,28 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.fctName && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="fctNmSearchInput"
|
||||
className="mr-2 text-label"
|
||||
>
|
||||
시설명
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-4 h-[3.2rem] w-[14rem] rounded-sm !text-[1.3rem]"
|
||||
id="fctNmSearchInput"
|
||||
value={values.fctName}
|
||||
onChange={(e) =>
|
||||
setValues({
|
||||
...values,
|
||||
fctName: e.target.value,
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.isDetail && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label cursor-pointer" htmlFor="isDetail">
|
||||
@@ -283,7 +305,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">선택 안함</SelectItem>
|
||||
{[1,2,3,4,5,6,7,8,9,10].map((i) => (
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => (
|
||||
<SelectItem value={i.toString()} key={i.toString()}>{`${i}개년`}</SelectItem>
|
||||
))
|
||||
}
|
||||
@@ -323,43 +345,6 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{(mergedProps.fctName || mergedProps.userMenual) && (
|
||||
<div className="flex items-center p-2">
|
||||
{mergedProps.fctName && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="fctNmSearchInput"
|
||||
className="mr-2 text-label"
|
||||
>
|
||||
시설명
|
||||
</Label>
|
||||
<Input
|
||||
className="mr-4 h-[3.2rem] w-[14rem] rounded-sm !text-[1.3rem]"
|
||||
id="fctNmSearchInput"
|
||||
value={values.fctName}
|
||||
onChange={(e) =>
|
||||
setValues({
|
||||
...values,
|
||||
fctName: e.target.value,
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.userMenual && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
className="h-[3.2rem] w-[124px]"
|
||||
type="button"
|
||||
>
|
||||
<i className="diveicon di-person-fill" />
|
||||
사용자 메뉴얼
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type Dispatch,
|
||||
type MutableRefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { RealGridReact } from "realgrid-react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -8,25 +19,477 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||
import type { CellEditedCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
type VerificationResultGridRow = Record<string, string | number>;
|
||||
|
||||
export type VerificationExceptionSubmitItem = {
|
||||
vrfcSn: number;
|
||||
prcsOpnnExpln: string;
|
||||
};
|
||||
|
||||
type GridViewWithCheckItem = {
|
||||
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
|
||||
};
|
||||
|
||||
function setRowCheckByItemIndex(
|
||||
gridRef: MutableRefObject<RealGridReact | null>,
|
||||
itemIndex: number,
|
||||
checked: boolean,
|
||||
) {
|
||||
const gv = gridRef.current?.gridView as GridViewWithCheckItem | undefined;
|
||||
try {
|
||||
gv?.checkItem?.(itemIndex, checked, false);
|
||||
} catch {
|
||||
/* RealGrid 체크 API 미지원 등 */
|
||||
}
|
||||
}
|
||||
|
||||
const PRCS_YN_LABEL: Record<string, string> = {
|
||||
"0": "미통과",
|
||||
"1": "통과",
|
||||
"2": "예외처리",
|
||||
"3": "반려",
|
||||
};
|
||||
|
||||
/** History와 동일 + 편집 가능한 처리의견(예외사유, API 필드명 `prcsOpnnExpln`과 동일) */
|
||||
const verificationSubmitResultColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", 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 },
|
||||
{
|
||||
name: "prcsOpnnExpln",
|
||||
fieldName: "prcsOpnnExpln",
|
||||
header: { text: "예외사유" },
|
||||
editable: true,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYn",
|
||||
"pbcpAprvYn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"prcsOpnnExpln",
|
||||
],
|
||||
};
|
||||
|
||||
export interface VerificationResultDialogQueryContext {
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
}
|
||||
|
||||
export interface VerificationResultDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
queryContext?: VerificationResultDialogQueryContext | null;
|
||||
title?: string;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
export function VerificationResultDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
queryContext,
|
||||
title = "검증 결과",
|
||||
emptyMessage = "표시할 검증 결과가 없습니다.",
|
||||
}: VerificationResultDialogProps) {
|
||||
/** 부모가 매 렌더마다 새 객체를 넘기면 queryContext 참조만 바뀌어 loadInitial이 재실행되므로 필드 값 기준으로 안정화 */
|
||||
const stableQueryContext = useMemo((): VerificationResultDialogQueryContext | null => {
|
||||
if (!queryContext) return null;
|
||||
return queryContext;
|
||||
}, [
|
||||
queryContext?.sewerBzmnCd,
|
||||
queryContext?.ctpvNm,
|
||||
queryContext?.sggNm,
|
||||
queryContext?.statsYr,
|
||||
queryContext?.statsCd,
|
||||
]);
|
||||
|
||||
const [normalPageIndex, setNormalPageIndex] = useState(0);
|
||||
const [imrPageIndex, setImrPageIndex] = useState(0);
|
||||
const [normalPageCount, setNormalPageCount] = useState(1);
|
||||
const [imrPageCount, setImrPageCount] = useState(1);
|
||||
const [normalLoading, setNormalLoading] = useState(false);
|
||||
const [imrLoading, setImrLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [normalRows, setNormalRows] = useState<VerificationResultGridRow[]>([]);
|
||||
const [imrRows, setImrRows] = useState<VerificationResultGridRow[]>([]);
|
||||
const [normalCheckedIndices, setNormalCheckedIndices] = useState<Set<number>>(
|
||||
() => new Set(),
|
||||
);
|
||||
const [imrCheckedIndices, setImrCheckedIndices] = useState<Set<number>>(() => new Set());
|
||||
|
||||
const normalGridRef = useRef<RealGridReact | null>(null);
|
||||
const imrGridRef = useRef<RealGridReact | null>(null);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const onNormalPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => {
|
||||
if (params.fieldName !== "prcsOpnnExpln") return;
|
||||
const text = String(params.newValue ?? "");
|
||||
const hasValue = text.trim().length > 0;
|
||||
/** DataGrid는 onCellEdited 시 React rowData를 갱신하지 않아, 제출용 상태를 여기서 맞춤 */
|
||||
setNormalRows((prev) => {
|
||||
const next = [...prev];
|
||||
const row = next[params.itemIndex];
|
||||
if (!row) return prev;
|
||||
next[params.itemIndex] = { ...row, prcsOpnnExpln: text };
|
||||
return next;
|
||||
});
|
||||
setRowCheckByItemIndex(normalGridRef, params.itemIndex, hasValue);
|
||||
setNormalCheckedIndices((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (hasValue) next.add(params.itemIndex);
|
||||
else next.delete(params.itemIndex);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onImrPrcsOpnnExplnEdited = useCallback((params: CellEditedCallbackParams) => {
|
||||
if (params.fieldName !== "prcsOpnnExpln") return;
|
||||
const text = String(params.newValue ?? "");
|
||||
const hasValue = text.trim().length > 0;
|
||||
setImrRows((prev) => {
|
||||
const next = [...prev];
|
||||
const row = next[params.itemIndex];
|
||||
if (!row) return prev;
|
||||
next[params.itemIndex] = { ...row, prcsOpnnExpln: text };
|
||||
return next;
|
||||
});
|
||||
setRowCheckByItemIndex(imrGridRef, params.itemIndex, hasValue);
|
||||
setImrCheckedIndices((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (hasValue) next.add(params.itemIndex);
|
||||
else next.delete(params.itemIndex);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setErrorMessage(null);
|
||||
setNormalRows([]);
|
||||
setImrRows([]);
|
||||
setNormalCheckedIndices(new Set());
|
||||
setImrCheckedIndices(new Set());
|
||||
}, []);
|
||||
|
||||
const mapItemToRow = useCallback((item: VerificationResultReportItem): VerificationResultGridRow => {
|
||||
return {
|
||||
vrfcSn: item.vrfcSn,
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: 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,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
prcsOpnnExpln: item.prcsOpnnExpln ?? "",
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchVerificationResultPage = useCallback(
|
||||
async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => {
|
||||
if (!stableQueryContext) return null;
|
||||
const param: VerificationResultListRequest = {
|
||||
sewerBzmnCd: stableQueryContext.sewerBzmnCd,
|
||||
ctpvNm: stableQueryContext.ctpvNm,
|
||||
sggNm: stableQueryContext.sggNm,
|
||||
statsYr: stableQueryContext.statsYr,
|
||||
statsCd: stableQueryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0",
|
||||
page: pageIndex + 1,
|
||||
size: 10000,
|
||||
};
|
||||
return statisticsApi.getVerificationResultList(param);
|
||||
},
|
||||
[stableQueryContext],
|
||||
);
|
||||
|
||||
const loadInitial = useCallback(async () => {
|
||||
if (!stableQueryContext) {
|
||||
setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
setImrLoading(true);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
try {
|
||||
const [nvRes, imrRes] = await Promise.all([
|
||||
fetchVerificationResultPage("NV", 0),
|
||||
fetchVerificationResultPage("IMR", 0),
|
||||
]);
|
||||
const nvList = nvRes?.data?.verifyResultList ?? [];
|
||||
const imrList = imrRes?.data?.verifyResultList ?? [];
|
||||
setNormalRows(nvList.map(mapItemToRow));
|
||||
setImrRows(imrList.map(mapItemToRow));
|
||||
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
} catch {
|
||||
setNormalRows([]);
|
||||
setImrRows([]);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setErrorMessage("검증 결과 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setNormalCheckedIndices(new Set());
|
||||
setImrCheckedIndices(new Set());
|
||||
}
|
||||
}, [fetchVerificationResultPage, mapItemToRow, stableQueryContext]);
|
||||
|
||||
const handleNormalPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === normalPageIndex || !stableQueryContext) return;
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
try {
|
||||
const response = await fetchVerificationResultPage("NV", pageIndex);
|
||||
const list = response?.data?.verifyResultList ?? [];
|
||||
setNormalRows(list.map(mapItemToRow));
|
||||
setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setNormalPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
setNormalCheckedIndices(new Set());
|
||||
}
|
||||
},
|
||||
[fetchVerificationResultPage, mapItemToRow, normalPageIndex, stableQueryContext],
|
||||
);
|
||||
|
||||
const handleImrPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === imrPageIndex || !stableQueryContext) return;
|
||||
setErrorMessage(null);
|
||||
setImrLoading(true);
|
||||
try {
|
||||
const response = await fetchVerificationResultPage("IMR", pageIndex);
|
||||
const list = response?.data?.verifyResultList ?? [];
|
||||
setImrRows(list.map(mapItemToRow));
|
||||
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setImrLoading(false);
|
||||
setImrCheckedIndices(new Set());
|
||||
}
|
||||
},
|
||||
[fetchVerificationResultPage, mapItemToRow, imrPageIndex, stableQueryContext],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
void loadInitial();
|
||||
}, [loadInitial, open, resetState]);
|
||||
|
||||
/** 조회된 행에 예외사유 텍스트가 있으면 체크바·체크 인덱스와 맞춤 */
|
||||
useEffect(() => {
|
||||
if (normalLoading) return;
|
||||
const id = requestAnimationFrame(() => {
|
||||
setNormalCheckedIndices((prev) => {
|
||||
const next = new Set(prev);
|
||||
normalRows.forEach((row, itemIndex) => {
|
||||
const has = String(row.prcsOpnnExpln ?? "").trim().length > 0;
|
||||
setRowCheckByItemIndex(normalGridRef, itemIndex, has);
|
||||
if (has) next.add(itemIndex);
|
||||
});
|
||||
return next;
|
||||
});
|
||||
});
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [normalRows, normalLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (imrLoading) return;
|
||||
const id = requestAnimationFrame(() => {
|
||||
setImrCheckedIndices((prev) => {
|
||||
const next = new Set(prev);
|
||||
imrRows.forEach((row, itemIndex) => {
|
||||
const has = String(row.prcsOpnnExpln ?? "").trim().length > 0;
|
||||
setRowCheckByItemIndex(imrGridRef, itemIndex, has);
|
||||
if (has) next.add(itemIndex);
|
||||
});
|
||||
return next;
|
||||
});
|
||||
});
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [imrRows, imrLoading]);
|
||||
|
||||
const handleExceptionSubmitClick = useCallback(() => {
|
||||
const payload: VerificationExceptionSubmitItem[] = [];
|
||||
const pushIfHasExpln = (row: VerificationResultGridRow | undefined) => {
|
||||
if (!row) return;
|
||||
const prcsOpnnExpln = String(row.prcsOpnnExpln ?? "").trim();
|
||||
if (!prcsOpnnExpln) return;
|
||||
payload.push({
|
||||
vrfcSn: Number(row.vrfcSn),
|
||||
prcsOpnnExpln,
|
||||
});
|
||||
};
|
||||
normalCheckedIndices.forEach((idx) => pushIfHasExpln(normalRows[idx]));
|
||||
imrCheckedIndices.forEach((idx) => pushIfHasExpln(imrRows[idx]));
|
||||
if (payload.length === 0) {
|
||||
alert("작성된 예외 제출 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
console.log(payload);
|
||||
}, [alert, imrCheckedIndices, imrRows, normalCheckedIndices, normalRows]);
|
||||
|
||||
const renderGrid = (
|
||||
rows: VerificationResultGridRow[],
|
||||
setRows: Dispatch<SetStateAction<VerificationResultGridRow[]>>,
|
||||
pageIndex: number,
|
||||
pageCount: number,
|
||||
onPageIndexChange: (page: number) => void,
|
||||
loading: boolean,
|
||||
gridRef: MutableRefObject<RealGridReact | null>,
|
||||
onPrcsOpnnExplnCellEdited: (params: CellEditedCallbackParams) => void,
|
||||
setCheckedIndices: Dispatch<SetStateAction<Set<number>>>,
|
||||
) => {
|
||||
if (!loading && rows.length === 0) {
|
||||
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative space-y-2">
|
||||
<DataGrid
|
||||
columnInfo={verificationSubmitResultColumnInfo}
|
||||
rowData={rows}
|
||||
setRowData={setRows as Dispatch<SetStateAction<Record<string, string | number>[]>>}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox
|
||||
enableSort={false}
|
||||
getGridRef={gridRef}
|
||||
onCellEdited={onPrcsOpnnExplnCellEdited}
|
||||
onItemChecked={(itemIndex, checked) => {
|
||||
setCheckedIndices((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (checked) next.add(itemIndex);
|
||||
else next.delete(itemIndex);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onItemAllChecked={(checked) => {
|
||||
setCheckedIndices(
|
||||
checked ? new Set(rows.map((_, i) => i)) : new Set(),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<VerificationResultHistoryPagination
|
||||
pageCount={Math.max(pageCount, 1)}
|
||||
pageIndex={Math.max(pageIndex, 0)}
|
||||
onPageIndexChange={onPageIndexChange}
|
||||
className="mx-2 mb-2"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-10 rounded-md [data-theme='black']:bg-transparent bg-white/40 flex items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const titleSuffix = useMemo(() => {
|
||||
if (!stableQueryContext) return "";
|
||||
const sgg = stableQueryContext.sggNm?.trim() ? ` ${stableQueryContext.sggNm}` : "";
|
||||
return ` - ${stableQueryContext.statsYr} ${stableQueryContext.ctpvNm}${sgg}`;
|
||||
}, [stableQueryContext]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[640px]" aria-describedby={undefined}>
|
||||
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>검증 결과</DialogTitle>
|
||||
<DialogTitle>
|
||||
{title}
|
||||
{titleSuffix}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="min-h-[120px]" />
|
||||
<div className="max-h-[85vh] space-y-5 overflow-auto">
|
||||
{errorMessage && (
|
||||
<p className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[1.2rem] text-destructive">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">일반검증 내역</h3>
|
||||
{renderGrid(
|
||||
normalRows,
|
||||
setNormalRows,
|
||||
normalPageIndex,
|
||||
normalPageCount,
|
||||
handleNormalPageChange,
|
||||
normalLoading,
|
||||
normalGridRef,
|
||||
onNormalPrcsOpnnExplnEdited,
|
||||
setNormalCheckedIndices,
|
||||
)}
|
||||
</section>
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">I-MR 검증내역</h3>
|
||||
{renderGrid(
|
||||
imrRows,
|
||||
setImrRows,
|
||||
imrPageIndex,
|
||||
imrPageCount,
|
||||
handleImrPageChange,
|
||||
imrLoading,
|
||||
imrGridRef,
|
||||
onImrPrcsOpnnExplnEdited,
|
||||
setImrCheckedIndices,
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" color="primary" onClick={handleExceptionSubmitClick}>
|
||||
예외 제출
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
닫기
|
||||
@@ -37,4 +500,3 @@ export function VerificationResultDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import { useTabStore as useGridTabStore } from "@rsone/components";
|
||||
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import {
|
||||
findMenuItemByUrlPath,
|
||||
resolveInputPageNavigationFromStatsCd,
|
||||
} from "@/shared/lib/stats-cd-input-navigation";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
type BadgeProps,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
|
||||
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
||||
NV: "gray",
|
||||
IMR: "red",
|
||||
};
|
||||
|
||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
NV: "일반",
|
||||
IMR: "I-MR",
|
||||
};
|
||||
|
||||
type VerificationSelectionReportProps = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const VerificationSelectionReport = ({
|
||||
onClose,
|
||||
}: VerificationSelectionReportProps) => {
|
||||
const menuList = useTabStore((s) => s.menuList);
|
||||
const addTab = useTabStore((s) => s.addTab);
|
||||
const setCurrentMenuInfo = useMenuStore((s) => s.setCurrentMenuInfo);
|
||||
const addTabForGrid = useGridTabStore((s) => s.addTab);
|
||||
const setPendingInputTab = useStatisticsInputFocusStore(
|
||||
(s) => s.setPendingInputTab,
|
||||
);
|
||||
const alert = useAlertStore((s) => s.setMessage);
|
||||
|
||||
const statsYr = String(dayjs().year() - 1);
|
||||
|
||||
const baseRequest = {
|
||||
statsYr,
|
||||
pbcpAprvYn: "N" as const,
|
||||
page: 1,
|
||||
size: 10000,
|
||||
prcsYnList: "0",
|
||||
};
|
||||
|
||||
const [nvQuery, imrQuery] = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
"NV",
|
||||
baseRequest.pbcpAprvYn,
|
||||
baseRequest.page,
|
||||
baseRequest.size,
|
||||
baseRequest.prcsYnList,
|
||||
],
|
||||
queryFn: () =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
...baseRequest,
|
||||
vrfcMthdCd: "NV",
|
||||
}),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
"IMR",
|
||||
baseRequest.pbcpAprvYn,
|
||||
baseRequest.page,
|
||||
baseRequest.size,
|
||||
baseRequest.prcsYnList,
|
||||
],
|
||||
queryFn: () =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
...baseRequest,
|
||||
vrfcMthdCd: "IMR",
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const isLoading = nvQuery.isLoading || imrQuery.isLoading;
|
||||
const error = nvQuery.error ?? imrQuery.error;
|
||||
|
||||
const nvList = nvQuery.data?.data?.verifyResultList ?? [];
|
||||
const imrList = imrQuery.data?.data?.verifyResultList ?? [];
|
||||
|
||||
const openInputFromStatsCd = useCallback(
|
||||
(item: VerificationResultReportItem) => {
|
||||
const nav = resolveInputPageNavigationFromStatsCd(item.statsCd);
|
||||
if (!nav) {
|
||||
alert(
|
||||
`입력 화면으로 연결되지 않는 통계코드입니다: ${item.statsCd}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!menuList) {
|
||||
alert(
|
||||
"메뉴 정보를 불러오는 중입니다. 잠시 후 다시 시도해 주세요.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const menu = findMenuItemByUrlPath(menuList, nav.urlPath);
|
||||
if (!menu) {
|
||||
alert(
|
||||
"해당 입력 메뉴를 찾을 수 없습니다. 메뉴 권한을 확인해 주세요.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
setPendingInputTab(nav.urlPath, nav.tabKey, {
|
||||
statsYr: item.statsYr,
|
||||
sewerBzmnCd: item.sewerBzmnCd,
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm ?? "",
|
||||
});
|
||||
setCurrentMenuInfo(menu);
|
||||
const ok = addTab(menu);
|
||||
if (ok) {
|
||||
addTabForGrid(menu);
|
||||
} else {
|
||||
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
}
|
||||
},
|
||||
[
|
||||
menuList,
|
||||
addTab,
|
||||
addTabForGrid,
|
||||
setCurrentMenuInfo,
|
||||
setPendingInputTab,
|
||||
alert,
|
||||
],
|
||||
);
|
||||
|
||||
const renderSection = (
|
||||
typeCode: "IMR" | "NV",
|
||||
items: VerificationResultReportItem[],
|
||||
) => (
|
||||
<section className="flex min-h-0 flex-1 flex-col rounded-sm border border-border h-[50rem]">
|
||||
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
||||
<Badge color={TYPE_CODE_COLOR_MAP[typeCode]} className="rounded-full px-2 py-1 text-lg">
|
||||
{TYPE_CODE_NAME_MAP[typeCode]} {items.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-2">
|
||||
{items.length === 0 ? (
|
||||
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
|
||||
항목이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 tracking-tight">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={`${typeCode}-${item.vrfcSn}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="flex flex-col rounded-sm border border-border p-3 cursor-pointer hover:bg-gray-100"
|
||||
onClick={() => openInputFromStatsCd(item)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
openInputFromStatsCd(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.statsNm}
|
||||
</span>
|
||||
<Badge
|
||||
color={
|
||||
TYPE_CODE_COLOR_MAP[item.vrfcMthdCd] ??
|
||||
"gray"
|
||||
}
|
||||
className="text-sm"
|
||||
>
|
||||
{TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
|
||||
item.vrfcMthdNm}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{item.rsnExpln}
|
||||
</p>
|
||||
<div className="flex justify-between text-lg text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>
|
||||
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
|
||||
{item.compVlCn ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-1/5 bg-container p-4 !pb-2 flex border-r border-border">
|
||||
<div className="card card-border flex-1 bg-background !px-0">
|
||||
<div className="border-b border-border flex justify-between">
|
||||
<h2 className="text-[2.4rem] font-semibold text-title p-4 ">검증 미통과 내역</h2>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="닫기"
|
||||
className="mt-2 mr-4 flex flex-shrink-0 cursor-pointer items-start justify-center text-3xl leading-none hover:text-[#2094fa]"
|
||||
onClick={() => onClose?.()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onClose?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="ri-close-line" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] p-2.5">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && (
|
||||
<div className="grid grid-rows-2 gap-6">
|
||||
{renderSection("IMR", imrList)}
|
||||
{renderSection("NV", nvList)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationSelectionReport;
|
||||
@@ -1,24 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import SearchForm from '@/components/module/SearchForm';
|
||||
import { DataGrid } from '@rsone/components';
|
||||
import DataGrid from '@/components/module/DataGrid';
|
||||
import SplitDataGrid from '@/components/module/SplitDataGrid';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@common/components';
|
||||
import SubmitButton from '@/components/button/SumitButton';
|
||||
import SaveButton from '@/components/button/SaveButton';
|
||||
import SubmitExceptionButton from '@/components/button/SubmitExceptionButton';
|
||||
import OperateDataButton from '@/components/button/OperateDataButton';
|
||||
import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton"
|
||||
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
|
||||
import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton';
|
||||
|
||||
import { PublicSewageFactInpColumn } from '@/components/util/column/input/PublicSewageFactInpColumn';
|
||||
import type { ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type';
|
||||
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
|
||||
|
||||
// 하수도 통계 관리 > 06. 공공하수처리시설
|
||||
const PublicSewageFacilityInp = () => {
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
console.log('submitException');
|
||||
// 예외제출 로직
|
||||
};
|
||||
|
||||
|
||||
const submitFunc = () => {
|
||||
console.log('submit');
|
||||
// 제출 로직
|
||||
@@ -28,7 +24,7 @@ const PublicSewageFacilityInp = () => {
|
||||
console.log('save');
|
||||
// 저장 로직
|
||||
};
|
||||
|
||||
|
||||
const getOperateDataFunc = () => {
|
||||
console.log('get operate data');
|
||||
// 운영 데이터 가져오기
|
||||
@@ -39,33 +35,109 @@ const PublicSewageFacilityInp = () => {
|
||||
// 운영 데이터 업데이트 로직
|
||||
};
|
||||
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isNotApplicable, setIsNotApplicable] = useState(false);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [activeTab, setActiveTab] = useState<string>(
|
||||
PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001',
|
||||
);
|
||||
|
||||
const getGridColumnInfo = (
|
||||
tab: ColumnInfoList,
|
||||
): ColumnInfo => ({
|
||||
columns: tab.columns ?? [],
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm fctName={true} />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
|
||||
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
|
||||
<OperateDataButton getOperateDataFunc={getOperateDataFunc} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />
|
||||
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
|
||||
<OperateDataButton getOperateDataFunc={getOperateDataFunc} />
|
||||
</div>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={ PublicSewageFactInpColumn }
|
||||
rowStartNum={3}
|
||||
fixedColumns={2}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
/>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
{PublicSewageFactInpColumnList.map((tab) => (
|
||||
<TabsTrigger key={tab.tabKey} value={tab.tabKey}>
|
||||
{tab.tabLabel || tab.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{PublicSewageFactInpColumnList.map((tab) => {
|
||||
if (tab.isSplitGrid && tab.left && tab.right) {
|
||||
const leftKey = tab.left.tabKey ?? `${tab.tabKey}_left`;
|
||||
const rightKey = tab.right.tabKey ?? `${tab.tabKey}_right`;
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<SplitDataGrid
|
||||
leftColumnInfo={getGridColumnInfo(tab.left)}
|
||||
rightColumnInfo={getGridColumnInfo(tab.right)}
|
||||
leftFixedColumns={tab.left.fixedColumns}
|
||||
rightFixedColumns={tab.right.fixedColumns}
|
||||
showHeaderCheckbox={false}
|
||||
enableSort={false}
|
||||
leftRowData={rowDataMap.get(leftKey) ?? []}
|
||||
rightRowData={rowDataMap.get(rightKey) ?? []}
|
||||
setLeftRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(leftKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(leftKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
setRightRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(rightKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(rightKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
}
|
||||
|
||||
const gridColumnInfo = getGridColumnInfo(tab);
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={tab.rowStartNum}
|
||||
fixedColumns={tab.fixedColumns}
|
||||
rowData={rowDataMap.get(tab.tabKey) ?? []}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tab.tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(tab.tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
showHeaderCheckbox={false}
|
||||
enableSort={false}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useRef, useEffect } from "react";
|
||||
import { useState, useMemo, useRef, useEffect, useLayoutEffect } from "react";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
@@ -8,12 +8,17 @@ import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
||||
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
||||
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
||||
|
||||
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
|
||||
import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
import {
|
||||
STATISTICS_INPUT_URL_PATH,
|
||||
verificationSearchToFormValues,
|
||||
} from "@/shared/lib/stats-cd-input-navigation";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
@@ -26,12 +31,20 @@ import type {
|
||||
SeweragePipeInputVerificationResult,
|
||||
SeweragePipeRequestParams,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
|
||||
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
import {
|
||||
type CheckInputPeriodItem,
|
||||
getVerificationPeriodActionBlockedMessage,
|
||||
isInputPeriodCurrent,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn";
|
||||
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { cancelSubmitRequest } from "@/entities/statistics/model/statistics-interface";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
/** 탭별 status + 입력기간으로 편집 가능 여부 */
|
||||
const computeTabEnterable = (
|
||||
@@ -133,6 +146,7 @@ const SewagePipeInp = () => {
|
||||
const [statsCd, setStatsCd] = useState<string>('');
|
||||
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
|
||||
/** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
/** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */
|
||||
@@ -156,6 +170,14 @@ const SewagePipeInp = () => {
|
||||
/** 검색 완료 후(API 연결 전) 탭별 지연 로그용 — 첫 탭에 행이 없어도 동작하도록 보조 */
|
||||
const searchDoneRef = useRef(false);
|
||||
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
|
||||
const postVerificationSearchRef = useRef<{
|
||||
values: SearchFormValues;
|
||||
tabKey: string;
|
||||
} | null>(null);
|
||||
|
||||
const [verificationSearchDefaults, setVerificationSearchDefaults] =
|
||||
useState<SearchFormValues | null>(null);
|
||||
const [searchFormRemountKey, setSearchFormRemountKey] = useState(0);
|
||||
|
||||
const firstTabKey = SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001";
|
||||
const activeTabRef = useRef<string>(firstTabKey);
|
||||
@@ -164,6 +186,25 @@ const SewagePipeInp = () => {
|
||||
activeTabRef.current = activeTab;
|
||||
}, [activeTab]);
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
|
||||
useLayoutEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
|
||||
const pending = useStatisticsInputFocusStore
|
||||
.getState()
|
||||
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE);
|
||||
if (!pending) return;
|
||||
const { tabKey, search } = pending;
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
if (search) {
|
||||
const values = verificationSearchToFormValues(search);
|
||||
setVerificationSearchDefaults(values);
|
||||
postVerificationSearchRef.current = { values, tabKey };
|
||||
setSearchFormRemountKey((k) => k + 1);
|
||||
}
|
||||
}, [activeTabId, inputFocusNonce]);
|
||||
|
||||
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
|
||||
if (loadedTabKey === activeTabRef.current) {
|
||||
setStatsCd(nextStatsCd);
|
||||
@@ -186,13 +227,15 @@ const SewagePipeInp = () => {
|
||||
activeTabRef.current = firstTabKey;
|
||||
setActiveTab(firstTabKey);
|
||||
setStatsCd("");
|
||||
setPeriodCheckList([]);
|
||||
setVerificationSearchDefaults(null);
|
||||
setSearchFormRemountKey((k) => k + 1);
|
||||
};
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
|
||||
const canEdit =
|
||||
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) ||
|
||||
isAdminInfo;
|
||||
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo;
|
||||
const applyEditable = (cols: ColumnConfig[] | undefined) =>
|
||||
(cols ?? []).map((col: ColumnConfig) =>
|
||||
canEdit
|
||||
@@ -416,7 +459,10 @@ const SewagePipeInp = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
const handleSearch = async (
|
||||
values: SearchFormValues,
|
||||
options?: { forcedTabKey?: string },
|
||||
) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
@@ -430,7 +476,9 @@ const SewagePipeInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const periodOk = isInputPeriodCurrent(periodResponse.data);
|
||||
const periods = Array.isArray(periodResponse.data) ? periodResponse.data : [];
|
||||
setPeriodCheckList(periods);
|
||||
const periodOk = isInputPeriodCurrent(periods);
|
||||
periodEnterableRef.current = periodOk;
|
||||
setInputPeriodEnterable(periodOk);
|
||||
|
||||
@@ -456,9 +504,17 @@ const SewagePipeInp = () => {
|
||||
setHasSearched(true);
|
||||
setLastSearchedYear(values.year ?? null);
|
||||
|
||||
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTab;
|
||||
const forcedTab = options?.forcedTabKey;
|
||||
const tabKeyToLoad = forcedTab
|
||||
? forcedTab
|
||||
: yearChangedFromLastSearch
|
||||
? firstTabKey
|
||||
: activeTab;
|
||||
|
||||
if (yearChangedFromLastSearch) {
|
||||
if (forcedTab) {
|
||||
activeTabRef.current = forcedTab;
|
||||
setActiveTab(forcedTab);
|
||||
} else if (yearChangedFromLastSearch) {
|
||||
activeTabRef.current = firstTabKey;
|
||||
setActiveTab(firstTabKey);
|
||||
} else {
|
||||
@@ -472,6 +528,14 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const job = postVerificationSearchRef.current;
|
||||
if (!job) return;
|
||||
postVerificationSearchRef.current = null;
|
||||
void handleSearch(job.values, { forcedTabKey: job.tabKey });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
@@ -524,10 +588,53 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
|
||||
const submitFunc = () => {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
console.log('submit');
|
||||
// 제출 로직
|
||||
};
|
||||
|
||||
const cnacelSubmitFunc = async () => {
|
||||
try {
|
||||
if (await confirm('제출된 내용을 취소하시겠습니까?')) {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
const requestData: cancelSubmitRequest = {
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
||||
ctpvNm: searchValues?.sido ?? '',
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
statsYr: searchValues?.year ?? '',
|
||||
mdfrId: userInfo.userUnqId,
|
||||
statsCd: statsCd,
|
||||
};
|
||||
|
||||
const response = await statisticsApi.cancelSubmit(requestData);
|
||||
|
||||
if (response.status === 'success') {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (values) {
|
||||
const refreshedParam = buildSewagePipeSearchParam(values);
|
||||
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
|
||||
}
|
||||
alert('제출 취소되었습니다.');
|
||||
} else {
|
||||
alert(response.message ?? '제출 취소 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
alert('제출 취소 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const refreshConnectionStatusAfterPopupSave = async () => {
|
||||
const searchParam = searchParamRef.current;
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
@@ -545,6 +652,11 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
|
||||
const saveFunc = async () => {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
if (!(await confirm("저장 하시겠습니까?"))) return;
|
||||
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
@@ -743,10 +855,11 @@ const SewagePipeInp = () => {
|
||||
const gugunOkOrNotDoList = buildGugunOkForToolbar(lastSearchedValues);
|
||||
|
||||
const searchContextForToolbar = lastSearchedValues ?? searchValues;
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
const activeTabToolbar =
|
||||
toolbarSnapshotByTab.get(activeTab) ??
|
||||
computeTabToolbarSnapshot(
|
||||
tabStatusInfoMap.get(activeTab) ?? null,
|
||||
activeTabStatus,
|
||||
tabRowDataExists(rowDataMap, activeTab),
|
||||
hasSearched,
|
||||
searchContextForToolbar,
|
||||
@@ -754,15 +867,26 @@ const SewagePipeInp = () => {
|
||||
isAdminInfo,
|
||||
);
|
||||
|
||||
const hideToolbarForConnectionTab = activeTab === "ST00020006";
|
||||
const {
|
||||
showSaveInToolbar,
|
||||
showEnterableToolbarButtons,
|
||||
showActionToolbarCard,
|
||||
showSaveInToolbar: baseShowSaveInToolbar,
|
||||
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
|
||||
showActionToolbarCard: baseShowActionToolbarCard,
|
||||
} = activeTabToolbar;
|
||||
const showSaveInToolbar = !hideToolbarForConnectionTab && baseShowSaveInToolbar;
|
||||
const showEnterableToolbarButtons =
|
||||
!hideToolbarForConnectionTab && baseShowEnterableToolbarButtons;
|
||||
const showActionToolbarCard = !hideToolbarForConnectionTab && baseShowActionToolbarCard;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
|
||||
<SearchForm
|
||||
key={searchFormRemountKey}
|
||||
defaultValues={verificationSearchDefaults ?? undefined}
|
||||
onSubmit={handleSearch}
|
||||
onReset={handleReset}
|
||||
onValuesChange={setSearchValues}
|
||||
/>
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
@@ -775,6 +899,9 @@ const SewagePipeInp = () => {
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
)}
|
||||
{activeTabStatus?.laeSbmsnSttsCd === "2" && (
|
||||
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
|
||||
)}
|
||||
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
STATISTICS_INPUT_URL_PATH,
|
||||
verificationSearchToFormValues,
|
||||
} from "@/shared/lib/stats-cd-input-navigation";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { sewagePipeMaintainApiService } from "@/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service";
|
||||
import type { SewagePipeMaintainRequestParams } from "@/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface";
|
||||
@@ -16,6 +20,8 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
|
||||
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
function buildSewagePipeMaintainSearchParam(
|
||||
values: SearchFormValues,
|
||||
@@ -40,6 +46,13 @@ const SewagePipeMaintainInp = () => {
|
||||
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const searchParamRef = useRef<SewagePipeMaintainRequestParams | null>(null);
|
||||
const postVerificationSearchRef = useRef<{
|
||||
values: SearchFormValues;
|
||||
tabKey: string;
|
||||
} | null>(null);
|
||||
const [verificationSearchDefaults, setVerificationSearchDefaults] =
|
||||
useState<SearchFormValues | null>(null);
|
||||
const [searchFormRemountKey, setSearchFormRemountKey] = useState(0);
|
||||
|
||||
// tab List
|
||||
const SewagePipeMaintainInpTabList = [
|
||||
@@ -47,6 +60,24 @@ const SewagePipeMaintainInp = () => {
|
||||
PipeDredgingStatusInpColumn
|
||||
];
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
|
||||
useLayoutEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
|
||||
const pending = useStatisticsInputFocusStore
|
||||
.getState()
|
||||
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN);
|
||||
if (!pending) return;
|
||||
const { tabKey, search } = pending;
|
||||
setActiveTab(tabKey);
|
||||
if (search) {
|
||||
const values = verificationSearchToFormValues(search);
|
||||
setVerificationSearchDefaults(values);
|
||||
postVerificationSearchRef.current = { values, tabKey };
|
||||
setSearchFormRemountKey((k) => k + 1);
|
||||
}
|
||||
}, [activeTabId, inputFocusNonce]);
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
console.log('submit exception');
|
||||
// 예외 제출 로직
|
||||
@@ -136,9 +167,14 @@ const SewagePipeMaintainInp = () => {
|
||||
setLastSearchedYear(null);
|
||||
setHasSearched(false);
|
||||
searchParamRef.current = null;
|
||||
setVerificationSearchDefaults(null);
|
||||
setSearchFormRemountKey((k) => k + 1);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
const handleSearch = async (
|
||||
values: SearchFormValues,
|
||||
opts?: { forcedTabKey?: string },
|
||||
) => {
|
||||
if (!values.sido || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
@@ -151,13 +187,18 @@ const SewagePipeMaintainInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabForLoad = opts?.forcedTabKey ?? activeTab;
|
||||
if (opts?.forcedTabKey) {
|
||||
setActiveTab(opts.forcedTabKey);
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const searchParam = buildSewagePipeMaintainSearchParam(values);
|
||||
searchParamRef.current = searchParam;
|
||||
setLastSearchedValues(values);
|
||||
setLastSearchedYear(values.year ?? null);
|
||||
setHasSearched(true);
|
||||
await loadActiveTabData(activeTab, searchParam, false);
|
||||
await loadActiveTabData(tabForLoad, searchParam, false);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
@@ -165,6 +206,14 @@ const SewagePipeMaintainInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const job = postVerificationSearchRef.current;
|
||||
if (!job) return;
|
||||
postVerificationSearchRef.current = null;
|
||||
void handleSearch(job.values, { forcedTabKey: job.tabKey });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
const hasRows = (rowDataMap.get(tabKey) ?? []).length > 0;
|
||||
@@ -183,7 +232,13 @@ const SewagePipeMaintainInp = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
|
||||
<SearchForm
|
||||
key={searchFormRemountKey}
|
||||
defaultValues={verificationSearchDefaults ?? undefined}
|
||||
onSubmit={handleSearch}
|
||||
onReset={handleReset}
|
||||
onValuesChange={setSearchValues}
|
||||
/>
|
||||
{(hasRowData || hasSearched) && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { useState, useMemo, useRef, useLayoutEffect, useEffect } from "react";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
// import { DataGrid } from "@rsone/components";
|
||||
@@ -15,6 +15,10 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import SubmitDailog from "./SubmitDailog";
|
||||
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
import {
|
||||
STATISTICS_INPUT_URL_PATH,
|
||||
verificationSearchToFormValues,
|
||||
} from "@/shared/lib/stats-cd-input-navigation";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
@@ -43,6 +47,8 @@ import type {
|
||||
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
@@ -118,6 +124,27 @@ const SeweragePenetrationRateInp = () => {
|
||||
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
||||
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
||||
|
||||
const postVerificationSearchRef = useRef<SearchFormValues | null>(null);
|
||||
const [verificationSearchDefaults, setVerificationSearchDefaults] =
|
||||
useState<SearchFormValues | null>(null);
|
||||
const [searchFormRemountKey, setSearchFormRemountKey] = useState(0);
|
||||
|
||||
const activeTabId = useTabStore((s) => s.activeTabId);
|
||||
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
|
||||
useLayoutEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) {
|
||||
return;
|
||||
}
|
||||
const pending = useStatisticsInputFocusStore
|
||||
.getState()
|
||||
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE);
|
||||
if (!pending?.search) return;
|
||||
const values = verificationSearchToFormValues(pending.search);
|
||||
setVerificationSearchDefaults(values);
|
||||
postVerificationSearchRef.current = values;
|
||||
setSearchFormRemountKey((k) => k + 1);
|
||||
}, [activeTabId, inputFocusNonce]);
|
||||
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
@@ -156,6 +183,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
setLastSearchedYear(null);
|
||||
setLastSearchedValues(null);
|
||||
setFooterFromApi(null);
|
||||
setVerificationSearchDefaults(null);
|
||||
setSearchFormRemountKey((k) => k + 1);
|
||||
};
|
||||
|
||||
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => {
|
||||
@@ -238,6 +267,14 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const values = postVerificationSearchRef.current;
|
||||
if (!values) return;
|
||||
postVerificationSearchRef.current = null;
|
||||
void handleSearch(values);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
@@ -623,7 +660,13 @@ const SeweragePenetrationRateInp = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
|
||||
<SearchForm
|
||||
key={searchFormRemountKey}
|
||||
defaultValues={verificationSearchDefaults ?? undefined}
|
||||
onSubmit={handleSearch}
|
||||
onReset={handleReset}
|
||||
onValuesChange={setSearchValues}
|
||||
/>
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
@@ -713,6 +756,20 @@ const SeweragePenetrationRateInp = () => {
|
||||
<VerificationResultDialog
|
||||
open={verificationResultDialogOpen}
|
||||
onOpenChange={setVerificationResultDialogOpen}
|
||||
queryContext={
|
||||
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
|
||||
? {
|
||||
sewerBzmnCd: searchValues.bsnmCd,
|
||||
ctpvNm: searchValues.sido,
|
||||
sggNm:
|
||||
searchValues.gugun === "ALL"
|
||||
? ""
|
||||
: (searchValues.gugun ?? ""),
|
||||
statsYr: searchValues.year,
|
||||
statsCd,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -175,12 +175,12 @@ const SubmitDailog = ({
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DialogFooter className="mt-6">
|
||||
<Button type="button" color="gray" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" color="primary">
|
||||
{submitMode === 'normal' ? '제출' : '예외 제출'}
|
||||
</Button>
|
||||
<Button type="button" color="gray" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* statistics/input 화면과 탭(·분할 그리드) 키 매칭용 상수.
|
||||
* 각 컴포넌트에 실제로 쓰이는 `tabKey`를 모두 포함합니다.
|
||||
*/
|
||||
|
||||
/** `SewagePipeInp.tsx` — 메인 탭 + `ConnectionStatusInpColumn` 좌·우 그리드 서브 키 */
|
||||
export const SEWAGE_PIPE_INP_TAB_KEYS = [
|
||||
"ST00020001",
|
||||
"ST00020002",
|
||||
"ST00020003",
|
||||
"ST00020004",
|
||||
"ST00020005",
|
||||
"ST00020006",
|
||||
"treatmentPlantConnectionInp",
|
||||
"adminAreaConnectionInp",
|
||||
] as const;
|
||||
|
||||
/** `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;
|
||||
|
||||
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 const STATISTICS_INPUT_TAB_KEYS_BY_SCREEN = {
|
||||
SewagePipeInp: SEWAGE_PIPE_INP_TAB_KEYS,
|
||||
SewagePipeMaintainInp: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
|
||||
SeweragePenetrationRateInp: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
|
||||
} as const;
|
||||
@@ -1,211 +0,0 @@
|
||||
import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
/**
|
||||
* 하수도 통계 관리 > 06. 공공하수처리시설
|
||||
* 단일 Tab
|
||||
*/
|
||||
export const PublicSewageFactInpColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'sidoCd', fieldName: '시도', header: { text: '시도' }, editable: false },
|
||||
{ name: 'gugun', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } },
|
||||
|
||||
// 시설명
|
||||
{ name: 'bscFctNm', fieldName: '시설명(500m³/일 이상/미만)', header: { text: '시설명(500m³/일 이상/미만)' }, editable: false },
|
||||
|
||||
// 소재지
|
||||
{ name: 'addr', fieldName: '소재지', header: { text: '소재지' }, editable: false },
|
||||
|
||||
// 시설용량
|
||||
{ name: 'fctCpc', fieldName: '시설용량(500m³/일 이상/미만)', header: { text: '시설용량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// (m³/일)
|
||||
{ name: 'fctCpc1', fieldName: '물리적', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'fctCpc2', fieldName: '생물학적', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'fctCpc3', fieldName: '고도', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 유입하수량
|
||||
{ name: 'totInflAmntAve', fieldName: '유입하수량(500m³/일 이상/미만)', header: { text: '유입하수량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// (m³/일)
|
||||
{ name: 'pInflAmntAve', fieldName: '물리적', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'oInflAmntAve', fieldName: '생물학적', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'aInflAmntAve', fieldName: '고도', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 방류량
|
||||
{ name: 'totTputAve', fieldName: '방류량(500m³/일 이상/미만)', header: { text: '방류량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 방류량 - (m³/일)
|
||||
{ name: 'pTputAve', fieldName: '물리적', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'oTputAve', fieldName: '생물학적', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'aTputAve', fieldName: '고도', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 수질
|
||||
{ name: 'inflAssctBfrWqltBod', fieldName: '유입BOD', header: { text: '유입BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'efflWqltBod', fieldName: '방류BOD', header: { text: '방류BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflAssctBfrWqltToc', fieldName: '유입TOC', header: { text: '유입TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'efflWqltToc', fieldName: '방류TOC', header: { text: '방류TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflAssctBfrWqltSs', fieldName: '유입SS', header: { text: '유입SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'efflWqltSs', fieldName: '방류SS', header: { text: '방류SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflAssctBfrWqltTn', fieldName: '유입T-N', header: { text: '유입T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'efflWqltTn', fieldName: '방류T-N', header: { text: '방류T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'inflAssctBfrWqltTp', fieldName: '유입T-P', header: { text: '유입T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'efflWqltTp', fieldName: '방류T-P', header: { text: '방류T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'inflAssctBfrWqltClfgrpcnt', fieldName: '유입총대장균군수', header: { text: '유입총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'efflWqltClfgrpcnt', fieldName: '방류총대장균군수', header: { text: '방류총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 처리특성
|
||||
{ name: 'tmEfficiency', fieldName: '처리효율(%)', header: { text: '처리효율(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pcLoad', fieldName: '처리부하량(kgBOD/D)', header: { text: '처리부하량(kgBOD/D)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'trtFrfHanNm', fieldName: '처리방법', header: { text: '처리방법' }, editable: false },
|
||||
|
||||
// 연계처리량
|
||||
{ name: 'excretionsTot', fieldName: '분뇨', header: { text: '분뇨' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'stkrsngWstwtrTot', fieldName: '축산', header: { text: '축산' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'lchtTot', fieldName: '침출수', header: { text: '침출수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'etcTot', fieldName: '기타', header: { text: '기타' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 시설정보
|
||||
{ name: 'tel', fieldName: '전화번호', header: { text: '전화번호' }, editable: false },
|
||||
{ name: 'rnDt', fieldName: '준공일', header: { text: '준공일' }, editable: false },
|
||||
{ name: 'compDt', fieldName: '가동개시일', header: { text: '가동개시일' }, editable: false },
|
||||
{ name: 'tot', fieldName: '사업비', header: { text: '사업비' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 방류수 소독방법
|
||||
{ name: 'dsfctNm', fieldName: '방류수 소독방법', header: { text: '방류수 소독방법' }, editable: false },
|
||||
|
||||
// 방류수역
|
||||
{ name: 'wbsnNm', fieldName: '수계', header: { text: '수계' }, editable: false },
|
||||
{ name: 'rvNm', fieldName: '지류', header: { text: '지류' }, editable: false },
|
||||
{ name: 'mdmbsn', fieldName: '세부단위구역', header: { text: '세부단위구역' }, editable: false },
|
||||
{ name: 'efflWqltStndrdNm', fieldName: '지역구분', header: { text: '지역구분' }, editable: false },
|
||||
|
||||
// 방류수역 > 중권역
|
||||
{ name: 'lgbsn', fieldName: '명칭', header: { text: '명칭' }, editable: false },
|
||||
{ name: 'lgbsnBod', fieldName: '목표수질(BOD)', header: { text: '목표수질(BOD)' }, editable: false },
|
||||
|
||||
// 에너지 자립률
|
||||
{ name: 'toeRt', fieldName: '에너지자립률(%)', header: { text: '에너지자립률(%)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 에너지 자체 생산량
|
||||
{ name: 'useQnty', fieldName: '에너지자체생산량(TOE)', header: { text: '에너지자체생산량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 연간 총 에너지 사용량
|
||||
{ name: 'toeSum', fieldName: '계(TOE)', header: { text: '계(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'elcpwAmnt', fieldName: '총 전력사용량(kWh)', header: { text: '총 전력사용량(kWh)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'toeQnty', fieldName: '자체 에너지사용량(TOE)', header: { text: '자체 에너지사용량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 하수처리량당 CO2배출량
|
||||
{ name: 'co2Emissions', fieldName: '하수처리량당 CO2배출량(kgCO2/m³)', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 운영주체
|
||||
{ name: 'oprClsNm', fieldName: '운영주체(자체/공기업/민간대행)', header: { text: '운영주체(자체/공기업/민간대행)' }, editable: false },
|
||||
|
||||
// 위탁현황
|
||||
{ name: 'trstOprEtpNm', fieldName: '위탁업체명', header: { text: '위탁업체명' } },
|
||||
{ name: 'yrstpTrstCst', fieldName: '위탁비용(백만원/년)', header: { text: '위탁비용(백만원/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'trstOprDt', fieldName: '위탁계약기간(yyyy-mm-dd~yyyy-mm-dd)', header: { text: '위탁계약기간(yyyy-mm-dd~yyyy-mm-dd)' } },
|
||||
|
||||
// 기타
|
||||
{ name: 'cntrTrtFctNm', fieldName: '중심처리시설', header: { text: '중심처리시설' } },
|
||||
{ name: 'totWtrQrm', fieldName: '직원총수(명)', header: { text: '직원총수(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'safeAcdntCo', fieldName: '안전사고건수', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'administrative',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: ['sidoCd', 'gugun'],
|
||||
},
|
||||
'bscFctNm',
|
||||
'addr',
|
||||
'fctCpc',
|
||||
{
|
||||
name: 'facilityCapacityDetail',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '(m³/일)' },
|
||||
items: ['fctCpc1', 'fctCpc2', 'fctCpc3'],
|
||||
},
|
||||
'totInflAmntAve',
|
||||
{
|
||||
name: 'influentWaterDetail',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '(m³/일)' },
|
||||
items: ['pInflAmntAve', 'oInflAmntAve', 'aInflAmntAve'],
|
||||
},
|
||||
'totTputAve',
|
||||
{
|
||||
name: 'effluentWaterDetail',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '(m³/일)' },
|
||||
items: ['pTputAve', 'oTputAve', 'aTputAve'],
|
||||
},
|
||||
'inflAssctBfrWqltBod',
|
||||
'efflWqltBod',
|
||||
'inflAssctBfrWqltToc',
|
||||
'efflWqltToc',
|
||||
'inflAssctBfrWqltSs',
|
||||
'efflWqltSs',
|
||||
'inflAssctBfrWqltTn',
|
||||
'efflWqltTn',
|
||||
'inflAssctBfrWqltTp',
|
||||
'efflWqltTp',
|
||||
'inflAssctBfrWqltClfgrpcnt',
|
||||
'efflWqltClfgrpcnt',
|
||||
'tmEfficiency',
|
||||
'pcLoad',
|
||||
'trtFrfHanNm',
|
||||
{
|
||||
name: 'linkedTreatment',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '연계처리량(m³/일) (500m³/일 이상/미만)' },
|
||||
items: ['excretionsTot', 'stkrsngWstwtrTot', 'lchtTot', 'etcTot'],
|
||||
},
|
||||
'tel',
|
||||
'rnDt',
|
||||
'compDt',
|
||||
'tot',
|
||||
'dsfctNm',
|
||||
{
|
||||
name: 'dischargeArea',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '방류수역' },
|
||||
items: [
|
||||
'wbsnNm',
|
||||
'rvNm',
|
||||
'mdmbsn',
|
||||
'efflWqltStndrdNm',
|
||||
{
|
||||
name: 'mediumWatershed',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '중권역' },
|
||||
items: ['lgbsn', 'lgbsnBod'],
|
||||
},
|
||||
],
|
||||
},
|
||||
'toeRt',
|
||||
'useQnty',
|
||||
{
|
||||
name: 'energy',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '연간 총 에너지 사용량(TOE)' },
|
||||
items: ['toeSum', 'elcpwAmnt', 'toeQnty'],
|
||||
},
|
||||
'co2Emissions',
|
||||
'oprClsNm',
|
||||
{
|
||||
name: 'consignmentStatus',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '위탁현황' },
|
||||
items: ['trstOprEtpNm', 'yrstpTrstCst', 'trstOprDt'],
|
||||
},
|
||||
'cntrTrtFctNm',
|
||||
'totWtrQrm',
|
||||
'safeAcdntCo',
|
||||
'rmk',
|
||||
]
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
export const MainFacilityColumn = {
|
||||
columns: [
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명' }, editable: false },
|
||||
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false },
|
||||
{ name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' }, editable: false },
|
||||
{ name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, editable: false },
|
||||
{ name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' }, editable: false },
|
||||
{ name: 'isMainYn', fieldName: 'isMainYn', header: { text: '중심처리시설여부' }, editable: false },
|
||||
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
],
|
||||
columnLayout: [
|
||||
'swtrmtFcltNm',
|
||||
'fcltPstnCn',
|
||||
'prxyBzentyNmCn',
|
||||
'cnsgnCstVl',
|
||||
'prxyCtrtCn',
|
||||
'isMainYn',
|
||||
'admdstCd',
|
||||
'fcltSn',
|
||||
'swtrmtFcltCd',
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
],
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 1,
|
||||
tabKey: 'MainFacility',
|
||||
tabLabel: '지자체별공공하수처리시설'
|
||||
};
|
||||
|
||||
// Tab 6-2: 하수처리장별 연결 현황 (왼쪽 그리드)
|
||||
export const MainFacilityTargetColumn = {
|
||||
columns: [
|
||||
// 하수처리장 정보
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명' }, editable: false },
|
||||
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false },
|
||||
{ name: 'mappedYn', fieldName: 'mappedYn', header: { text: '연결여부' }, editable: false },
|
||||
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'mainFcltSn', fieldName: 'mainFcltSn', header: { text: '연결된중심시설일련번호' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'isMainYn', fieldName: 'isMainYn', header: { text: '중심처리시설여부' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
],
|
||||
columnLayout: [
|
||||
'swtrmtFcltNm',
|
||||
'fcltPstnCn',
|
||||
'mappedYn',
|
||||
'fcltSn',
|
||||
'swtrmtFcltCd',
|
||||
'admdstNm',
|
||||
'mainFcltSn',
|
||||
'isMainYn',
|
||||
],
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 1,
|
||||
tabKey: 'MainFacilityTarget',
|
||||
tabLabel: '연결대상시설'
|
||||
};
|
||||
|
||||
export const MainFacilityInpColumn = {
|
||||
tabKey: 'ST00060002',
|
||||
tabLabel: '중심 처리 시설',
|
||||
isSplitGrid: true,
|
||||
left: MainFacilityColumn,
|
||||
right: MainFacilityTargetColumn,
|
||||
};
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
/**
|
||||
* 하수도 통계 관리 > 06. 공공하수처리시설
|
||||
*/
|
||||
export const PublicSewageFactColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, footer: { text: '합계' }, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 시설명
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명(500m³/일 이상/미만)' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 소재지
|
||||
{ name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', 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' },
|
||||
|
||||
// (m³/일)
|
||||
{ name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 유입하수량
|
||||
{ name: 'inflwSwageQnt', fieldName: 'inflwSwageQnt', header: { text: '유입하수량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// (m³/일)
|
||||
{ name: 'inflwSwagePhysQnt', fieldName: 'inflwSwagePhysQnt', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflwSwageBoilQnt', fieldName: 'inflwSwageBoilQnt', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'inflwSwageAltdQnt', fieldName: 'inflwSwageAltdQnt', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 방류량
|
||||
{ name: 'prcsQnt', fieldName: 'prcsQnt', header: { text: '방류량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 방류량 - (m³/일)
|
||||
{ name: 'prcsPhysCpct', fieldName: 'prcsPhysCpct', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'prcsBoilCpct', fieldName: 'prcsBoilCpct', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'prcsAltdCpct', fieldName: 'prcsAltdCpct', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 수질
|
||||
{ name: 'inflwBodVl', fieldName: 'inflwBodVl', header: { text: '유입BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'dschgBodVl', fieldName: 'dschgBodVl', header: { text: '방류BOD' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'inflwTocQnt', fieldName: 'inflwTocQnt', header: { text: '유입TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'dschgTocQnt', fieldName: 'dschgTocQnt', header: { text: '방류TOC' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'inflwSsQnt', fieldName: 'inflwSsQnt', header: { text: '유입SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'dschgSsQnt', fieldName: 'dschgSsQnt', header: { text: '방류SS' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'inflwTnVl', fieldName: 'inflwTnVl', header: { text: '유입T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'dschgTnVl', fieldName: 'dschgTnVl', header: { text: '방류T-N' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'inflwTpVl', fieldName: 'inflwTpVl', header: { text: '유입T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'dschgTpVl', fieldName: 'dschgTpVl', header: { text: '방류T-P' }, editable: false, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'inflwGrssClfgVl', fieldName: 'inflwGrssClfgVl', header: { text: '유입총대장균군수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ 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: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리방법' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 연계처리량
|
||||
{ name: 'wholHmnQnt', fieldName: 'wholHmnQnt', header: { text: '분뇨' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'wholLvstQnt', fieldName: 'wholLvstQnt', header: { text: '축산' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'wholEfntQnt', fieldName: 'wholEfntQnt', header: { text: '침출수' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'wholEtcQnt', fieldName: 'wholEtcQnt', header: { text: '기타' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 시설정보
|
||||
{ name: 'telNo', fieldName: 'telNo', header: { text: '전화번호' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'cmptnStrtYmd', fieldName: 'cmptnStrtYmd', header: { text: '준공일' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시일' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'wholBizVl', fieldName: 'wholBizVl', header: { text: '사업비' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 방류수 소독방법
|
||||
{ name: 'eflntDsnfMthdExpln', fieldName: 'eflntDsnfMthdExpln', header: { text: '방류수 소독방법' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 방류수역
|
||||
{ name: 'wtsysNm', fieldName: 'wtsysNm', header: { text: '수계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'dtlUnitZoneCn', fieldName: 'dtlUnitZoneCn', header: { text: '세부단위구역' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'rgnSeNm', fieldName: 'rgnSeNm', header: { text: '지역구분' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 방류수역 > 중권역
|
||||
{ name: 'msirbCn', fieldName: 'msirbCn', header: { text: '명칭' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ 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: 'engyItslfPrdctnQnt', fieldName: 'engyItslfPrdctnQnt', header: { text: '에너지자체생산량(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 연간 총 에너지 사용량
|
||||
{ name: 'anlEngyWholUseQnt', fieldName: 'anlEngyWholUseQnt', header: { text: '계(TOE)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '총 전력사용량(kWh)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ 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: 'operMnbdCn', fieldName: 'operMnbdCn', header: { text: '운영주체(자체/공기업/민간대행)' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 위탁현황
|
||||
{ name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' } },
|
||||
|
||||
// 기타
|
||||
{ name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수(명)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'administrative',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: ['ctpvNm', 'sggNm'],
|
||||
},
|
||||
'swtrmtFcltNm',
|
||||
'impdFcltPstnCn',
|
||||
'prcsFcltCpct',
|
||||
{
|
||||
name: 'facilityCapacityDetail',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '(m³/일)' },
|
||||
items: ['prcsFcltPhysCpct', 'prcsFcltBoilCpct', 'prcsFcltAltdCpct'],
|
||||
},
|
||||
'inflwSwageQnt',
|
||||
{
|
||||
name: 'influentWaterDetail',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '(m³/일)' },
|
||||
items: ['inflwSwagePhysQnt', 'inflwSwageBoilQnt', 'inflwSwageAltdQnt'],
|
||||
},
|
||||
'prcsQnt',
|
||||
{
|
||||
name: 'effluentWaterDetail',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '(m³/일)' },
|
||||
items: ['prcsPhysCpct', 'prcsBoilCpct', 'prcsAltdCpct'],
|
||||
},
|
||||
'inflwBodVl',
|
||||
'dschgBodVl',
|
||||
'inflwTocQnt',
|
||||
'dschgTocQnt',
|
||||
'inflwSsQnt',
|
||||
'dschgSsQnt',
|
||||
'inflwTnVl',
|
||||
'dschgTnVl',
|
||||
'inflwTpVl',
|
||||
'dschgTpVl',
|
||||
'inflwGrssClfgVl',
|
||||
'dschgGrssClfgVl',
|
||||
'prcsRt',
|
||||
'prcsLodQnt',
|
||||
'prcsMthdExpln',
|
||||
{
|
||||
name: 'linkedTreatment',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '연계처리량(m³/일) (500m³/일 이상/미만)' },
|
||||
items: ['wholHmnQnt', 'wholLvstQnt', 'wholEfntQnt', 'wholEtcQnt'],
|
||||
},
|
||||
'telNo',
|
||||
'cmptnStrtYmd',
|
||||
'oprtngStrtYmd',
|
||||
'wholBizVl',
|
||||
'eflntDsnfMthdExpln',
|
||||
{
|
||||
name: 'dischargeArea',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '방류수역' },
|
||||
items: [
|
||||
'wtsysNm',
|
||||
'trbtrNm',
|
||||
'dtlUnitZoneCn',
|
||||
'rgnSeNm',
|
||||
{
|
||||
name: 'mediumWatershed',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '중권역' },
|
||||
items: ['msirbCn', 'goalWqNo'],
|
||||
},
|
||||
],
|
||||
},
|
||||
'engySrlneRt',
|
||||
'engyItslfPrdctnQnt',
|
||||
{
|
||||
name: 'energy',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '연간 총 에너지 사용량(TOE)' },
|
||||
items: ['anlEngyWholUseQnt', 'anlElpwrUseQnt', 'itslfEngyUseQnt'],
|
||||
},
|
||||
'swtrmtEmsnQnt',
|
||||
'operMnbdCn',
|
||||
{
|
||||
name: 'consignmentStatus',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '위탁현황' },
|
||||
items: ['prxyBzentyNmCn', 'cnsgnCstVl', 'prxyCtrtCn'],
|
||||
},
|
||||
'intgPrcsFcltCn',
|
||||
'wholNope',
|
||||
'sftyAcdntEvntcsCnt',
|
||||
'rmk',
|
||||
'admdstCd',
|
||||
'fcltSn',
|
||||
'swtrmtFcltCd',
|
||||
],
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 2,
|
||||
tabKey: 'ST00060001',
|
||||
tabLabel: '공공 하수처리 시설'
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import { MainFacilityInpColumn } from "./MainFacilityInpColumn";
|
||||
import { PublicSewageFactColumn } from "./PublicSewageFactColumn";
|
||||
|
||||
export const PublicSewageFactInpColumnList: ColumnInfoList[] = [
|
||||
PublicSewageFactColumn,
|
||||
MainFacilityInpColumn,
|
||||
];
|
||||
@@ -41,11 +41,14 @@ export function useDashboardUnSubmitCount(
|
||||
params: DashboardSearchParams | null,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: createDashboardQueryKey("un-submit-count", params),
|
||||
queryKey: createDashboardQueryKey("submission-status", params),
|
||||
queryFn: async () => {
|
||||
if (!params)
|
||||
throw new Error("Dashboard search params are required.");
|
||||
const response = await dashboardApi.getUnSubmitCount(params);
|
||||
const response = await dashboardApi.getUnSubmitCount({
|
||||
...params,
|
||||
unSubmitOnly: true,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
enabled: hasRequiredDashboardParams(params),
|
||||
|
||||
@@ -35,7 +35,7 @@ async function getUnSubmitCount(
|
||||
): Promise<DashboardApiResponse<UnSubmitCountResponse>> {
|
||||
const response = await stClient.get<
|
||||
DashboardApiResponse<UnSubmitCountResponse>
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/un-submit-count`, { params });
|
||||
>(`${ENDPOINT.ST_SERVICE.DASHBOARD}/submission-status`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
type SubmitExceptionResponse,
|
||||
type ImrResponse,
|
||||
type ImrStatusResponse,
|
||||
type SubmitVerifyExceptionRequest,
|
||||
type SubmitVerifyExceptionResponse,
|
||||
} from "../model/statistics-interface";
|
||||
|
||||
export class StatisticsApiService implements StatisticsService {
|
||||
@@ -164,6 +166,14 @@ export class StatisticsApiService implements StatisticsService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise<SubmitVerifyExceptionResponse> {
|
||||
const response = await stClient.put<SubmitVerifyExceptionResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results/exception`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsApi = new StatisticsApiService();
|
||||
|
||||
@@ -3,6 +3,10 @@ export interface DashboardSearchParams {
|
||||
dtlInstCd: string;
|
||||
enoCd?: string;
|
||||
laeSeCd?: string;
|
||||
sewerBzmnCd?: string;
|
||||
sido?: string;
|
||||
sggNm?: string;
|
||||
unSubmitOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardApiResponse<T> {
|
||||
@@ -34,13 +38,19 @@ export interface VrfcErrCountResponse {
|
||||
|
||||
export interface UnSubmitCountResponse {
|
||||
statsYr: string;
|
||||
unSubmitCount: number;
|
||||
unSubmitList: UnSubmitStatsResponse[];
|
||||
statusList: UnSubmitStatsResponse[];
|
||||
submittedCount: number;
|
||||
totalCount: number;
|
||||
unSubmittedCount: number;
|
||||
}
|
||||
|
||||
export interface UnSubmitStatsResponse {
|
||||
sbmsnYn: string;
|
||||
sidoSggNm: string;
|
||||
statsCd: string;
|
||||
statsNm: string;
|
||||
statusCd: string;
|
||||
statusNm: string;
|
||||
}
|
||||
|
||||
export interface StatsSubmissionResponse {
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ export function getVerificationPeriodActionBlockedMessage(
|
||||
const to = formatCheckInputYmdAsDash(v.endYmd);
|
||||
const range =
|
||||
from && to ? `${from} ~ ${to}` : [from, to].filter(Boolean).join(" ~ ");
|
||||
return `검증기간에는 저장, 제출, 제출 취소를 할 수 없습니다.\n검증기간: ${range}`;
|
||||
return `검증기간에는 저장, 제출, 제출 취소를\n 할 수 없습니다.\n(검증기간: ${range})`;
|
||||
}
|
||||
|
||||
export interface StatisticsInputSewSupplyRateService {
|
||||
|
||||
@@ -268,6 +268,24 @@ export interface ReviewSubmitResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface SubmitVerifyExceptionRequest {
|
||||
sewerBzmnCd: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
userId: string;
|
||||
exceptionList: [
|
||||
{
|
||||
vrfcSn: number;
|
||||
prcsOpnnExpln: string;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export interface SubmitVerifyExceptionResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface StatisticsService {
|
||||
getSidoList(params?: { instSeCd?: string }): Promise<ApiResponse<SearchCondition>>;
|
||||
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
|
||||
@@ -280,7 +298,8 @@ export interface StatisticsService {
|
||||
saveReviewOpinion(param: ReviewOpinionSaveRequest): Promise<ReviewOpinionSaveResponse>;
|
||||
getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise<getReviewOpinionDetailResponse>;
|
||||
saveReviewReturn(param: ReviewReturnSaveRequest): Promise<ReviewReturnSaveResponse>;
|
||||
submitReview(param :ReviewSubmitRequest): Promise<ReviewSubmitResponse>;
|
||||
submitReview(param: ReviewSubmitRequest): Promise<ReviewSubmitResponse>;
|
||||
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
|
||||
getImrStatus(param: string): Promise<ImrStatusResponse>;
|
||||
SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise<SubmitVerifyExceptionResponse>;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
|
||||
<h3 className="text-2xl font-semibold">미제출 지표</h3>
|
||||
{data && (
|
||||
<Badge size="lg" className="rounded-full !px-3">
|
||||
{data.unSubmitCount}건
|
||||
{data.unSubmittedCount ?? 0}건
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -43,13 +43,13 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
|
||||
)}
|
||||
{data && (
|
||||
<div className="h-full">
|
||||
{data.unSubmitList.length === 0 ? (
|
||||
{data.statusList.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
미제출 지표가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[48rem] space-y-2 overflow-y-auto">
|
||||
{data.unSubmitList.map((item) => (
|
||||
{data.statusList.map((item) => (
|
||||
<div
|
||||
key={item.statsCd}
|
||||
className="rounded-sm border border-border p-3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useEffect, useRef } from "react";
|
||||
import { Suspense, useEffect, useMemo, useRef } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { SkeletonPage } from "@/shared/ui/skeleton-page";
|
||||
@@ -8,11 +8,20 @@ import { SkeletonPage } from "@/shared/ui/skeleton-page";
|
||||
import "@rsone/components/index.css";
|
||||
|
||||
import Dashboard from "@/components/statistics/Dashboard";
|
||||
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
const StatisticsPage = () => {
|
||||
const { tabs, activeTabId } = useTabStore();
|
||||
|
||||
const tabsForBody = useMemo(
|
||||
() =>
|
||||
tabs.filter(
|
||||
(tab) => !isVerificationSelectionReportMenu(tab.menuItem),
|
||||
),
|
||||
[tabs],
|
||||
);
|
||||
const fetchAdminInfo = useAdminStore((state) => state.fetchAdminInfo);
|
||||
|
||||
// 컴포넌트 캐시를 useRef로 유지 (재렌더링 시에도 유지됨)
|
||||
@@ -45,7 +54,7 @@ const StatisticsPage = () => {
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6 border-border bg-background">
|
||||
{tabs.map((tab) => {
|
||||
{tabsForBody.map((tab) => {
|
||||
// 렌더링 시점에 컴포넌트가 없으면 바로 로드
|
||||
if (!componentCache.current.has(tab.id)) {
|
||||
const componentName = tab.urlPath;
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { doNameList } from "@/components/util/column/input/DoList";
|
||||
import {
|
||||
SEWAGE_PIPE_INP_TAB_KEYS,
|
||||
SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
|
||||
SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
|
||||
} from "@/components/util/MenuMatchList";
|
||||
|
||||
/** 검증 결과에서 입력 화면 자동 조회에 사용하는 조건 */
|
||||
export type StatisticsInputPendingSearch = {
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
};
|
||||
|
||||
export function verificationSearchToFormValues(
|
||||
s: StatisticsInputPendingSearch,
|
||||
): SearchFormValues {
|
||||
const ctpvNmTrimmed = s.ctpvNm?.trim() ?? "";
|
||||
const sggNmTrimmed = s.sggNm?.trim() ?? "";
|
||||
/** 시군구 필수 도(`DoList.ts`의 `doNameList`)일 때만 API `sggNm`을 검색폼에 반영 */
|
||||
const useSggNm =
|
||||
sggNmTrimmed.length > 0 && doNameList.includes(ctpvNmTrimmed);
|
||||
|
||||
return {
|
||||
year: s.statsYr,
|
||||
bsnmCd: s.sewerBzmnCd,
|
||||
sido: s.ctpvNm,
|
||||
gugun: useSggNm ? sggNmTrimmed : "ALL",
|
||||
};
|
||||
}
|
||||
|
||||
/** `statistics-page` 동적 import 경로의 파일명(확장자 제외)과 일치해야 함 */
|
||||
export const STATISTICS_INPUT_URL_PATH = {
|
||||
SEWAGE_PIPE: "SewagePipeInp",
|
||||
SEWAGE_PIPE_MAINTAIN: "SewagePipeMaintainInp",
|
||||
SEWERAGE_PENETRATION_RATE: "SeweragePenetrationRateInp",
|
||||
} as const;
|
||||
|
||||
function normalizeSewagePipeTabKey(tabKey: string): string {
|
||||
if (
|
||||
tabKey === "adminAreaConnectionInp" ||
|
||||
tabKey === "treatmentPlantConnectionInp"
|
||||
) {
|
||||
return "ST00020006";
|
||||
}
|
||||
return tabKey;
|
||||
}
|
||||
|
||||
const NAV_ROWS: { urlPath: string; tabKeys: readonly string[] }[] = [
|
||||
{ urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE, tabKeys: SEWAGE_PIPE_INP_TAB_KEYS },
|
||||
{
|
||||
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN,
|
||||
tabKeys: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
|
||||
},
|
||||
{
|
||||
urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE,
|
||||
tabKeys: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 검증 결과 등의 `statsCd`(또는 입력 화면 탭 키와 동일한 코드)로
|
||||
* 열 입력 화면 `urlPath`와 활성 탭 키를 반환합니다.
|
||||
*/
|
||||
export function resolveInputPageNavigationFromStatsCd(statsCd: string): {
|
||||
urlPath: string;
|
||||
tabKey: string;
|
||||
} | null {
|
||||
const trimmed = statsCd.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
for (const row of NAV_ROWS) {
|
||||
if (row.tabKeys.includes(trimmed)) {
|
||||
const tabKey =
|
||||
row.urlPath === STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE
|
||||
? normalizeSewagePipeTabKey(trimmed)
|
||||
: trimmed;
|
||||
return { urlPath: row.urlPath, tabKey };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findMenuItemByUrlPath(
|
||||
menus: MenuItem[],
|
||||
urlPath: string,
|
||||
): MenuItem | null {
|
||||
for (const item of menus) {
|
||||
if (item.urlPath === urlPath) return item;
|
||||
if (item.children?.length) {
|
||||
const found = findMenuItemByUrlPath(item.children, urlPath);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
export const VERIFICATION_SELECTION_REPORT_MARKER =
|
||||
"VerificationSelectionReport";
|
||||
|
||||
export function isVerificationSelectionReportMenu(item: MenuItem): boolean {
|
||||
return (
|
||||
item.menuEngNm === VERIFICATION_SELECTION_REPORT_MARKER ||
|
||||
!!item.urlPath?.includes(VERIFICATION_SELECTION_REPORT_MARKER)
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function StatisticsPageLayout({ children }: { children: React.ReactNode }) {
|
||||
const menuPaths = pageTitle?.path?.split(">").map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
<div className="h-auto min-h-full bg-container pb-2 flex-1">
|
||||
<TabBar />
|
||||
<PageTitle
|
||||
title={pageTitle?.label ?? ""}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
import type { StatisticsInputPendingSearch } from "@/shared/lib/stats-cd-input-navigation";
|
||||
|
||||
type Pending = {
|
||||
urlPath: string;
|
||||
tabKey: string;
|
||||
search: StatisticsInputPendingSearch | null;
|
||||
};
|
||||
|
||||
export interface StatisticsInputFocusStore {
|
||||
pending: Pending | null;
|
||||
/** `setPendingInputTab` 호출 시마다 증가 — 이미 해당 화면이 활성일 때도 탭 적용 effect가 돌도록 함 */
|
||||
nonce: number;
|
||||
setPendingInputTab: (
|
||||
urlPath: string,
|
||||
tabKey: string,
|
||||
search?: StatisticsInputPendingSearch | null,
|
||||
) => void;
|
||||
takePendingForUrl: (
|
||||
urlPath: string,
|
||||
) => { tabKey: string; search: StatisticsInputPendingSearch | null } | undefined;
|
||||
}
|
||||
|
||||
export const useStatisticsInputFocusStore = create<StatisticsInputFocusStore>(
|
||||
(set, get) => ({
|
||||
pending: null,
|
||||
nonce: 0,
|
||||
setPendingInputTab: (urlPath, tabKey, search = null) =>
|
||||
set((state) => ({
|
||||
pending: { urlPath, tabKey, search: search ?? null },
|
||||
nonce: state.nonce + 1,
|
||||
})),
|
||||
takePendingForUrl: (urlPath) => {
|
||||
const p = get().pending;
|
||||
if (!p || p.urlPath !== urlPath) return undefined;
|
||||
set({ pending: null });
|
||||
return { tabKey: p.tabKey, search: p.search };
|
||||
},
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user