Merge branch 'iipbs/st/dev'
This commit is contained in:
@@ -35,6 +35,9 @@
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-biz-sum {
|
||||
@apply !bg-[#ffe9cf];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-summary {
|
||||
@apply !bg-[#feee85];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
|
||||
@apply !bg-[#fff7bf];
|
||||
}
|
||||
@@ -42,6 +45,9 @@
|
||||
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-biz-sum {
|
||||
@apply !bg-[#ffe9cf];
|
||||
}
|
||||
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-summary {
|
||||
@apply !bg-[#feee85];
|
||||
}
|
||||
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-other {
|
||||
@apply !bg-[#fff7bf];
|
||||
}
|
||||
@@ -49,6 +55,10 @@
|
||||
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-biz-sum {
|
||||
@apply !bg-[#ffe9cf];
|
||||
}
|
||||
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-summary,
|
||||
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-summary {
|
||||
@apply !bg-[#feee85];
|
||||
}
|
||||
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-rowtype-other,
|
||||
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
|
||||
@apply !bg-[#fff7bf];
|
||||
@@ -56,13 +66,13 @@
|
||||
|
||||
/* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */
|
||||
.data-grid-status-bubble {
|
||||
@apply rounded-lg border border-slate-200/80 bg-slate-50/90 px-4 py-2.5;
|
||||
@apply rounded-lg border border-slate-200/80 bg-white px-8 py-2.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
.data-grid-status-bubble span {
|
||||
@apply text-base text-slate-700;
|
||||
.data-grid-status-bubble > span {
|
||||
@apply text-xl text-slate-700 font-bold;
|
||||
}
|
||||
.data-grid-status-bubble span + span {
|
||||
.data-grid-status-bubble > span + span {
|
||||
@apply ml-4 border-l border-slate-200/70 pl-4;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ interface RealGridDataProviderComparer {
|
||||
getValue?(row: number, field: string | number): unknown;
|
||||
}
|
||||
|
||||
type GridViewWithCheckApi = GridViewWithSetColumns & {
|
||||
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
|
||||
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
|
||||
checkAll?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
|
||||
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
|
||||
return columns.map((col) => {
|
||||
@@ -72,6 +78,8 @@ const CompactDataGrid = ({
|
||||
getGridDataRef,
|
||||
cellStyleCallback,
|
||||
onRowClick,
|
||||
onItemChecked,
|
||||
onItemAllChecked,
|
||||
compactFullWidth = false,
|
||||
compactGridHeightClass = "h-[65vh]",
|
||||
}: DataGridProps) => {
|
||||
@@ -96,6 +104,32 @@ const CompactDataGrid = ({
|
||||
(dataProvider as { setRows: (r: unknown[]) => void }).setRows(rowData);
|
||||
}
|
||||
|
||||
// __checked 플래그(1/true/Y) 기반으로 체크바 상태 동기화
|
||||
if (showHeaderCheckbox) {
|
||||
const checkGrid = gridView as GridViewWithCheckApi;
|
||||
const checkByRowFlag = (value: unknown) =>
|
||||
value === true ||
|
||||
value === 1 ||
|
||||
value === "1" ||
|
||||
value === "Y" ||
|
||||
value === "y" ||
|
||||
value === "true";
|
||||
try {
|
||||
checkGrid.checkAll?.(false);
|
||||
rowData.forEach((row, itemIndex) => {
|
||||
const checked = checkByRowFlag((row as Record<string, unknown>).__checked);
|
||||
if (!checked) return;
|
||||
if (checkGrid.checkItem) {
|
||||
checkGrid.checkItem(itemIndex, true, false);
|
||||
return;
|
||||
}
|
||||
checkGrid.checkRow?.(itemIndex, true, false);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("check sync:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// setDataComparer로 숫자 정렬 (setFields 미사용 → 데이터 유실·필드 중복 오류 없음)
|
||||
const dp = dataProvider as RealGridDataProviderComparer;
|
||||
if (typeof dp.setDataComparer === "function" && typeof dp.getValue === "function") {
|
||||
@@ -337,6 +371,32 @@ const CompactDataGrid = ({
|
||||
};
|
||||
}, [onRowClick]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView) return;
|
||||
|
||||
if (onItemChecked) {
|
||||
gridView.onItemChecked = (_grid, itemIndex, checked) => {
|
||||
onItemChecked(itemIndex, checked);
|
||||
};
|
||||
} else {
|
||||
gridView.onItemChecked = undefined;
|
||||
}
|
||||
|
||||
if (onItemAllChecked) {
|
||||
gridView.onItemAllChecked = (_grid, checked) => {
|
||||
onItemAllChecked(checked);
|
||||
};
|
||||
} else {
|
||||
gridView.onItemAllChecked = undefined;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (gridView.onItemChecked) gridView.onItemChecked = undefined;
|
||||
if (gridView.onItemAllChecked) gridView.onItemAllChecked = undefined;
|
||||
};
|
||||
}, [onItemChecked, onItemAllChecked]);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-[0.6rem] ${compactFullWidth ? "w-full min-w-0" : "w-1/2"}`}>
|
||||
<div className={compactGridHeightClass}>
|
||||
|
||||
@@ -580,10 +580,10 @@ const DataGrid = ({
|
||||
) {
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
];
|
||||
if (
|
||||
cell &&
|
||||
@@ -659,7 +659,7 @@ const DataGrid = ({
|
||||
const col = range.s.c + colIndex;
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
];
|
||||
const headerName =
|
||||
headers[colIndex] || `Column${colIndex + 1}`;
|
||||
@@ -783,13 +783,13 @@ const DataGrid = ({
|
||||
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
|
||||
}
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
|
||||
(gridView.displayOptions as DisplayOptions).fitStyle =
|
||||
GridFitStyle.EVEN;
|
||||
@@ -959,7 +959,7 @@ const DataGrid = ({
|
||||
) {
|
||||
(
|
||||
column.footer[
|
||||
index
|
||||
index
|
||||
] as FooterItemWithCallback
|
||||
).valueCallback =
|
||||
footerItem.valueCallback;
|
||||
@@ -1012,11 +1012,11 @@ const DataGrid = ({
|
||||
getGridDataRef.current = () => {
|
||||
const gv = gridRef.current?.gridView as
|
||||
| {
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
if (!gv?.getDataSource) return [];
|
||||
const ds = gv.getDataSource();
|
||||
@@ -1189,29 +1189,37 @@ const DataGrid = ({
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-[0.6rem] px-[1.2rem]">
|
||||
<div className={`flex items-center ${statusInfo ? 'justify-between' : 'justify-end'} gap-[0.6rem] pr-[1.2rem]`}>
|
||||
{statusInfo && (
|
||||
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
<span>
|
||||
진행상황:{" "}
|
||||
{statusInfo.laeSbmsnSttsCd === null ||
|
||||
statusInfo.laeSbmsnSttsCd === "0"
|
||||
statusInfo.laeSbmsnSttsCd === "0"
|
||||
? "미작성"
|
||||
: statusInfo.laeSbmsnSttsCd === "1"
|
||||
? "작성중"
|
||||
: statusInfo.laeSbmsnSttsCd === "2"
|
||||
? "제출"
|
||||
: statusInfo.laeSbmsnSttsCd === "3"
|
||||
? "승인"
|
||||
: statusInfo.laeSbmsnSttsCd === "4"
|
||||
? "반려"
|
||||
: "검증중"}
|
||||
? <span className="animate-pulse">작성중</span>
|
||||
: statusInfo.laeSbmsnSttsCd === "2"
|
||||
? <span className="!text-[#2094fa]">제출</span>
|
||||
: statusInfo.laeSbmsnSttsCd === "3"
|
||||
? <span className="!text-[#1fd078]">승인</span>
|
||||
: statusInfo.laeSbmsnSttsCd === "4"
|
||||
? (
|
||||
<span className="relative inline-flex !text-[#ff6c64] pr-3">
|
||||
반려
|
||||
<span className="absolute -right-0.5 -top-0.5 inline-flex size-[0.6125rem]">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#ff6c64] opacity-75" />
|
||||
<span className="relative inline-flex size-[0.6125rem] rounded-full bg-[#ff6c64]" />
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
: <span className="animate-pulse">검증중</span>}
|
||||
</span>
|
||||
<span>
|
||||
제출일:{" "}
|
||||
{statusInfo.laeSbmsnYmd
|
||||
? statusInfo.laeSbmsnYmd.length === 8 &&
|
||||
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
|
||||
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
|
||||
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
|
||||
: statusInfo.laeSbmsnYmd
|
||||
: "미제출"}
|
||||
@@ -1228,26 +1236,28 @@ const DataGrid = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isExcelImport && (
|
||||
<Button
|
||||
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
|
||||
type="button"
|
||||
onClick={importExcel}
|
||||
>
|
||||
<i className="diveicon di-file-excel" />
|
||||
<span>가져오기</span>
|
||||
</Button>
|
||||
)}
|
||||
{isExcelExport && (
|
||||
<Button
|
||||
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
|
||||
type="button"
|
||||
onClick={exportExcel}
|
||||
>
|
||||
<i className="diveicon di-file-excel" />
|
||||
<span>내보내기</span>
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-[0.6rem]">
|
||||
{isExcelImport && (
|
||||
<Button
|
||||
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
|
||||
type="button"
|
||||
onClick={importExcel}
|
||||
>
|
||||
<i className="diveicon di-file-excel" />
|
||||
<span>가져오기</span>
|
||||
</Button>
|
||||
)}
|
||||
{isExcelExport && (
|
||||
<Button
|
||||
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
|
||||
type="button"
|
||||
onClick={exportExcel}
|
||||
>
|
||||
<i className="diveicon di-file-excel" />
|
||||
<span>내보내기</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: `${gridHeight}vh` }}>
|
||||
<RealGridReact
|
||||
@@ -1266,24 +1276,24 @@ const DataGrid = ({
|
||||
) => boolean;
|
||||
}
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
const dataProvider = gridRef.current
|
||||
?.dataProvider as unknown as
|
||||
| {
|
||||
setFields?(f: unknown[]): void;
|
||||
setRows?(
|
||||
r: Record<string, string | number>[],
|
||||
): void;
|
||||
getJsonRow?(
|
||||
r: number,
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
setFields?(f: unknown[]): void;
|
||||
setRows?(
|
||||
r: Record<string, string | number>[],
|
||||
): void;
|
||||
getJsonRow?(
|
||||
r: number,
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
| undefined;
|
||||
if (dataProvider?.setFields) {
|
||||
dataProvider.setFields(
|
||||
@@ -1436,14 +1446,14 @@ const DataGrid = ({
|
||||
})}
|
||||
footer={
|
||||
column.footer &&
|
||||
!Array.isArray(column.footer)
|
||||
!Array.isArray(column.footer)
|
||||
? {
|
||||
text: column.footer.text,
|
||||
expression:
|
||||
column.footer.expression,
|
||||
numberFormat:
|
||||
column.footer.numberFormat,
|
||||
}
|
||||
text: column.footer.text,
|
||||
expression:
|
||||
column.footer.expression,
|
||||
numberFormat:
|
||||
column.footer.numberFormat,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
mergeRule={
|
||||
|
||||
@@ -4,15 +4,20 @@ import { useState } from "react";
|
||||
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
|
||||
|
||||
import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm";
|
||||
import type { SearchFormValues } from "../type/SearchForm.type";
|
||||
|
||||
type GridTopActionButtonProps = {
|
||||
text: string;
|
||||
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
|
||||
/** 상위 화면 검색 통계연도 — 시설 조회 다이얼로그에 전달 */
|
||||
statsYr?: string;
|
||||
/** 상위 화면 최근 검색조건 */
|
||||
initialSearchValues?: Pick<SearchFormValues, "sido" | "gugun" | "bsnmCd"> | null;
|
||||
/** 다이얼로그 저장 성공 후 부모 갱신 */
|
||||
onSaveSuccess?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
const GridTopActionButton = ({ text, onClick, statsYr }: GridTopActionButtonProps) => {
|
||||
const GridTopActionButton = ({ text, onClick, statsYr, initialSearchValues, onSaveSuccess }: GridTopActionButtonProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [title, setTitle] = useState(text);
|
||||
|
||||
@@ -41,7 +46,11 @@ const GridTopActionButton = ({ text, onClick, statsYr }: GridTopActionButtonProp
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SidoSggFacilityNameSearchForm statsYr={statsYr} />
|
||||
<SidoSggFacilityNameSearchForm
|
||||
statsYr={statsYr}
|
||||
initialSearchValues={initialSearchValues}
|
||||
onSaveSuccess={onSaveSuccess}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
@@ -89,6 +89,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
fctName: props.defaultValues?.fctName || "",
|
||||
bsnmCd: props.defaultValues?.bsnmCd || "",
|
||||
isDetail: false,
|
||||
isSummary: props.defaultValues?.isSummary || "",
|
||||
});
|
||||
|
||||
const sggKey: [string, string, string, string] | null =
|
||||
@@ -132,6 +133,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
fctName: "",
|
||||
bsnmCd: "",
|
||||
isDetail: false,
|
||||
isSummary: "",
|
||||
};
|
||||
setValues(resetValues);
|
||||
props.onReset?.();
|
||||
@@ -153,7 +155,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
const selectedBsnm =
|
||||
value === "ALL"
|
||||
? sidoList?.data.sidoList.find((v) => v.sido === values.sido)
|
||||
?.bsnmCd || ""
|
||||
?.bsnmCd || ""
|
||||
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
|
||||
|
||||
setValues((prev) => ({
|
||||
@@ -250,10 +252,11 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
)}
|
||||
{mergedProps.isDetail && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
<Label className="text-label cursor-pointer" htmlFor="isDetail">
|
||||
세부 데이터 포함
|
||||
</Label>
|
||||
<Checkbox
|
||||
id="isDetail"
|
||||
checked={values.isDetail}
|
||||
onCheckedChange={(value) =>
|
||||
setValues({
|
||||
@@ -264,6 +267,30 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.isSummary && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label cursor-pointer">
|
||||
요약표 포함
|
||||
</Label>
|
||||
<Select
|
||||
value={values.isSummary}
|
||||
onValueChange={(value) =>
|
||||
setValues({ ...values, isSummary: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택 안함"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">선택 안함</SelectItem>
|
||||
{[1,2,3,4,5,6,7,8,9,10].map((i) => (
|
||||
<SelectItem value={i.toString()} key={i.toString()}>{`${i}개년`}</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.order && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">차수</Label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
@@ -29,8 +29,13 @@ import type { ColumnInfo } from "../type/DataGrid.type";
|
||||
import type {
|
||||
SeweragePipeFacilitiesListRequest,
|
||||
SeweragePipeFacilityAdminAreaListRequest,
|
||||
SeweragePipeFactConnectionSaveRequest,
|
||||
SeweragePipeFactConnectionSaveItem,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
|
||||
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
|
||||
import type { SearchFormValues } from "../type/SearchForm.type";
|
||||
import { doNameList } from "../util/column/input/DoList";
|
||||
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
||||
|
||||
/** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */
|
||||
const treatmentFacilityColumnInfo: ColumnInfo = {
|
||||
@@ -48,21 +53,35 @@ const treatmentFacilityColumnInfo: ColumnInfo = {
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "fctNm",
|
||||
fieldName: "fctNm",
|
||||
name: "fcltNm",
|
||||
fieldName: "fcltNm",
|
||||
header: { text: "시설명" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "fctCpc",
|
||||
fieldName: "fctCpc",
|
||||
name: "fcltCd",
|
||||
fieldName: "fcltCd",
|
||||
header: { text: "시설코드" },
|
||||
editable: false,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
name: "fcltSn",
|
||||
fieldName: "fcltSn",
|
||||
header: { text: "시설일련번호" },
|
||||
editable: false,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
name: "fcltCpct",
|
||||
fieldName: "fcltCpct",
|
||||
header: { text: "시설용량(m³/일)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
},
|
||||
],
|
||||
columnLayout: ["ctpvNm", "sggNm", "fctNm", "fctCpc"],
|
||||
columnLayout: ["ctpvNm", "sggNm", "fcltNm", "fcltCpct", "fcltCd"],
|
||||
};
|
||||
|
||||
/** 다이얼로그 우측: 행정구역 정보 (순번 없음, 헤더 체크박스는 showHeaderCheckbox) */
|
||||
@@ -103,12 +122,26 @@ const getSggAllListFetcher = async (
|
||||
type SidoSggFacilityNameSearchFormProps = {
|
||||
/** 상위(SewagePipeInp 등) 검색 조건의 통계연도 — facilitiesList 등 API 필수값 */
|
||||
statsYr?: string;
|
||||
/** 상위(SewagePipeInp) 마지막 검색 조건 */
|
||||
initialSearchValues?: Pick<SearchFormValues, "sido" | "gugun" | "bsnmCd"> | null;
|
||||
/** 저장 성공 후 부모 탭 데이터 재조회 훅 */
|
||||
onSaveSuccess?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
const SidoSggFacilityNameSearchForm = ({
|
||||
statsYr,
|
||||
initialSearchValues,
|
||||
onSaveSuccess,
|
||||
}: SidoSggFacilityNameSearchFormProps) => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const initialSidoValue =
|
||||
initialSearchValues?.sido && initialSearchValues.sido !== "ALL"
|
||||
? initialSearchValues.sido
|
||||
: "ALL";
|
||||
const initialGugunValue =
|
||||
initialSearchValues?.gugun && initialSearchValues.gugun !== "ALL"
|
||||
? initialSearchValues.gugun
|
||||
: "ALL";
|
||||
|
||||
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
|
||||
"sido-list",
|
||||
@@ -134,9 +167,9 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
},
|
||||
);
|
||||
|
||||
const [sido, setSido] = useState<string>("ALL");
|
||||
const [gugun, setGugun] = useState<string>("ALL");
|
||||
const [fctNm, setFctNm] = useState<string>("");
|
||||
const [sido, setSido] = useState<string>(initialSidoValue);
|
||||
const [gugun, setGugun] = useState<string>(initialGugunValue);
|
||||
const [fcltNm, setFcltNm] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [treatmentFacilityRows, setTreatmentFacilityRows] = useState<
|
||||
@@ -145,6 +178,10 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
const [adminRegionRows, setAdminRegionRows] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
const [selectedFacilityRow, setSelectedFacilityRow] = useState<
|
||||
Record<string, string | number> | null
|
||||
>(null);
|
||||
const didAutoFetchRef = useRef(false);
|
||||
|
||||
const sggKey: [string, string] | null =
|
||||
sido && sido !== "ALL" ? ["sgg-all", sido] : null;
|
||||
@@ -198,7 +235,7 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const trimmedFclty = fctNm.trim();
|
||||
const trimmedFclty = fcltNm.trim();
|
||||
|
||||
const sewerBzmnCd = resolveSewerBzmnCd();
|
||||
|
||||
@@ -225,13 +262,15 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
list.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
fctNm: item.bscFctNm,
|
||||
fctCpc: item.fctCpc,
|
||||
bscFctCd: item.bscFctCd,
|
||||
fcltNm: item.fcltNm,
|
||||
fcltCpct: item.fcltCpct,
|
||||
fcltCd: item.fcltCd,
|
||||
fcltSn: item.fcltSn,
|
||||
admdstCd: item.admdstCd,
|
||||
statsYr: item.statsYr,
|
||||
})),
|
||||
);
|
||||
setSelectedFacilityRow(null);
|
||||
setAdminRegionRows([]);
|
||||
} catch {
|
||||
alert("처리시설 목록 조회 중 오류가 발생했습니다.");
|
||||
@@ -247,7 +286,7 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
||||
return;
|
||||
}
|
||||
const swtrmtFcltCd = String(row.bscFctCd ?? "").trim();
|
||||
const swtrmtFcltCd = String(row.fcltCd ?? "").trim();
|
||||
if (!swtrmtFcltCd) {
|
||||
alert("처리시설 코드가 없습니다.");
|
||||
return;
|
||||
@@ -261,13 +300,14 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
const params: SeweragePipeFacilityAdminAreaListRequest = {
|
||||
statsYr: statsYr.trim(),
|
||||
swtrmtFcltCd,
|
||||
sewerBzmnCd: String(row.admdstCd ?? "").trim(),
|
||||
sewerBzmnCd: initialSearchValues?.bsnmCd || '',
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
...(doNameList.includes(ctpvNm) ? { sggNm } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setSelectedFacilityRow(row);
|
||||
const response =
|
||||
await statisticsInputSewPipeApiService.getFacilityAdminAreaList(
|
||||
params,
|
||||
@@ -283,6 +323,8 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
sggNm: item.sggNm,
|
||||
emdNm: item.admdstNm,
|
||||
admdstCd: item.admdstCd,
|
||||
isConnection: item.isConnection,
|
||||
__checked: item.isConnection === "1" ? "1" : "0",
|
||||
})),
|
||||
);
|
||||
} catch {
|
||||
@@ -293,9 +335,172 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
};
|
||||
|
||||
const handleSaveAdminRegions = () => {
|
||||
alert("저장 API 연동 예정입니다.");
|
||||
if (!statsYr?.trim()) {
|
||||
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
||||
return;
|
||||
}
|
||||
if (!selectedFacilityRow) {
|
||||
alert("처리시설을 먼저 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanged = adminRegionRows.some(
|
||||
(row) =>
|
||||
(row.__checked === "1" && row.isConnection !== "1") ||
|
||||
(row.__checked !== "1" && row.isConnection === "1"),
|
||||
);
|
||||
|
||||
const saveList: SeweragePipeFactConnectionSaveItem[] = adminRegionRows
|
||||
.filter((row) => row.__checked === "1")
|
||||
.map((row) => ({
|
||||
admdstCd: String(row.admdstCd ?? ""),
|
||||
admdstNm: String(row.emdNm ?? ""),
|
||||
}))
|
||||
.filter((row) => row.admdstCd && row.admdstNm);
|
||||
|
||||
const deleteList = adminRegionRows
|
||||
.filter((row) => row.__checked !== "1" && row.isConnection === "1")
|
||||
.map((row) => String(row.admdstCd ?? ""))
|
||||
.filter(Boolean);
|
||||
|
||||
if (!hasChanged) {
|
||||
alert("변경된 연결 정보가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
const request: SeweragePipeFactConnectionSaveRequest = {
|
||||
ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
|
||||
sggNm: String(selectedFacilityRow.sggNm ?? ""),
|
||||
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
|
||||
statsYr: statsYr.trim(),
|
||||
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
|
||||
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
|
||||
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
|
||||
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
|
||||
userId: userInfo.userUnqId,
|
||||
saveList,
|
||||
deleteList,
|
||||
};
|
||||
|
||||
const response =
|
||||
await statisticsInputSewPipeApiService.saveFactConnection(
|
||||
request,
|
||||
);
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "연결관계 저장에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setAdminRegionRows((prev) =>
|
||||
prev.map((row) => ({
|
||||
...row,
|
||||
isConnection: row.__checked === "1" ? "1" : "0",
|
||||
})),
|
||||
);
|
||||
await onSaveSuccess?.();
|
||||
alert("저장되었습니다.");
|
||||
} catch {
|
||||
alert("연결관계 저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
};
|
||||
|
||||
const handleAdminRegionItemChecked = (
|
||||
itemIndex: number,
|
||||
checked: boolean,
|
||||
) => {
|
||||
setAdminRegionRows((prev) =>
|
||||
prev.map((row, idx) =>
|
||||
idx === itemIndex
|
||||
? { ...row, __checked: checked ? "1" : "0" }
|
||||
: row,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAdminRegionItemAllChecked = (checked: boolean) => {
|
||||
setAdminRegionRows((prev) =>
|
||||
prev.map((row) => ({ ...row, __checked: checked ? "1" : "0" })),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialSearchValues?.sido || initialSearchValues.sido === "ALL") return;
|
||||
setSido(initialSearchValues.sido);
|
||||
setGugun(
|
||||
initialSearchValues.gugun && initialSearchValues.gugun !== "ALL"
|
||||
? initialSearchValues.gugun
|
||||
: "ALL",
|
||||
);
|
||||
}, [initialSearchValues?.sido, initialSearchValues?.gugun]);
|
||||
|
||||
useEffect(() => {
|
||||
if (didAutoFetchRef.current) return;
|
||||
if (!statsYr?.trim()) return;
|
||||
if (!initialSearchValues?.sido || initialSearchValues.sido === "ALL") return;
|
||||
|
||||
const nextSido = initialSearchValues.sido;
|
||||
const nextGugun =
|
||||
initialSearchValues.gugun && initialSearchValues.gugun !== "ALL"
|
||||
? initialSearchValues.gugun
|
||||
: "ALL";
|
||||
|
||||
didAutoFetchRef.current = true;
|
||||
setSido(nextSido);
|
||||
setGugun(nextGugun);
|
||||
|
||||
const params: SeweragePipeFacilitiesListRequest = {
|
||||
statsYr: statsYr.trim(),
|
||||
...(initialSearchValues.bsnmCd?.trim()
|
||||
? { sewerBzmnCd: initialSearchValues.bsnmCd.trim() }
|
||||
: {}),
|
||||
ctpvNm: nextSido,
|
||||
...(nextGugun !== "ALL" ? { sggNm: nextGugun } : {}),
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response =
|
||||
await statisticsInputSewPipeApiService.getFacilitiesList(params);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "처리시설 목록 조회에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
const list = response.data ?? [];
|
||||
setTreatmentFacilityRows(
|
||||
list.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
fcltNm: item.fcltNm,
|
||||
fcltCpct: item.fcltCpct,
|
||||
fcltCd: item.fcltCd,
|
||||
fcltSn: item.fcltSn,
|
||||
admdstCd: item.admdstCd,
|
||||
statsYr: item.statsYr,
|
||||
})),
|
||||
);
|
||||
setSelectedFacilityRow(null);
|
||||
setAdminRegionRows([]);
|
||||
} catch {
|
||||
alert("처리시설 목록 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
}, [statsYr, initialSearchValues, alert]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 pt-2">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
@@ -346,8 +551,8 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
<Label className="shrink-0 text-label">시설명</Label>
|
||||
<Input
|
||||
className="min-w-[12rem] flex-1"
|
||||
value={fctNm}
|
||||
onChange={(e) => setFctNm(e.target.value)}
|
||||
value={fcltNm}
|
||||
onChange={(e) => setFcltNm(e.target.value)}
|
||||
placeholder="시설명 입력"
|
||||
/>
|
||||
</div>
|
||||
@@ -396,6 +601,8 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
rowData={adminRegionRows}
|
||||
setRowData={setAdminRegionRows}
|
||||
showHeaderCheckbox
|
||||
onItemChecked={handleAdminRegionItemChecked}
|
||||
onItemAllChecked={handleAdminRegionItemAllChecked}
|
||||
compactFullWidth
|
||||
// compactGridHeightClass="h-[min(40vh,28rem)] min-h-[16rem]"
|
||||
/>
|
||||
|
||||
@@ -63,9 +63,6 @@ function Dashboard() {
|
||||
items={panels.map((p) => p.id)}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
{/* TBD: 그리드 레이아웃 비율 정의 필요
|
||||
현재: 3열 그리드, 반응형 브레이크포인트 적용
|
||||
SPEC 레이아웃에 따라 조정 필요 */}
|
||||
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
|
||||
{panels.map((panel) => (
|
||||
<SortablePanel
|
||||
@@ -81,12 +78,12 @@ function Dashboard() {
|
||||
</DndContext>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl" />
|
||||
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
|
||||
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
|
||||
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
|
||||
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
|
||||
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
|
||||
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { useState, useMemo, useRef, useEffect } from "react";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
@@ -7,41 +7,44 @@ import GridTopActionButton from "@/components/module/GridTopActionButton";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
||||
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 } from "@/shared/lib";
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
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";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
|
||||
import type {
|
||||
SeweragePipeInputFactInstallStatusSaveRequest,
|
||||
SeweragePipeInputRegionalFactStatusSaveRequest,
|
||||
SeweragePipeInputStatusInfo,
|
||||
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 { 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";
|
||||
|
||||
const applyEnterableFromStatus = (
|
||||
/** 탭별 status + 입력기간으로 편집 가능 여부 */
|
||||
const computeTabEnterable = (
|
||||
statusInfo: SeweragePipeInputStatusInfo | null | undefined,
|
||||
periodEnterable: boolean,
|
||||
setIsEnterable: (v: boolean) => void
|
||||
) => {
|
||||
): boolean => {
|
||||
if (statusInfo?.laeSbmsnSttsCd) {
|
||||
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
|
||||
setIsEnterable(false);
|
||||
} else {
|
||||
setIsEnterable(periodEnterable);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
setIsEnterable(periodEnterable);
|
||||
return periodEnterable;
|
||||
}
|
||||
return periodEnterable;
|
||||
};
|
||||
|
||||
function buildSewagePipeSearchParam(
|
||||
@@ -58,6 +61,10 @@ function buildSewagePipeSearchParam(
|
||||
}
|
||||
|
||||
/** ST00020006(연결)은 행이 좌·우 그리드 키에만 있음 */
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SeweragePipeInputVerificationResult;
|
||||
};
|
||||
|
||||
function tabRowDataExists(
|
||||
m: Map<string, Record<string, string | number>[]>,
|
||||
tabKey: string,
|
||||
@@ -74,6 +81,43 @@ function tabRowDataExists(
|
||||
return Array.isArray(rows) && rows.length > 0;
|
||||
}
|
||||
|
||||
/** 조회 직후 해당 탭에만 저장하는 상단 툴바 노출 조건 */
|
||||
type TabToolbarSnapshot = {
|
||||
showSaveInToolbar: boolean;
|
||||
showEnterableToolbarButtons: boolean;
|
||||
showActionToolbarCard: boolean;
|
||||
};
|
||||
|
||||
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
|
||||
return doList.includes(values?.bsnmCd || "")
|
||||
? Boolean(values?.gugun && values.gugun !== "ALL")
|
||||
: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param searchContext 조회에 사용된 검색값(해당 요청 기준으로 구군·사업자 조건 반영)
|
||||
* @param treatAsSearched (hasRow || 이 값) 일 때 카드 후보 — load 내부에서는 searchDoneRef 반영용
|
||||
*/
|
||||
function computeTabToolbarSnapshot(
|
||||
statusInfo: SeweragePipeInputStatusInfo | null,
|
||||
hasRowData: boolean,
|
||||
treatAsSearched: boolean,
|
||||
searchContext: SearchFormValues | null,
|
||||
periodEnterable: boolean,
|
||||
isAdminInfo: boolean,
|
||||
): TabToolbarSnapshot {
|
||||
const gugunOk = buildGugunOkForToolbar(searchContext);
|
||||
const enterable = computeTabEnterable(statusInfo, periodEnterable);
|
||||
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
|
||||
const showEnterableToolbarButtons = enterable && gugunOk;
|
||||
const showActionToolbarCard =
|
||||
(hasRowData || treatAsSearched) &&
|
||||
(showSaveInToolbar ||
|
||||
(statusInfo?.laeSbmsnSttsCd === "2" &&
|
||||
(isAdminInfo || (enterable && gugunOk))));
|
||||
return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard };
|
||||
}
|
||||
|
||||
// 하수도 통계 관리 > 02. 하수관로
|
||||
const SewagePipeInp = () => {
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
@@ -81,19 +125,31 @@ const SewagePipeInp = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const [isEnterable, setIsEnterable] = useState<boolean>(true);
|
||||
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [statusInfo, setStatusInfo] = useState<SeweragePipeInputStatusInfo | null>(null);
|
||||
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SeweragePipeInputStatusInfo | null>>(new Map());
|
||||
// const [showDailog, setShowDailog] = useState<boolean>(false);
|
||||
// const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
|
||||
const [_statsCd, setStatsCd] = useState<string>('');
|
||||
const [statsCd, setStatsCd] = useState<string>('');
|
||||
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
/** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
// const { confirm } = useConfirm();
|
||||
/** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */
|
||||
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
|
||||
/** 탭별 조회 시점의 상단 버튼 노출 조건(해당 탭을 다시 조회할 때만 그 탭 항목 갱신) */
|
||||
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const { confirm } = useConfirm();
|
||||
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
||||
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
|
||||
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
||||
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
const getFactInstallGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
const searchParamRef = useRef<SeweragePipeRequestParams | null>(null);
|
||||
/** 검색 시 checkInputPeriod 결과 — 탭 전환 지연 조회 시 편집 가능 여부와 동일하게 맞춤 */
|
||||
const periodEnterableRef = useRef(true);
|
||||
@@ -102,23 +158,41 @@ const SewagePipeInp = () => {
|
||||
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const firstTabKey = SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001";
|
||||
const activeTabRef = useRef<string>(firstTabKey);
|
||||
|
||||
useEffect(() => {
|
||||
activeTabRef.current = activeTab;
|
||||
}, [activeTab]);
|
||||
|
||||
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
|
||||
if (loadedTabKey === activeTabRef.current) {
|
||||
setStatsCd(nextStatsCd);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setStatusInfo(null);
|
||||
setTabStatusInfoMap(new Map());
|
||||
setTabStatsCdMap(new Map());
|
||||
setLastSearchedYear(null);
|
||||
setLastSearchedValues(null);
|
||||
setHasSearched(false);
|
||||
setInputPeriodEnterable(true);
|
||||
setToolbarSnapshotByTab(new Map());
|
||||
searchParamRef.current = null;
|
||||
searchDoneRef.current = false;
|
||||
lazyTabLoggedRef.current.clear();
|
||||
periodEnterableRef.current = true;
|
||||
activeTabRef.current = firstTabKey;
|
||||
setActiveTab(firstTabKey);
|
||||
setStatsCd("");
|
||||
};
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const canEdit = isEnterable || isAdminInfo;
|
||||
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
|
||||
const canEdit =
|
||||
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) ||
|
||||
isAdminInfo;
|
||||
const applyEditable = (cols: ColumnConfig[] | undefined) =>
|
||||
(cols ?? []).map((col: ColumnConfig) =>
|
||||
canEdit
|
||||
@@ -138,14 +212,36 @@ const SewagePipeInp = () => {
|
||||
columns: applyEditable(tab.columns),
|
||||
};
|
||||
});
|
||||
}, [isEnterable, isAdminInfo]);
|
||||
}, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
statusInfo: SeweragePipeInputStatusInfo | null,
|
||||
hasRowData: boolean,
|
||||
searchContext: SearchFormValues,
|
||||
) => {
|
||||
setToolbarSnapshotByTab((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(
|
||||
tabKey,
|
||||
computeTabToolbarSnapshot(
|
||||
statusInfo,
|
||||
hasRowData,
|
||||
searchDoneRef.current,
|
||||
searchContext,
|
||||
periodEnterableRef.current,
|
||||
isAdminInfo,
|
||||
),
|
||||
);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
/** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */
|
||||
const loadSewagePipeActiveTabData = async (
|
||||
tabKey: string,
|
||||
searchParam: SeweragePipeRequestParams,
|
||||
values: SearchFormValues,
|
||||
checkEnterable: boolean,
|
||||
mergeIntoExistingMap: boolean,
|
||||
updateLastSearchedValues: boolean,
|
||||
): Promise<boolean> => {
|
||||
@@ -172,10 +268,16 @@ const SewagePipeInp = () => {
|
||||
...d,
|
||||
}));
|
||||
applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData)));
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
|
||||
if (updateLastSearchedValues) setLastSearchedValues(values);
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
response.data.statusInfo,
|
||||
convertedData.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
@@ -190,10 +292,16 @@ const SewagePipeInp = () => {
|
||||
...d,
|
||||
}));
|
||||
applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData)));
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
|
||||
if (updateLastSearchedValues) setLastSearchedValues(values);
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
response.data.statusInfo,
|
||||
convertedData.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
@@ -208,10 +316,16 @@ const SewagePipeInp = () => {
|
||||
...d,
|
||||
}));
|
||||
applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData)));
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
|
||||
if (updateLastSearchedValues) setLastSearchedValues(values);
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
response.data.statusInfo,
|
||||
convertedData.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
@@ -226,10 +340,16 @@ const SewagePipeInp = () => {
|
||||
...d,
|
||||
}));
|
||||
applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData)));
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
|
||||
if (updateLastSearchedValues) setLastSearchedValues(values);
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
response.data.statusInfo,
|
||||
convertedData.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
@@ -244,10 +364,16 @@ const SewagePipeInp = () => {
|
||||
...d,
|
||||
}));
|
||||
applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData)));
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
|
||||
if (updateLastSearchedValues) setLastSearchedValues(values);
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
response.data.statusInfo,
|
||||
convertedData.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
@@ -268,10 +394,16 @@ const SewagePipeInp = () => {
|
||||
m.set('treatmentPlantConnectionInp', toGridRows(facilityConvertedData));
|
||||
m.set('adminAreaConnectionInp', toGridRows(adminConvertedData));
|
||||
});
|
||||
setStatusInfo(response.data.statusInfo);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
applyEnterableFromStatus(response.data.statusInfo, checkEnterable, setIsEnterable);
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
|
||||
if (updateLastSearchedValues) setLastSearchedValues(values);
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
response.data.statusInfo,
|
||||
facilityConvertedData.length > 0 || adminConvertedData.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
@@ -281,7 +413,6 @@ const SewagePipeInp = () => {
|
||||
setRowDataMap(new Map());
|
||||
}
|
||||
if (updateLastSearchedValues) setLastSearchedValues(values);
|
||||
applyEnterableFromStatus(null, checkEnterable, setIsEnterable);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -299,8 +430,21 @@ const SewagePipeInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkEnterable = periodResponse.data;
|
||||
periodEnterableRef.current = checkEnterable;
|
||||
const periodOk = isInputPeriodCurrent(periodResponse.data);
|
||||
periodEnterableRef.current = periodOk;
|
||||
setInputPeriodEnterable(periodOk);
|
||||
|
||||
/** 연도가 바뀐 검색만 전 탭 캐시 초기화 + 첫 탭으로 이동. 연도 외 조건만 바뀌면 다른 탭은 이전 조회 데이터 유지 */
|
||||
const yearChangedFromLastSearch =
|
||||
lastSearchedValues != null &&
|
||||
(lastSearchedValues.year ?? "") !== (values.year ?? "");
|
||||
if (yearChangedFromLastSearch) {
|
||||
setRowDataMap(new Map());
|
||||
setTabStatusInfoMap(new Map());
|
||||
setTabStatsCdMap(new Map());
|
||||
setStatsCd("");
|
||||
setToolbarSnapshotByTab(new Map());
|
||||
}
|
||||
|
||||
lazyTabLoggedRef.current.clear();
|
||||
|
||||
@@ -312,7 +456,15 @@ const SewagePipeInp = () => {
|
||||
setHasSearched(true);
|
||||
setLastSearchedYear(values.year ?? null);
|
||||
|
||||
await loadSewagePipeActiveTabData(activeTab, searchParam, values, checkEnterable, true, true);
|
||||
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTab;
|
||||
|
||||
if (yearChangedFromLastSearch) {
|
||||
activeTabRef.current = firstTabKey;
|
||||
setActiveTab(firstTabKey);
|
||||
} else {
|
||||
activeTabRef.current = activeTab;
|
||||
}
|
||||
await loadSewagePipeActiveTabData(tabKeyToLoad, searchParam, values, true, true);
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -321,7 +473,9 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
setStatsCd(tabStatsCdMap.get(tabKey) ?? "");
|
||||
|
||||
const hasActiveTabData = tabRowDataExists(rowDataMap, tabKey);
|
||||
|
||||
@@ -339,14 +493,7 @@ const SewagePipeInp = () => {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const ok = await loadSewagePipeActiveTabData(
|
||||
tabKey,
|
||||
searchParam,
|
||||
values,
|
||||
periodEnterableRef.current,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
const ok = await loadSewagePipeActiveTabData(tabKey, searchParam, values, true, false);
|
||||
if (ok) lazyTabLoggedRef.current.add(tabKey);
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
@@ -355,11 +502,6 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
console.log('submit exception');
|
||||
// 예외 제출 로직
|
||||
};
|
||||
|
||||
const getLastYearDataFunc = async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.sido || values.sido === 'ALL') {
|
||||
@@ -373,7 +515,7 @@ const SewagePipeInp = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const param = buildSewagePipeSearchParam(values, { mode: 'PRE' });
|
||||
await loadSewagePipeActiveTabData(activeTab, param, values, periodEnterableRef.current, true, false);
|
||||
await loadSewagePipeActiveTabData(activeTab, param, values, true, false);
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
@@ -386,115 +528,237 @@ const SewagePipeInp = () => {
|
||||
// 제출 로직
|
||||
};
|
||||
|
||||
const saveFunc = async () => {
|
||||
if (activeTab !== "ST00020001") {
|
||||
const refreshConnectionStatusAfterPopupSave = async () => {
|
||||
const searchParam = searchParamRef.current;
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!searchParam || !values) return;
|
||||
await loadSewagePipeActiveTabData("ST00020006", searchParam, values, true, false);
|
||||
};
|
||||
|
||||
const openVerificationHistoryDialog = () => {
|
||||
const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? statsCd;
|
||||
if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !activeTabStatsCd) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
setVerificationHistoryOpen(true);
|
||||
};
|
||||
|
||||
const saveFunc = async () => {
|
||||
if (!(await confirm("저장 하시겠습니까?"))) return;
|
||||
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.year) {
|
||||
alert("검색 후 저장해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const currentData = getGridDataRef.current?.();
|
||||
if (!currentData || currentData.length === 0) {
|
||||
alert("저장할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.year) {
|
||||
alert("검색 후 저장해 주세요.");
|
||||
return;
|
||||
}
|
||||
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
|
||||
|
||||
const columnFormatMap = new Map<string, string>();
|
||||
RegionalFactStatusInpColumn.columns.forEach((col) => {
|
||||
if (col.fieldName && col.dataFormat) {
|
||||
columnFormatMap.set(col.fieldName, col.dataFormat);
|
||||
switch (activeTab) {
|
||||
case "ST00020001": {
|
||||
const currentData = getGridDataRef.current?.();
|
||||
if (!currentData || currentData.length === 0) {
|
||||
alert("저장할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const columnFormatMap = new Map<string, string>();
|
||||
RegionalFactStatusInpColumn.columns.forEach((col) => {
|
||||
if (col.fieldName && col.dataFormat) {
|
||||
columnFormatMap.set(col.fieldName, col.dataFormat);
|
||||
}
|
||||
});
|
||||
|
||||
const formatNumber = (value: string | number | null | undefined, fieldName: string): number => {
|
||||
if (value === null || value === undefined || value === "") return 0;
|
||||
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
||||
if (isNaN(num)) return 0;
|
||||
const format = columnFormatMap.get(fieldName);
|
||||
if (!format) return num;
|
||||
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
|
||||
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
|
||||
};
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
const saveList = currentData.map((d) => ({
|
||||
admdstCd: String(d.admdstCd ?? ""),
|
||||
sggNm: String(d.sggNm ?? ""),
|
||||
admdstNm: String(d.admdstNm ?? ""),
|
||||
goalYr: String(d.goalYr ?? ""),
|
||||
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
||||
wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"),
|
||||
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
|
||||
swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, "swagePplPsplyRt"),
|
||||
cbsysPplMtncPlanArea: formatNumber(d.cbsysPplMtncPlanArea, "cbsysPplMtncPlanArea"),
|
||||
cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"),
|
||||
cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"),
|
||||
cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"),
|
||||
cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"),
|
||||
cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"),
|
||||
cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"),
|
||||
cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"),
|
||||
drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"),
|
||||
drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"),
|
||||
drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"),
|
||||
drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"),
|
||||
drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"),
|
||||
drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"),
|
||||
exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"),
|
||||
exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"),
|
||||
exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"),
|
||||
exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"),
|
||||
exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"),
|
||||
exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"),
|
||||
exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"),
|
||||
exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"),
|
||||
cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"),
|
||||
sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"),
|
||||
sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"),
|
||||
drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"),
|
||||
exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"),
|
||||
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
|
||||
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
|
||||
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
|
||||
sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"),
|
||||
rmrk: (d.rmrk as string | null) ?? null,
|
||||
}));
|
||||
|
||||
const param: SeweragePipeInputRegionalFactStatusSaveRequest = {
|
||||
statsYr: values.year ?? "",
|
||||
ctpvNm: values.sido ?? "",
|
||||
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
|
||||
sewerBzmnCd: values.bsnmCd ?? "",
|
||||
userId: userInfo.userUnqId,
|
||||
superAdmin: isAdminInfo ? "superAdmin" : "",
|
||||
saveList,
|
||||
};
|
||||
|
||||
const response =
|
||||
await statisticsInputSewPipeApiService.saveRegionalFactStatus(param);
|
||||
if (response.status === "success") {
|
||||
setGlobalProgress(50);
|
||||
const refreshedParam = buildSewagePipeSearchParam(values);
|
||||
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
|
||||
setGlobalProgress(100);
|
||||
alert("저장되었습니다.");
|
||||
} else {
|
||||
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
case "ST00020002": {
|
||||
const currentData = getFactInstallGridDataRef.current?.();
|
||||
if (!currentData || currentData.length === 0) {
|
||||
alert("저장할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formatNumber = (value: string | number | null | undefined, fieldName: string): number => {
|
||||
if (value === null || value === undefined || value === "") return 0;
|
||||
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
||||
if (isNaN(num)) return 0;
|
||||
const format = columnFormatMap.get(fieldName);
|
||||
if (!format) return num;
|
||||
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
|
||||
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
|
||||
};
|
||||
const columnFormatMap = new Map<string, string>();
|
||||
FactInstallStatusInpColumn.columns.forEach((col) => {
|
||||
if (col.fieldName && col.dataFormat) {
|
||||
columnFormatMap.set(col.fieldName, col.dataFormat);
|
||||
}
|
||||
});
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
const formatNumber = (value: string | number | null | undefined, fieldName: string): number => {
|
||||
if (value === null || value === undefined || value === "") return 0;
|
||||
const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
|
||||
if (isNaN(num)) return 0;
|
||||
const format = columnFormatMap.get(fieldName);
|
||||
if (!format) return num;
|
||||
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
|
||||
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
|
||||
};
|
||||
|
||||
const saveList = currentData.map((d) => ({
|
||||
admdstCd: String(d.admdstCd ?? ""),
|
||||
sggNm: String(d.sggNm ?? ""),
|
||||
admdstNm: String(d.admdstNm ?? ""),
|
||||
goalYr: String(d.goalYr ?? ""),
|
||||
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
|
||||
wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"),
|
||||
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
|
||||
swagePplPsplyRt: formatNumber(d.swagePplPsplyRt, "swagePplPsplyRt"),
|
||||
cbsysPplMtncPlanArea: formatNumber(d.cbsysPplMtncPlanArea, "cbsysPplMtncPlanArea"),
|
||||
cbsysPplMtncPlanPrlg: formatNumber(d.cbsysPplMtncPlanPrlg, "cbsysPplMtncPlanPrlg"),
|
||||
cbsysPlanPrlg: formatNumber(d.cbsysPlanPrlg, "cbsysPlanPrlg"),
|
||||
cbsysFcltPrlg: formatNumber(d.cbsysFcltPrlg, "cbsysFcltPrlg"),
|
||||
cbsysSqrePrlg: formatNumber(d.cbsysSqrePrlg, "cbsysSqrePrlg"),
|
||||
cbsysCiclPrlg: formatNumber(d.cbsysCiclPrlg, "cbsysCiclPrlg"),
|
||||
cbsysOpnPrlg: formatNumber(d.cbsysOpnPrlg, "cbsysOpnPrlg"),
|
||||
cbsysGttrPrlg: formatNumber(d.cbsysGttrPrlg, "cbsysGttrPrlg"),
|
||||
drtwPplMtncPlanArea: formatNumber(d.drtwPplMtncPlanArea, "drtwPplMtncPlanArea"),
|
||||
drtwPplMtncPlanPrlg: formatNumber(d.drtwPplMtncPlanPrlg, "drtwPplMtncPlanPrlg"),
|
||||
drtwPplPlanPrlg: formatNumber(d.drtwPplPlanPrlg, "drtwPplPlanPrlg"),
|
||||
drtwPplFcltPrlg: formatNumber(d.drtwPplFcltPrlg, "drtwPplFcltPrlg"),
|
||||
drtwPplSqrePrlg: formatNumber(d.drtwPplSqrePrlg, "drtwPplSqrePrlg"),
|
||||
drtwPplCiclPrlg: formatNumber(d.drtwPplCiclPrlg, "drtwPplCiclPrlg"),
|
||||
exlnPplMtncPlanArea: formatNumber(d.exlnPplMtncPlanArea, "exlnPplMtncPlanArea"),
|
||||
exlnPplMtncPlanPrlg: formatNumber(d.exlnPplMtncPlanPrlg, "exlnPplMtncPlanPrlg"),
|
||||
exlnPplPlanPrlg: formatNumber(d.exlnPplPlanPrlg, "exlnPplPlanPrlg"),
|
||||
exlnPplFcltPrlg: formatNumber(d.exlnPplFcltPrlg, "exlnPplFcltPrlg"),
|
||||
exlnPplSqrePrlg: formatNumber(d.exlnPplSqrePrlg, "exlnPplSqrePrlg"),
|
||||
exlnPplCiclPrlg: formatNumber(d.exlnPplCiclPrlg, "exlnPplCiclPrlg"),
|
||||
exlnPplOpnPrlg: formatNumber(d.exlnPplOpnPrlg, "exlnPplOpnPrlg"),
|
||||
exlnPplGttrPrlg: formatNumber(d.exlnPplGttrPrlg, "exlnPplGttrPrlg"),
|
||||
cbsysMnhlCnt: formatNumber(d.cbsysMnhlCnt, "cbsysMnhlCnt"),
|
||||
sprsysDrtwMnhlCnt: formatNumber(d.sprsysDrtwMnhlCnt, "sprsysDrtwMnhlCnt"),
|
||||
sprsysExlnMnhlCnt: formatNumber(d.sprsysExlnMnhlCnt, "sprsysExlnMnhlCnt"),
|
||||
drtwRcvprpCnt: formatNumber(d.drtwRcvprpCnt, "drtwRcvprpCnt"),
|
||||
exlnRcvprpCnt: formatNumber(d.exlnRcvprpCnt, "exlnRcvprpCnt"),
|
||||
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
|
||||
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
|
||||
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
|
||||
sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"),
|
||||
rmrk: (d.rmrk as string | null) ?? null,
|
||||
}));
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
const param: SeweragePipeInputRegionalFactStatusSaveRequest = {
|
||||
statsYr: values.year ?? "",
|
||||
ctpvNm: values.sido ?? "",
|
||||
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
|
||||
sewerBzmnCd: values.bsnmCd ?? "",
|
||||
userId: userInfo.userUnqId,
|
||||
superAdmin: isAdminInfo ? "superAdmin" : "",
|
||||
saveList,
|
||||
};
|
||||
const saveList = currentData.map((d) => ({
|
||||
admdstCd: String(d.admdstCd ?? ""),
|
||||
sggNm: String(d.sggNm ?? ""),
|
||||
admdstNm: String(d.admdstNm ?? ""),
|
||||
fcltSn: String(d.fcltSn ?? ""),
|
||||
swtrmtFcltCd: String(d.swtrmtFcltCd ?? ""),
|
||||
swtrmtFcltNm: String(d.swtrmtFcltNm ?? ""),
|
||||
prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"),
|
||||
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
|
||||
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
|
||||
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
|
||||
sprsysVartnRt: formatNumber(d.sprsysVartnRt, "sprsysVartnRt"),
|
||||
mltvEqpmCnt: formatNumber(d.mltvEqpmCnt, "mltvEqpmCnt"),
|
||||
cbsysMltvEqpmCnt: formatNumber(d.cbsysMltvEqpmCnt, "cbsysMltvEqpmCnt"),
|
||||
sprsysMltvEqpmCnt: formatNumber(d.sprsysMltvEqpmCnt, "sprsysMltvEqpmCnt"),
|
||||
sprsysMltvLnkgCnt: formatNumber(d.sprsysMltvLnkgCnt, "sprsysMltvLnkgCnt"),
|
||||
sprsysMltvYetLnkgCnt: formatNumber(d.sprsysMltvYetLnkgCnt, "sprsysMltvYetLnkgCnt"),
|
||||
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
|
||||
wholIcptgAtlfcsPrlg: formatNumber(d.wholIcptgAtlfcsPrlg, "wholIcptgAtlfcsPrlg"),
|
||||
icptgPrlg: formatNumber(d.icptgPrlg, "icptgPrlg"),
|
||||
atlfcsPrlg: formatNumber(d.atlfcsPrlg, "atlfcsPrlg"),
|
||||
cbsysStofcbrCnt: formatNumber(d.cbsysStofcbrCnt, "cbsysStofcbrCnt"),
|
||||
sprsysStofcbrCnt: formatNumber(d.sprsysStofcbrCnt, "sprsysStofcbrCnt"),
|
||||
sprsysExlnOtflCnt: formatNumber(d.sprsysExlnOtflCnt, "sprsysExlnOtflCnt"),
|
||||
wholBrchlnPrlg: formatNumber(d.wholBrchlnPrlg, "wholBrchlnPrlg"),
|
||||
cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"),
|
||||
exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"),
|
||||
drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"),
|
||||
rmrk: String(d.rmrk ?? ""),
|
||||
}));
|
||||
|
||||
console.log(param);
|
||||
const param: SeweragePipeInputFactInstallStatusSaveRequest = {
|
||||
statsYr: values.year ?? "",
|
||||
ctpvNm: values.sido ?? "",
|
||||
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
|
||||
sewerBzmnCd: values.bsnmCd ?? "",
|
||||
userId: userInfo.userUnqId,
|
||||
superAdmin: isAdminInfo ? "superAdmin" : "",
|
||||
saveList,
|
||||
};
|
||||
|
||||
const response =
|
||||
await statisticsInputSewPipeApiService.saveFactInstallStatus(param);
|
||||
if (response.status === "success") {
|
||||
setGlobalProgress(50);
|
||||
const refreshedParam = buildSewagePipeSearchParam(values);
|
||||
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
|
||||
setGlobalProgress(100);
|
||||
alert("저장되었습니다.");
|
||||
} else {
|
||||
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
alert("현재 탭 저장 API 연동 전입니다.");
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
alert("저장 준비 중 오류가 발생했습니다.");
|
||||
alert("데이터 저장 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
hideGlobalProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '')
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
|
||||
: true;
|
||||
const gugunOkOrNotDoList = buildGugunOkForToolbar(lastSearchedValues);
|
||||
|
||||
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
||||
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
||||
const searchContextForToolbar = lastSearchedValues ?? searchValues;
|
||||
const activeTabToolbar =
|
||||
toolbarSnapshotByTab.get(activeTab) ??
|
||||
computeTabToolbarSnapshot(
|
||||
tabStatusInfoMap.get(activeTab) ?? null,
|
||||
tabRowDataExists(rowDataMap, activeTab),
|
||||
hasSearched,
|
||||
searchContextForToolbar,
|
||||
inputPeriodEnterable,
|
||||
isAdminInfo,
|
||||
);
|
||||
|
||||
/** rowDataMap — 행이 하나라도 있을 때만 툴바·엑셀 의미 있음 */
|
||||
const hasRowData = Array.from(rowDataMap.values()).some((rows) => rows.length > 0);
|
||||
const excelToolbarEnabled = hasRowData && isEnterable && gugunOkOrNotDoList;
|
||||
|
||||
const showActionToolbarCard =
|
||||
(hasRowData || hasSearched) &&
|
||||
(showSaveInToolbar ||
|
||||
(statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList))));
|
||||
const {
|
||||
showSaveInToolbar,
|
||||
showEnterableToolbarButtons,
|
||||
showActionToolbarCard,
|
||||
} = activeTabToolbar;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -502,9 +766,11 @@ const SewagePipeInp = () => {
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
/>
|
||||
{showEnterableToolbarButtons && (
|
||||
<>
|
||||
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
|
||||
<LastYearDataButton getLastYearDataFunc={() => void getLastYearDataFunc()} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
@@ -513,7 +779,7 @@ const SewagePipeInp = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
{isLoading && !globalProgressVisible && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
@@ -534,6 +800,14 @@ const SewagePipeInp = () => {
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
const tabEnterableForGrid = computeTabEnterable(
|
||||
tabStatusInfoMap.get(tabKey) ?? null,
|
||||
inputPeriodEnterable,
|
||||
);
|
||||
const excelToolbarEnabledForTab =
|
||||
tabRowDataExists(rowDataMap, tabKey) &&
|
||||
tabEnterableForGrid &&
|
||||
gugunOkOrNotDoList;
|
||||
|
||||
if (info.isSplitGrid && info.left && info.right) {
|
||||
const leftKey = info.left.tabKey || `${tabKey}_left`;
|
||||
@@ -544,7 +818,7 @@ const SewagePipeInp = () => {
|
||||
const showSplitTopButton =
|
||||
Boolean(splitTopButton?.text) &&
|
||||
(splitTopButton?.visible ?? true) &&
|
||||
excelToolbarEnabled;
|
||||
excelToolbarEnabledForTab;
|
||||
const leftColumnInfo: ColumnInfo = {
|
||||
columns: info.left.columns ?? [],
|
||||
columnLayout: info.left.columnLayout ?? [],
|
||||
@@ -563,6 +837,16 @@ const SewagePipeInp = () => {
|
||||
text={splitTopButton?.text ?? ""}
|
||||
onClick={splitTopButton?.onClick}
|
||||
statsYr={lastSearchedYear ?? undefined}
|
||||
initialSearchValues={
|
||||
lastSearchedValues
|
||||
? {
|
||||
sido: lastSearchedValues.sido,
|
||||
gugun: lastSearchedValues.gugun,
|
||||
bsnmCd: lastSearchedValues.bsnmCd,
|
||||
}
|
||||
: null
|
||||
}
|
||||
onSaveSuccess={refreshConnectionStatusAfterPopupSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -618,6 +902,7 @@ const SewagePipeInp = () => {
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
key={(tabEnterableForGrid || isAdminInfo) ? "enterable" : "readonly"}
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
@@ -632,18 +917,69 @@ const SewagePipeInp = () => {
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={excelToolbarEnabled}
|
||||
isExcelImport={excelToolbarEnabledForTab}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
getGridDataRef={tabKey === "ST00020001" ? getGridDataRef : undefined}
|
||||
statusInfo={tabStatusInfoMap.get(tabKey) ?? null}
|
||||
getGridDataRef={
|
||||
tabKey === "ST00020001"
|
||||
? getGridDataRef
|
||||
: tabKey === "ST00020002"
|
||||
? getFactInstallGridDataRef
|
||||
: undefined
|
||||
}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
const hasImr = errors.some((e) => e.vrfcCd === "IMR");
|
||||
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
|
||||
}
|
||||
}}
|
||||
cellTooltipCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR" ? "IMR검증 - " : "일반검증 - ";
|
||||
const comp = e.compVlCn ?? "";
|
||||
const now = e.nowVlCn ?? "";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${comp} / 현재 입력값: ${now})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
<VerificationResultHistoryDialog
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={
|
||||
searchValues?.year &&
|
||||
searchValues?.sido &&
|
||||
searchValues?.bsnmCd &&
|
||||
(tabStatsCdMap.get(activeTab) ?? statsCd)
|
||||
? {
|
||||
sewerBzmnCd: searchValues.bsnmCd,
|
||||
ctpvNm: searchValues.sido,
|
||||
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
|
||||
statsYr: searchValues.year,
|
||||
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,13 +24,16 @@ import { getUserInfo } from "../../../../../../packages/common/components/src/ap
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import type {
|
||||
SewerageSupplyRateItem,
|
||||
SewerageSupplyRateRequestParams,
|
||||
SewerageSupplyRateSaveItem,
|
||||
SewerageSupplyRateVerificationResult,
|
||||
SewerageSupplyRateSaveRequest,
|
||||
SewerageSupplyRateStatusInfo,
|
||||
import {
|
||||
type CheckInputPeriodItem,
|
||||
type SewerageSupplyRateItem,
|
||||
type SewerageSupplyRateRequestParams,
|
||||
type SewerageSupplyRateSaveItem,
|
||||
type SewerageSupplyRateVerificationResult,
|
||||
type SewerageSupplyRateSaveRequest,
|
||||
type SewerageSupplyRateStatusInfo,
|
||||
getVerificationPeriodActionBlockedMessage,
|
||||
isInputPeriodCurrent,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
import type {
|
||||
cancelSubmitRequest,
|
||||
@@ -100,6 +103,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [statusInfo, setStatusInfo] = useState<SewerageSupplyRateStatusInfo | null>(null);
|
||||
/** 마지막 조회 시 check-input-period 응답 (검증기간 여부 판단) */
|
||||
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
|
||||
const [showDailog, setShowDailog] = useState<boolean>(false);
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
||||
@@ -147,6 +152,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setStatusInfo(null);
|
||||
setPeriodCheckList([]);
|
||||
setLastSearchedYear(null);
|
||||
setLastSearchedValues(null);
|
||||
setFooterFromApi(null);
|
||||
@@ -218,8 +224,9 @@ const SeweragePenetrationRateInp = () => {
|
||||
const response = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || '');
|
||||
|
||||
if (response.status === 'success') {
|
||||
const checkEnterable = response.data;
|
||||
// setIsEnterable(response.data);
|
||||
const periods = Array.isArray(response.data) ? response.data : [];
|
||||
setPeriodCheckList(periods);
|
||||
const checkEnterable = isInputPeriodCurrent(periods);
|
||||
setLastSearchedYear(values.year || null);
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable);
|
||||
setLastSearchedValues(values);
|
||||
@@ -232,6 +239,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
const params: SubmitExceptionRequest = {
|
||||
@@ -309,6 +321,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showGlobalProgress({ label: "제출 진행 중...", progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
@@ -388,7 +405,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData);
|
||||
if (response.status === 'success') {
|
||||
setGlobalProgress(50);
|
||||
|
||||
|
||||
if (response.data.finalStatus === '5') {
|
||||
const imrParam = {
|
||||
user_id: userInfo.userUnqId,
|
||||
@@ -442,6 +459,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
const saveFunc = async () => {
|
||||
try {
|
||||
if (await confirm('저장 하시겠습니까?')) {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
@@ -520,9 +542,9 @@ const SeweragePenetrationRateInp = () => {
|
||||
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
|
||||
};
|
||||
|
||||
// if (isAdminInfo) {
|
||||
// requestData.superAdmin = 'superAdmin';
|
||||
// }
|
||||
if (isAdminInfo) {
|
||||
requestData.superAdmin = 'superAdmin';
|
||||
}
|
||||
|
||||
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
|
||||
|
||||
@@ -547,6 +569,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
const cnacelSubmitFunc = async () => {
|
||||
try {
|
||||
if (await confirm('제출된 내용을 취소하시겠습니까?')) {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
@@ -600,11 +627,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
/>
|
||||
{showEnterableToolbarButtons && (
|
||||
<>
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
/>
|
||||
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
|
||||
<SubmitButton submitFunc={() => submitFunc(false)} />
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
Sgg,
|
||||
StatisticsLog,
|
||||
StatisticsLogRequest,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const CellColorInfo = () => {
|
||||
return (
|
||||
<div className="m-[0.6rem] flex justify-end">
|
||||
<div className="flex justify-end items-center gap-[1.2rem] !text-xl bg-white w-fit py-3 px-5 border border-slate-200/80 rounded-md">
|
||||
<div className="flex justify-center items-center gap-x-2">
|
||||
<div className="w-8 h-6 bg-[#feee85]"></div>
|
||||
<span>요약표</span>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-x-2">
|
||||
<div className="w-8 h-6 bg-[#fff7bf]"></div>
|
||||
<span>합계</span>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-x-2">
|
||||
<div className="w-8 h-6 bg-[#ffe9cf]"></div>
|
||||
<span>소계</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CellColorInfo;
|
||||
@@ -1,10 +1,202 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { SewagePipeMaintainColumnListTot } from "@/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SewagePipeMaintainTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface";
|
||||
|
||||
const firstTabKey = SewagePipeMaintainColumnListTot[0]?.tabKey ?? "ST00030001";
|
||||
|
||||
function buildSewagePipeMaintainTotSearchParam(values: SearchFormValues): SewagePipeMaintainTotalRequest {
|
||||
return {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
};
|
||||
}
|
||||
|
||||
const SewagePipeMaintainTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = SewagePipeMaintainColumnListTot;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const loadSewagePipeMaintainTotTabData = async (
|
||||
tabKey: string,
|
||||
values: SearchFormValues,
|
||||
mergeIntoExistingMap: boolean,
|
||||
): Promise<boolean> => {
|
||||
const param = buildSewagePipeMaintainTotSearchParam(values);
|
||||
|
||||
const applyRows = (rows: Record<string, string | number>[]) => {
|
||||
if (mergeIntoExistingMap) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, rows);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
const next = new Map<string, Record<string, string | number>[]>();
|
||||
next.set(tabKey, rows);
|
||||
setRowDataMap(next);
|
||||
}
|
||||
};
|
||||
|
||||
if (tabKey === "ST00030001") {
|
||||
const response = await statisticsTotalStatusApi.getSewagePipeMaintainRepairTotal(param);
|
||||
if (response.status === "success" && response.data) {
|
||||
applyRows(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tabKey === "ST00030002") {
|
||||
const response = await statisticsTotalStatusApi.getSewagePipeMaintainDredgingTotal(param);
|
||||
if (response.status === "success" && response.data) {
|
||||
applyRows(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let ok = false;
|
||||
if (activeTab === "ST00030001" || activeTab === "ST00030002") {
|
||||
ok = await loadSewagePipeMaintainTotTabData(activeTab, values, true);
|
||||
}
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
const hasData = Array.isArray(existing) && existing.length > 0;
|
||||
if (hasData) return;
|
||||
|
||||
if (tabKey === "ST00030001" || tabKey === "ST00030002") {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadSewagePipeMaintainTotTabData(tabKey, values, true);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => void handleActiveTabChange(v)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? "");
|
||||
if (rowType === "DETAIL") return undefined;
|
||||
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
|
||||
return "rg-cell-rowtype-other";
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
@@ -6,71 +6,165 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SewagePipeTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface";
|
||||
|
||||
const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001";
|
||||
|
||||
function buildSewagePipeTotSearchParam(values: SearchFormValues): SewagePipeTotalRequest {
|
||||
return {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
};
|
||||
}
|
||||
|
||||
const SewagePipeTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
/** 조회 성공 후 탭 전환 시 동일 조건으로 지연 조회 */
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = useMemo(
|
||||
() =>
|
||||
SewagePipeColumnInfoListTot.map((tab: ColumnInfoList) => ({
|
||||
...tab,
|
||||
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? ("rg-cell-readonly" as const),
|
||||
})),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
const columnInfo = SewagePipeColumnInfoListTot;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
/** SewagePipeInp의 `loadSewagePipeActiveTabData`와 동일 역할: 탭키별 API 호출 후 `rowDataMap` 반영 */
|
||||
const loadSewagePipeTotTabData = async (
|
||||
tabKey: string,
|
||||
values: SearchFormValues,
|
||||
mergeIntoExistingMap: boolean,
|
||||
): Promise<boolean> => {
|
||||
const param = buildSewagePipeTotSearchParam(values);
|
||||
|
||||
const applyRows = (rows: Record<string, string | number>[]) => {
|
||||
if (mergeIntoExistingMap) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, rows);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
const next = new Map<string, Record<string, string | number>[]>();
|
||||
next.set(tabKey, rows);
|
||||
setRowDataMap(next);
|
||||
}
|
||||
};
|
||||
|
||||
if (tabKey === "ST00020001") {
|
||||
const response = await statisticsTotalStatusApi.getSewagePipeRegionalStatusList(param);
|
||||
if (response.status === "success" && response.data) {
|
||||
applyRows(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tabKey === "ST00020002") {
|
||||
const response = await statisticsTotalStatusApi.getSewagePipeFactInstallStatus(param);
|
||||
if (response.status === "success" && response.data) {
|
||||
applyRows(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tabKey === "ST00020003") {
|
||||
const response = await statisticsTotalStatusApi.getSewagePipePipeTypeFacilityStatus(param);
|
||||
if (response.status === "success" && response.data) {
|
||||
applyRows(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tabKey === "ST00020004") {
|
||||
const response = await statisticsTotalStatusApi.getSewagePipePipeDiamStatus(param);
|
||||
if (response.status === "success" && response.data) {
|
||||
applyRows(toGridRows(response.data as object[]));
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
setActiveTab(firstTabKey);
|
||||
setRowDataMap(new Map());
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const param = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
};
|
||||
|
||||
const response = await statisticsTotalStatusApi.getSewagePipeRegionalStatusList(param);
|
||||
|
||||
if (response.status === "success" && response.data) {
|
||||
const next = new Map<string, Record<string, string | number>[]>();
|
||||
next.set("ST00020001", toGridRows(response.data as object[]));
|
||||
setRowDataMap(next);
|
||||
// SewagePipeInp과 동일: 검색 시 활성 탭 데이터만 조회·맵 병합
|
||||
let ok = false;
|
||||
if (
|
||||
activeTab === "ST00020001" ||
|
||||
activeTab === "ST00020002" ||
|
||||
activeTab === "ST00020003" ||
|
||||
activeTab === "ST00020004"
|
||||
) {
|
||||
ok = await loadSewagePipeTotTabData(activeTab, values, true);
|
||||
}
|
||||
if (ok) {
|
||||
lastSearchedValuesRef.current = values;
|
||||
} else {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchedValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
const hasData = Array.isArray(existing) && existing.length > 0;
|
||||
if (hasData) return;
|
||||
|
||||
if (
|
||||
tabKey === "ST00020001" ||
|
||||
tabKey === "ST00020002" ||
|
||||
tabKey === "ST00020003" ||
|
||||
tabKey === "ST00020004"
|
||||
) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadSewagePipeTotTabData(tabKey, values, true);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[SewagePipeTot] tabKey (API 미연결):", tabKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} />
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
@@ -81,7 +175,7 @@ const SewagePipeTot = () => {
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
onValueChange={(v) => void handleActiveTabChange(v)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import CellColorInfo from "./CellColorInfo";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
const SeweragePenetrationRateTot = () => {
|
||||
@@ -19,12 +20,17 @@ const SeweragePenetrationRateTot = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const isSummaryEnabled = values.isSummary !== 'ALL';
|
||||
const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined;
|
||||
|
||||
const param = {
|
||||
ctpvNm: values.sido === 'ALL' ? '' : values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail
|
||||
rowDataYn: values.isDetail,
|
||||
summaryYn: isSummaryEnabled,
|
||||
...(isSummaryEnabled ? { summaryYearCnt } : {}),
|
||||
};
|
||||
|
||||
const response = await statisticsTotalStatusApi.getSewerageSupplyRateList(param);
|
||||
@@ -35,7 +41,8 @@ const SeweragePenetrationRateTot = () => {
|
||||
} else {
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -44,12 +51,13 @@ const SeweragePenetrationRateTot = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} sidoSelectText="전체" />
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" />
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<CellColorInfo />
|
||||
<DataGrid
|
||||
columnInfo={SeweragePentrationRateTotColumn}
|
||||
rowStartNum={2}
|
||||
@@ -61,6 +69,7 @@ const SeweragePenetrationRateTot = () => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import type { ColumnConfig, ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { ComplaintInpColumn } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn";
|
||||
import { ResidentFriendlyFactTotColumn } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn";
|
||||
@@ -17,81 +17,111 @@ const SewerageServiceLevelInpTabList = [
|
||||
ComplaintInpColumn,
|
||||
ResidentFriendlyFactTotColumn,
|
||||
];
|
||||
const SUPPORTED_TAB_KEYS = [ComplaintInpColumn.tabKey, ResidentFriendlyFactTotColumn.tabKey] as const;
|
||||
|
||||
const SewerageServiceLevelTot = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState(ComplaintInpColumn.tabKey);
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const lastSearchValuesRef = useRef<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = useMemo(
|
||||
() =>
|
||||
SewerageServiceLevelInpTabList.map((tab: ColumnInfoList) => ({
|
||||
...tab,
|
||||
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? ("rg-cell-readonly" as const),
|
||||
})),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
const columnInfo = SewerageServiceLevelInpTabList;
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(ComplaintInpColumn.tabKey);
|
||||
setLastSearchValues(null);
|
||||
lastSearchValuesRef.current = null;
|
||||
};
|
||||
|
||||
const fetchByTab = async (tabKey: string, values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const loadSewerageServiceLevelTotTabData = async (
|
||||
tabKey: string,
|
||||
values: SearchFormValues,
|
||||
mergeIntoExistingMap: boolean,
|
||||
) => {
|
||||
const param = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
};
|
||||
|
||||
const param = {
|
||||
ctpvNm: values.sido === "ALL" ? "" : values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
};
|
||||
const fetcherMap = {
|
||||
[ComplaintInpColumn.tabKey]: statisticsTotalStatusApi.getSewerageServiceLevelComplaint.bind(
|
||||
statisticsTotalStatusApi,
|
||||
),
|
||||
[ResidentFriendlyFactTotColumn.tabKey]:
|
||||
statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact.bind(
|
||||
statisticsTotalStatusApi,
|
||||
),
|
||||
};
|
||||
const fetcher = fetcherMap[tabKey];
|
||||
if (!fetcher) {
|
||||
return false;
|
||||
}
|
||||
const response = await fetcher(param);
|
||||
|
||||
let response;
|
||||
if (tabKey === ResidentFriendlyFactTotColumn.tabKey) {
|
||||
response =
|
||||
await statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact(param);
|
||||
} else {
|
||||
response =
|
||||
await statisticsTotalStatusApi.getSewerageServiceLevelComplaint(param);
|
||||
}
|
||||
|
||||
if (response.status === "success" && response.data) {
|
||||
if (response.status === "success" && response.data) {
|
||||
const rows = toGridRows(response.data as object[]);
|
||||
if (mergeIntoExistingMap) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, toGridRows(response.data as object[]));
|
||||
next.set(tabKey, rows);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
const next = new Map<string, Record<string, string | number>[]>();
|
||||
next.set(tabKey, rows);
|
||||
setRowDataMap(next);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
let ok = false;
|
||||
if (SUPPORTED_TAB_KEYS.includes(activeTab)) {
|
||||
ok = await loadSewerageServiceLevelTotTabData(activeTab, values, true);
|
||||
}
|
||||
if (ok) {
|
||||
lastSearchValuesRef.current = values;
|
||||
} else {
|
||||
lastSearchValuesRef.current = null;
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
lastSearchValuesRef.current = null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
setLastSearchValues(values);
|
||||
setRowDataMap(new Map());
|
||||
await fetchByTab(activeTab, values);
|
||||
};
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSearchValues == null) return;
|
||||
if (rowDataMap.has(activeTab)) return;
|
||||
void fetchByTab(activeTab, lastSearchValues);
|
||||
}, [activeTab]);
|
||||
const values = lastSearchValuesRef.current;
|
||||
if (!values) return;
|
||||
|
||||
const existing = rowDataMap.get(tabKey);
|
||||
const hasData = Array.isArray(existing) && existing.length > 0;
|
||||
if (hasData) return;
|
||||
|
||||
if (SUPPORTED_TAB_KEYS.includes(tabKey)) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadSewerageServiceLevelTotTabData(tabKey, values, true);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -103,20 +133,60 @@ const SewerageServiceLevelTot = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? "");
|
||||
if (rowType === "DETAIL") return undefined;
|
||||
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
|
||||
return "rg-cell-rowtype-other";
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n = typeof updater === "function" ? updater(cur) : updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
isExcelImport={false}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? "");
|
||||
if (rowType === "DETAIL") return undefined;
|
||||
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
|
||||
return "rg-cell-rowtype-other";
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { NoData } from '@/shared/ui/no-data';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
|
||||
@@ -3,9 +3,15 @@ import React from "react";
|
||||
import { ValueType } from "realgrid";
|
||||
import type { RealGridReact } from "realgrid-react";
|
||||
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
// RealGrid 타입 정의
|
||||
|
||||
/** DataGrid 상단 진행상황/제출일/담당자 표시에 사용 (보급률·하수관로 등 공통) */
|
||||
export type DataGridStatusBannerInfo = {
|
||||
laeSbmsnSttsCd: string | null;
|
||||
laeSbmsnYmd: string | null;
|
||||
laePicNm: string | null;
|
||||
laePicTelNo: string | null;
|
||||
};
|
||||
/** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */
|
||||
export type RealGridColumnInfo = {
|
||||
name: string;
|
||||
@@ -19,30 +25,30 @@ export type RealGridColumnInfo = {
|
||||
mergeRule?: { criteria: "value" | string };
|
||||
sortable?: boolean;
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>;
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>;
|
||||
};
|
||||
|
||||
/** setColumns 메서드가 있는 GridView */
|
||||
@@ -111,21 +117,21 @@ export type DataFieldValueCallback = (
|
||||
export type ComputedFieldConfig =
|
||||
| { fields: string[]; type: "sum" | "avg" }
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "pblswrgPopltnSuprtRate";
|
||||
}
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "pblswrgPopltnSuprtRate";
|
||||
}
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "adtrtPopltnSuprtRate";
|
||||
}
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
type: "adtrtPopltnSuprtRate";
|
||||
}
|
||||
| {
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
thirdFields: string[];
|
||||
type: "swrgInstlrtRate";
|
||||
};
|
||||
numeratorFields: string[];
|
||||
denominatorFields: string[];
|
||||
thirdFields: string[];
|
||||
type: "swrgInstlrtRate";
|
||||
};
|
||||
|
||||
export type ColumnConfig = {
|
||||
name: string;
|
||||
@@ -148,31 +154,31 @@ export type ColumnConfig = {
|
||||
valueType?: ValueType;
|
||||
dataFormat?: string;
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
multiRow?: boolean; // 여러 행 footer 사용 여부
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
|
||||
| {
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
multiRow?: boolean; // 여러 행 footer 사용 여부
|
||||
}
|
||||
| Array<{
|
||||
text?: string;
|
||||
expression?: "sum" | "avg" | "count" | "max" | "min";
|
||||
numberFormat?: string;
|
||||
valueCallback?: (
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: ColumnFooter,
|
||||
value: number | string | undefined,
|
||||
) => number | string;
|
||||
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
|
||||
};
|
||||
|
||||
export type FooterUserSpan = {
|
||||
@@ -184,16 +190,16 @@ export type FooterUserSpan = {
|
||||
export type ColumnLayoutItem =
|
||||
| string
|
||||
| {
|
||||
name: string;
|
||||
hideChildHeaders?: boolean;
|
||||
header: { text: string };
|
||||
items: (string | ColumnLayoutItem)[];
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
}
|
||||
name: string;
|
||||
hideChildHeaders?: boolean;
|
||||
header: { text: string };
|
||||
items: (string | ColumnLayoutItem)[];
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
}
|
||||
| {
|
||||
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
};
|
||||
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
};
|
||||
|
||||
export type ColumnInfo = {
|
||||
columns: ColumnConfig[];
|
||||
@@ -285,7 +291,7 @@ export type DataGridProps = {
|
||||
/** CheckBar 헤더 전체 선택/해제 */
|
||||
onItemAllChecked?: (checked: boolean) => void;
|
||||
onScrollToBottom?: () => void;
|
||||
statusInfo?: SewerageSupplyRateStatusInfo | null;
|
||||
statusInfo?: DataGridStatusBannerInfo | null;
|
||||
/** 강조 및 스크롤할 행의 ID */
|
||||
highlightedRowId?: string | number | null;
|
||||
/** ID로 사용할 필드명 (기본값: 'id') */
|
||||
|
||||
@@ -6,6 +6,7 @@ export type SearchFormValues = {
|
||||
fctName?: string;
|
||||
bsnmCd?: string;
|
||||
isDetail?: boolean;
|
||||
isSummary?: string;
|
||||
};
|
||||
|
||||
export type SearchFormProps = {
|
||||
@@ -17,6 +18,7 @@ export type SearchFormProps = {
|
||||
fctName?: boolean;
|
||||
userMenual?: boolean;
|
||||
isDetail?: boolean;
|
||||
isSummary?: boolean;
|
||||
onSubmit?: (values: SearchFormValues) => void;
|
||||
onReset?: () => void;
|
||||
onValuesChange?: (values: SearchFormValues) => void;
|
||||
@@ -33,4 +35,5 @@ export const defaultSearchFormProps: Required<
|
||||
fctName: false,
|
||||
userMenual: false,
|
||||
isDetail: false,
|
||||
isSummary: false,
|
||||
};
|
||||
@@ -20,4 +20,15 @@ export const needBsnmFrgnrPoplSmSidoList = [
|
||||
"4700000000",
|
||||
"4800000000",
|
||||
"5000000000",
|
||||
];
|
||||
|
||||
export const doNameList = [
|
||||
"경기도",
|
||||
"강원특별자치도",
|
||||
"충청북도",
|
||||
"충청남도",
|
||||
"전북특별자치도",
|
||||
"전라남도",
|
||||
"경상북도",
|
||||
"경상남도",
|
||||
];
|
||||
@@ -49,18 +49,18 @@ export const AdminAreaConnectionInpColumn = {
|
||||
export const TreatPlantConnectionInpColumn = {
|
||||
columns: [
|
||||
// 하수처리장 정보
|
||||
{ name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' }, editable: false },
|
||||
{ name: 'fctCpc', fieldName: 'fctCpc', header: { text: '용량' }, valueType: ValueType.NUMBER, dataFormat: '#,##0', editable: false },
|
||||
{ name: 'bscFctCd', fieldName: 'bscFctCd', header: { text: '하수처리장코드' }, editable: false },
|
||||
{ name: 'fcltNm', fieldName: 'fcltNm', header: { text: '하수처리장명' }, editable: false },
|
||||
{ name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '용량' }, valueType: ValueType.NUMBER, dataFormat: '#,##0', editable: false },
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '하수처리장코드' }, editable: false },
|
||||
// 연결여부
|
||||
{ name: 'isConnection', fieldName: 'isConnection', header: { text: '연결여부' }, editable: false },
|
||||
// 연결지역
|
||||
{ name: 'connectionAreaList', fieldName: 'connectionAreaList', header: { text: '연결지역' }, editable: false },
|
||||
],
|
||||
columnLayout: [
|
||||
'bscFctNm',
|
||||
'fctCpc',
|
||||
'bscFctCd',
|
||||
'fcltNm',
|
||||
'fcltCpct',
|
||||
'fcltSn',
|
||||
'isConnection',
|
||||
'connectionAreaList',
|
||||
],
|
||||
|
||||
@@ -58,7 +58,7 @@ export const FactInstallStatusInpColumn = {
|
||||
|
||||
// 식별·키 필드 (API SeweragePipeInputFactInstallStatusItem, 그리드 비표시 — fixedColumns와 겹치지 않도록 맨 뒤)
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'prcsFcltSn', fieldName: 'prcsFcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT },
|
||||
{ name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
|
||||
],
|
||||
columnLayout: [
|
||||
@@ -146,7 +146,7 @@ export const FactInstallStatusInpColumn = {
|
||||
},
|
||||
'rmrk',
|
||||
'admdstCd',
|
||||
'prcsFcltSn',
|
||||
'fcltSn',
|
||||
'swtrmtFcltCd',
|
||||
],
|
||||
rowStartNum: 4,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import dayjs from "dayjs";
|
||||
import { headers } from "next/headers";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
/**
|
||||
@@ -7,9 +5,9 @@ import { ValueType } from "realgrid";
|
||||
* 항목은 검토(SewagePipeRvwColumn)와 동일하나 footer·mergeCells·구분(displayYear)·비고(rmrk) 제외.
|
||||
* 표시 형식은 SeweragePentrationRateTotColumn과 같이 dataFormat + editable: false.
|
||||
*/
|
||||
const currentYear = dayjs().year() - 1;
|
||||
const startYear = 1996;
|
||||
const totalCount = currentYear - startYear + 1 + 1;
|
||||
|
||||
/** ST00020003 관종별: 입력 `SeweragePipeInputPipeTypeStatusItem`·합계 `SewagePipePipeTypeFacilityStatusItem`과 동일 (yy1=1996년 이전 … yy30) */
|
||||
const YY_PIPE_TYPE_FACILITY_MAX = 30;
|
||||
|
||||
const rawSewagePipeTotList = [
|
||||
{
|
||||
@@ -642,11 +640,11 @@ const rawSewagePipeTotList = [
|
||||
dataFormat: "#,##0.0",
|
||||
editable: false,
|
||||
},
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
const dtoNum = totalCount - i;
|
||||
const wholField = `yy${dtoNum}PrlgWhol`;
|
||||
const lyngField = `yy${dtoNum}LyngPrlg`;
|
||||
const dmltnField = `yy${dtoNum}DmltnPrlg`;
|
||||
...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX }, (_, i) => {
|
||||
const yyIdx = i + 1;
|
||||
const wholField = `yy${yyIdx}PrlgWhol`;
|
||||
const lyngField = `yy${yyIdx}LyngPrlg`;
|
||||
const dmltnField = `yy${yyIdx}DmltnPrlg`;
|
||||
return [
|
||||
{
|
||||
name: wholField,
|
||||
@@ -687,13 +685,18 @@ const rawSewagePipeTotList = [
|
||||
header: { text: "총계" },
|
||||
items: ["pipeByPrstWholSum", "lyngPrlgSum", "dmltnPrlgSum"],
|
||||
},
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
const dtoNum = totalCount - i;
|
||||
const yearLabel = i === 0 ? "1996년이전" : `${startYear + (i - 1)}년`;
|
||||
{
|
||||
name: "year1996Before",
|
||||
header: { text: "1996년이전" },
|
||||
items: ["yy1PrlgWhol", "yy1LyngPrlg", "yy1DmltnPrlg"],
|
||||
},
|
||||
...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX - 1 }, (_, i) => {
|
||||
const yyIdx = i + 2;
|
||||
const year = 1994 + yyIdx;
|
||||
return {
|
||||
name: `group_${yearLabel}`,
|
||||
header: { text: yearLabel },
|
||||
items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`],
|
||||
name: `group_yy${yyIdx}`,
|
||||
header: { text: `${year}년` },
|
||||
items: [`yy${yyIdx}PrlgWhol`, `yy${yyIdx}LyngPrlg`, `yy${yyIdx}DmltnPrlg`],
|
||||
};
|
||||
}),
|
||||
],
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
// 하수도 통계 관리 > 03.하수관로 유지관리 > tab 2. 관로준설
|
||||
export const PipeDredgingStatusTotColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' } },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' } },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' } },
|
||||
|
||||
// 하수관로 총 시설연장
|
||||
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '하수관로총시설연장(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
|
||||
// 준설연장 및 개소수 > 관로(m)
|
||||
{ name: 'wholSwagePpl', fieldName: 'wholSwagePpl', header: { text: '합계' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'cbsysPplPrlg', fieldName: 'cbsysPplPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 준설연장 및 개소수 > 관로(m) > 분류식
|
||||
{ name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 준설연장 및 개소수 > 맨홀, 오·우수받이, 토실·토구
|
||||
{ name: 'mnhlCnt', fieldName: 'mnhlCnt', header: { text: '맨홀(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'exlnDrtwRcvprpCnt', fieldName: 'exlnDrtwRcvprpCnt', header: { text: '오·우수받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'stofcbrOtflCnt', fieldName: 'stofcbrOtflCnt', header: { text: '토실·토구(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 계획 & 비율
|
||||
{ name: 'drdgnPlanPrlg', fieldName: 'drdgnPlanPrlg', header: { text: '준설 계획 연장(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'drdgnTrgtCtrsDrdgnRt', fieldName: 'drdgnTrgtCtrsDrdgnRt', header: { text: '계획대비 준설율(%)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'wholPrlgCtrsDrdgnRt', fieldName: 'wholPrlgCtrsDrdgnRt', header: { text: '총시설연장대비 준설율(%)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 준설실적
|
||||
{ name: 'drdgnAmt', fieldName: 'drdgnAmt', header: { text: '금액(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'drdgnQnt', fieldName: 'drdgnQnt', header: { text: '준설량(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 준설토사처리
|
||||
{ name: 'drdgnUntprcAmt', fieldName: 'drdgnUntprcAmt', header: { text: '준설토처리비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wholDrdgnPrcsQnt', fieldName: 'wholDrdgnPrcsQnt', header: { text: '처리량 계(톤)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'rclmQnt', fieldName: 'rclmQnt', header: { text: '매립' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'oceanEmsnQnt', fieldName: 'oceanEmsnQnt', header: { text: '해양투기' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타(소각, 고형화, 시멘트원료 등)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
].map((col) => ({ ...col, editable: false })),
|
||||
columnLayout: [
|
||||
{ name: 'group_admin', header: { text: '행정구역' }, items: ['ctpvNm', 'sggNm', 'admdstNm'] },
|
||||
'wholFcltPrlg',
|
||||
{
|
||||
name: 'group_dredge_info',
|
||||
header: { text: '준설연장 및 개소수' },
|
||||
items: [
|
||||
{
|
||||
name: 'group_pipe',
|
||||
header: { text: '관로(m)' },
|
||||
items: [
|
||||
'wholSwagePpl', 'cbsysPplPrlg',
|
||||
{ name: 'group_sep', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] }
|
||||
]
|
||||
},
|
||||
'mnhlCnt', 'exlnDrtwRcvprpCnt', 'stofcbrOtflCnt'
|
||||
]
|
||||
},
|
||||
'drdgnPlanPrlg', 'drdgnTrgtCtrsDrdgnRt', 'wholPrlgCtrsDrdgnRt',
|
||||
{
|
||||
name: 'group_perf',
|
||||
header: { text: '준설실적' },
|
||||
items: ['drdgnAmt', 'drdgnQnt']
|
||||
},
|
||||
{
|
||||
name: 'group_soil_treat',
|
||||
header: { text: '준설토사처리' },
|
||||
items: ['drdgnUntprcAmt', 'wholDrdgnPrcsQnt', 'rclmQnt', 'oceanEmsnQnt', 'rcycQnt', 'etcQnt']
|
||||
},
|
||||
],
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 1,
|
||||
tabKey: 'ST00030002',
|
||||
tabLabel: '관로준설'
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
// 하수도 통계 관리 > 03.하수관로 유지관리 > tab 1. 개보수
|
||||
export const RepairStatusTotColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' }, editable: false },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false },
|
||||
|
||||
// 개·보수관로 > 전체보수(m)
|
||||
{ name: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '계' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 개·보수관로 > 전체보수(m) > 분류식
|
||||
{ name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 개·보수관로 > 부분보수(개소)
|
||||
{ name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '계' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 개·보수관로 > 부분보수(개소) > 분류식
|
||||
{ name: 'sprsysExlnMendngPrlg', fieldName: 'sprsysExlnMendngPrlg', header: { text: '우수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'sprsysDrtwMendngPrlg', fieldName: 'sprsysDrtwMendngPrlg', header: { text: '오수' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 맨홀(개소)
|
||||
{ name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 맨홀(개소) > 분류식
|
||||
{ name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 받이
|
||||
{ name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 토실, 토구
|
||||
{ name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '토실(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'otflCnt', fieldName: 'otflCnt', header: { text: '토구(개소)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 수밀검사CCTV조사량(m)
|
||||
{ name: 'cctvExmnPrlg', fieldName: 'cctvExmnPrlg', header: { text: '수밀검사·CCTV조사량(m)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
].map((col) => ({ ...col, editable: false })),
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'group_admin',
|
||||
header: { text: '행정구역' },
|
||||
items: ['ctpvNm', 'sggNm', 'admdstNm']
|
||||
},
|
||||
{
|
||||
name: 'group_repair_main',
|
||||
header: { text: '개·보수관로' },
|
||||
items: [
|
||||
{
|
||||
name: 'group_full_repair',
|
||||
header: { text: '전체보수(m)' },
|
||||
items: [
|
||||
'mendngWhol',
|
||||
'cbsysMendngPrlg',
|
||||
{
|
||||
name: 'group_full_sep',
|
||||
header: { text: '분류식' },
|
||||
items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'group_part_repair',
|
||||
header: { text: '부분보수(개소)' },
|
||||
items: [
|
||||
'partMendngWhol',
|
||||
'cbsysPartMendngPrlg',
|
||||
{
|
||||
name: 'group_part_sep',
|
||||
header: { text: '분류식' },
|
||||
items: ['sprsysExlnMendngPrlg', 'sprsysDrtwMendngPrlg']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'group_manhole',
|
||||
header: { text: '맨홀(개소)' },
|
||||
items: [
|
||||
'cbsysMnhlCnt',
|
||||
{
|
||||
name: 'group_mh_sep',
|
||||
header: { text: '분류식' },
|
||||
items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'group_inlet',
|
||||
header: { text: '받이' },
|
||||
items: ['drtwRcvprpCnt', 'exlnRcvprpCnt']
|
||||
},
|
||||
{
|
||||
name: 'group_chamber',
|
||||
header: { text: '토실,토구' },
|
||||
items: ['stofcbrCnt', 'otflCnt']
|
||||
},
|
||||
'cctvExmnPrlg',
|
||||
],
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 1,
|
||||
tabKey: 'ST00030001',
|
||||
tabLabel: '개보수'
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { PipeDredgingStatusTotColumn } from "./PipeDredgingStatusTotColumn"
|
||||
import { RepairStatusTotColumn } from "./RepairStatusTotColumn"
|
||||
|
||||
const sewagePipeMaintainColumn = [
|
||||
RepairStatusTotColumn,
|
||||
PipeDredgingStatusTotColumn
|
||||
];
|
||||
|
||||
export const SewagePipeMaintainColumnListTot = sewagePipeMaintainColumn.map((tab) => ({
|
||||
...tab,
|
||||
columns: tab.columns.map((col) => ({ ...col, editable: false as const })),
|
||||
}));
|
||||
@@ -3,6 +3,8 @@ import { stClient } from "@/shared/api/client";
|
||||
import type {
|
||||
SeweragePipeFacilityAdminAreaListRequest,
|
||||
SeweragePipeFacilityAdminAreaListResponse,
|
||||
SeweragePipeFactConnectionSaveRequest,
|
||||
SeweragePipeFactConnectionSaveResponse,
|
||||
SeweragePipeFacilitiesListRequest,
|
||||
SeweragePipeFacilitiesListResponse,
|
||||
SeweragePipeInputConnectionStatusResponse,
|
||||
@@ -12,9 +14,13 @@ import type {
|
||||
SeweragePipeInputPipeTypeResponse,
|
||||
SeweragePipeInputRegionalFactStatusSaveRequest,
|
||||
SeweragePipeInputRegionalFactStatusSaveResponse,
|
||||
SeweragePipeInputRegionalFactStatusSubmitRequest,
|
||||
SeweragePipeInputRegionalFactStatusSubmitResponse,
|
||||
SeweragePipeInputRegionalResponse,
|
||||
SeweragePipeRequestParams,
|
||||
StatisticsInputSewPipeService,
|
||||
SeweragePipeInputFactInstallStatusSaveRequest,
|
||||
SeweragePipeInputFactInstallStatusSaveResponse,
|
||||
} from "../../model/input/statistics-sewage-pipe-input-interface";
|
||||
|
||||
export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeService {
|
||||
@@ -82,6 +88,14 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise<SeweragePipeFactConnectionSaveResponse> {
|
||||
const response = await stClient.post<SeweragePipeFactConnectionSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/save`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveResponse> {
|
||||
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`,
|
||||
@@ -89,6 +103,22 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise<SeweragePipeInputRegionalFactStatusSubmitResponse> {
|
||||
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSubmitResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/submit`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise<SeweragePipeInputFactInstallStatusSaveResponse> {
|
||||
const response = await stClient.post<SeweragePipeInputFactInstallStatusSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/save`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
export const statisticsInputSewPipeApiService = new StatisticsInputSewPipeApiService();
|
||||
@@ -2,7 +2,18 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { stClient } from "@/shared/api/client";
|
||||
|
||||
import type { SewerageSupplyRateRequest, SewerageSupplyRateResponse } from "../model/statistics-total/statistics-sewage-supply-rate-total-interface";
|
||||
import type { SewagePipeTotalRequest, SewagePipeTotalResponse } from "../model/statistics-total/statistics-sewage-pipe-total-interface";
|
||||
import type {
|
||||
SewagePipeMaintainDredgingTotalResponse,
|
||||
SewagePipeMaintainTotalRequest,
|
||||
SewagePipeMaintainRepairTotalResponse,
|
||||
} from "../model/statistics-total/statistics-sewage-pipe-maintain-total-interface";
|
||||
import type {
|
||||
SewagePipeFactInstallStatusResponse,
|
||||
SewagePipePipeDiamStatusResponse,
|
||||
SewagePipePipeTypeFacilityStatusResponse,
|
||||
SewagePipeTotalRequest,
|
||||
SewagePipeTotalResponse,
|
||||
} from "../model/statistics-total/statistics-sewage-pipe-total-interface";
|
||||
import type {
|
||||
SewerageServiceLevelComplaintResponse,
|
||||
SewerageServiceLevelRequest,
|
||||
@@ -41,6 +52,60 @@ export class StatisticsTotalApiService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSewagePipeFactInstallStatus(
|
||||
params: SewagePipeTotalRequest,
|
||||
): Promise<SewagePipeFactInstallStatusResponse> {
|
||||
const response = await stClient.get<SewagePipeFactInstallStatusResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/facility-installation-status`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** ST00020003 — 관종·관경별 시설현황(합계) */
|
||||
public async getSewagePipePipeTypeFacilityStatus(
|
||||
params: SewagePipeTotalRequest,
|
||||
): Promise<SewagePipePipeTypeFacilityStatusResponse> {
|
||||
const response = await stClient.get<SewagePipePipeTypeFacilityStatusResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/pipe-type-facility-status`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** ST00020004 — 관경별 시설현황(합계) */
|
||||
public async getSewagePipePipeDiamStatus(
|
||||
params: SewagePipeTotalRequest,
|
||||
): Promise<SewagePipePipeDiamStatusResponse> {
|
||||
const response = await stClient.get<SewagePipePipeDiamStatusResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/pipe-diam-status`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** ST00030001 — 하수관로 유지관리 > 개보수(합계) */
|
||||
public async getSewagePipeMaintainRepairTotal(
|
||||
params: SewagePipeMaintainTotalRequest,
|
||||
): Promise<SewagePipeMaintainRepairTotalResponse> {
|
||||
const response = await stClient.get<SewagePipeMaintainRepairTotalResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe-mtc/repair`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** ST00030002 — 하수관로 유지관리 > 관로준설(합계) */
|
||||
public async getSewagePipeMaintainDredgingTotal(
|
||||
params: SewagePipeMaintainTotalRequest,
|
||||
): Promise<SewagePipeMaintainDredgingTotalResponse> {
|
||||
const response = await stClient.get<SewagePipeMaintainDredgingTotalResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe-mtc/dredging`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsTotalStatusApi = new StatisticsTotalApiService();
|
||||
+95
-12
@@ -22,6 +22,10 @@ export interface SeweragePipeInputStatusInfo {
|
||||
export interface VerifyResultItem {
|
||||
vrfcCd: string;
|
||||
rsnExpln: string;
|
||||
/** 현재값(문자열로 내려오는 경우가 많음) */
|
||||
nowVlCn?: string;
|
||||
/** 비교값 */
|
||||
compVlCn?: string;
|
||||
}
|
||||
|
||||
/** 필드명 -> 검증 결과 배열 */
|
||||
@@ -106,6 +110,12 @@ export type SeweragePipeInputRegionalFactStatusSaveItem = Omit<
|
||||
"verificationResult"
|
||||
>;
|
||||
|
||||
/** Tab2 저장 — saveList */
|
||||
export type SeweragePipeInputFactInstallStatusSaveItem = Omit<
|
||||
SeweragePipeInputFactInstallStatusItem,
|
||||
"verificationResult"
|
||||
>;
|
||||
|
||||
/** 지역별 시설현황 저장 API 요청 */
|
||||
export interface SeweragePipeInputRegionalFactStatusSaveRequest {
|
||||
statsYr: string;
|
||||
@@ -118,7 +128,50 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest {
|
||||
}
|
||||
|
||||
export interface SeweragePipeInputRegionalFactStatusSaveResponse {
|
||||
data: string;
|
||||
data?: string;
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** 지역별 시설현황 제출 API 요청 (save item 구조 재사용) */
|
||||
export interface SeweragePipeInputRegionalFactStatusSubmitRequest {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
laePicNm: string;
|
||||
laePicTelNo: string;
|
||||
laeKbrdrDeptNm: string;
|
||||
laeKbrdrJbgdNm: string;
|
||||
laeIdfrNm: string;
|
||||
laeIdfrTelNo: string;
|
||||
laeIdfrDeptNm: string;
|
||||
laeIdfrJbgdNm: string;
|
||||
submitList: SeweragePipeInputRegionalFactStatusSaveItem[];
|
||||
}
|
||||
|
||||
export interface SeweragePipeInputRegionalFactStatusSubmitResponse {
|
||||
data: {
|
||||
finalStatus: string;
|
||||
nvpassed: boolean;
|
||||
}
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface SeweragePipeInputFactInstallStatusSaveRequest {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
superAdmin: string;
|
||||
saveList: SeweragePipeInputFactInstallStatusSaveItem[];
|
||||
}
|
||||
|
||||
export interface SeweragePipeInputFactInstallStatusSaveResponse {
|
||||
data?: string;
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
@@ -128,7 +181,7 @@ export interface SeweragePipeInputFactInstallStatusItem {
|
||||
admdstCd: string;
|
||||
sggNm: string;
|
||||
admdstNm: string;
|
||||
prcsFcltSn: number;
|
||||
fcltSn: string;
|
||||
swtrmtFcltCd: string;
|
||||
swtrmtFcltNm: string;
|
||||
prcsFcltCpct: number;
|
||||
@@ -425,9 +478,9 @@ export interface SeweragePipeInputManholeResponse {
|
||||
|
||||
/** 시설별 현황 관리 - 하수처리장별 연결 현황 목록 항목 */
|
||||
export interface SeweragePipeInputFacilityConnectionItem {
|
||||
bscFctNm: string;
|
||||
fctCpc: number;
|
||||
bscFctCd: string;
|
||||
fcltNm: string;
|
||||
fcltCpct: number;
|
||||
fcltSn: string;
|
||||
isConnection: string;
|
||||
connectionAreaList: string;
|
||||
}
|
||||
@@ -472,9 +525,10 @@ export interface SeweragePipeFacilitiesListItem {
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
admdstCd: string;
|
||||
bscFctNm: string;
|
||||
fctCpc: number;
|
||||
bscFctCd: string;
|
||||
fcltNm: string;
|
||||
fcltCpct: number;
|
||||
fcltCd: string;
|
||||
fcltSn: string;
|
||||
}
|
||||
|
||||
/** facilitiesList API 응답 */
|
||||
@@ -490,7 +544,7 @@ export interface SeweragePipeFacilityAdminAreaListRequest {
|
||||
swtrmtFcltCd: string;
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sggNm?: string;
|
||||
}
|
||||
|
||||
/** 처리시설별 행정구역 목록 항목 */
|
||||
@@ -509,6 +563,34 @@ export interface SeweragePipeFacilityAdminAreaListResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 시설별 연결관계 저장 — saveList 항목 */
|
||||
export interface SeweragePipeFactConnectionSaveItem {
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
}
|
||||
|
||||
/** 시설별 연결관계 저장 요청 */
|
||||
export interface SeweragePipeFactConnectionSaveRequest {
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
fcltSn: string;
|
||||
statsYr: string;
|
||||
swtrmtFcltCd: string;
|
||||
swtrmtFcltNm: string;
|
||||
prcsFcltCpct: number;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
saveList: SeweragePipeFactConnectionSaveItem[];
|
||||
deleteList: string[];
|
||||
}
|
||||
|
||||
/** 시설별 연결관계 저장 응답 */
|
||||
export interface SeweragePipeFactConnectionSaveResponse {
|
||||
status: string;
|
||||
data?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface StatisticsInputSewPipeService {
|
||||
getRegionalFactStatus(param: SeweragePipeRequestParams): Promise<SeweragePipeInputRegionalResponse>;
|
||||
getFactInstallStatus(param: SeweragePipeRequestParams): Promise<SeweragePipeInputFactInstallResponse>;
|
||||
@@ -518,7 +600,8 @@ export interface StatisticsInputSewPipeService {
|
||||
getConnectionStatus(param: SeweragePipeRequestParams): Promise<SeweragePipeInputConnectionStatusResponse>;
|
||||
getFacilitiesList(param: SeweragePipeFacilitiesListRequest): Promise<SeweragePipeFacilitiesListResponse>;
|
||||
getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise<SeweragePipeFacilityAdminAreaListResponse>;
|
||||
saveRegionalFactStatus(
|
||||
param: SeweragePipeInputRegionalFactStatusSaveRequest,
|
||||
): Promise<SeweragePipeInputRegionalFactStatusSaveResponse>;
|
||||
saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise<SeweragePipeFactConnectionSaveResponse>;
|
||||
saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveResponse>;
|
||||
submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise<SeweragePipeInputRegionalFactStatusSubmitResponse>;
|
||||
saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise<SeweragePipeInputFactInstallStatusSaveResponse>;
|
||||
}
|
||||
+52
-2
@@ -167,11 +167,61 @@ export interface SewerageSupplyRateSubmitResponse {
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 하수도 통계 입력 기간 조회
|
||||
// 하수도 통계 입력 기간 조회 (입력 INPT / 검증 VRFC 구간)
|
||||
export interface CheckInputPeriodItem {
|
||||
statsYr: string;
|
||||
seCd: number;
|
||||
seType: string;
|
||||
bgngYmd: string;
|
||||
endYmd: string;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
export interface CheckInputPeriodResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
data: boolean;
|
||||
data: CheckInputPeriodItem[];
|
||||
}
|
||||
|
||||
/** 입력기간(INPT) 중 현재 구간이면 true */
|
||||
export function isInputPeriodCurrent(periods: CheckInputPeriodItem[] | null | undefined): boolean {
|
||||
return (periods ?? []).some((p) => p.seType === "INPT" && p.current === true);
|
||||
}
|
||||
|
||||
/** `YYYYMMDD` → `yyyy-mm-dd` (그 외 형식은 그대로 반환) */
|
||||
export function formatCheckInputYmdAsDash(ymd: string | null | undefined): string {
|
||||
if (ymd == null || ymd === "") return "";
|
||||
if (ymd.length === 8 && /^\d{8}$/.test(ymd)) {
|
||||
return `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}`;
|
||||
}
|
||||
return ymd;
|
||||
}
|
||||
|
||||
/** 검증기간(VRFC) 중 `current`인 항목 (없으면 null) */
|
||||
export function getCurrentVerificationPeriod(
|
||||
periods: CheckInputPeriodItem[] | null | undefined,
|
||||
): CheckInputPeriodItem | null {
|
||||
return (periods ?? []).find((p) => p.seType === "VRFC" && p.current === true) ?? null;
|
||||
}
|
||||
|
||||
/** 검증기간(VRFC) 중 현재 구간이면 true — 저장·제출·제출 취소 불가 */
|
||||
export function isVerificationPeriodCurrent(periods: CheckInputPeriodItem[] | null | undefined): boolean {
|
||||
return getCurrentVerificationPeriod(periods) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 검증기간이면 차단 안내 문구(줄바꿈 + yyyy-mm-dd ~ yyyy-mm-dd), 아니면 null
|
||||
*/
|
||||
export function getVerificationPeriodActionBlockedMessage(
|
||||
periods: CheckInputPeriodItem[] | null | undefined,
|
||||
): string | null {
|
||||
const v = getCurrentVerificationPeriod(periods);
|
||||
if (!v) return null;
|
||||
const from = formatCheckInputYmdAsDash(v.bgngYmd);
|
||||
const to = formatCheckInputYmdAsDash(v.endYmd);
|
||||
const range =
|
||||
from && to ? `${from} ~ ${to}` : [from, to].filter(Boolean).join(" ~ ");
|
||||
return `검증기간에는 저장, 제출, 제출 취소를 할 수 없습니다.\n검증기간: ${range}`;
|
||||
}
|
||||
|
||||
export interface StatisticsInputSewSupplyRateService {
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
export interface SewagePipeMaintainTotalRequest {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
statsYr?: string;
|
||||
sewerBzmnCd?: string;
|
||||
rowDataYn?: boolean;
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainRepairTotalItem {
|
||||
ctpvNm: string;
|
||||
sggNm: string | null;
|
||||
statsYr: string;
|
||||
rowType: string;
|
||||
sewerBzmnCd: string | null;
|
||||
admdstNm: string;
|
||||
mendngWhol: number;
|
||||
cbsysMendngPrlg: number;
|
||||
sprsysDrtwPrlg: number;
|
||||
sprsysExlnPrlg: number;
|
||||
partMendngWhol: number;
|
||||
cbsysPartMendngPrlg: number;
|
||||
sprsysDrtwMendngPrlg: number;
|
||||
sprsysExlnMendngPrlg: number;
|
||||
cbsysMnhlCnt: number;
|
||||
sprsysDrtwMnhlCnt: number;
|
||||
sprsysExlnMnhlCnt: number;
|
||||
drtwRcvprpCnt: number;
|
||||
exlnRcvprpCnt: number;
|
||||
stofcbrCnt: number;
|
||||
otflCnt: number;
|
||||
cctvExmnPrlg: number;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainRepairTotalResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: SewagePipeMaintainRepairTotalItem[];
|
||||
}
|
||||
|
||||
/** `GET .../sewer-pipe-mtc/dredging` 목록 항목 */
|
||||
export interface SewagePipeMaintainDredgingTotalItem {
|
||||
ctpvNm: string;
|
||||
sggNm: string | null;
|
||||
admdstNm: string;
|
||||
admdstCd: string;
|
||||
statsYr: string;
|
||||
rowType: string;
|
||||
sewerBzmnCd: string | null;
|
||||
wholFcltPrlg: number;
|
||||
wholSwagePpl: number;
|
||||
cbsysPplPrlg: number;
|
||||
sprsysDrtwPrlg: number;
|
||||
sprsysExlnPrlg: number;
|
||||
mnhlCnt: number;
|
||||
exlnDrtwRcvprpCnt: number;
|
||||
stofcbrOtflCnt: number;
|
||||
drdgnPlanPrlg: number;
|
||||
drdgnTrgtCtrsDrdgnRt: number;
|
||||
wholPrlgCtrsDrdgnRt: number;
|
||||
drdgnAmt: number;
|
||||
drdgnQnt: number;
|
||||
drdgnUntprcAmt: number;
|
||||
wholDrdgnPrcsQnt: number;
|
||||
rclmQnt: number;
|
||||
oceanEmsnQnt: number;
|
||||
rcycQnt: number;
|
||||
etcQnt: number;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
/** `GET .../sewer-pipe-mtc/dredging` 응답 */
|
||||
export interface SewagePipeMaintainDredgingTotalResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: SewagePipeMaintainDredgingTotalItem[];
|
||||
}
|
||||
+177
@@ -57,3 +57,180 @@ export interface SewagePipeTotalResponse {
|
||||
message: string;
|
||||
data: SewagePipeTotalItem[];
|
||||
}
|
||||
|
||||
export interface SewagePipeFactInstallStatusItem {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string | null;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
swtrmtFcltNm: string;
|
||||
prcsFcltCpct: number | null;
|
||||
rowType: string;
|
||||
prcsZoneArea: number;
|
||||
cbsysPrcsZoneArea: number;
|
||||
sprsysPrcsZoneArea: number;
|
||||
sprsysVartnRt: number;
|
||||
mltvEqpmCnt: number;
|
||||
cbsysMltvEqpmCnt: number;
|
||||
sprsysMltvEqpmCnt: number;
|
||||
sprsysMltvLnkgCnt: number;
|
||||
sprsysMltvYetLnkgCnt: number;
|
||||
planPrcsZoneArea: number;
|
||||
cbsysPlanPrcsZoneArea: number;
|
||||
sprsysPlanPrcsZoneArea: number;
|
||||
wholFcltPrlg: number;
|
||||
wholIcptgAtlfcPrlg: number | null;
|
||||
icptgPrlg: number;
|
||||
atlfcsPrlg: number;
|
||||
cbsysStofcbrCnt: number;
|
||||
sprsysStofcbrCnt: number;
|
||||
sprsysExlnOtflCnt: number;
|
||||
wholBrchlnPrlg: number;
|
||||
cbsysBrchlnPrlg: number;
|
||||
exlnBrchlnPrlg: number;
|
||||
drtwBrchlnPrlg: number;
|
||||
}
|
||||
|
||||
export interface SewagePipeFactInstallStatusResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: SewagePipeFactInstallStatusItem[];
|
||||
}
|
||||
|
||||
/** ST00020003 합계 — `GET .../sewer-pipe/pipe-type-facility-status` */
|
||||
export interface SewagePipePipeTypeFacilityStatusItem {
|
||||
pipeByPrstWhol: number;
|
||||
lyngPrlg: number;
|
||||
dmltnPrlg: number;
|
||||
yy1PrlgWhol: number;
|
||||
yy1LyngPrlg: number;
|
||||
yy1DmltnPrlg: number;
|
||||
yy2PrlgWhol: number;
|
||||
yy2LyngPrlg: number;
|
||||
yy2DmltnPrlg: number;
|
||||
yy3PrlgWhol: number;
|
||||
yy3LyngPrlg: number;
|
||||
yy3DmltnPrlg: number;
|
||||
yy4PrlgWhol: number;
|
||||
yy4LyngPrlg: number;
|
||||
yy4DmltnPrlg: number;
|
||||
yy5PrlgWhol: number;
|
||||
yy5LyngPrlg: number;
|
||||
yy5DmltnPrlg: number;
|
||||
yy6PrlgWhol: number;
|
||||
yy6LyngPrlg: number;
|
||||
yy6DmltnPrlg: number;
|
||||
yy7PrlgWhol: number;
|
||||
yy7LyngPrlg: number;
|
||||
yy7DmltnPrlg: number;
|
||||
yy8PrlgWhol: number;
|
||||
yy8LyngPrlg: number;
|
||||
yy8DmltnPrlg: number;
|
||||
yy9PrlgWhol: number;
|
||||
yy9LyngPrlg: number;
|
||||
yy9DmltnPrlg: number;
|
||||
yy10PrlgWhol: number;
|
||||
yy10LyngPrlg: number;
|
||||
yy10DmltnPrlg: number;
|
||||
yy11PrlgWhol: number;
|
||||
yy11LyngPrlg: number;
|
||||
yy11DmltnPrlg: number;
|
||||
yy12PrlgWhol: number;
|
||||
yy12LyngPrlg: number;
|
||||
yy12DmltnPrlg: number;
|
||||
yy13PrlgWhol: number;
|
||||
yy13LyngPrlg: number;
|
||||
yy13DmltnPrlg: number;
|
||||
yy14PrlgWhol: number;
|
||||
yy14LyngPrlg: number;
|
||||
yy14DmltnPrlg: number;
|
||||
yy15PrlgWhol: number;
|
||||
yy15LyngPrlg: number;
|
||||
yy15DmltnPrlg: number;
|
||||
yy16PrlgWhol: number;
|
||||
yy16LyngPrlg: number;
|
||||
yy16DmltnPrlg: number;
|
||||
yy17PrlgWhol: number;
|
||||
yy17LyngPrlg: number;
|
||||
yy17DmltnPrlg: number;
|
||||
yy18PrlgWhol: number;
|
||||
yy18LyngPrlg: number;
|
||||
yy18DmltnPrlg: number;
|
||||
yy19PrlgWhol: number;
|
||||
yy19LyngPrlg: number;
|
||||
yy19DmltnPrlg: number;
|
||||
yy20PrlgWhol: number;
|
||||
yy20LyngPrlg: number;
|
||||
yy20DmltnPrlg: number;
|
||||
yy21PrlgWhol: number;
|
||||
yy21LyngPrlg: number;
|
||||
yy21DmltnPrlg: number;
|
||||
yy22PrlgWhol: number;
|
||||
yy22LyngPrlg: number;
|
||||
yy22DmltnPrlg: number;
|
||||
yy23PrlgWhol: number;
|
||||
yy23LyngPrlg: number;
|
||||
yy23DmltnPrlg: number;
|
||||
yy24PrlgWhol: number;
|
||||
yy24LyngPrlg: number;
|
||||
yy24DmltnPrlg: number;
|
||||
yy25PrlgWhol: number;
|
||||
yy25LyngPrlg: number;
|
||||
yy25DmltnPrlg: number;
|
||||
yy26PrlgWhol: number;
|
||||
yy26LyngPrlg: number;
|
||||
yy26DmltnPrlg: number;
|
||||
yy27PrlgWhol: number;
|
||||
yy27LyngPrlg: number;
|
||||
yy27DmltnPrlg: number;
|
||||
yy28PrlgWhol: number;
|
||||
yy28LyngPrlg: number;
|
||||
yy28DmltnPrlg: number;
|
||||
yy29PrlgWhol: number;
|
||||
yy29LyngPrlg: number;
|
||||
yy29DmltnPrlg: number;
|
||||
yy30PrlgWhol: number;
|
||||
yy30LyngPrlg: number;
|
||||
yy30DmltnPrlg: number;
|
||||
ctpvNm: string;
|
||||
sggNm: string | null;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
swrgPipeNm: string;
|
||||
statsYr: string;
|
||||
rowType: string;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
export interface SewagePipePipeTypeFacilityStatusResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: SewagePipePipeTypeFacilityStatusItem[];
|
||||
}
|
||||
|
||||
/** ST00020004 합계 — `GET .../sewer-pipe/pipe-diam-status` */
|
||||
export interface SewagePipePipeDiamStatusItem {
|
||||
ctpvNm: string;
|
||||
sggNm: string | null;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
statsYr: string;
|
||||
swrgPipeNm: string;
|
||||
rowType: string;
|
||||
pdmtByTsmWhol: number;
|
||||
step1PdmtPrlg: number;
|
||||
step2PdmtPrlg: number;
|
||||
step3PdmtPrlg: number;
|
||||
step4PdmtPrlg: number;
|
||||
step5PdmtPrlg: number;
|
||||
sqrePdmtPrlg: number;
|
||||
etcPdmtPrlg: number;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
export interface SewagePipePipeDiamStatusResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: SewagePipePipeDiamStatusItem[];
|
||||
}
|
||||
|
||||
+2
@@ -4,6 +4,8 @@ export interface SewerageSupplyRateRequest {
|
||||
statsYr?: string;
|
||||
sewerBzmnCd?: string;
|
||||
rowDataYn?: boolean;
|
||||
summaryYn?: boolean;
|
||||
summaryYearCnt?: number;
|
||||
}
|
||||
|
||||
export interface SewerageSupplyRateTotalItem {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel";
|
||||
import { ScheduleInfoPanel } from "../ui/panels/schedule-info-panel";
|
||||
import { StatsSubmissionPanel } from "../ui/panels/stats-submission-panel";
|
||||
import { UnSubmitPanel } from "../ui/panels/unsubmit-panel";
|
||||
import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel";
|
||||
// import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel";
|
||||
import { VrfcReportPanel } from "../ui/panels/vrfc-report-panel";
|
||||
|
||||
import type { PanelItem } from "./types";
|
||||
@@ -36,23 +36,23 @@ export function getInitialPanels({
|
||||
component: <NoticePanel />,
|
||||
className: "",
|
||||
},
|
||||
// {
|
||||
// id: "panel-d",
|
||||
// component: <VrfcErrPanel params={dashboardParams} />,
|
||||
// className: "",
|
||||
// },
|
||||
{
|
||||
id: "panel-d",
|
||||
component: <VrfcErrPanel params={dashboardParams} />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-e",
|
||||
component: <ScheduleCalendarPanel />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-f",
|
||||
id: "panel-e",
|
||||
component: <VrfcReportPanel params={dashboardParams} />,
|
||||
className: "",
|
||||
},
|
||||
{
|
||||
id: "panel-g",
|
||||
id: "panel-f",
|
||||
component: <UnSubmitPanel params={dashboardParams} />,
|
||||
className: "",
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export function NoticePanel() {
|
||||
|
||||
const recentPostListQuery = useRecentPostList({
|
||||
page: 0,
|
||||
size: 3,
|
||||
size: 4,
|
||||
keywordType: "bbsTypeCd",
|
||||
keyword: "BBSNOTICE",
|
||||
});
|
||||
@@ -45,14 +45,18 @@ export function NoticePanel() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">공지사항</h3>
|
||||
<Button onClick={handleMoreClick} className="h-8">
|
||||
<h3 className="text-2xl font-semibold">공지사항</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
className="!text-xl"
|
||||
onClick={handleMoreClick}
|
||||
>
|
||||
더보기
|
||||
</Button>
|
||||
</div>
|
||||
@@ -67,17 +71,16 @@ const noticeColumns = [
|
||||
columnHelper.accessor("pstTtl", {
|
||||
header: "제목",
|
||||
cell: ({ row }) => (
|
||||
<div className="py-2">
|
||||
<div className="truncate font-medium">
|
||||
{row.original.pstTtl}
|
||||
</div>
|
||||
<div className="mt-1 flex justify-between text-sm text-muted-foreground">
|
||||
<span>{row.original.wrtrNm}</span>
|
||||
<span>{formatDate(row.original.regDt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-[20rem] truncate">{row.original.pstTtl}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("wrtrNm", {
|
||||
header: "작성자",
|
||||
}),
|
||||
columnHelper.accessor("regDt", {
|
||||
header: "등록일",
|
||||
cell: ({ row }) => formatDate(row.original.regDt),
|
||||
}),
|
||||
];
|
||||
|
||||
function formatDate(value: string) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import dayjs from "dayjs";
|
||||
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { useSchedules } from "@/entities/schedule";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
@@ -31,21 +32,24 @@ export function ScheduleCalendarPanel() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">일정관리</h3>
|
||||
<h3 className="text-2xl font-semibold">일정관리</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="py-8 text-center">로딩 중...</div>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="h-[400px]">
|
||||
{/* TBD: FullCalendar 상세 설정 (locale, plugins, 이벤트 색상 등) */}
|
||||
<div className="h-[48rem]">
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, timeGridPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
@@ -67,7 +71,6 @@ export function ScheduleCalendarPanel() {
|
||||
// TBD: allDay 판단 기준 (1439분 이상?)
|
||||
allDay: true,
|
||||
}))}
|
||||
// TBD: 이벤트 클릭 핸들러 (현재 SPEC에서 제거됨)
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { ScheduleInfoSearchParams } from "@/entities/statistics";
|
||||
import { useScheduleInfo } from "@/entities/statistics";
|
||||
import {
|
||||
Badge,
|
||||
type BadgeProps,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
@@ -9,63 +16,121 @@ interface ScheduleInfoPanelProps {
|
||||
params: ScheduleInfoSearchParams | null;
|
||||
}
|
||||
|
||||
const CURRENT_STATUS_LABEL_MAP = {
|
||||
INPT: "입력중",
|
||||
VRFC: "검증중",
|
||||
NONE: "기간외",
|
||||
};
|
||||
|
||||
const CURRENT_STATUS_COLOR_MAP = {
|
||||
INPT: "",
|
||||
VRFC: "gray",
|
||||
NONE: "lightGray",
|
||||
};
|
||||
|
||||
export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useScheduleInfo(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">일정안내</h3>
|
||||
<h3 className="text-2xl font-semibold">일정안내</h3>
|
||||
{data && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl tracking-tight">
|
||||
{data.ddayLabel}
|
||||
</span>
|
||||
<Badge
|
||||
size="lg"
|
||||
color="orange"
|
||||
className="rounded-full !px-3 text-xl font-semibold"
|
||||
>
|
||||
{data.dday}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="py-8 text-center">로딩 중...</div>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-center text-red-500">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
{/* D-day 표시 */}
|
||||
<div className="rounded-lg bg-blue-50 p-4 text-center">
|
||||
<div className="mb-1 text-sm text-muted-foreground">
|
||||
{data.ddayLabel}
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{data.dday}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
{data.currentSe}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TBD: 일정 목록 표시 형식 정의 필요 */}
|
||||
<div className="space-y-2">
|
||||
<div className="rounded border p-3">
|
||||
<div className="mb-1 text-sm font-medium">
|
||||
입력기간
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.inptBgngYmd} ~ {data.inptEndYmd}
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<h4 className="px-2 text-2xl font-semibold tracking-tight text-label">
|
||||
이번 주 주요 일정
|
||||
</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="tracking-tight text-label">
|
||||
현재 진행 상태
|
||||
</span>
|
||||
<Badge
|
||||
color={
|
||||
CURRENT_STATUS_COLOR_MAP[
|
||||
data.currentSe as keyof typeof CURRENT_STATUS_COLOR_MAP
|
||||
] as BadgeProps["color"]
|
||||
}
|
||||
className="rounded-full !px-3"
|
||||
>
|
||||
{
|
||||
CURRENT_STATUS_LABEL_MAP[
|
||||
data.currentSe as keyof typeof CURRENT_STATUS_LABEL_MAP
|
||||
]
|
||||
}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded border p-3">
|
||||
<div className="mb-1 text-sm font-medium">
|
||||
검증기간
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between rounded-sm border border-border p-3">
|
||||
<Badge size="lg" className="font-semibold">
|
||||
{data.inptSeCd}차 입력기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.inptBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.inptEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.vrfcBgngYmd} ~ {data.vrfcEndYmd}
|
||||
<div className="flex items-center justify-between rounded-sm border border-border p-3">
|
||||
<Badge
|
||||
size="lg"
|
||||
color="gray"
|
||||
className="font-semibold"
|
||||
>
|
||||
{data.vrfcSeCd}차 검증기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.vrfcBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.vrfcEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardStatsSubmissionRate } from "@/entities/statistics";
|
||||
import { Frown, Laugh, Meh, OctagonAlert, OctagonX, Smile } from "lucide-react";
|
||||
|
||||
import {
|
||||
useDashboardEcptReqCount,
|
||||
useDashboardStatsSubmissionRate,
|
||||
useDashboardVrfcErrCount,
|
||||
} from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { LoadingWithAnimation, Progress } from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
@@ -10,70 +16,147 @@ interface StatsSubmissionPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
const STATUS_LEVEL_TEXT_COLOR = {
|
||||
GREAT: "text-blue-500",
|
||||
GOOD: "text-green-500",
|
||||
WARNING: "text-yellow-500",
|
||||
BAD: "text-red-500",
|
||||
};
|
||||
|
||||
const STATUS_LEVEL_EMOJI = {
|
||||
GREAT: <Laugh size={160} className={STATUS_LEVEL_TEXT_COLOR.GREAT} />,
|
||||
GOOD: <Smile size={160} className={STATUS_LEVEL_TEXT_COLOR.GOOD} />,
|
||||
WARNING: <Meh size={160} className={STATUS_LEVEL_TEXT_COLOR.WARNING} />,
|
||||
BAD: <Frown size={160} className={STATUS_LEVEL_TEXT_COLOR.BAD} />,
|
||||
};
|
||||
|
||||
export function StatsSubmissionPanel({ params }: StatsSubmissionPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useDashboardStatsSubmissionRate(params);
|
||||
const vrfcErrQuery = useDashboardVrfcErrCount(params);
|
||||
const ecptReqQuery = useDashboardEcptReqCount(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
{/* <div
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">통계입력현황</h3>
|
||||
</div>
|
||||
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center py-8 text-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
{/* TBD: statusLevel별 이모지 매핑 규칙 필요 */}
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">
|
||||
{/* TBD: statusLevel 값에 따른 이모지 표시 */}
|
||||
{data.statusLevel === "GOOD"
|
||||
? "😊"
|
||||
: data.statusLevel === "WARNING"
|
||||
? "⚠️"
|
||||
: "📊"}
|
||||
<h3 className="text-2xl font-semibold">통계입력현황</h3>
|
||||
</div> */}
|
||||
<div
|
||||
className={`card-body cursor-grab py-2 active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
>
|
||||
<div className="flex h-full gap-6 px-2">
|
||||
<div className="h-full flex-1 flex-col">
|
||||
<h3 className="text-2xl font-semibold">통계입력현황</h3>
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.statusLevel}
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center py-8 text-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full">
|
||||
{data && (
|
||||
<div className="flex h-full flex-col items-center gap-4">
|
||||
{/* 통계제출률 상태 이모지 */}
|
||||
<div className="flex justify-center">
|
||||
{
|
||||
STATUS_LEVEL_EMOJI[
|
||||
data.statusLevel as keyof typeof STATUS_LEVEL_EMOJI
|
||||
]
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* 통계제출률 프로그레스바 */}
|
||||
<div>
|
||||
<div className="mb-2 flex justify-between text-sm">
|
||||
<span>통계제출률</span>
|
||||
<span className="font-semibold">
|
||||
{data.submissionRate?.toFixed(1)}%
|
||||
</span>
|
||||
{/* 통계제출률 프로그레스바 */}
|
||||
<div className="w-full space-y-3">
|
||||
<div className="flex w-full justify-between">
|
||||
<span className="text-2xl">
|
||||
통계제출률
|
||||
</span>
|
||||
<span className="text-2xl font-semibold">
|
||||
{data.submissionRate?.toFixed(
|
||||
1,
|
||||
)}
|
||||
%
|
||||
<span className="ml-1">
|
||||
({data.submittedCount}/
|
||||
{data.totalCount})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={data.submissionRate}
|
||||
className="h-6 bg-muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full flex-[0.5]">
|
||||
<h3 className="text-2xl font-semibold">
|
||||
검증오류 및 예외
|
||||
</h3>
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* 검증오류 수 카드 */}
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-100 p-4 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<OctagonX
|
||||
className="text-red-600"
|
||||
size={20}
|
||||
/>
|
||||
<span className="text-lg">검증오류 수</span>
|
||||
</div>
|
||||
{vrfcErrQuery.isLoading && (
|
||||
<div className="text-2xl font-bold">-</div>
|
||||
)}
|
||||
{vrfcErrQuery.error && (
|
||||
<div className="text-sm">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{vrfcErrQuery.data && (
|
||||
<div className="text-5xl font-bold text-red-600">
|
||||
{vrfcErrQuery.data.vrfcErrCount.toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-2.5 w-full rounded-full bg-gray-200">
|
||||
<div
|
||||
className="h-2.5 rounded-full bg-blue-600 transition-all"
|
||||
style={{ width: `${data.submissionRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 flex justify-between text-xs text-muted-foreground">
|
||||
<span>제출: {data.submittedCount}</span>
|
||||
<span>전체: {data.totalCount}</span>
|
||||
|
||||
{/* 예외신청 수 카드 */}
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-orange-200 bg-orange-100 p-4 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<OctagonAlert
|
||||
className="text-orange-600"
|
||||
size={20}
|
||||
/>
|
||||
<span className="text-lg">예외신청 수</span>
|
||||
</div>
|
||||
{ecptReqQuery.isLoading && (
|
||||
<div className="text-2xl font-bold">-</div>
|
||||
)}
|
||||
{ecptReqQuery.error && (
|
||||
<div className="text-sm">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{ecptReqQuery.data && (
|
||||
<div className="text-5xl font-bold text-orange-600">
|
||||
{ecptReqQuery.data.ecptReqCount.toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,23 +15,20 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
|
||||
const { data, isLoading, error } = useDashboardUnSubmitCount(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">미제출 지표</h3>
|
||||
<h3 className="text-2xl font-semibold">미제출 지표</h3>
|
||||
{data && (
|
||||
<Badge
|
||||
color="red"
|
||||
className="text-md rounded-full px-2 py-1 text-destructive-foreground"
|
||||
>
|
||||
<Badge size="lg" className="rounded-full !px-3">
|
||||
{data.unSubmitCount}건
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
@@ -51,11 +48,11 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
|
||||
미제출 지표가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[32rem] space-y-2 overflow-y-auto">
|
||||
<div className="max-h-[48rem] space-y-2 overflow-y-auto">
|
||||
{data.unSubmitList.map((item) => (
|
||||
<div
|
||||
key={item.statsCd}
|
||||
className="rounded-sm border p-3"
|
||||
className="rounded-sm border border-border p-3"
|
||||
>
|
||||
<p className="font-medium">
|
||||
{item.statsNm}
|
||||
|
||||
@@ -20,15 +20,15 @@ export function VrfcErrPanel({ params }: VrfcErrPanelProps) {
|
||||
const ecptReqQuery = useDashboardEcptReqCount(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-xl font-semibold">검증오류 및 예외</h3>
|
||||
<h3 className="text-2xl font-semibold">검증오류 및 예외</h3>
|
||||
</div>
|
||||
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
<div className="grid h-full grid-cols-2 gap-4">
|
||||
{/* 검증오류 수 카드 */}
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-100 p-4 text-center">
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
import { useDashboardVrfcReport } from "@/entities/statistics";
|
||||
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
|
||||
import {
|
||||
Badge,
|
||||
type BadgeProps,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
@@ -9,79 +14,107 @@ interface VrfcReportPanelProps {
|
||||
params: DashboardSearchParams | null;
|
||||
}
|
||||
|
||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
NV: "일반",
|
||||
IMR: "I-MR",
|
||||
};
|
||||
|
||||
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
||||
NV: "gray",
|
||||
IMR: "red",
|
||||
};
|
||||
|
||||
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const { data, isLoading, error } = useDashboardVrfcReport(params);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full">
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
|
||||
{...dragHandleProps}
|
||||
title="헤더를 드래그하여 패널 순서 변경"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">이상치 선별보고서</h3>
|
||||
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="py-8 text-center">로딩 중...</div>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="py-8 text-center text-red-500">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
|
||||
{data.typeCounts &&
|
||||
Object.keys(data.typeCounts).length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(data.typeCounts).map(
|
||||
([type, count]) => (
|
||||
<span
|
||||
<Badge
|
||||
key={type}
|
||||
className="rounded-full bg-gray-100 px-3 py-1 text-sm"
|
||||
color={
|
||||
TYPE_CODE_COLOR_MAP[type]
|
||||
}
|
||||
className="rounded-full px-2 py-1 text-lg"
|
||||
>
|
||||
{type}: {count}
|
||||
</span>
|
||||
{TYPE_CODE_NAME_MAP[type]}:{" "}
|
||||
{count}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
<div className="max-h-[44rem] overflow-y-auto">
|
||||
{data.list.length === 0 ? (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
이상치 항목이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 tracking-tight">
|
||||
{data.list.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="cursor-pointer rounded border p-3 transition-colors hover:bg-accent"
|
||||
/* TBD: 행 클릭 시 이동 라우트 */
|
||||
className="flex flex-col rounded-sm border border-border p-3"
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<div className="font-medium">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.statsNm}
|
||||
</div>
|
||||
{/* TBD: vrfcCd 값별 표시 레이블 */}
|
||||
<span className="rounded bg-red-100 px-2 py-0.5 text-xs text-red-700">
|
||||
{item.vrfcCd}
|
||||
</span>
|
||||
<Badge
|
||||
color={
|
||||
TYPE_CODE_COLOR_MAP[
|
||||
item.vrfcCd
|
||||
]
|
||||
}
|
||||
className="text-sm"
|
||||
>
|
||||
{
|
||||
TYPE_CODE_NAME_MAP[
|
||||
item.vrfcCd
|
||||
]
|
||||
}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mb-1 text-sm text-muted-foreground">
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{item.rsnExpln}
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
</p>
|
||||
<div className="flex justify-between text-lg text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>
|
||||
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
|
||||
현재: {item.nowVlCn} / 비교:{" "}
|
||||
{item.compVlCn}
|
||||
현재: {item.nowVlCn ?? "-"}{" "}
|
||||
/ 비교:{" "}
|
||||
{item.compVlCn ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
|
||||
|
||||
@@ -37,14 +37,14 @@ const StatisticsPage = () => {
|
||||
// 탭이 없으면 Dashboard 표시
|
||||
if (tabs.length === 0) {
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border !mt-6 border-border bg-background">
|
||||
<Dashboard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border !mt-6 border-border bg-background">
|
||||
{tabs.map((tab) => {
|
||||
// 렌더링 시점에 컴포넌트가 없으면 바로 로드
|
||||
if (!componentCache.current.has(tab.id)) {
|
||||
|
||||
Reference in New Issue
Block a user