wr: 페이징 수정

This commit is contained in:
2026-06-25 18:09:04 +09:00
parent 6110abebb4
commit 07db5b1fbd
7 changed files with 510 additions and 374 deletions
@@ -40,11 +40,10 @@ import { useTranslation } from "react-i18next";
const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }];
export default function HtdflSttn() {
const { auth, code, htdflSttn, htdflSttnSearch, initHtdfl, getLaeSeCdCheck } = useWrStore((state) => state);
const { auth, code, htdflSttn, htdflSttnSearch, getLaeSeCdCheck } = useWrStore((state) => state);
const { laeSeCd } = auth; // 지자체구분코드
const { htdflSttnRes, htdflSttnResCount } = htdflSttn;
const isDownloadTriggered = useRef(false);
const skipNextSearchRef = useRef(true);
const initializedSearchKeyRef = useRef<string | null>(null);
const { t } = useTranslation('base');
const [pageNation, setPageNation] = useState(1);
const [isChHead, setIsChHead] = useState(false);
@@ -61,6 +60,10 @@ export default function HtdflSttn() {
// chBuilding: chgBuilding[];
// }>({ isOpen: false, chBuilding: [] });
const [fcltNm, setFcltNm] = useState<string>("");
const [appliedSearch, setAppliedSearch] = useState<{
conValue: string;
fcltNm: string;
} | null>(null);
const alert = useAlertStore((state) => state.setMessage);
const isAllAreaUser = laeSeCd === "0";
const isLaeSeCdReady =
@@ -89,7 +92,7 @@ export default function HtdflSttn() {
setObjSearch({ ...objSearch, sgg: String(val) })
};
const getConValue = (): string => {
const getConValue = useCallback((): string => {
if (!objSearch.city || objSearch.city === "0") {
return "0";
}
@@ -97,7 +100,7 @@ export default function HtdflSttn() {
return String(objSearch.sgg);
}
return String(objSearch.city);
};
}, [objSearch]);
const getFixedConValue = useCallback((): string => {
if (laeSeCd === undefined || laeSeCd === "0") {
@@ -105,7 +108,7 @@ export default function HtdflSttn() {
}
return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2);
}, [laeSeCd, objSearch]);
}, [laeSeCd, getConValue]);
const cellClick = (index: number | undefined, headId: string) => {
if (
@@ -117,14 +120,39 @@ export default function HtdflSttn() {
}
};
const excelClick = useCallback(() => {
if (!isLaeSeCdReady) {
const excelClick = useCallback(async () => {
if (!isLaeSeCdReady || !appliedSearch) {
return;
}
isDownloadTriggered.current = true;
htdflSttnSearch(true, 1, htdflSttnResCount, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
}, [htdflSttnSearch, fcltNm, htdflSttnResCount, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]);
const excelData = await htdflSttnSearch(
true,
1,
htdflSttnResCount,
appliedSearch.conValue,
appliedSearch.fcltNm,
code.cityCd,
code.sggCd
);
if (excelData && excelData.length > 0) {
const sheetArray: SheetParameter[] = [{
data: excelData,
headerNum: 3,
headerRange: "A1:R3",
sheetName: "sheet1",
startCol: "A",
decimalArray0: [6, 11, 13, 15],
}];
excelDownloadNumStyle(
"온배수시설 현황",
"sample_htdflSttn.xlsx",
sheetArray
);
return;
}
alert(t("excelDownErrMsg"));
}, [htdflSttnSearch, htdflSttnResCount, appliedSearch, code.cityCd, code.sggCd, isLaeSeCdReady, alert, t]);
useEffect(() => {
getLaeSeCdCheck();
@@ -150,52 +178,39 @@ export default function HtdflSttn() {
});
}, [laeSeCd, code.sggCd, isLaeSeCdReady]);
useEffect(() => {
if (!isDownloadTriggered.current) {
return;
}
if (htdflSttn.excelList.length > 0) {
const sheetArray: SheetParameter[] = [{
data: htdflSttn.excelList,
headerNum: 3,
headerRange: "A1:R3",
sheetName: 'sheet1',
startCol: 'A',
decimalArray0: [6,11,13,15], // 0부터 시작
}];
excelDownloadNumStyle(
"온배수시설 현황",
"sample_htdflSttn.xlsx",
sheetArray
);
} else {
alert(t('excelDownErrMsg'));
}
isDownloadTriggered.current = false;
}, [htdflSttn.excelList, code]);
useEffect(() => {
if (!isLaeSeCdReady || htdflSttn.isInit) {
return;
}
skipNextSearchRef.current = true;
initHtdfl(false, 1, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
}, [htdflSttn.isInit, initHtdfl, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]);
useEffect(() => {
if (!isLaeSeCdReady) {
return;
}
if (skipNextSearchRef.current) {
skipNextSearchRef.current = false;
const initialSearchKey = laeSeCd ?? "0";
if (initializedSearchKeyRef.current === initialSearchKey) {
return;
}
htdflSttnSearch(false, pageNation, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
const initialConValue =
laeSeCd === undefined || laeSeCd === "0"
? "0"
: laeSeCd.length === 5
? laeSeCd
: laeSeCd.slice(0, 2);
initializedSearchKeyRef.current = initialSearchKey;
setAppliedSearch({
conValue: initialConValue,
fcltNm: "",
});
setPageNation(1);
}, [isLaeSeCdReady, laeSeCd]);
useEffect(() => {
if (!isLaeSeCdReady || !appliedSearch) {
return;
}
htdflSttnSearch(false, pageNation, htdflSttnSize, appliedSearch.conValue, appliedSearch.fcltNm, code.cityCd, code.sggCd);
isResetPage(false)
}, [pageNation, htdflSttnSearch, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]);
}, [pageNation, htdflSttnSearch, appliedSearch, code.cityCd, code.sggCd, isLaeSeCdReady]);
useEffect(() => {
const data = tableData(htdflSttnRes, { code });
@@ -215,6 +230,19 @@ export default function HtdflSttn() {
setBdstTable(chData);
}, [code, htdflSttnRes, htdflSttn.htdflSttHeadList]);
const handleSearch = useCallback(() => {
if (!isLaeSeCdReady) {
return;
}
setAppliedSearch({
conValue: String(getFixedConValue()),
fcltNm,
});
isResetPage(true);
setPageNation(1);
}, [fcltNm, getFixedConValue, isLaeSeCdReady]);
return (
<>
<ContentsDiv>
@@ -266,13 +294,7 @@ export default function HtdflSttn() {
<div className="flex flex-row space-x-2 items-center">
<Button
className={btnClass}
onClick={() => {
if (!isLaeSeCdReady) {
return;
}
htdflSttnSearch(false, 1,htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
isResetPage(true)
}}
onClick={handleSearch}
>
조회
</Button>
@@ -1,5 +1,5 @@
/**
* 빗물이용시설 현황
* 재이용시설 현황 > 빗물이용시설 현황
*/
"use client";
import { useWrStore } from "@/app/store";
@@ -38,7 +38,7 @@ import { useTranslation } from "react-i18next";
const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }];
export default function RainSttn() {
const { auth, code, rainSttn, rainSttnSearch, rainSttnInit, getLaeSeCdCheck } = useWrStore(
const { auth, code, rainSttn, rainSttnSearch, getLaeSeCdCheck } = useWrStore(
(state) => state
);
const { rainSttnRes, rainSttnResCount } = rainSttn;
@@ -49,7 +49,6 @@ export default function RainSttn() {
const [bdstTable, setBdstTable] = useState<customRainBdst[]>();
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions);
const [resetPage, isResetPage] = useState(false);
const [isDownloadTriggered, setIsDownloadTriggered] = useState(false);
const [objSearch, setObjSearch] = useState<objSearchType>({
sgg: "",
city: "",
@@ -60,14 +59,13 @@ export default function RainSttn() {
}>({ data: undefined, isOpen: false });
const [fcltNm, setFcltNm] = useState<string>("");
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<string | null>(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<string, SelectDataType[]>
);
}, [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<string, SelectDataType[]>
);
if (excelData && excelData.length > 0) {
const sheetArray: SheetParameter[] = [{
data: excelData,
headerNum: 3,
headerRange: "A1:W3",
sheetName: "sheet1",
startCol: "A",
decimalArray0: [6, 10, 12, 14, 16],
}];
excelDownloadNumStyle(
"빗물이용시설 현황",
"sample_rainSttn.xlsx",
sheetArray
);
return;
}
alert(t("excelDownErrMsg"));
}, [
rainSttnSearch,
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 });
@@ -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<customRswtBdst[]>();
const isDownloadTriggered = useRef(false);
const skipNextSearchRef = useRef(true);
const initializedSearchKeyRef = useRef<string | null>(null);
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions)
const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" });
const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false })
const [fcltNm, setFcltNm] = useState<string>("");
const [appliedSearch, setAppliedSearch] = useState({
conValue: "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<string, SelectDataType[]>
);
}, [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<string, SelectDataType[]>
);
if (excelData && excelData.length > 0) {
const sheetArray: SheetParameter[] = [{
data: rswtSttn.excelList,
data: excelData,
headerNum: 3,
headerRange: "A1:W3",
sheetName: 'sheet1',
startCol: 'A',
decimalArray0: [6,10,12,16], // 0부터 시작
sheetName: "sheet1",
startCol: "A",
decimalArray0: [6, 10, 12, 16],
}];
excelDownloadNumStyle(
"중수도시설 현황",
"sample_rswtSttn.xlsx",
sheetArray
);
} else {
alert(t('excelDownErrMsg'));
}
isDownloadTriggered.current = false;
}, [rswtSttn.excelList]);
const excelClick = useCallback(() => {
if (!isLaeSeCdReady) {
return;
}
isDownloadTriggered.current = true;
rswtSttnSearch(true, 1, rswtSttnResCount, appliedSearch.conValue, appliedSearch.fcltNm, code as unknown as Record<string, SelectDataType[]>);
}, [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<string, SelectDataType[]>);
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 (
@@ -1,5 +1,5 @@
import type { CommonTableProps } from "@/app/store/common/type/header";
import { fetchType, getFetchWithCount } from "@/components/lib/api";
import { getFetchWithCount } from "@/components/lib/api";
import type { SelectDataType } from "@common/business_components/ui";
import type { z } from "zod";
import type { customHtdflBdst } from "../../fcltReg/htdflReg";
@@ -26,10 +26,10 @@ export type htdflSttnState = {
type htdflSttnActions = {
initHtdfl: (isExcel: boolean, page: number, total: number, con: string, txt: string,
sidoCode: SelectDataType[],
sggCode: SelectDataType[]) => void;
sggCode: SelectDataType[]) => Promise<string[][] | void>;
htdflSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string,
sidoCode: SelectDataType[],
sggCode: SelectDataType[]) => void;
sggCode: SelectDataType[]) => Promise<string[][] | void>;
htdflChSttnHead: (
headList: z.infer<typeof sttnHeadListType>,
) => void;
@@ -63,36 +63,51 @@ const initFetch = async (page: number, total: number, con: string, txt: string)
export const htdflSttnActionsExport: (set: any) => htdflSttnActions = (
set: any
) => {
const fetchHtdflSttn = async (
isExcel: boolean,
page: number,
total: number,
con: string,
txt: string,
sidoCode: SelectDataType[],
sggCode: SelectDataType[]
): Promise<string[][] | void> => {
const htdflSttnRes = await initFetch(page, total, con, txt);
if (isExcel) {
const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode);
set((state: htdflSttnState) => {
return {
...state,
htdflSttn: {
...state.htdflSttn,
excelList: htdflExcel,
htdflSttnResCount: htdflSttnRes.count,
},
};
});
return htdflExcel;
}
set((state: htdflSttnState) => {
return {
...state,
htdflSttn: {
...state.htdflSttn,
htdflSttnRes: htdflSttnRes.data,
htdflSttnResCount: htdflSttnRes.count,
isInit: false,
},
};
});
return;
};
return {
initHtdfl: async (isExcel: boolean, page: number, total: number, con: string, txt: string,
sidoCode: SelectDataType[],
sggCode: SelectDataType[]) => {
try {
const htdflSttnRes = await initFetch(page, total, con, txt);
if (isExcel) {
const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode);
set((state: htdflSttnState) => {
return {
...state,
htdflSttn: {
...state.htdflSttn,
excelList: htdflExcel,
},
};
});
} else {
set((state: htdflSttnState) => {
return {
...state,
htdflSttn: {
...state.htdflSttn,
htdflSttnRes: htdflSttnRes.data,
htdflSttnResCount: htdflSttnRes.count,
isInit: false,
},
};
});
}
return await fetchHtdflSttn(isExcel, page, total, con, txt, sidoCode, sggCode);
} catch (error) {
console.log(error);
}
@@ -101,38 +116,7 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = (
sidoCode: SelectDataType[],
sggCode: SelectDataType[]) => {
try {
const htdflSttnRes = await getFetchWithCount(
fetchType.WR,
`/drain/facilityList` +
(page ? `/${page}` : "/1") +
(total ? `/${total}` : "/20") +
(con ? `/${con}` : "/0") +
(txt ? `/${txt}` : "")
);
if (isExcel) {
const htdflExcel = excelHtdflSttnParsing(htdflSttnRes, sidoCode, sggCode);
set((state: htdflSttnState) => {
return {
...state,
htdflSttn: {
...state.htdflSttn,
excelList: htdflExcel,
},
};
});
} else
set((state: htdflSttnState) => {
return {
...state,
htdflSttn: {
...state.htdflSttn,
htdflSttnRes: htdflSttnRes.data,
htdflSttnResCount: htdflSttnRes.count,
isInit: false,
},
};
});
return await fetchHtdflSttn(isExcel, page, total, con, txt, sidoCode, sggCode);
} catch (erTxt) {
console.log("error", erTxt);
}
@@ -154,4 +138,4 @@ export const htdflSttnActionsExport: (set: any) => htdflSttnActions = (
}
}
}
@@ -102,8 +102,8 @@ export type rainSttnState = {
};
};
type rainSttnActions = {
rainSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]> ) => void
rainSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]>) => void;
rainSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]> ) => Promise<string[][] | void>
rainSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string, SelectDataType[]>) => Promise<string[][] | void>;
rainChSttnHead: (
headList: z.infer<typeof sttnHeadListType>,
) => void;
@@ -129,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<string, SelectDataType[]>
): Promise<string[][] | void> => {
const rainSttnRes = await getFetchWithCount(
fetchType.WR,
`/reuseFc/rainyInfoList` +
(page ? `/${page}` : "/1") +
(total ? `/${total}` : "/15") +
(con ? `/${con}` : "/0") +
(txt ? `/${txt}` : "")
);
if (isExcel) {
const rainExcel = excelRainSttnParsing(rainSttnRes, code);
set((state: rainSttnState) => {
return {
...state,
rainSttn: {
...state.rainSttn,
excelList: rainExcel,
rainSttnResCount: rainSttnRes.count,
},
};
});
return rainExcel;
}
set((state: rainSttnState) => {
return {
...state,
rainSttn: {
...state.rainSttn,
rainSttnRes: rainSttnRes.data,
rainSttnResCount: rainSttnRes.count,
isInit: false,
},
};
});
return;
};
return {
rainSttnInit: async (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string, SelectDataType[]>) => {
try {
const rainSttnRes = await getFetchWithCount(
fetchType.WR,
`/reuseFc/rainyInfoList` +
(page ? `/${page}` : "/1") +
(total ? `/${total}` : "/15") +
(con ? `/${con}` : "/0") +
(txt ? `/${txt}` : "")
);
if (isExcel) {
const rainExcel = excelRainSttnParsing(rainSttnRes, code);
set((state: rainSttnState) => {
return {
...state,
rainSttn: {
...state.rainSttn,
excelList: rainExcel,
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<string, SelectDataType[]>) => {
try {
const rainSttnRes = await getFetchWithCount(
fetchType.WR,
`/reuseFc/rainyInfoList` +
(page ? `/${page}` : "/1") +
(total ? `/${total}` : "/15") +
(con ? `/${con}` : "/0") +
(txt ? `/${txt}` : "")
);
if (isExcel) {
const rainExcel = excelRainSttnParsing(rainSttnRes,code);
set((state: rainSttnState) => {
return {
...state,
rainSttn: {
...state.rainSttn,
excelList: rainExcel,
rainSttnResCount: rainSttnRes.count,
},
};
});
} else
set((state: rainSttnState) => {
return {
...state,
rainSttn: {
...state.rainSttn,
rainSttnRes: rainSttnRes.data,
rainSttnResCount: rainSttnRes.count,
isInit: false,
},
};
});
await fetchRainSttn(isExcel, page, total, con, txt, code);
} catch (erTxt) {
console.log("error", erTxt);
}
@@ -30,8 +30,8 @@ export type rswtSttnState = {
};
type rswtSttnActions = {
rswtSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => void;
rswtSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => void;
rswtSttnInit: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => Promise<string[][] | void>;
rswtSttnSearch: (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => Promise<string[][] | void>;
rswtChSttnHead: (
headList: z.infer<typeof sttnHeadListType>,
) => void;
@@ -55,79 +55,62 @@ export const rswtSttnStateExport: rswtSttnState = {
export const rswtSttnActionsExport: (set: any) => rswtSttnActions = (
set: any
) => {
const fetchRswtSttn = async (
isExcel: boolean,
page: number,
total: number,
con: string,
txt: string,
code: Record<string, SelectDataType[]>
): Promise<string[][] | void> => {
const rswtSttnRes = await getFetchWithCount(
fetchType.WR,
`/reuseFc/waterreuseFc` +
(page ? `/${page}` : "/1") +
(total ? `/${total}` : "/10") +
(con ? `/${con}` : "/0") +
(txt ? `/${txt}` : "")
);
if (isExcel) {
const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code);
set((state: rswtSttnState) => {
return {
...state,
rswtSttn: {
...state.rswtSttn,
excelList: rswtExcel,
rswtSttnResCount: rswtSttnRes.count,
},
};
});
return rswtExcel;
}
set((state: rswtSttnState) => {
return {
...state,
rswtSttn: {
...state.rswtSttn,
rswtSttnRes: rswtSttnRes.data,
rswtSttnResCount: rswtSttnRes.count,
isInit: false,
},
};
});
return;
};
return {
rswtSttnInit: async (isExcel: boolean, page: number, total: number, con: string, txt: string, code: Record<string,SelectDataType[]>) => {
try {
const rswtSttnRes = await getFetchWithCount(
fetchType.WR,
`/reuseFc/waterreuseFc` +
(page ? `/${page}` : "/1") +
(total ? `/${total}` : "/10") +
(con ? `/${con}` : "/0") +
(txt ? `/${txt}` : "")
);
if (isExcel) {
const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code);
set((state: rswtSttnState) => {
return {
...state,
rswtSttn: {
...state.rswtSttn,
excelList: rswtExcel,
},
};
});
}
set((state: rswtSttnState) => {
return {
...state,
rswtSttn: {
...state.rswtSttn,
rswtSttnRes: rswtSttnRes.data,
rswtSttnResCount: rswtSttnRes.count,
isInit: false,
},
};
});
return await fetchRswtSttn(isExcel, page, total, con, txt, code);
} catch (e) {
console.log(e)
}
},
rswtSttnSearch: async (isExcel: boolean, page: number, total: number, con: string, txt: string,code: Record<string,SelectDataType[]>) => {
try {
const rswtSttnRes = await getFetchWithCount(
fetchType.WR,
`/reuseFc/waterreuseFc` +
(page ? `/${page}` : "/1") +
(total ? `/${total}` : "/10") +
(con ? `/${con}` : "/0") +
(txt ? `/${txt}` : "")
);
if (isExcel) {
const rswtExcel = excelRSwtSttnParsing(rswtSttnRes, code);
set((state: rswtSttnState) => {
return {
...state,
rswtSttn: {
...state.rswtSttn,
excelList: rswtExcel,
},
};
});
} else
set((state: rswtSttnState) => {
return {
...state,
rswtSttn: {
...state.rswtSttn,
rswtSttnRes: rswtSttnRes.data,
rswtSttnResCount: rswtSttnRes.count,
isInit: false,
},
};
});
return await fetchRswtSttn(isExcel, page, total, con, txt, code);
} catch (erTxt) {
console.log("error", erTxt);
}
@@ -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, ...)` 반환값으로 직접 처리”