From 07db5b1fbd4b54317395cc970878f07137f3f91f Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 25 Jun 2026 18:09:04 +0900 Subject: [PATCH] =?UTF-8?q?wr:=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruseFcltMng/fcltSttn/htdflSttn/page.tsx | 132 +++++++----- .../ruseFcltMng/fcltSttn/rainSttn/page.tsx | 122 +++++------ .../ruseFcltMng/fcltSttn/rswtSttn/page.tsx | 90 ++++---- .../ruseFcltMng/fcltSttn/htdflSttn/index.tsx | 106 ++++----- .../ruseFcltMng/fcltSttn/rainSttn/index.tsx | 118 +++++----- .../ruseFcltMng/fcltSttn/rswtSttn/index.tsx | 115 +++++----- apps/wr/docs/작업계획/호출병합2_20260625.md | 201 ++++++++++++++++++ 7 files changed, 510 insertions(+), 374 deletions(-) create mode 100644 apps/wr/docs/작업계획/호출병합2_20260625.md diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx index 5705faabc..99f2645cf 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx @@ -40,11 +40,10 @@ import { useTranslation } from "react-i18next"; const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }]; export default function HtdflSttn() { - const { auth, code, htdflSttn, htdflSttnSearch, initHtdfl, getLaeSeCdCheck } = useWrStore((state) => state); + const { auth, code, htdflSttn, htdflSttnSearch, getLaeSeCdCheck } = useWrStore((state) => state); const { laeSeCd } = auth; // 지자체구분코드 const { htdflSttnRes, htdflSttnResCount } = htdflSttn; - const isDownloadTriggered = useRef(false); - const skipNextSearchRef = useRef(true); + const initializedSearchKeyRef = useRef(null); const { t } = useTranslation('base'); const [pageNation, setPageNation] = useState(1); const [isChHead, setIsChHead] = useState(false); @@ -61,6 +60,10 @@ export default function HtdflSttn() { // chBuilding: chgBuilding[]; // }>({ isOpen: false, chBuilding: [] }); const [fcltNm, setFcltNm] = useState(""); + const [appliedSearch, setAppliedSearch] = useState<{ + conValue: string; + fcltNm: string; + } | null>(null); const alert = useAlertStore((state) => state.setMessage); const isAllAreaUser = laeSeCd === "0"; const isLaeSeCdReady = @@ -89,7 +92,7 @@ export default function HtdflSttn() { setObjSearch({ ...objSearch, sgg: String(val) }) }; - const getConValue = (): string => { + const getConValue = useCallback((): string => { if (!objSearch.city || objSearch.city === "0") { return "0"; } @@ -97,7 +100,7 @@ export default function HtdflSttn() { return String(objSearch.sgg); } return String(objSearch.city); - }; + }, [objSearch]); const getFixedConValue = useCallback((): string => { if (laeSeCd === undefined || laeSeCd === "0") { @@ -105,7 +108,7 @@ export default function HtdflSttn() { } return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2); - }, [laeSeCd, objSearch]); + }, [laeSeCd, getConValue]); const cellClick = (index: number | undefined, headId: string) => { if ( @@ -117,14 +120,39 @@ export default function HtdflSttn() { } }; - const excelClick = useCallback(() => { - if (!isLaeSeCdReady) { + const excelClick = useCallback(async () => { + if (!isLaeSeCdReady || !appliedSearch) { return; } - isDownloadTriggered.current = true; - htdflSttnSearch(true, 1, htdflSttnResCount, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd); - }, [htdflSttnSearch, fcltNm, htdflSttnResCount, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]); + const excelData = await htdflSttnSearch( + true, + 1, + htdflSttnResCount, + appliedSearch.conValue, + appliedSearch.fcltNm, + code.cityCd, + code.sggCd + ); + if (excelData && excelData.length > 0) { + const sheetArray: SheetParameter[] = [{ + data: excelData, + headerNum: 3, + headerRange: "A1:R3", + sheetName: "sheet1", + startCol: "A", + decimalArray0: [6, 11, 13, 15], + }]; + excelDownloadNumStyle( + "온배수시설 현황", + "sample_htdflSttn.xlsx", + sheetArray + ); + return; + } + + alert(t("excelDownErrMsg")); + }, [htdflSttnSearch, htdflSttnResCount, appliedSearch, code.cityCd, code.sggCd, isLaeSeCdReady, alert, t]); useEffect(() => { getLaeSeCdCheck(); @@ -150,52 +178,39 @@ export default function HtdflSttn() { }); }, [laeSeCd, code.sggCd, isLaeSeCdReady]); - useEffect(() => { - if (!isDownloadTriggered.current) { - return; - } - if (htdflSttn.excelList.length > 0) { - const sheetArray: SheetParameter[] = [{ - data: htdflSttn.excelList, - headerNum: 3, - headerRange: "A1:R3", - sheetName: 'sheet1', - startCol: 'A', - decimalArray0: [6,11,13,15], // 0부터 시작 - }]; - excelDownloadNumStyle( - "온배수시설 현황", - "sample_htdflSttn.xlsx", - sheetArray - ); - } else { - alert(t('excelDownErrMsg')); - } - isDownloadTriggered.current = false; - }, [htdflSttn.excelList, code]); - - useEffect(() => { - if (!isLaeSeCdReady || htdflSttn.isInit) { - return; - } - - skipNextSearchRef.current = true; - initHtdfl(false, 1, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd); - }, [htdflSttn.isInit, initHtdfl, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]); - useEffect(() => { if (!isLaeSeCdReady) { return; } - if (skipNextSearchRef.current) { - skipNextSearchRef.current = false; + const initialSearchKey = laeSeCd ?? "0"; + if (initializedSearchKeyRef.current === initialSearchKey) { return; } - htdflSttnSearch(false, pageNation, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd); + const initialConValue = + laeSeCd === undefined || laeSeCd === "0" + ? "0" + : laeSeCd.length === 5 + ? laeSeCd + : laeSeCd.slice(0, 2); + + initializedSearchKeyRef.current = initialSearchKey; + setAppliedSearch({ + conValue: initialConValue, + fcltNm: "", + }); + setPageNation(1); + }, [isLaeSeCdReady, laeSeCd]); + + useEffect(() => { + if (!isLaeSeCdReady || !appliedSearch) { + return; + } + + htdflSttnSearch(false, pageNation, htdflSttnSize, appliedSearch.conValue, appliedSearch.fcltNm, code.cityCd, code.sggCd); isResetPage(false) - }, [pageNation, htdflSttnSearch, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]); + }, [pageNation, htdflSttnSearch, appliedSearch, code.cityCd, code.sggCd, isLaeSeCdReady]); useEffect(() => { const data = tableData(htdflSttnRes, { code }); @@ -215,6 +230,19 @@ export default function HtdflSttn() { setBdstTable(chData); }, [code, htdflSttnRes, htdflSttn.htdflSttHeadList]); + const handleSearch = useCallback(() => { + if (!isLaeSeCdReady) { + return; + } + + setAppliedSearch({ + conValue: String(getFixedConValue()), + fcltNm, + }); + isResetPage(true); + setPageNation(1); + }, [fcltNm, getFixedConValue, isLaeSeCdReady]); + return ( <> @@ -266,13 +294,7 @@ export default function HtdflSttn() {
diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx index 3d87dbbbf..fae118ae3 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rainSttn/page.tsx @@ -1,5 +1,5 @@ /** - * 빗물이용시설 현황 + * 재이용시설 현황 > 빗물이용시설 현황 */ "use client"; import { useWrStore } from "@/app/store"; @@ -38,7 +38,7 @@ import { useTranslation } from "react-i18next"; const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }]; export default function RainSttn() { - const { auth, code, rainSttn, rainSttnSearch, rainSttnInit, getLaeSeCdCheck } = useWrStore( + const { auth, code, rainSttn, rainSttnSearch, getLaeSeCdCheck } = useWrStore( (state) => state ); const { rainSttnRes, rainSttnResCount } = rainSttn; @@ -49,7 +49,6 @@ export default function RainSttn() { const [bdstTable, setBdstTable] = useState(); const [selectSgg, setSelectSgg] = useState(defaultSigunOptions); const [resetPage, isResetPage] = useState(false); - const [isDownloadTriggered, setIsDownloadTriggered] = useState(false); const [objSearch, setObjSearch] = useState({ sgg: "", city: "", @@ -60,14 +59,13 @@ export default function RainSttn() { }>({ data: undefined, isOpen: false }); const [fcltNm, setFcltNm] = useState(""); - const [appliedSearch, setAppliedSearch] = useState({ - conValue: "0", - fcltNm: "", - }); - const [searchTrigger, setSearchTrigger] = useState(0); + const [appliedSearch, setAppliedSearch] = useState<{ + conValue: string; + fcltNm: string; + } | null>(null); const { t } = useTranslation("base"); const alert = useAlertStore((state) => state.setMessage); - const skipNextSearchRef = useRef(true); + const initializedSearchKeyRef = useRef(null); const isAllAreaUser = laeSeCd === "0"; const isLaeSeCdReady = laeSeCd === "0" || laeSeCd.length === 2 || laeSeCd.length === 5; @@ -100,7 +98,7 @@ export default function RainSttn() { setObjSearch({ ...objSearch, sgg: String(val) }); }; - const getConValue = (): string => { + const getConValue = useCallback((): string => { if (!objSearch.city || objSearch.city === "0") { return "0"; } @@ -108,7 +106,7 @@ export default function RainSttn() { return String(objSearch.sgg); } return String(objSearch.city); - }; + }, [objSearch]); const getFixedConValue = useCallback((): string => { if (laeSeCd === undefined || laeSeCd === "0") { @@ -116,7 +114,7 @@ export default function RainSttn() { } return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2); - }, [laeSeCd, objSearch]); + }, [laeSeCd, getConValue]); useEffect(() => { getLaeSeCdCheck(); @@ -143,7 +141,12 @@ export default function RainSttn() { }, [laeSeCd, code.sggCd, isLaeSeCdReady]); useEffect(() => { - if (!isLaeSeCdReady || rainSttn.isInit) { + if (!isLaeSeCdReady) { + return; + } + + const initialSearchKey = laeSeCd ?? "0"; + if (initializedSearchKeyRef.current === initialSearchKey) { return; } @@ -154,32 +157,16 @@ export default function RainSttn() { ? laeSeCd : laeSeCd.slice(0, 2); - skipNextSearchRef.current = true; + initializedSearchKeyRef.current = initialSearchKey; setAppliedSearch({ conValue: initialConValue, fcltNm: "", }); - rainSttnInit( - false, - 1, - rainSttnSize, - initialConValue, - "", - code as unknown as Record - ); - }, [rainSttn.isInit, rainSttnInit, code, isLaeSeCdReady, laeSeCd]); + setPageNation(1); + }, [isLaeSeCdReady, laeSeCd]); useEffect(() => { - if (!isLaeSeCdReady) { - return; - } - - if (skipNextSearchRef.current) { - skipNextSearchRef.current = false; - return; - } - - if (searchTrigger === 0) { + if (!isLaeSeCdReady || !appliedSearch) { return; } @@ -194,49 +181,18 @@ export default function RainSttn() { isResetPage(false); }, [ pageNation, - searchTrigger, rainSttnSearch, code, - appliedSearch.conValue, - appliedSearch.fcltNm, + appliedSearch, isLaeSeCdReady, ]); - useEffect(() => { - if (!isLaeSeCdReady) { + const excelClick = useCallback(async () => { + if (!isLaeSeCdReady || !appliedSearch) { return; } - if (!isDownloadTriggered) { - return; - } - if (rainSttn.excelList.length > 0) { - const sheetArray: SheetParameter[] = [{ - data: rainSttn.excelList, - headerNum: 3, - headerRange: "A1:W3", - sheetName: 'sheet1', - startCol: 'A', - decimalArray0: [6,10,12,14,16] // 0부터 시작 - }]; - excelDownloadNumStyle( - "빗물이용시설 현황", - "sample_rainSttn.xlsx", // public 폴더에 있는 template 파일 경로 - sheetArray - ); - } else { - alert(t("excelDownErrMsg")); - } - setIsDownloadTriggered(false); - }, [rainSttn.excelList, isDownloadTriggered, alert, t, isLaeSeCdReady]); - - const excelClick = useCallback(() => { - if (!isLaeSeCdReady) { - return; - } - - setIsDownloadTriggered(true); - rainSttnSearch( + const excelData = await rainSttnSearch( true, 1, rainSttnResCount, @@ -244,16 +200,39 @@ export default function RainSttn() { appliedSearch.fcltNm, code as unknown as Record ); + if (excelData && excelData.length > 0) { + const sheetArray: SheetParameter[] = [{ + data: excelData, + headerNum: 3, + headerRange: "A1:W3", + sheetName: "sheet1", + startCol: "A", + decimalArray0: [6, 10, 12, 14, 16], + }]; + excelDownloadNumStyle( + "빗물이용시설 현황", + "sample_rainSttn.xlsx", + sheetArray + ); + return; + } + + alert(t("excelDownErrMsg")); }, [ rainSttnSearch, rainSttnResCount, code, - appliedSearch.conValue, - appliedSearch.fcltNm, + appliedSearch, isLaeSeCdReady, + alert, + t, ]); const handleSearch = useCallback(() => { + if (!isLaeSeCdReady) { + return; + } + const nextSearch = { conValue: String(getFixedConValue()), fcltNm, @@ -262,8 +241,7 @@ export default function RainSttn() { setAppliedSearch(nextSearch); isResetPage(true); setPageNation(1); - setSearchTrigger((prev) => prev + 1); - }, [fcltNm, getFixedConValue]); + }, [fcltNm, getFixedConValue, isLaeSeCdReady]); useEffect(() => { const data = tableData(rainSttnRes, { code }); diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx index 4e76336c5..ab5092f47 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx @@ -38,7 +38,7 @@ import { useTranslation } from "react-i18next"; const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }]; export default function RswtSttn() { - const { auth, code, rswtSttn, rswtSttnSearch, rswtSttnInit, getLaeSeCdCheck } = useWrStore( + const { auth, code, rswtSttn, rswtSttnSearch, getLaeSeCdCheck } = useWrStore( (state) => state ); @@ -49,17 +49,15 @@ export default function RswtSttn() { const [isChHead, setIsChHead] = useState(false); const [resetPage, isResetPage] = useState(false); const [bdstTable, setBdstTable] = useState(); - const isDownloadTriggered = useRef(false); - const skipNextSearchRef = useRef(true); + const initializedSearchKeyRef = useRef(null); const [selectSgg, setSelectSgg] = useState(defaultSigunOptions) const [objSearch, setObjSearch] = useState({ sgg: "", city: "" }); const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false }) const [fcltNm, setFcltNm] = useState(""); - const [appliedSearch, setAppliedSearch] = useState({ - conValue: "0", - fcltNm: "", - }); - const [searchTrigger, setSearchTrigger] = useState(0); + const [appliedSearch, setAppliedSearch] = useState<{ + conValue: string; + fcltNm: string; + } | null>(null); const { t } = useTranslation('base'); const alert = useAlertStore((state) => state.setMessage); const isAllAreaUser = laeSeCd === "0"; @@ -95,7 +93,7 @@ export default function RswtSttn() { setObjSearch({ ...objSearch, sgg: String(val) }) }; - const getConValue = (): string => { + const getConValue = useCallback((): string => { if (!objSearch.city || objSearch.city === "0") { return "0"; } @@ -103,7 +101,7 @@ export default function RswtSttn() { return String(objSearch.sgg); } return String(objSearch.city); - }; + }, [objSearch]); const getFixedConValue = useCallback((): string => { if (laeSeCd === undefined || laeSeCd === "0") { @@ -111,7 +109,7 @@ export default function RswtSttn() { } return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2); - }, [laeSeCd, objSearch]); + }, [laeSeCd, getConValue]); useEffect(() => { getLaeSeCdCheck(); @@ -138,7 +136,12 @@ export default function RswtSttn() { }, [laeSeCd, code.sggCd, isLaeSeCdReady]); useEffect(() => { - if (!isLaeSeCdReady || rswtSttn.isInit) { + if (!isLaeSeCdReady) { + return; + } + + const initialSearchKey = laeSeCd ?? "0"; + if (initializedSearchKeyRef.current === initialSearchKey) { return; } @@ -149,20 +152,13 @@ export default function RswtSttn() { ? laeSeCd : laeSeCd.slice(0, 2); - skipNextSearchRef.current = true; + initializedSearchKeyRef.current = initialSearchKey; setAppliedSearch({ conValue: initialConValue, fcltNm: "", }); - rswtSttnInit( - false, - 1, - rswtSttnSize, - initialConValue, - "", - code as unknown as Record - ); - }, [rswtSttn.isInit, rswtSttnInit, code, isLaeSeCdReady, laeSeCd]); + setPageNation(1); + }, [isLaeSeCdReady, laeSeCd]); useEffect(() => { const data = tableData(rswtSttnRes, { code }); @@ -194,56 +190,47 @@ export default function RswtSttn() { setBdstTable(chData); }, [code, rswtSttnRes]); - useEffect(() => { - if (!isDownloadTriggered.current) { + const excelClick = useCallback(async () => { + if (!isLaeSeCdReady || !appliedSearch) { return; } - if (rswtSttn.excelList.length > 0) { + + const excelData = await rswtSttnSearch( + true, + 1, + rswtSttnResCount, + appliedSearch.conValue, + appliedSearch.fcltNm, + code as unknown as Record + ); + if (excelData && excelData.length > 0) { const sheetArray: SheetParameter[] = [{ - data: rswtSttn.excelList, + data: excelData, headerNum: 3, headerRange: "A1:W3", - sheetName: 'sheet1', - startCol: 'A', - decimalArray0: [6,10,12,16], // 0부터 시작 + sheetName: "sheet1", + startCol: "A", + decimalArray0: [6, 10, 12, 16], }]; excelDownloadNumStyle( "중수도시설 현황", "sample_rswtSttn.xlsx", sheetArray ); - } else { - alert(t('excelDownErrMsg')); - } - isDownloadTriggered.current = false; - }, [rswtSttn.excelList]); - - const excelClick = useCallback(() => { - if (!isLaeSeCdReady) { return; } - isDownloadTriggered.current = true; - rswtSttnSearch(true, 1, rswtSttnResCount, appliedSearch.conValue, appliedSearch.fcltNm, code as unknown as Record); - }, [rswtSttnSearch, rswtSttnResCount, code, appliedSearch.conValue, appliedSearch.fcltNm, isLaeSeCdReady]); + alert(t("excelDownErrMsg")); + }, [rswtSttnSearch, rswtSttnResCount, code, appliedSearch, isLaeSeCdReady, alert, t]); useEffect(() => { - if (!isLaeSeCdReady) { - return; - } - - if (skipNextSearchRef.current) { - skipNextSearchRef.current = false; - return; - } - - if (searchTrigger === 0) { + if (!isLaeSeCdReady || !appliedSearch) { return; } rswtSttnSearch(false, pageNation, rswtSttnSize, appliedSearch.conValue, appliedSearch.fcltNm, code as unknown as Record); isResetPage(false) - }, [pageNation, searchTrigger, rswtSttnSearch, code, appliedSearch.conValue, appliedSearch.fcltNm, isLaeSeCdReady]); + }, [pageNation, rswtSttnSearch, code, appliedSearch, isLaeSeCdReady]); const handleSearch = useCallback(() => { if (!isLaeSeCdReady) { @@ -258,7 +245,6 @@ export default function RswtSttn() { setAppliedSearch(nextSearch); isResetPage(true); setPageNation(1); - setSearchTrigger((prev) => prev + 1); }, [fcltNm, getFixedConValue, isLaeSeCdReady]); return ( diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx index 82e256a6e..e79398c18 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/htdflSttn/index.tsx @@ -1,5 +1,5 @@ import type { CommonTableProps } from "@/app/store/common/type/header"; -import { fetchType, getFetchWithCount } from "@/components/lib/api"; +import { getFetchWithCount } from "@/components/lib/api"; import type { SelectDataType } from "@common/business_components/ui"; import type { z } from "zod"; import type { customHtdflBdst } from "../../fcltReg/htdflReg"; @@ -26,10 +26,10 @@ export type htdflSttnState = { type htdflSttnActions = { initHtdfl: (isExcel: boolean, page: number, total: number, con: string, txt: string, sidoCode: SelectDataType[], - sggCode: SelectDataType[]) => void; + sggCode: SelectDataType[]) => Promise; htdflSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, sidoCode: SelectDataType[], - sggCode: SelectDataType[]) => void; + sggCode: SelectDataType[]) => Promise; htdflChSttnHead: ( headList: z.infer, ) => void; @@ -63,36 +63,51 @@ const initFetch = async (page: number, total: number, con: string, txt: string) export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( set: any ) => { + const fetchHtdflSttn = async ( + isExcel: boolean, + page: number, + total: number, + con: string, + txt: string, + sidoCode: SelectDataType[], + sggCode: SelectDataType[] + ): Promise => { + const htdflSttnRes = await initFetch(page, total, con, txt); + if (isExcel) { + const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); + set((state: htdflSttnState) => { + return { + ...state, + htdflSttn: { + ...state.htdflSttn, + excelList: htdflExcel, + htdflSttnResCount: htdflSttnRes.count, + }, + }; + }); + return htdflExcel; + } + + set((state: htdflSttnState) => { + return { + ...state, + htdflSttn: { + ...state.htdflSttn, + htdflSttnRes: htdflSttnRes.data, + htdflSttnResCount: htdflSttnRes.count, + isInit: false, + }, + }; + }); + return; + }; + return { initHtdfl: async (isExcel: boolean, page: number, total: number, con: string, txt: string, sidoCode: SelectDataType[], sggCode: SelectDataType[]) => { try { - const htdflSttnRes = await initFetch(page, total, con, txt); - if (isExcel) { - const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - excelList: htdflExcel, - }, - }; - }); - } else { - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - htdflSttnRes: htdflSttnRes.data, - htdflSttnResCount: htdflSttnRes.count, - isInit: false, - }, - }; - }); - } + return await fetchHtdflSttn(isExcel, page, total, con, txt, sidoCode, sggCode); } catch (error) { console.log(error); } @@ -101,38 +116,7 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( sidoCode: SelectDataType[], sggCode: SelectDataType[]) => { try { - const htdflSttnRes = await getFetchWithCount( - fetchType.WR, - `/drain/facilityList` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/20") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - - if (isExcel) { - const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode); - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - excelList: htdflExcel, - }, - }; - }); - } else - set((state: htdflSttnState) => { - return { - ...state, - htdflSttn: { - ...state.htdflSttn, - htdflSttnRes: htdflSttnRes.data, - htdflSttnResCount: htdflSttnRes.count, - isInit: false, - }, - }; - }); + return await fetchHtdflSttn(isExcel, page, total, con, txt, sidoCode, sggCode); } catch (erTxt) { console.log("error", erTxt); } @@ -154,4 +138,4 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = ( } -} \ No newline at end of file +} diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx index 67de7f687..57f95c078 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rainSttn/index.tsx @@ -102,8 +102,8 @@ export type rainSttnState = { }; }; type rainSttnActions = { - rainSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record ) => void - rainSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record) => void; + rainSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record ) => Promise + rainSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record) => Promise; rainChSttnHead: ( headList: z.infer, ) => void; @@ -129,81 +129,63 @@ export const rainSttnStateExport: rainSttnState = { export const rainSttnActionsExport: (set: any) => rainSttnActions = ( set: any ) => { + const fetchRainSttn = async ( + isExcel: boolean, + page: number, + total: number, + con: string, + txt: string, + code: Record + ): Promise => { + const rainSttnRes = await getFetchWithCount( + fetchType.WR, + `/reuseFc/rainyInfoList` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/15") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); + + if (isExcel) { + const rainExcel = excelRainSttnParsing(rainSttnRes, code); + set((state: rainSttnState) => { + return { + ...state, + rainSttn: { + ...state.rainSttn, + excelList: rainExcel, + rainSttnResCount: rainSttnRes.count, + }, + }; + }); + return rainExcel; + } + + set((state: rainSttnState) => { + return { + ...state, + rainSttn: { + ...state.rainSttn, + rainSttnRes: rainSttnRes.data, + rainSttnResCount: rainSttnRes.count, + isInit: false, + }, + }; + }); + return; + }; + return { rainSttnInit: async (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record) => { try { - - const rainSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/rainyInfoList` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/15") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rainExcel = excelRainSttnParsing(rainSttnRes, code); - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - excelList: rainExcel, - rainSttnResCount: rainSttnRes.count, - }, - }; - }); - } else { - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - rainSttnRes: rainSttnRes.data, - rainSttnResCount: rainSttnRes.count, - isInit: false, - }, - }; - }); - } + await fetchRainSttn(isExcel, page, total, con, txt, code); } catch (e) { console.log(e) } }, rainSttnSearch: async (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record) => { try { - const rainSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/rainyInfoList` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/15") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rainExcel = excelRainSttnParsing(rainSttnRes,code); - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - excelList: rainExcel, - rainSttnResCount: rainSttnRes.count, - }, - }; - }); - } else - set((state: rainSttnState) => { - return { - ...state, - rainSttn: { - ...state.rainSttn, - rainSttnRes: rainSttnRes.data, - rainSttnResCount: rainSttnRes.count, - isInit: false, - }, - }; - }); + await fetchRainSttn(isExcel, page, total, con, txt, code); } catch (erTxt) { console.log("error", erTxt); } diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx index 47444ce32..df5cd9735 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/rswtSttn/index.tsx @@ -30,8 +30,8 @@ export type rswtSttnState = { }; type rswtSttnActions = { - rswtSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record) => void; - rswtSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record) => void; + rswtSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record) => Promise; + rswtSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record) => Promise; rswtChSttnHead: ( headList: z.infer, ) => void; @@ -55,79 +55,62 @@ export const rswtSttnStateExport: rswtSttnState = { export const rswtSttnActionsExport: (set: any) => rswtSttnActions = ( set: any ) => { + const fetchRswtSttn = async ( + isExcel: boolean, + page: number, + total: number, + con: string, + txt: string, + code: Record + ): Promise => { + const rswtSttnRes = await getFetchWithCount( + fetchType.WR, + `/reuseFc/waterreuseFc` + + (page ? `/${page}` : "/1") + + (total ? `/${total}` : "/10") + + (con ? `/${con}` : "/0") + + (txt ? `/${txt}` : "") + ); + if (isExcel) { + const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code); + set((state: rswtSttnState) => { + return { + ...state, + rswtSttn: { + ...state.rswtSttn, + excelList: rswtExcel, + rswtSttnResCount: rswtSttnRes.count, + }, + }; + }); + return rswtExcel; + } + + set((state: rswtSttnState) => { + return { + ...state, + rswtSttn: { + ...state.rswtSttn, + rswtSttnRes: rswtSttnRes.data, + rswtSttnResCount: rswtSttnRes.count, + isInit: false, + }, + }; + }); + return; + }; + return { rswtSttnInit: async (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record) => { try { - const rswtSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/waterreuseFc` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/10") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code); - set((state: rswtSttnState) => { - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - excelList: rswtExcel, - }, - }; - }); - } - set((state: rswtSttnState) => { - - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - rswtSttnRes: rswtSttnRes.data, - rswtSttnResCount: rswtSttnRes.count, - isInit: false, - }, - }; - }); + return await fetchRswtSttn(isExcel, page, total, con, txt, code); } catch (e) { console.log(e) } }, rswtSttnSearch: async (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record) => { try { - const rswtSttnRes = await getFetchWithCount( - fetchType.WR, - `/reuseFc/waterreuseFc` + - (page ? `/${page}` : "/1") + - (total ? `/${total}` : "/10") + - (con ? `/${con}` : "/0") + - (txt ? `/${txt}` : "") - ); - if (isExcel) { - const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code); - set((state: rswtSttnState) => { - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - excelList: rswtExcel, - }, - }; - }); - } else - set((state: rswtSttnState) => { - - return { - ...state, - rswtSttn: { - ...state.rswtSttn, - rswtSttnRes: rswtSttnRes.data, - rswtSttnResCount: rswtSttnRes.count, - isInit: false, - }, - }; - }); + return await fetchRswtSttn(isExcel, page, total, con, txt, code); } catch (erTxt) { console.log("error", erTxt); } diff --git a/apps/wr/docs/작업계획/호출병합2_20260625.md b/apps/wr/docs/작업계획/호출병합2_20260625.md new file mode 100644 index 000000000..e35d07fad --- /dev/null +++ b/apps/wr/docs/작업계획/호출병합2_20260625.md @@ -0,0 +1,201 @@ +# apps/wr 호출 병합 2차 작업계획 + +작성일: 2026-06-25 +대상 앱: `D:\iipbs\frontend\apps\wr` +대상 화면: +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/rswtSttn/page.tsx` +- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/htdflSttn/page.tsx` + +## 1. 작업 배경 + +- `rainSttn`에서 정리한 호출 병합 방향을 `rswtSttn`, `htdflSttn`에도 확장할 필요가 있다. +- `rswtSttn`은 `searchTrigger`, `skipNextSearchRef`, `rswtSttnInit` 분리 구조가 그대로 남아 있어 `rainSttn`과 동일한 페이징 불량 가능성이 높다. +- `htdflSttn`은 `searchTrigger`는 없지만, 조회 버튼/페이징/엑셀 다운로드가 모두 `objSearch`, `fcltNm`을 직접 읽고 있어 “입력 중 값”과 “실제 적용된 값”이 섞일 수 있다. +- 두 화면 모두 엑셀 다운로드는 store의 `excelList` 반영 타이밍에 의존하고 있어, 첫 클릭 시 "다운로드할 데이터가 없습니다."가 발생할 수 있다. + +## 2. 현재 구조 분석 + +### 2.1 rswtSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `laeSeCd` 준비 후 `rswtSttnInit(false, 1, rswtSttnSize, ...)` 호출 +- 이후 페이징/조회: + - `pageNation`, `searchTrigger`, `appliedSearch`를 감시하는 effect에서 `rswtSttnSearch(...)` 호출 +- 추가 제어: + - `skipNextSearchRef`로 초기 effect 1회를 건너뜀 + - `searchTrigger === 0`이면 검색 effect를 중단 +- 엑셀: + - `isDownloadTriggered.current = true` 설정 + - `rswtSttnSearch(true, ...)` 호출 + - 이후 `rswtSttn.excelList` 변경 effect에서 다운로드 실행 + +### 2.2 htdflSttn 현재 흐름 + +- 초기 진입: + - `getLaeSeCdCheck()` 실행 + - `initHtdfl(false, 1, htdflSttnSize, String(getFixedConValue()), fcltNm, ...)` 호출 +- 조회 버튼: + - 클릭 시 즉시 `htdflSttnSearch(false, 1, ..., String(getFixedConValue()), fcltNm, ...)` 호출 + - `pageNation`은 1로 돌리지 않고 `resetPageNo`만 켬 +- 페이징: + - `pageNation` 변경 effect에서 `String(getFixedConValue())`, `fcltNm`을 직접 읽어 `htdflSttnSearch(...)` 호출 +- 엑셀: + - `isDownloadTriggered.current = true` 설정 + - `htdflSttnSearch(true, ...)` 호출 + - 이후 `htdflSttn.excelList` 변경 effect에서 다운로드 실행 + +### 2.3 실제 문제 지점 + +- `rswtSttn` + - 사용자가 조회 버튼을 누르기 전에는 `searchTrigger === 0` 때문에 페이지 이동 시 API 호출이 막힌다. + - 초기 조회와 이후 조회가 `init/search`로 나뉘어 동일한 목록 조회인데도 상태 플래그에 의존한다. +- `htdflSttn` + - 페이지 이동 시 `objSearch`, `fcltNm`의 현재 입력값을 그대로 읽으므로, 마지막 조회 결과와 다른 조건으로 페이지가 바뀔 수 있다. + - 조회 버튼이 `pageNation`과 분리되어 있어 "1페이지로 되돌아간 뒤 같은 조건으로 재조회" 흐름이 일관되지 않다. +- 공통 + - 엑셀 다운로드는 클릭 직후 `excelList`를 검사할 수 있어 비동기 응답 전에 빈 배열로 판정될 수 있다. + - 첫 클릭 뒤 뒤늦게 store에 엑셀 데이터가 남으면, 다음 클릭에서 이전 응답 결과로 다운로드될 위험도 있다. + +## 3. 목표 구조 + +### 3.1 조회 호출 단일화 + +- 초기 진입도 `search` 계열 호출로 통일한다. +- 같은 파라미터라면 초기 진입, 조회 버튼, 페이지 이동, 엑셀 다운로드가 모두 동일한 조건 계산 경로를 사용한다. +- 조회 버튼은 “적용 조건 확정 + 페이지 1 초기화”만 담당하고, 실제 목록 조회는 하나의 경로로만 실행한다. + +### 3.2 적용 조건 분리 + +- 입력 중 값은 `objSearch`, `fcltNm`으로 유지한다. +- 실제 조회 기준 값은 `appliedSearch`로 분리한다. +- 페이지 이동, 재조회, 엑셀 다운로드는 반드시 `appliedSearch`만 사용한다. + +### 3.3 엑셀 다운로드 직접 처리 + +- `excelClick`에서 `await ...Search(true, ...)` 완료를 직접 기다린다. +- store의 `excelList` 변경 effect를 기다리지 않고, 반환받은 엑셀 데이터로 즉시 다운로드를 실행한다. +- store는 필요하면 기존 `excelList`도 유지할 수 있으나, 다운로드 성공 여부는 반환값 기준으로 판단한다. + +## 4. 수정 대상 + +### 4.1 화면 + +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\rswtSttn\page.tsx` +- `D:\iipbs\frontend\apps\wr\app\(with-layout)\ruseFcltMng\fcltSttn\htdflSttn\page.tsx` + +### 4.2 store + +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\rswtSttn\index.tsx` +- `D:\iipbs\frontend\apps\wr\app\store\ruseFcltMng\fcltSttn\htdflSttn\index.tsx` + +## 5. 권장 구현 방향 + +### 5.1 rswtSttn 화면 정리 + +- `rswtSttnInit()` 직접 호출 제거 +- `searchTrigger` 제거 +- `skipNextSearchRef` 제거 또는 최소화 +- `appliedSearch`를 `null` 가능 상태로 두고, 초기 진입 시 1회 세팅 +- 목록 조회 effect는 `pageNation`, `appliedSearch`, `isLaeSeCdReady`만 감시 +- 조회 버튼은 `appliedSearch` 갱신 후 `setPageNation(1)`만 수행 +- 엑셀 다운로드는 `await rswtSttnSearch(true, ...)` 후 반환값으로 직접 다운로드 + +### 5.2 rswtSttn store 정리 + +- `rswtSttnInit`는 삭제 `rswtSttnSearch`으로 병합. +- `isExcel === true`일 때 파싱된 엑셀 행 데이터를 반환하도록 시그니처 확장 +- 목록 조회일 때는 기존처럼 `rswtSttnRes`, `rswtSttnResCount`를 갱신 + +### 5.3 htdflSttn 화면 정리 + +- `objSearch`, `fcltNm`와 별도로 `appliedSearch`를 추가 +- 초기 진입 시 `laeSeCd` 기준 초기 `appliedSearch`를 세팅하고, 목록 조회는 `htdflSttnSearch(false, 1, ...)` 경로로 통일 +- 조회 버튼은 `appliedSearch` 갱신 후 `setPageNation(1)`만 수행 +- 페이지 이동 effect는 반드시 `appliedSearch` 기준으로만 호출 +- 현재 `initHtdfl(false, ..., String(getFixedConValue()), fcltNm, ...)`처럼 입력 중 값에 직접 의존하는 구조를 제거 +- 엑셀 다운로드는 `await htdflSttnSearch(true, ...)` 후 반환값으로 직접 다운로드 + +### 5.4 htdflSttn store 정리 + +- `initHtdfl`는 삭제 `htdflSttnSearch` 으로 병합 +- `isExcel === true`일 때 파싱된 엑셀 행 데이터를 반환하도록 시그니처 확장 +- 목록 조회 시 `htdflSttnRes`, `htdflSttnResCount` 갱신 경로는 유지 + +## 6. 상세 작업 단계 + +### 단계 1. rswtSttn 호출 병합 + +- `page.tsx` + - `rswtSttnInit` 제거 + - `searchTrigger` 제거 + - `skipNextSearchRef` 제거 + - 초기 effect에서 `appliedSearch`만 세팅 + - 목록 조회 effect는 `appliedSearch` 준비 후 항상 `rswtSttnSearch(false, pageNation, ...)` 호출 +- `index.tsx` + - 공통 fetch 함수 도입 + - `rswtSttnSearch(true, ...)`가 `string[][]` 반환 가능하도록 수정 + +### 단계 2. rswtSttn 엑셀 다운로드 직접화 + +- `isDownloadTriggered` ref 제거 +- `rswtSttn.excelList` 감시 effect 제거 +- `excelClick`를 `async`로 변경 +- `const excelData = await rswtSttnSearch(true, ...)` 후: + - 데이터가 있으면 즉시 `excelDownloadNumStyle(...)` + - 없으면 `excelDownErrMsg` 노출 + +### 단계 3. htdflSttn 적용 조건 분리 + +- `appliedSearch` 상태 추가 +- 초기 진입 시 `laeSeCd` 기준 기본 조건을 `appliedSearch`에 반영 +- 조회 버튼은 입력값을 `appliedSearch`로 확정한 뒤 페이지를 1로 초기화 +- 페이지 이동 effect는 `appliedSearch`만 기준으로 목록 조회 +- `resetPageNo`는 UI 초기화용으로만 유지 + +### 단계 4. htdflSttn store 병합 및 엑셀 직접화 + +- `initHtdfl`는 삭제 `htdflSttnSearch` 으로 병합 +- `htdflSttnSearch(true, ...)`가 엑셀 데이터 반환 가능하도록 수정 +- 화면에서 `await htdflSttnSearch(true, ...)` 결과로 직접 다운로드 +- `isDownloadTriggered` ref 및 `htdflSttn.excelList` 감시 effect 제거 + +## 7. 구현 시 주의사항 + +- `objSearch`는 입력 UI 상태이고, API 호출 기준은 `appliedSearch`다. +- `setPageNation(1)` 직후 중복 조회가 생기지 않도록, 조회 버튼에서 직접 API를 다시 호출하지 말고 하나의 effect 경로만 사용한다. +- `htdflSttn`은 현재 조회 버튼이 `setPageNation(1)` 없이 직접 API를 호출하고 있으므로 이 부분을 반드시 통일해야 한다. +- 엑셀은 전체 건수 기준 조회를 유지하되, 조건 계산은 일반 목록 조회와 동일해야 한다. +- store에서 엑셀 데이터 반환을 추가하더라도 기존 목록 갱신 로직은 깨지지 않게 분기해야 한다. + +## 8. 검증 계획 + +### 8.1 rswtSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 초기 진입 직후 조회 버튼을 누르지 않고 2페이지로 이동하면 API가 호출된다. +- 조회 조건 변경 후 조회 버튼 클릭 시 1페이지로 이동하며 API가 1회만 호출된다. +- 페이지 2, 3으로 이동해도 같은 `appliedSearch` 조건이 유지된다. +- 첫 엑셀다운로드 클릭에서 즉시 파일이 내려오고, "다운로드할 데이터가 없습니다."가 잘못 뜨지 않는다. + +### 8.2 htdflSttn 검증 + +- 초기 진입 시 1페이지 목록이 조회된다. +- 조회 버튼 클릭 시 항상 1페이지 기준으로 재조회된다. +- 조회 후 입력값만 바꾸고 조회 버튼을 누르지 않은 상태에서 페이지 이동해도, 마지막 조회 조건으로 페이지가 유지된다. +- 첫 엑셀다운로드 클릭에서도 현재 적용 조건 기준으로 정상 다운로드된다. + +### 8.3 회귀 검증 + +- 상세 모달 열기 동작 유지 +- 권한별 `laeSeCd` 초기 조건 유지 +- 시도/시군구 선택 제한 유지 +- 조회항목설정 모달 동작 유지 + +## 9. 결론 + +- `rswtSttn`은 `rainSttn`과 거의 동일한 구조적 문제를 갖고 있으므로 같은 방식으로 정리하는 것이 맞다. +- `htdflSttn`은 페이징 가드 문제는 없지만, 적용 조건 분리와 엑셀 다운로드 처리 방식은 동일하게 손봐야 한다. +- 이번 2차 작업의 핵심은 다음 두 가지다. +- “목록 조회는 항상 `appliedSearch` 기준의 단일 경로로 통일” +- “엑셀 다운로드는 store 반영 effect가 아니라 `await ...Search(true, ...)` 반환값으로 직접 처리”