[st] feat: 입력- 저장,제출 로직 수정(01,02) & 02-6tab 로직 수정, 집계- 로직 수정 및 요약표 포함 조건 추가
This commit is contained in:
@@ -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]"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user