Merge branch 'iipbs/wr/dev'

This commit is contained in:
2026-07-02 09:10:14 +09:00
26 changed files with 1560 additions and 852 deletions
@@ -46,7 +46,8 @@ export const mapService = {
return response.json();
},
getPbnstRuseFclts: async (ruseFcltPk:any) => {
const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}`, {
const year = new Date().getFullYear();
const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}?year=${year}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
+3 -2
View File
@@ -2,7 +2,8 @@ import { getFetch, postFectch, fetchType } from "@/components/lib/api";
const fetchpbnstDetail = async (featureCd: string, setTabData: any) => {
try {
const response = await getFetch("wr", `/reuseFc/pubFc/${featureCd}`);
const year = new Date().getFullYear();
const response = await getFetch("wr", `/reuseFc/pubFc/${featureCd}?year=${year}`);
// 중복 데이터 추가 방지
setTabDataFunc(response, featureCd, setTabData);
} catch (error) {
@@ -55,4 +56,4 @@ const detainSttn = async (featureCd: string, layerNm: string) => {
}
}
export { fetchHasuDetail, fetchpbnstDetail, detainSttn };
export { fetchHasuDetail, fetchpbnstDetail, detainSttn };
@@ -1,12 +1,17 @@
/**
* 하수재이용 시설 현황
* 재이용시설 현황 - 하수재이용시설 현황
/**
* 하수재이용시설 현황
* 재이용시설 현황 > 하수재이용시설 현황
*
* 사용 API
* 하수재이용시설 현황 목록 조회 reuse-service /reuseFc/pbnstList
* 하수처리시설 기본정보 조회 operation-service /prcsFclt/ruseInfo
* 상세조회 모달 전달을 위한 하수재이용시설 상세 조회 reuse-service /reuseFc/pubFc/{pbnstRuseFcltId}
*
* - layout.tsx에 의해 공통 레이아웃이 적용됨
* - with-layout 적용
* 하수처리시설. 하수재이용시설 , 하수재이용시설 실적에 대한 정보를 확인한다.
* 모달 : 테이블의 헤더를 원하는대로 조절해서 볼수 있다.
* 클릭시 해당 재이용ㅇ
* */
* - 하수처리시설, 하수재이용시설, 하수재이용시설 실적에 대한 정보를 확인한다.
* - 모달에서 테이블 헤더를 원하는대로 조절해서 볼 수 있다.
*/
"use client";
import { useWrStore } from "@/app/store";
import { btnClass } from "@/app/store/common/code";
@@ -113,8 +118,7 @@ export default function PbnstSttn() {
"100000",
fixedSearchValue,
pbnRuseNm,
searchYear,
[]
searchYear
);
}, [pbnstSttnList, pbnRuseNm, searchYear, getFixedSearchValue]);
@@ -122,10 +126,10 @@ export default function PbnstSttn() {
(index: number | undefined) => {
if (index !== undefined && pbnstSttnRes && pbnstSttnRes[index]) {
const pbnstRuseFcltId = pbnstSttnRes[index].pbnstRuseFcltId;
getPbnStnDetail(pbnstRuseFcltId);
getPbnStnDetail(pbnstRuseFcltId, searchYear);
}
},
[getPbnStnDetail, pbnstSttnRes]
[getPbnStnDetail, pbnstSttnRes, searchYear]
);
useEffect(() => {
@@ -145,8 +149,7 @@ export default function PbnstSttn() {
String(DEFAULT_LIST_SIZE),
getFixedSearchValue(ref.current),
pbnRuseNm,
searchYear,
[]
searchYear
);
}
}, [
@@ -265,8 +268,7 @@ export default function PbnstSttn() {
String(DEFAULT_LIST_SIZE),
fixedSearchValue,
pbnRuseNm,
searchYear,
[]
searchYear
);
}}
excelClick={excelClick}
@@ -303,8 +305,7 @@ export default function PbnstSttn() {
String(DEFAULT_LIST_SIZE),
fixedSearchValue,
pbnRuseNm,
searchYear,
[]
searchYear
);
}}
widthFixed={true}
@@ -1,5 +1,9 @@
/**
/**
* 재이용실적등록 > 하수재이용실적등록
*
* 🔗 사용 API
* 하수재이용시설 목록 조회 reuse-service /reuseFc/pbnstPrfRegList
* 하수재이용시설 실적 상세 조회 reuse-service /reuseFc/pbnstPrfRegDtl
*/
"use client";
import { useWrStore } from "@/app/store";
@@ -10,13 +14,11 @@ import {
initValOutUsage,
subTotalObj,
totalObj,
type FcPrfType,
type PbnstPrfRegDtlType,
type PbnstPrfRegListType,
type PrfType,
} from "@/app/store/prfmncReg/pbnstPrfmncReg/type";
import {
excelDownload,
yearOptions,
} from "@/components/lib/func/common";
import { excelDownload, yearOptions } from "@/components/lib/func/common";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { pbnstTableBody } from "@/components/lib/func/TableData";
import PbnPrfExcelModal from "@/components/ui/modal/chkgd/pbnPrfExcelModal/pbnPrfExcelModal";
@@ -32,18 +34,104 @@ import {
UiTable,
} from "@common/business_components/ui";
import ListDiv from "@common/business_components/ui/list_div";
import {
Button,
Label,
Table,
TableBody,
TableHeader,
} from "@common/components/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button, Label, Table, TableBody, TableHeader } from "@common/components/ui";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAlertStore } from "@/src/shared/lib";
import { useTranslation } from "react-i18next";
import { headMakeprfmnc } from "../chkgdnReg/TableHead";
type PbnstPrfmncRow = {
sido: string;
gugun: string;
bscFctNm: string;
ruseUtztnFcltNm: string;
pbnstRuseFcltId: string;
};
const buildInputData = (detail: PbnstPrfRegDtlType | null, searchYear: string) => {
if (!detail?.prfList?.length) {
return [];
}
const inUsageData = structuredClone(initValInUsage).map((row) => ({
...row,
구분: "장내용수",
}));
const outUsageData = structuredClone(initValOutUsage).map((row) => ({
...row,
구분: "장외용수",
}));
const inSubtotal = {
...structuredClone(subTotalObj),
구분: "장내용수",
};
const outSubtotal = {
...structuredClone(subTotalObj),
구분: "장외용수",
};
detail.prfList.forEach((prfItem) => {
const yr = Number(prfItem.ym?.slice(0, 4));
if (yr !== Number(searchYear)) {
return;
}
const month = Number(prfItem?.ym?.slice(4));
const monthKey = `${month}월`;
inUsageData.forEach((inData: any) => {
const usageKey = inData.usage;
const value = (prfItem as any)[usageKey] ?? 0;
inData[monthKey] = value;
inData.total += value;
if (usageKey !== "subtotal") {
(inSubtotal as any)[monthKey] =
((inSubtotal as any)[monthKey] ?? 0) + value;
inSubtotal.total += value;
}
});
outUsageData.forEach((outData: any) => {
const usageKey = outData.usage;
const value = (prfItem as any)[usageKey] ?? 0;
outData[monthKey] = value;
outData.total += value;
if (usageKey !== "subtotal") {
(outSubtotal as any)[monthKey] =
((outSubtotal as any)[monthKey] ?? 0) + value;
outSubtotal.total += value;
}
});
});
if (inSubtotal.total === 0 && outSubtotal.total === 0) {
return [];
}
inUsageData.push(inSubtotal);
outUsageData.push(outSubtotal);
const usageTotalObj: { [key: string]: any } = {
...structuredClone(totalObj),
구분: "총합계",
용수명: "",
};
for (const key in usageTotalObj) {
if (["usage", "gubun", "edit"].includes(key)) continue;
const val =
(Number((inSubtotal as any)[key]) || 0) +
(Number((outSubtotal as any)[key]) || 0);
(usageTotalObj as any)[key] = val;
}
return [...inUsageData, ...outUsageData, usageTotalObj];
};
export default function PrfmncPrfmncReg() {
const {
auth,
@@ -51,59 +139,85 @@ export default function PrfmncPrfmncReg() {
prfmncHasuInit,
prfmnRegData,
prfmncHasuNmSearch,
getPrfFmcRes,
getPbnstPrfRegDtl,
getLaeSeCdCheck,
returnCodeText,
} = useWrStore((state) => state);
const {
prfPbnFclts,
pbnstPrfmncHeadList,
monthHeadList,
prfmncRes,
prfmncResCount,
} = prfmnc;
const { pbnstPrfmncHeadList, monthHeadList, prfmncRes, prfmncResCount, prfmncDetail } =
prfmnc;
const { regAuthrtYn, mdfcnAuthrtYn, laeSeCd } = auth;
const [isReset, setIsReset] = useState(false);
const [bdstTable, setbdstTable] = useState<FcPrfType[]>();
const [searchTxt, setSearchTxt] = useState<string>();
const [searchTxt, setSearchTxt] = useState<string>("");
const [selectedFacilityId, setSelectedFacilityId] = useState<string>("");
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [inputData, setInputData] = useState<any[]>([]);
const [isExcelOpen, setIsExcelOPen] = useState<{
isModal: boolean;
data: FcPrfType;
}>({ isModal: false, data: {} as FcPrfType });
const [inputData, setInputData] = useState<any[]>();
const [pbnstRuseFcltId, setpbnstRuseFcltId] = useState<string>();
const [cellNum, setCellNum] = useState<number>(1);
const [searchYear, setSearchYear] = useState<string>(
String(new Date().getFullYear())
);
data: PbnstPrfRegDtlType;
}>({ isModal: false, data: {} as PbnstPrfRegDtlType });
const [searchYear, setSearchYear] = useState<string>(String(new Date().getFullYear()));
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { t } = useTranslation("base");
const useInitRef = useRef(false);
const bdstTable = useMemo<PbnstPrfmncRow[]>(
() =>
prfmncRes.map((item: PbnstPrfRegListType) => ({
sido: returnCodeText(item.sidoCd, "cityCd"),
gugun:
item.sggCd && String(item.sggCd).trim().length === 5
? returnCodeText(String(item.sggCd), "sggCd")
: "",
bscFctNm: item.swageBscFctNm,
ruseUtztnFcltNm: item.ruseUtztnFcltNm,
pbnstRuseFcltId: item.pbnstRuseFcltId,
})),
[prfmncRes, returnCodeText]
);
const derivedInputData = useMemo(
() => buildInputData(prfmncDetail, searchYear),
[prfmncDetail, searchYear]
);
const excelClick = useCallback(() => {
excelDownload(
[[]],
"하수재이용실적",
"sample_pbnstPrfmncReg.xlsx",
"A1:O1"
);
}, [prfmnc.excelList]);
excelDownload([[]], "하수재이용실적", "sample_pbnstPrfmncReg.xlsx", "A1:O1");
}, []);
const loadDetail = useCallback(
async (facilityId: string) => {
if (!facilityId) return;
const detail = await getPbnstPrfRegDtl(searchYear, facilityId);
if (detail) {
setSelectedFacilityId(facilityId);
setIsExcelOPen((prev) => ({
...prev,
data: detail,
}));
}
},
[getPbnstPrfRegDtl, searchYear]
);
const handleSearch = useCallback(() => {
if (!laeSeCd) return;
setIsReset(true);
}, [laeSeCd]);
const { t } = useTranslation("base");
const onSave = useCallback(async () => {
const handleConfirm = async () => {
if (!pbnstRuseFcltId) return;
if (!selectedFacilityId) return;
const message = await prfmnRegData(
1,
laeSeCd,
excelToPbnPrfData(
inputData as unknown as JSON[],
searchYear,
pbnstRuseFcltId
) as unknown as PrfType,
selectedFacilityId
) as unknown as PrfType[],
searchYear,
prfPbnFclts
selectedFacilityId
);
alert(message);
};
@@ -111,126 +225,47 @@ export default function PrfmncPrfmncReg() {
if (await confirm(t("isUpdateMsg"))) {
await handleConfirm();
}
}, [inputData, searchYear, pbnstRuseFcltId, cellNum, prfPbnFclts, confirm, t, laeSeCd, prfmnRegData]);
const cellClick = useCallback(
(index: number | undefined) => {
if (index !== undefined) {
setIsExcelOPen({
isModal: false,
data: prfmncRes[index] || ({} as FcPrfType),
});
const inUsageData = structuredClone(initValInUsage).map((row) => ({
...row,
구분: "장내용수",
}));
const outUsageData = structuredClone(initValOutUsage).map((row) => ({
...row,
구분: "장외용수",
}));
const inSubtotal = {
...structuredClone(subTotalObj),
구분: "장내용수",
};
const outSubtotal = {
...structuredClone(subTotalObj),
구분: "장외용수",
};
if (prfmncRes[index]?.prfList) {
prfmncRes[index].prfList?.forEach((prfItem) => {
const yr = Number(prfItem.ym?.slice(0, 4)); // "YYYY"
if (yr === Number(searchYear)) {
const month = Number(prfItem?.ym?.slice(4)); // "MM"
const monthKey = `${month}월`;
inUsageData.forEach((inData: any) => {
const usageKey = inData.usage;
const value = (prfItem as any)[usageKey] ?? 0;
inData[monthKey] = value;
inData.total += value;
if (inSubtotal && usageKey !== "subtotal") {
(inSubtotal as any)[monthKey] =
((inSubtotal as any)[monthKey] ?? 0) + value;
inSubtotal.total += value;
}
});
outUsageData.forEach((outData: any) => {
const usageKey = outData.usage;
const value = (prfItem as any)[usageKey] ?? 0;
outData[monthKey] = value;
outData.total += value;
if (usageKey !== "subtotal") {
(outSubtotal as any)[monthKey] =
((outSubtotal as any)[monthKey] ?? 0) + value;
outSubtotal.total += value;
}
});
}
});
if (inSubtotal.total !== 0 && outSubtotal.total !== 0) {
inUsageData.push(inSubtotal);
outUsageData.push(outSubtotal);
const usageTotalObj: { [key: string]: any } = {
...structuredClone(totalObj),
구분: "총합계",
용수명: "",
};
for (const key in usageTotalObj) {
if (["usage", "gubun", "edit"].includes(key)) continue;
const val =
(Number((inSubtotal as any)[key]) || 0) +
(Number((outSubtotal as any)[key]) || 0);
(usageTotalObj as any)[key] = val;
}
const tableData = [...inUsageData, ...outUsageData, usageTotalObj];
setpbnstRuseFcltId(prfmncRes[index].pbnstRuseFcltId);
setInputData(tableData);
} else {
setInputData([]);
}
} else {
setInputData([]);
}
}
},
[prfmncRes, searchYear]
);
useEffect(() => {
getLaeSeCdCheck();
useInitRef.current = true;
}, []);
useEffect(() => {
if (useInitRef.current == true && laeSeCd !== undefined) {
getPrfFmcRes();
useInitRef.current = true;
}
}, [useInitRef.current, laeSeCd]);
}, [alert, confirm, inputData, prfmnRegData, searchYear, selectedFacilityId, t]);
useEffect(() => {
const year = new Date().getFullYear();
if(laeSeCd === undefined || laeSeCd ==='') return;
prfmncHasuInit(1, laeSeCd, String(year), prfPbnFclts);
}, [prfPbnFclts,laeSeCd]);
void getLaeSeCdCheck();
}, [getLaeSeCdCheck]);
useEffect(() => {
if (prfmncRes && prfmncRes.length > 0) {
setbdstTable(prfmncRes);
if (!laeSeCd) return;
void prfmncHasuInit(1, laeSeCd);
}, [laeSeCd, prfmncHasuInit]);
useEffect(() => {
if (!prfmncRes.length) {
setSelectedFacilityId("");
setSelectedIndex(0);
setInputData([]);
setIsExcelOPen((prev) => ({ ...prev, data: {} as PbnstPrfRegDtlType }));
return;
}
// cellNum이 0일 경우(첫 번째 행)도 유효한 인덱스이므로 undefined 여부만 체크합니다.
if (cellNum !== undefined) {
cellClick(cellNum);
const nextIndex = Math.max(
prfmncRes.findIndex((item) => item.pbnstRuseFcltId === selectedFacilityId),
0
);
const nextFacility = prfmncRes[nextIndex];
if (!nextFacility) {
return;
}
}, [prfmncRes, cellNum, cellClick]);
setSelectedIndex(nextIndex);
void loadDetail(nextFacility.pbnstRuseFcltId);
}, [loadDetail, prfmncRes]);
useEffect(() => {
if (!selectedFacilityId) return;
void loadDetail(selectedFacilityId);
}, [loadDetail, searchYear, selectedFacilityId]);
useEffect(() => {
setInputData(derivedInputData);
}, [derivedInputData]);
return (
<>
@@ -249,9 +284,7 @@ export default function PrfmncPrfmncReg() {
selectClass={""}
selectArray={yearOptions}
value={searchYear}
onSelectValue={(val: string | number) =>
setSearchYear(String(val))
}
onSelectValue={(val: string | number) => setSearchYear(String(val))}
/>
</div>
<div className="col-span-3 flex flex-row space-x-2">
@@ -261,7 +294,7 @@ export default function PrfmncPrfmncReg() {
<SearchInput
className={"w-full text-2xl"}
label=""
textValue={searchTxt || ""}
textValue={searchTxt}
setText={(e) => {
setSearchTxt(e);
}}
@@ -271,12 +304,7 @@ export default function PrfmncPrfmncReg() {
<SearchFormRight className="w-3/12 mx-5 flex flex-col items-center justify-center space-x-2 ">
<div className="space-y">
<div className="flex flex-row space-x-2">
<Button
className={btnClass}
onClick={() => {
setIsReset(true);
}}
>
<Button className={btnClass} onClick={handleSearch}>
조회
</Button>
<Button
@@ -291,12 +319,11 @@ export default function PrfmncPrfmncReg() {
<Button
className={btnClass}
onClick={() => {
if (cellNum === undefined) {
if (!selectedFacilityId) {
alert("하수처리재이용시설을 선택해주세요.");
return;
} else {
setIsExcelOPen({ ...isExcelOpen, isModal: true });
}
setIsExcelOPen((prev) => ({ ...prev, isModal: true }));
}}
disabled={regAuthrtYn === "N"}
>
@@ -318,30 +345,29 @@ export default function PrfmncPrfmncReg() {
<ScrollArea className="h-[60vh]">
<UiTable
key={"leftTable"}
tableData={bdstTable || []}
tableData={bdstTable}
headlist={pbnstPrfmncHeadList}
pageSize={10}
total={prfmncResCount}
headName={"하수재이용시설 목록"}
cellClick={(index: number | undefined) => {
cellClick(index);
setCellNum(index ?? 0);
if (index === undefined) return;
const selected = prfmncRes[index];
if (!selected) return;
setSelectedIndex(index);
void loadDetail(selected.pbnstRuseFcltId);
}}
onPageChange={(page: number) => {
if (searchTxt && searchTxt?.length > 0) {
prfmncHasuNmSearch(
page,
laeSeCd,
searchYear,
searchTxt,
prfPbnFclts
);
if (searchTxt.trim().length > 0) {
void prfmncHasuNmSearch(page, laeSeCd, searchTxt);
} else {
prfmncHasuInit(page, laeSeCd, searchYear, prfPbnFclts);
void prfmncHasuInit(page, laeSeCd);
}
setIsReset(false);
}}
resetPageNo={isReset}
index={selectedIndex}
/>
</ScrollArea>
</ListDiv>
@@ -353,19 +379,21 @@ export default function PrfmncPrfmncReg() {
{"하수처리수재이용 실적(m³) 등록결과"}
</h3>
</div>
<Table variant="secondary">
<TableHeader className="font-bold">
{headMakeprfmnc(monthHeadList, [])}
</TableHeader>
<TableBody key={prfmnc.pageKey + "pageKey"}>
{pbnstTableBody(
inputData ?? [],
monthHeadList,
setInputData,
mdfcnAuthrtYn === "N" ? false : true
)}
</TableBody>
</Table>
<div className="card !m-[1.2rem]">
<Table variant="secondary">
<TableHeader className="font-bold">
{headMakeprfmnc(monthHeadList, [])}
</TableHeader>
<TableBody key={prfmnc.pageKey + "pageKey"}>
{pbnstTableBody(
inputData,
monthHeadList,
setInputData,
mdfcnAuthrtYn === "N" ? false : true
)}
</TableBody>
</Table>
</div>
</ScrollArea>
</ListDiv>
</div>
@@ -376,22 +404,20 @@ export default function PrfmncPrfmncReg() {
<PbnPrfExcelModal
data={isExcelOpen.data}
setIsExcelOPen={(open) => {
setIsExcelOPen({ ...isExcelOpen, isModal: open });
setIsExcelOPen((prev) => ({ ...prev, isModal: open }));
}}
setModalExcelData={async (excelData) => {
const message = await prfmnRegData(1, laeSeCd, excelData, searchYear, prfPbnFclts);
if (!selectedFacilityId) return;
const message = await prfmnRegData(excelData, searchYear, selectedFacilityId);
alert(message);
// 데이터 저장 후 UI 업데이트는 useEffect가 감지하여 처리하므로 여기서는 호출하지 않습니다.
}}
></PbnPrfExcelModal>
}
open={isExcelOpen.isModal}
onOpenChange={() => {
setIsExcelOPen({ ...isExcelOpen, isModal: false });
setIsExcelOPen((prev) => ({ ...prev, isModal: false }));
}}
></UiModal>
</>
);
}
@@ -25,7 +25,6 @@ export default function PbnstPrfmncSttn() {
pbnstPrfmncSearch,
getFlctsSttn,
prfmnc,
pbnNmSearch,
getLaeSeCdCheck
} = useWrStore((state) => state);
const { laeSeCd } = auth;
@@ -584,11 +583,6 @@ export default function PbnstPrfmncSttn() {
if (prfmncRes?.[0]?.pbnstRuseFcltId) setChartId(prfmncRes[0].pbnstRuseFcltId)
}, [prfmncRes]);
useEffect(() => {
if (pbnRuseNm.length > 1)
pbnNmSearch("1", "23", getFixedSearchValue(ref.current), pbnRuseNm, searchYear, []);
}, [pbnRuseNm, getFixedSearchValue, pbnNmSearch, searchYear]);
const cellClick = (index: number | undefined, headId: string) => {
if (index !== undefined && headId !== undefined) {
setChartId(prfmncRes[index].pbnstRuseFcltId)
+173 -55
View File
@@ -1,4 +1,6 @@
import { fetchType, getFetch, postFectch } from "@/components/lib/api";
import type { FcPrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type";
import moment from "moment";
import type { CommonTableProps } from "../../common/type/header";
import type { WaterReuseFacility } from "../../ruseFcltMng/fcltSttn/pbnstSttn/type";
import {
@@ -6,10 +8,39 @@ import {
pbnSttHeadListExport,
} from "../../ruseFcltMng/fcltSttn/pbnstSttn/headList";
import type { ReturnType } from "@common/business_components/ui";
import { hasuListFetch } from "../../ruseFcltMng/fcltSttn/pbnstSttn";
import type { customPbnst } from "../../ruseFcltMng/fcltReg/pbnstReg/type";
import type { customPbnst, pubHasuType } from "../../ruseFcltMng/fcltReg/pbnstReg/type";
import { excelPbnstSttnParsing } from "../../ruseFcltMng/fcltSttn/pbnstSttn/excel";
const getCurrentYear = () => String(new Date().getFullYear());
const sumPrfUsage = (prfList: FcPrfType["prfList"] = []) => {
return prfList.reduce(
(acc, cur) => {
acc.inUsage +=
cur.wshwtr +
cur.coolUswtr +
cur.cleanUswtr +
cur.drwtUswtr +
cur.dltnUswtr +
cur.ithlRswtr +
cur.ithlEtcUswtr;
acc.outUsage +=
cur.cleanToilUswtr +
cur.resn +
cur.lndwtr +
cur.hydUswtr +
cur.ugwtChr +
cur.farmUswtr +
cur.rvrKeepUswtr +
cur.indstUswtr;
acc.total = acc.inUsage + acc.outUsage;
return acc;
},
{ total: 0, inUsage: 0, outUsage: 0 }
);
};
export const ruseMapSize = 15;
export type ruseMapState = {
ruseMap: {
@@ -61,6 +92,141 @@ export const ruseMapStateExport: ruseMapState = {
},
};
const hasuListFetch = async (
page: string,
total: string,
reurnValue: ReturnType,
searchNm: string | undefined,
year: string,
flcts: string[]
) => {
if (flcts.length === 0) {
return { data: [], count: 0 };
}
let url = ``;
let filteredFclt = flcts;
let nameCd = "0",
partStr = "A",
sidogunValue = "0";
switch (reurnValue.partValue) {
case "R":
if (reurnValue.regionValue !== "0") {
if (reurnValue.unitWaterShedValue === "0") nameCd = reurnValue.regionValue;
else if (reurnValue.middleRegionValue === "0") nameCd = reurnValue.unitWaterShedValue;
else nameCd = reurnValue.middleRegionValue;
}
partStr = reurnValue.partValue;
break;
default:
if (reurnValue.sidoValue && reurnValue.sidoValue !== "0") nameCd = reurnValue.sidoValue;
if (reurnValue.sigunValue && reurnValue.sigunValue !== "0") nameCd = reurnValue.sigunValue;
sidogunValue = nameCd;
partStr = reurnValue.envPartValue !== "0" ? reurnValue.envPartValue : "A";
break;
}
if (sidogunValue && sidogunValue !== "0") {
filteredFclt = flcts.filter((fclt: string) => {
return sidogunValue.length == 2
? fclt.slice(0, 2) === sidogunValue
: fclt.slice(0, 5) === sidogunValue;
});
}
url = `${partStr}/${nameCd}/0/1/SW`;
if (searchNm && searchNm.length > 0) {
url += "/" + searchNm;
}
if (filteredFclt.length === 0) {
return { data: [], count: 0 };
}
const pbnList = await postFectch(
`/prcsFclt/ruseInfo/1/${filteredFclt.length}/` + url,
filteredFclt,
fetchType.COMMON
);
const checkRuseFcltList = pbnList.data.map((item) => item.bscFctCd);
const getRuseList = await postFectch(
`/reuseFc/pubDetailList/${year}/${sidogunValue}/${page}/${total}`,
checkRuseFcltList,
fetchType.WR
);
const checkWrRegListRes = getRuseList.data;
const data: WaterReuseFacility[] = checkWrRegListRes
.map((item: FcPrfType) => {
const findData = pbnList.data.find(
(fclt: pubHasuType) =>
fclt.bscFctCd === item.pbnstRuseFcltId.substring(0, 11)
);
if (findData) {
const usage = item.prfList.reduce(
(acc, cur) => {
acc.inUsage +=
cur.wshwtr +
cur.coolUswtr +
cur.cleanUswtr +
cur.drwtUswtr +
cur.dltnUswtr +
cur.ithlRswtr +
cur.ithlEtcUswtr;
acc.outUsage +=
cur.cleanToilUswtr +
cur.resn +
cur.lndwtr +
cur.hydUswtr +
cur.ugwtChr +
cur.farmUswtr +
cur.rvrKeepUswtr +
cur.indstUswtr;
acc.total = acc.inUsage + acc.outUsage;
return acc;
},
{ total: 0, inUsage: 0, outUsage: 0 }
);
return {
rvbsn: findData?.rvbsn || "-",
lgbsn: findData?.lgbsn || "-",
mdmbsn: findData?.mdmbsn || "-",
rgnNm: findData?.rgnNm || "-",
sido: findData?.sido || "-",
gugun: findData?.gugun || "-",
bscFctNm: findData?.bscFctNm || "-",
efflWqltStndrdNm: findData?.efflWqltStndrdNm || "-",
fctCpc: Number(findData?.fctCpc) || 0,
tel: findData?.tel ?? "-",
ruseUtztnFcltNm: item?.ruseUtztnFcltNm || "-",
fcltCpct: item?.fcltCpct || "-",
splyExtraCpct: item?.splyExtraCpct || "-",
ruseCpct: item?.ruseCpct || "-",
pbnstRuseFcltId: item.pbnstRuseFcltId || "-",
usageTotal: usage?.total,
inUsage: usage?.inUsage,
outUsage: usage?.outUsage,
operMthd: item.operList[0]?.operMthd || "-",
rprtvBzenty: item.operList[0]?.rprtvBzenty,
bgngDt: moment(item.operList[0]?.bgngDt).format("YYYY-MM-DD") ?? "",
endDt: moment(item.operList[0]?.endDt).format("YYYY-MM-DD") ?? "",
searchNm: findData?.bscFctNm || "-",
searchPk: findData?.bscFctCd || "-",
prfList: item?.prfList ?? [],
};
}
})
.filter(Boolean);
return { data: data, count: getRuseList.count };
};
export const ruseMapActionsExport: (set: any) => ruseMapActions = (
set: any
) => {
@@ -111,22 +277,12 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = (
try {
const ruseDetailList = await getFetch(
fetchType.WR,
`/reuseFc/pubFc/${bscFctCd}`
);
const year = new Date().getFullYear();
const data = [bscFctCd];
const rusePrfList = await postFectch(
`/reuseFc/pubDetailList/${year}/0/1/10`,
data,
fetchType.WR
`/reuseFc/pubFc/${bscFctCd}?year=${getCurrentYear()}`
);
const dataList = ruseDetailList.map((ruse) => {
const matchRuse = rusePrfList?.data.find(
(prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId
);
return {
...ruse,
prfList: matchRuse.prfList,
prfList: ruse.prfList ?? [],
};
});
console.log("/dataList ", dataList);
@@ -195,51 +351,13 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = (
try {
const ruseDetailList = await getFetch(
fetchType.WR,
`/reuseFc/pubFc/${bscFctCd}`
);
const year = new Date().getFullYear();
const data = [bscFctCd];
const rusePrfList = await postFectch(
`/reuseFc/pubDetailList/${year}/0/1/10`,
data,
fetchType.WR
`/reuseFc/pubFc/${bscFctCd}?year=${getCurrentYear()}`
);
const dataList = ruseDetailList.map((ruse) => {
const matchRuse = rusePrfList?.data.find(
(prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId
);
const usage = matchRuse.prfList.reduce(
(acc, cur) => {
acc.inUsage +=
cur.wshwtr +
cur.coolUswtr +
cur.cleanUswtr +
cur.drwtUswtr +
cur.dltnUswtr +
cur.ithlRswtr +
cur.ithlEtcUswtr +
cur.cleanToilUswtr +
cur.resn;
acc.outUsage +=
cur.lndwtr +
cur.hydUswtr +
cur.ugwtChr +
cur.farmUswtr +
cur.rvrKeepUswtr +
cur.indstUswtr;
acc.total = acc.inUsage + acc.outUsage;
return acc;
},
{ total: 0, inUsage: 0, outUsage: 0 }
);
const usage = sumPrfUsage(ruse.prfList ?? []);
return {
...ruse,
prfList: matchRuse.prfList,
prfList: ruse.prfList ?? [],
inUsage: usage?.inUsage,
outUsage: usage?.outUsage,
@@ -1,229 +1,155 @@
/**
/**
* 재이용실적등록 > 하수재이용실적등록
*
* 🔗 사용 API
* - 하수재이용시설 실적 조회: POST /api/v1/reuseFc/pubDetailList/{year}/{sggOrCity}/{page}/{pageSize} (reuse-service)
* - 하수재이용시설 실적 등록: POST /api/v1/reuseFc/pubFcPrf (reuse-service)
* - 공공재이용시설 목록 조회: POST /api/v1/prcsFclt/ruseInfo/{page}/{pageSize}/{type}/{sggOrCity}/{etc1}/{etc2}/{etc3} (server-service)
* - 공공재이용시설 목록 조회(이름검색): POST /api/v1/prcsFclt/ruseInfo/{page}/{pageSize}/{type}/{sggOrCity}/{searchTxt} (server-service)
* - 공공재이용시설 조회: GET /api/v1/reuseFc/pubFc (reuse-service)
*
* 🔗 사용 API
* - 좌측 목록 조회: GET /api/v1/reuseFc/pbnstPrfRegList/{page}/{pageSize}/{searchingCd}/{nameCd}
* - 우측 상세 조회: GET /api/v1/reuseFc/pbnstPrfRegDtl/{year}/{pbnstRuseFcltId}
* - 하수재이용시설 실적 등록: POST /api/v1/reuseFc/pubFcPrf
**/
import { fetchType, getFetch, postFectch } from "@/components/lib/api";
import { fetchType, getFetch, getFetchWithCount, postFectch } from "@/components/lib/api";
import { pbnstPrfmncHeadListExport, monthHeadListExport } from "./headList";
import type { pubHasuType } from "../../ruseFcltMng/fcltReg/pbnstReg/type";
import type { FcPrfType, PrfType } from "./type";
import type { PbnstPrfRegDtlType, PbnstPrfRegListType, PrfType } from "./type";
import type { TableUpperProps } from "../../common/type/header";
const pbnstPrfmncHeadList = pbnstPrfmncHeadListExport;
const monthHeadList = monthHeadListExport;
export const prfRegPageSize = 10;
export type pbnstPrfmncState = {
prfmnc: {
isInit: boolean;
pbnstPrfmncHeadList: TableUpperProps[];
monthHeadList: TableUpperProps[];
prfmncRes: FcPrfType[];
prfmncRes: PbnstPrfRegListType[];
prfmncResCount: number;
excelList: number[];
pageKey: number;
prfPbnFclts: string[];
prfmncDetail: PbnstPrfRegDtlType | null;
};
};
export const pbnstPrfmncStateExport: pbnstPrfmncState = {
prfmnc: {
isInit: false,
pbnstPrfmncHeadList: pbnstPrfmncHeadList,
monthHeadList: monthHeadList,
pbnstPrfmncHeadList,
monthHeadList,
prfmncRes: [],
prfPbnFclts: [],
prfmncResCount: 1,
excelList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
pageKey: 1,
prfmncDetail: null,
},
};
export type pbnstPrfmncType = pbnstPrfmncState & pbnstPrfmncActions;
type pbnstPrfmncActions = {
prfmncHasuInit: (
page: number,
sggOrCity: string,
year: string,
prfPbnFclts: string[]
) => void;
prfmncHasuNmSearch: (
page: number,
sggOrCity: string,
year: string,
searchTxt: string,
prfPbnFclts: string[]
) => void;
prfmnRegData: (
page: number,
sggOrCity: string,
data: PrfType,
year: string,
prfPbnFclts: string[]
) => Promise<string>;
getPrfFmcRes: () => void;
prfmncHasuInit: (page: number, sggOrCity: string) => void;
prfmncHasuNmSearch: (page: number, sggOrCity: string, searchTxt: string) => void;
prfmnRegData: (data: PrfType[], year: string, pbnstRuseFcltId: string) => Promise<string>;
getPbnstPrfRegDtl: (year: string, pbnstRuseFcltId: string) => Promise<PbnstPrfRegDtlType | undefined>;
};
const hasuListFetch = async (
const fetchPbnstPrfRegList = async (
page: number,
sggOrCity: string,
year: string,
prfPbnFclts: string[],
searchTxt?: string
) => {
let filteredFclt = prfPbnFclts;
if (sggOrCity !== "0") {
filteredFclt = prfPbnFclts?.filter((fclt: string) => {
return sggOrCity.length == 2
? fclt.slice(0, 2) === sggOrCity
: fclt.slice(0, 5) === sggOrCity;
});
let url = `/reuseFc/pbnstPrfRegList/${page}/${prfRegPageSize}/A/${sggOrCity}`;
if (searchTxt && searchTxt.trim().length > 0) {
url += `?swtrmtFcltNm=${encodeURIComponent(searchTxt.trim())}`;
}
//let url = `/prcsFclt/ruseInfo/1/${filteredFclt.length + 1}/A/${sggOrCity}/0/1/SW`;
let url = `/prcsFclt/ruseInfo/1/${filteredFclt.length + 1}`;
if (searchTxt && searchTxt?.length > 0) {
url += `/${searchTxt}`;
}
const rainRes = await postFectch(url, filteredFclt, fetchType.COMMON);
const checkRuseFcltList =
searchTxt && searchTxt?.length > 0
? rainRes.data.map((item) => item.bscFctCd)
: filteredFclt;
const pubRuseList = await postFectch(
`/reuseFc/pubDetailList/${year}/${sggOrCity}/${page}/${prfRegPageSize}`,
checkRuseFcltList,
fetchType.WR
);
const rusePrfList = pubRuseList.data
.map((item: FcPrfType) => {
const findData = rainRes.data?.find(
(fclt: pubHasuType) =>
item.pbnstRuseFcltId.substring(0, 11) === fclt?.bscFctCd
);
if (findData !== undefined) {
return {
sido: findData.sido,
gugun: findData.gugun,
bscFctNm: findData.bscFctNm,
bscFctCd: findData.bscFctCd,
ruseUtztnFcltNm: item.ruseUtztnFcltNm,
pbnstRuseFcltId: item.pbnstRuseFcltId,
prfList: item.prfList,
};
}
})
.filter(Boolean); // undefined 값을 제거합니다.
return { data: rusePrfList, count: pubRuseList.count };
const response = await getFetchWithCount(fetchType.WR, url);
return {
data: (response?.data ?? []) as PbnstPrfRegListType[],
count: response?.count ?? 0,
};
};
export const pbnstPrfmncActionsExport: (set: any) => pbnstPrfmncActions = (
set: any
) => {
return {
getPrfFmcRes: async () => {
prfmncHasuInit: async (page: number, sggOrCity: string) => {
try {
const pbnPrfRes = await getFetch(fetchType.WR, "/reuseFc/pubFc");
set((state: pbnstPrfmncState) => ({
const prfmncRes = await fetchPbnstPrfRegList(page, sggOrCity);
return set((state: pbnstPrfmncState) => ({
...state,
prfmnc: {
...state.prfmnc,
prfPbnFclts: pbnPrfRes,
prfmncRes: prfmncRes.data,
prfmncResCount: prfmncRes.count,
prfmncDetail: null,
isInit: false,
},
}));
} catch (error) {
console.log(error);
}
},
prfmncHasuInit: async (
page: number,
sggOrCity: string,
year: string,
prfPbnFclts: string[]
) => {
try {
const prfmncRes = await hasuListFetch(
page,
sggOrCity,
year,
prfPbnFclts
);
return set((state: pbnstPrfmncState) => ({
...state,
prfmnc: {
...state.prfmnc,
prfmncRes: prfmncRes.data,
prfmncResCount: prfmncRes.count,
isInit: false,
},
}));
} catch (errorHasu) {
console.log(errorHasu);
}
},
prfmncHasuNmSearch: async (
page: number,
sggOrCity: string,
year: string,
searchTxt: string,
prfPbnFclts: string[]
searchTxt: string
) => {
try {
const prfmncRes = await hasuListFetch(
page,
sggOrCity,
year,
prfPbnFclts,
searchTxt
);
const prfmncRes = await fetchPbnstPrfRegList(page, sggOrCity, searchTxt);
return set((state: pbnstPrfmncState) => ({
...state,
prfmnc: {
...state.prfmnc,
prfmncRes: prfmncRes.data,
prfmncResCount: prfmncRes.count,
prfmncDetail: null,
isInit: false,
},
}));
} catch (errorHasu) {
console.log(errorHasu);
} catch (error) {
console.log(error);
}
},
prfmnRegData: async (
page: number,
sggOrCity: string,
data: PrfType,
year: string,
prfPbnFclts: string[]
) => {
getPbnstPrfRegDtl: async (year: string, pbnstRuseFcltId: string) => {
try {
const detail = (await getFetch(
fetchType.WR,
`/reuseFc/pbnstPrfRegDtl/${year}/${pbnstRuseFcltId}`
)) as PbnstPrfRegDtlType;
set((state: pbnstPrfmncState) => ({
...state,
prfmnc: {
...state.prfmnc,
prfmncDetail: detail,
},
}));
return detail;
} catch (error) {
console.log(error);
return undefined;
}
},
prfmnRegData: async (data: PrfType[], year: string, pbnstRuseFcltId: string) => {
const failMessage = "하수재이용 시설 실적 등록이 실패하였습니다.";
try {
const ressponse = await postFectch(
const response = await postFectch(
"/reuseFc/pubFcPrf",
data,
fetchType.WR
);
if (ressponse.status === 200) {
const prfmncRes = await hasuListFetch(
page,
sggOrCity,
year,
prfPbnFclts
);
if (response.status === 200) {
const detail = (await getFetch(
fetchType.WR,
`/reuseFc/pbnstPrfRegDtl/${year}/${pbnstRuseFcltId}`
)) as PbnstPrfRegDtlType;
set((state: pbnstPrfmncState) => ({
...state,
prfmnc: {
...state.prfmnc,
prfmncRes: prfmncRes.data,
prfmncResCount: prfmncRes.count,
prfmncDetail: detail,
isInit: false,
pageKey: state.prfmnc.pageKey + 1,
},
@@ -54,8 +54,26 @@ export const FcPrfSchema = z.object({
bscFctNm: z.string().nullish(),
});
export const PbnstPrfRegListSchema = z.object({
swageBscFctNm: z.string().default(""),
ruseUtztnFcltNm: z.string().default(""),
pbnstRuseFcltId: z.string().default(""),
sidoCd: z.string().default(""),
sggCd: z.string().default(""),
totalCount: z.number().default(0),
});
export const PbnstPrfRegDtlSchema = z.object({
pbnstRuseFcltId: z.string().default(""),
ruseUtztnFcltNm: z.string().default(""),
swageBscFctNm: z.string().default(""),
prfList: z.array(PrfSchema).default([]),
});
export type FcPrfType = z.infer<typeof FcPrfSchema>;
export type PrfType = z.infer<typeof PrfSchema>;
export type PbnstPrfRegListType = z.infer<typeof PbnstPrfRegListSchema>;
export type PbnstPrfRegDtlType = z.infer<typeof PbnstPrfRegDtlSchema>;
export const initValInUsage:PrfUsageSummary[] = [
{
@@ -152,7 +152,8 @@ const hasuListFetch = async (
};
const initFetch = async (ruseFcltPk: string | undefined) => {
const pbnstRes = await getFetch("wr", `/reuseFc/pubFc/${ruseFcltPk}`);
const year = new Date().getFullYear();
const pbnstRes = await getFetch("wr", `/reuseFc/pubFc/${ruseFcltPk}?year=${year}`);
return pbnstRes;
};
@@ -302,7 +302,7 @@ export const pbnstRegZod = z.object({
fcOpers://z.array(fcOperSchema).default([]),
z .array(fcOperSchema)
.min(1, { message: "재이용시설의 운영정보를 최소 하나를 등록해주세요" }),
prfList: z.array(PrfSchema).nullish(),
prfList: z.array(PrfSchema).nullish(),
});
@@ -63,6 +63,7 @@ export const pbnstSttNodeListExport: CommonTableProps[] = [
{ id: "endDt", title: "종료일", upName: "mngInfo", visible: true, row: 2, rowSpan: 1, colSpan: 1, },
];
// 하수처리수 재이용시설 현황 tab 상단
export const ruseFcltNodeListExport: TableUpperProps[] = [
{ id: "cycl", title: "차수", visible: true, row: 1, rowSpan: 3, colSpan: 1 },
{ id: "seCd", title: "구분", visible: true, row: 1, rowSpan: 3, colSpan: 1 },
@@ -84,6 +85,7 @@ export const ruseFcltNodeListExport: TableUpperProps[] = [
{ id: "totalMoney", title: "합계", visible: true, row: 3, rowSpan: 1, colSpan: 1 },
];
// 재이용 원수 취수 현황
export const ruseSplyHeadListExport: TableUpperProps[] = [
{ id: "cycl", title: "차수", visible: true },
{ id: "orgwtWtritQnt", title: "원수취수량(m³/일)", visible: true },
@@ -125,6 +127,7 @@ export const ruseDmdHeadListExport: TableUpperProps[] = [
{ id: "splyFmYon", title: "공급유량계 유무", visible: true },
];
// 하수처리수 재이용시설 현황 tab 하단
export const ruseFcltHeadListExport: CommonTableProps[] = [
{ id: "cycl", title: "차수", visible: true},
{ id: "seCd", title: "구분", visible: true},
@@ -2,10 +2,8 @@ import type {
CommonTableProps,
TableUpperProps,
} from "@/app/store/common/type/header";
import type { FcPrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type";
import { fetchType, getFetch, getFetchWithCount, postFectch } from "@/components/lib/api";
import type { ReturnType } from "@common/business_components/ui";
import moment from "moment";
import { type z } from "zod";
import type { customPbnst, pubHasuType } from "../../fcltReg/pbnstReg/type";
import { controlHeadView } from "../rainSttn";
@@ -34,7 +32,6 @@ const pbnstSttNodeList = pbnstSttNodeListExport;
export type pbnstSttnState = {
pbnstSttn: {
pbnstSttnRes: WaterReuseFacility[];
pbnstSearchData: WaterReuseFacility[];
pbnstSttHeadList: CommonTableProps[];
pbnstSttNodeList: CommonTableProps[];
pbnstDetail: customPbnst;
@@ -46,8 +43,6 @@ export type pbnstSttnState = {
ruseFcltHeadList: TableUpperProps[];
pbnstSttnResCount: number;
excelList: [];
fclts: string[];
isFcltsLoaded: boolean; // wr 하수재이용시설 목록 조회 여부
isTableLoaded: boolean; // 현황 테이블 초기 조회 여부
};
};
@@ -60,35 +55,9 @@ type pbnstSttnActions = {
total: string,
reurnValue: ReturnType,
searchNm: string | undefined,
year: string,
fclts: string[]
) => void;
pbnstSttnSearch: (
isExcel: boolean,
page: string,
total: string,
reurnValue: ReturnType,
searchNm: string | undefined,
year: string,
fclts: string[]
) => void;
getFlcts: () => void;
pbnSttnInit: (
page: string,
total: string,
reurnValue: ReturnType,
year: string,
flcts: string[]
) => void;
getPbnStnDetail: (pbnstRuseFcltId: string) => void;
pbnNmSearch: (
page: string,
total: string,
reurnValue: ReturnType,
searchNm: string | undefined,
year: string,
flcts: string[]
year: string
) => void;
getPbnStnDetail: (pbnstRuseFcltId: string, year: string) => void;
resetPbnstDetail: () => void;
};
@@ -98,163 +67,20 @@ export const pbnstSttnStateExport: pbnstSttnState = {
pbnstSttn: {
pbnstSttHeadList: pbnstSttHeadList,
pbnstSttNodeList: pbnstSttNodeList,
ruseFcltNodeList: ruseFcltNodeListExport,
ruseFcltNodeList: ruseFcltNodeListExport, // 하수재이용시설 상세정보 - 하수처리수 재이용시설 현황 tab 상단
ruseSplyHeadList: ruseSplyHeadListExport,
rusePplHeadList: rusePplHeadListExport,
ruseGuranteeHeadList: ruseGuranteeHeadListExport,
ruseDmdHeadList: ruseDmdHeadListExport,
ruseFcltHeadList: ruseFcltHeadListExport,
ruseFcltHeadList: ruseFcltHeadListExport, // 하수재이용시설 상세정보 - 하수처리수 재이용시설 현황 tab 하단
pbnstSttnRes: [],
pbnstSttnResCount: 1,
pbnstDetail: {} as customPbnst,
pbnstSearchData: [],
excelList: [],
fclts: [],
isFcltsLoaded: false,
isTableLoaded: false,
},
};
export const hasuListFetch = async (
page: string,
total: string,
reurnValue: ReturnType,
searchNm: string | undefined,
year: string,
flcts: string[]
) => {
// 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환
if (flcts.length === 0) {
return { data: [], count: 0 };
}
let url = ``;
let filteredFclt = flcts;
let nameCd="0", partStr="A", sidogunValue="0";
switch (reurnValue.partValue) {
case 'R': // 유역별
if (reurnValue.regionValue !== '0') {
if(reurnValue.unitWaterShedValue==='0') nameCd = reurnValue.regionValue;
else if (reurnValue.middleRegionValue==='0') nameCd = reurnValue.unitWaterShedValue;
else nameCd = reurnValue.middleRegionValue;
}
partStr = reurnValue.partValue;
break;
default:
if(reurnValue.sidoValue && reurnValue.sidoValue !== '0') nameCd = reurnValue.sidoValue;
if(reurnValue.sigunValue &&reurnValue.sigunValue !== '0') nameCd = reurnValue.sigunValue;
sidogunValue = nameCd;
partStr = reurnValue.envPartValue !== '0' ? reurnValue.envPartValue:'A';
break;
}
if (sidogunValue && sidogunValue !== "0") { // 시도, 시군구 조회 조건이 있다면
filteredFclt = flcts.filter((fclt: string) => {
return sidogunValue.length == 2
? fclt.slice(0, 2) === sidogunValue
: fclt.slice(0, 5) === sidogunValue;
});
}
url = `${partStr}/${nameCd}/0/1/SW`;
if (searchNm && searchNm?.length > 0) {
url += "/" + searchNm;
}
// console.log(url);
// 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환
if (filteredFclt.length === 0) {
return { data: [], count: 0 };
}
// fo의 하수처리시설을 조회
const pbnList = await postFectch(
`/prcsFclt/ruseInfo/1/${filteredFclt.length}/` + url,
filteredFclt,
fetchType.COMMON // FO
);
const checkRuseFcltList = pbnList.data.map((item) => item.bscFctCd);
// console.log(checkRuseFcltList);
const getRuseList = await postFectch(
`/reuseFc/pubDetailList/${year}/${sidogunValue}/${page}/${total}`,
checkRuseFcltList,
fetchType.WR
);
const checkWrRegListRes = getRuseList.data;
// console.log(getRuseList);
const data: WaterReuseFacility[] = checkWrRegListRes
.map((item: FcPrfType) => {
const findData = pbnList.data.find(
(fclt: pubHasuType) =>
fclt.bscFctCd === item.pbnstRuseFcltId.substring(0, 11)
);
if (findData) {
const usage = item.prfList.reduce(
(acc, cur) => {
acc.inUsage +=
cur.wshwtr +
cur.coolUswtr +
cur.cleanUswtr +
cur.drwtUswtr +
cur.dltnUswtr +
cur.ithlRswtr +
cur.ithlEtcUswtr +
cur.cleanToilUswtr +
cur.resn;
acc.outUsage +=
cur.lndwtr +
cur.hydUswtr +
cur.ugwtChr +
cur.farmUswtr +
cur.rvrKeepUswtr +
cur.indstUswtr;
acc.total = acc.inUsage + acc.outUsage;
return acc;
},
{ total: 0, inUsage: 0, outUsage: 0 }
);
return {
rvbsn: findData?.rvbsn || "-",
lgbsn: findData?.lgbsn || "-",
mdmbsn: findData?.mdmbsn || "-",
rgnNm: findData?.rgnNm || "-",
sido: findData?.sido || "-",
gugun: findData?.gugun || "-",
bscFctNm: findData?.bscFctNm || "-",
efflWqltStndrdNm: findData?.efflWqltStndrdNm || "-",
fctCpc: Number(findData?.fctCpc) || 0,
tel: findData?.tel ?? "-",
ruseUtztnFcltNm: item?.ruseUtztnFcltNm || "-",
fcltCpct: item?.fcltCpct || "-",
splyExtraCpct: item?.splyExtraCpct || "-",
ruseCpct: item?.ruseCpct || "-",
pbnstRuseFcltId: item.pbnstRuseFcltId || "-",
usageTotal: usage?.total,
inUsage: usage?.inUsage,
outUsage: usage?.outUsage,
operMthd: item.operList[0]?.operMthd || "-",
rprtvBzenty: item.operList[0]?.rprtvBzenty,
bgngDt: moment(item.operList[0]?.bgngDt).format("YYYY-MM-DD") ?? "",
endDt: moment(item.operList[0]?.endDt).format("YYYY-MM-DD") ?? "",
searchNm: findData?.bscFctNm || "-",
searchPk: findData?.bscFctCd || "-",
prfList: item?.prfList ?? [],
};
}
})
.filter(Boolean);
return { data: data, count: getRuseList.count };
};
export const pbnstListFetch = async (
page: string,
total: string,
@@ -361,7 +187,6 @@ export const pbnstListFetch = async (
rprtvBzenty: item.rprtvBzenty || "-",
bgngDt: item.bgngDt || "",
endDt: item.endDt || "",
prfList: item.prfList ?? [],
};
});
@@ -372,25 +197,13 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = (
set: any
) => {
return {
//등록된 하수재이용시설을 조회한다.
getFlcts: async () => {
const fclts = await getFetch(fetchType.WR, "/reuseFc/pubFc");
return set((state: pbnstSttnState) => ({
pbnstSttn: {
...state.pbnstSttn,
isFcltsLoaded: true,
fclts: fclts,
},
}));
},
pbnstSttnList: async (
isExcel: boolean,
page: string,
total: string,
reurnValue: ReturnType,
searchNm: string | undefined,
year: string,
// _fclts: string[]
year: string
) => {
try {
const pbnstSttnRes = await pbnstListFetch(
@@ -440,90 +253,6 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = (
},
}));
},
//하수처리시설을 조회하여 하수재이용시설을 확인한다.
pbnstSttnSearch: async (
isExcel: boolean,
page: string,
total: string,
reurnValue: ReturnType, // 검색조건
searchNm: string | undefined,
year: string,
fclts: string[]
) => {
try {
// console.log(reurnValue);
const pbnstSttnRes = await hasuListFetch(
page,
total,
reurnValue,
searchNm,
year,
fclts
);
// if (Number(total) > 9000) {
if (isExcel) {
const pbnstExcel = excelPbnstSttnParsing(pbnstSttnRes.data);
set((state: pbnstSttnState) => {
return {
...state,
pbnstSttn: {
...state.pbnstSttn,
excelList: pbnstExcel,
},
};
});
} else {
set((state: pbnstSttnState) => {
return {
...state,
pbnstSttn: {
...state.pbnstSttn,
pbnstSttnRes: pbnstSttnRes.data,
pbnstSttnResCount: pbnstSttnRes.count,
isTableLoaded: true,
},
};
});
}
} catch (erTxt) {
console.log("error", erTxt);
}
},
//하수처리시설을 조회하여 하수재이용시설을 조회한다.
pbnSttnInit: async (
page: string,
total: string,
reurnValue: ReturnType,
year: string,
fclts: string[]
) => {
try {
const pbnstSttnRes = await hasuListFetch(
page,
total,
reurnValue,
undefined,
year,
fclts
);
set((state: pbnstSttnState) => {
return {
...state,
pbnstSttn: {
...state.pbnstSttn,
pbnstSttnRes: pbnstSttnRes.data,
pbnstSttnResCount: pbnstSttnRes.count,
pbnstDetail: {} as customPbnst,
isTableLoaded: true,
},
};
});
} catch (erTxt) {
console.log("error", erTxt);
}
},
// 팝업에 있는 데이터를 초기화한다.
resetPbnstDetail: async () => {
set((state: pbnstSttnState) => {
@@ -537,17 +266,18 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = (
});
},
//하수처리시설을 조회하여 하수재이용시설을 조회한다.
getPbnStnDetail: async (pbnstRuseFcltId: string) => {
getPbnStnDetail: async (pbnstRuseFcltId: string, year: string) => {
try {
let pbnstDetail = await getFetch(
const pbnstDetailList = await getFetch(
"wr",
`/reuseFc/pubFc/${pbnstRuseFcltId}`
`/reuseFc/pubFc/${pbnstRuseFcltId}?year=${year}`
);
if (pbnstDetail.length >= 0) {
pbnstDetail = pbnstDetail.find(
(item: customPbnst) => item.pbnstRuseFcltId === pbnstRuseFcltId
);
}
const pbnstDetail = Array.isArray(pbnstDetailList)
? pbnstDetailList.find(
(item: customPbnst) => item.pbnstRuseFcltId === pbnstRuseFcltId
)
: undefined;
console.log(pbnstDetail);
set((state: pbnstSttnState) => {
return {
...state,
@@ -561,38 +291,5 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = (
console.log(errorHasu);
}
},
//하수재이용시설 실적 현황 에서 사용하는 조회
pbnNmSearch: async (
page: string,
total: string,
reurnValue: ReturnType,
searchNm: string | undefined,
year: string,
flcts: string[]
) => {
try {
const pbnstSttnRes = await hasuListFetch(
page,
total,
reurnValue,
searchNm,
year,
flcts
);
set((state: pbnstSttnState) => {
return {
...state,
pbnstSttn: {
...state.pbnstSttn,
pbnstSearchData: pbnstSttnRes.data,
pbnstSttnResCount: pbnstSttnRes.count,
},
};
});
} catch (e) {
console.log(e);
}
},
};
};
@@ -1,5 +1,3 @@
import type { PrfType } from "@/app/store/prfmncReg/pbnstPrfmncReg/type";
export type WaterReuseFacility = {
swtrmtFcltId?: string;
bscFctNm: string; // 기본시설명 (예: "난지")
@@ -24,5 +22,4 @@ export type WaterReuseFacility = {
rprtvBzenty: string;
bgngDt: string;
endDt: string;
prfList:PrfType[];
};
@@ -4,7 +4,7 @@
import { excelToPbnPrfData } from "@/app/store/prfmncReg/pbnstPrfmncReg/excel";
import { pbnPrfRegExcelZod } from "@/app/store/prfmncReg/pbnstPrfmncReg/headList";
import type {
FcPrfType,
PbnstPrfRegDtlType,
PrfType,
} from "@/app/store/prfmncReg/pbnstPrfmncReg/type";
import { yearOptions } from "@/components/lib/func/common";
@@ -18,7 +18,7 @@ import {
} from "@common/business_components/ui";
import { Button, Input } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
// import toast from "react-hot-toast";
import type { z } from "zod";
@@ -29,13 +29,14 @@ export default function PbnPrfExcelModal({
setIsExcelOPen,
setModalExcelData,
}: {
data: FcPrfType;
data: PbnstPrfRegDtlType;
setIsExcelOPen: (open: boolean) => void;
setModalExcelData: (excelData: PrfType) => void;
setModalExcelData: (excelData: PrfType[]) => void;
}) {
const alert = useAlertStore((state) => state.setMessage);
const [file, setFile] = useState<File | null>(null);
const [excelData, setExcelData] = useState<PrfType[]>();
const fileInputRef = useRef<HTMLInputElement>(null);
const form = useForm<z.infer<typeof pbnPrfRegExcelZod>>({
resolver: zodResolver(pbnPrfRegExcelZod),
defaultValues: {
@@ -44,26 +45,32 @@ export default function PbnPrfExcelModal({
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setExcelData(undefined);
const selectedFile = e.target.files?.[0];
if (selectedFile) {
setFile(selectedFile);
}
};
const onSubmit = (values: z.infer<typeof pbnPrfRegExcelZod>) => {
if (excelData) {
////TODO: 궅이 서브밋을 써야하는 이유가 있는지 ?
console.log("values ", values);
// prfmnRegData(1,'41', excelData as unknown as PrfType); 활성화시 스토어 활성화 칠요
setModalExcelData(excelData as unknown as PrfType);
setIsExcelOPen(false);
if (!file) {
alert("엑셀 파일을 선택해 주세요.");
return;
}
if (!excelData || excelData.length === 0) {
alert("엑셀 파일을 먼저 확인해 주세요.");
return;
}
////TODO: 궅이 서브밋을 써야하는 이유가 있는지 ?
console.log("values ", values);
setModalExcelData(excelData);
setIsExcelOPen(false);
};
useEffect(() => {
if (data && data.bscFctNm)
form.setValue("pbnstRuseFcltId", data.pbnstRuseFcltId);
form.setValue("bscFctNm", data.bscFctNm || "");
form.setValue("pbnstRuseFcltNm", data.ruseUtztnFcltNm);
form.setValue("pbnstRuseFcltId", data.pbnstRuseFcltId || "");
form.setValue("bscFctNm", data.swageBscFctNm || "");
form.setValue("pbnstRuseFcltNm", data.ruseUtztnFcltNm || "");
}, [data]);
useEffect(() => {
@@ -76,20 +83,42 @@ export default function PbnPrfExcelModal({
return;
}
if (!form.getValues("pbnstRuseFcltId")) {
alert("재이용시설을 다시 선택한 뒤 파일등록을 진행해 주세요.");
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
return;
}
if (isFile) {
filePicked(isFile, 0).then((res) => {
if (res !== undefined)
setExcelData(
excelToPbnPrfData(
res as unknown as JSON[],
form.getValues("ym"),
form.getValues("pbnstRuseFcltId")
) as PrfType[]
);
if (res === undefined) {
alert("엑셀 파일을 읽지 못했습니다.");
return;
}
const parsedData = excelToPbnPrfData(
res as unknown as JSON[],
form.getValues("ym"),
form.getValues("pbnstRuseFcltId")
) as PrfType[];
if (!parsedData.length) {
alert("업로드 가능한 실적 데이터가 없습니다. 양식을 확인해 주세요.");
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
setFile(null);
return;
}
setExcelData(parsedData);
});
}
}
}, [file]);
}, [alert, file, form]);
return (
<>
@@ -177,10 +206,11 @@ export default function PbnPrfExcelModal({
<div className="space-y-4 p-4">
<Input
ref={fileInputRef}
type="file"
onChange={(file) => {
onChange={(event) => {
console.log(" field. valus", field.value);
if (file) handleFileChange(file);
handleFileChange(event);
}}
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
/>
@@ -1,3 +1,6 @@
/**
* 하수재이용시설 현황 상세정보 modal
*/
import { headMakeprfmnc } from "@/app/(with-layout)/ruseFcltMng/prfmncReg/chkgdnReg/TableHead";
import { useWrStore } from "@/app/store";
import type { TableUpperProps } from "@/app/store/common/type/header";
@@ -57,7 +60,7 @@ export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boo
useEffect(() => {
let tableData;
if (fcltTab === "fclt") {
if (fcltTab === "fclt") { // 하수처리수 재이용시설 현황
tableData = data.fcBizs?.map((item: fcBizType) => {
return {
cycl: item.cycl + "차",
@@ -81,7 +84,7 @@ export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boo
mainSplyPumpCpct: item.mainSplyPumpCpct ?? "-",
};
});
} else if (fcltTab === "sply") {
} else if (fcltTab === "sply") { // 재이용 원수 취수 현황
tableData = data.fcBizs.flatMap((item: fcBizType) => {
return item?.fcOrgwts?.map((fcOrgwt) => {
return {
@@ -93,7 +96,7 @@ export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boo
};
});
});
} else if (fcltTab === "ppi") {
} else if (fcltTab === "ppi") { // 재이용 공급관로 현황
tableData = data.fcBizs?.flatMap((item: fcBizType) => {
return item?.pplInstls?.map((ppl) => {
return {
@@ -104,7 +107,7 @@ export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boo
};
});
});
} else if (fcltTab === "guarantee") {
} else if (fcltTab === "guarantee") { // 재이용 보증수질 현황
tableData = data.fcBizs?.map((item: fcBizType) => {
return {
cycl: item.cycl + "차",
@@ -121,7 +124,7 @@ export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boo
cdtnv: item.cdtnv,
};
});
} else if (fcltTab === "dmdCo") {
} else if (fcltTab === "dmdCo") { // 수요처
tableData = data.fcBizs?.flatMap((item: fcBizType) => {
return item?.fcDemands?.map((dmd) => {
@@ -143,15 +146,15 @@ export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boo
}
}, [fcltTab, data]);
useEffect(() => {
if (fcltTab === "fclt") {
setRuseHeadList(pbnstSttn.ruseFcltHeadList);
} else if (fcltTab === "sply") {
setRuseHeadList(pbnstSttn.ruseSplyHeadList);
} else if (fcltTab === "ppi") {
if (fcltTab === "fclt") { // 하수처리수 재이용시설 현황 tab 하단
setRuseHeadList(pbnstSttn.ruseFcltHeadList);
} else if (fcltTab === "sply") { // 재이용 원수 취수 현황
setRuseHeadList(pbnstSttn.ruseSplyHeadList);
} else if (fcltTab === "ppi") { // 재이용 공급관로 현황
setRuseHeadList(pbnstSttn.rusePplHeadList);
} else if (fcltTab === "guarantee") {
} else if (fcltTab === "guarantee") { // 재이용 보증수질 현황
setRuseHeadList(pbnstSttn.ruseGuranteeHeadList);
} else if (fcltTab === "dmdCo") {
} else if (fcltTab === "dmdCo") { // 수요처
setRuseHeadList(pbnstSttn.ruseDmdHeadList);
}
}, [
@@ -225,26 +228,8 @@ export function PbnstSttnModal({ data, isMap }: { data: customPbnst; isMap?: boo
}
};
useEffect(() => {
if(isMap){
if(data.prfList &&data.prfList.length>0){
getTablePrfData(data.prfList);
}else{
getTablePrfData([]);
}
}else{
if (pbnstSttn.pbnstSttnRes && data) {
const prfList = pbnstSttn.pbnstSttnRes.find(
(item) => item.pbnstRuseFcltId === data.pbnstRuseFcltId
)?.prfList;
if (prfList) {
getTablePrfData(prfList);
}
}
}
}, [pbnstSttn.pbnstSttnRes, data,isMap]);
getTablePrfData(data.prfList ?? []);
}, [data]);
useEffect(() => {
@@ -0,0 +1,245 @@
# 하수재이용실적 등록 API 호출 분석
작성일: 2026-06-30
## 0. 공통코드 조회
- 가장 메인 진입점은 [apps/wr/src/app/providers/app-provider.tsx (line 38)](/D:/iipbs/frontend/apps/wr/src/app/providers/app-provider.tsx:38) 의 CommonCodeInitializer
- 실제 공통코드 조회 액션은 [apps/wr/app/store/common/code/index.ts (line 372)](/D:/iipbs/frontend/apps/wr/app/store/common/code/index.ts:372)
-
## 1. 대상 화면
- 화면: `재이용실적등록 > 하수재이용실적등록`
- 페이지 파일: [apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:47)
- 스토어 파일: [apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:1)
## 2. 분석 결론 요약
- 좌측 `하수재이용시설 목록`은 단일 API가 아니라 2개 API를 조합해 생성된다.
- 우측 `하수처리수재이용 실적(m³) 등록결과`는 별도 조회 API를 직접 호출하지 않는다.
- 우측 화면의 원본 데이터는 좌측 목록 조회 시 함께 내려오는 `prfList`이다.
- 우측 `저장`은 `POST /reuseFc/pubFcPrf`를 호출하고, 저장 성공 후 좌측 목록 조회 로직을 다시 호출해 화면을 갱신한다.
## 3. 좌측 하수재이용시설 목록 API 호출 분석
### 3.1 화면 진입 시 호출 순서
1. 권한 코드 조회
- `getLaeSeCdCheck()`
- 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:206)
2. WR에 등록된 하수재이용시설 기준 코드 목록 조회
- `getPrfFmcRes()`
- 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:210)
3. 좌측 목록 초기 조회
- `prfmncHasuInit(1, laeSeCd, year, prfPbnFclts)`
- 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:217)
### 3.2 기준 시설코드 목록 조회
`getPrfFmcRes()`는 먼저 WR에서 등록된 시설코드 목록을 가져온다.
- 함수 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:130)
- 호출 API:
```text
GET /reuseFc/pubFc
```
- 목적:
- WR에 등록된 하수재이용시설의 기반 시설코드 목록 `prfPbnFclts`를 확보
- 이후 좌측 목록 조회 시 FO/Common과 WR 조회의 기준 배열로 사용
### 3.3 좌측 목록 실제 조회 함수
좌측 목록은 `prfmncHasuInit()` 또는 `prfmncHasuNmSearch()`가 내부적으로 `hasuListFetch()`를 호출해 구성한다.
- `prfmncHasuInit()` 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:144)
- `prfmncHasuNmSearch()` 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:170)
- 공통 실제 조회 함수 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:72)
### 3.4 1차 호출: FO/Common 하수처리시설 기본정보 조회
`hasuListFetch()`는 먼저 `prfPbnFclts`를 `sggOrCity` 기준으로 필터링한다.
- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:79)
- 필터 기준:
- `sggOrCity` 길이 2: 시도 코드 비교
- 그 외: 앞 5자리 시군구 코드 비교
그 후 FO/Common API를 호출한다.
```text
POST /prcsFclt/ruseInfo/1/{filteredFclt.length + 1}
```
- 호출 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:88)
- 실제 호출부: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:92)
- 요청 바디:
- `filteredFclt`
- WR 기준코드 목록 배열
이름 검색 시에는 URL 뒤에 검색어가 붙는다.
```text
POST /prcsFclt/ruseInfo/1/{filteredFclt.length + 1}/{searchTxt}
```
- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:89)
이 호출의 목적은 다음과 같다.
- 하수처리시설의 기본정보 확보
- `sido`
- `gugun`
- `bscFctNm`
- `bscFctCd`
- 검색어가 있을 경우 실제 검색 결과 시설코드 집합 재산정
### 3.5 2차 호출: WR 하수재이용시설 상세 및 실적 조회
1차 FO/Common 조회 결과를 바탕으로 WR API를 호출한다.
```text
POST /reuseFc/pubDetailList/{year}/{sggOrCity}/{page}/{prfRegPageSize}
```
- 기본 페이지 크기: `prfRegPageSize = 10`
- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:19)
- 호출 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:99)
- 요청 바디:
- 검색어 없을 때: `filteredFclt`
- 검색어 있을 때: `rainRes.data.map((item) => item.bscFctCd)`
즉 검색 시에는 FO/Common 검색 결과에서 살아남은 시설코드만 WR로 다시 전달한다.
### 3.6 좌측 목록 최종 데이터 생성 방식
WR 응답 `pubRuseList.data`와 FO/Common 응답 `rainRes.data`를 다음 기준으로 매핑한다.
- 매핑 키:
- `item.pbnstRuseFcltId.substring(0, 11)`
- `fclt.bscFctCd`
- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:104)
최종 좌측 목록 `prfmncRes`에는 다음 필드가 들어간다.
- `sido`
- `gugun`
- `bscFctNm`
- `bscFctCd`
- `ruseUtztnFcltNm`
- `pbnstRuseFcltId`
- `prfList`
여기서 중요한 점은 `prfList`가 좌측 목록 데이터에 이미 포함된다는 점이다.
## 4. 우측 하수처리수재이용 실적(m³) 등록결과 조회 분석
### 4.1 결론
우측 `하수처리수재이용 실적(m³) 등록결과`는 별도 조회 API를 호출하지 않는다.
우측 테이블의 데이터 원본은 좌측 목록 조회 결과 `prfmncRes[index].prfList`이다.
### 4.2 데이터 생성 위치
좌측 행 클릭 시 `cellClick()`이 실행된다.
- 함수 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:116)
- 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:326)
`cellClick()` 내부 동작:
1. 선택한 행의 `prfmncRes[index]`를 읽음
2. 그 안의 `prfList`를 순회
3. 현재 선택 연도 `searchYear`와 일치하는 월별 데이터만 추출
4. `장내용수`, `장외용수`, `소계`, `총합계` 구조로 `inputData` 재구성
5. 우측 테이블은 `inputData`를 그대로 렌더링
우측 렌더링 위치:
- [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:356)
따라서 우측 등록결과는 조회 API 기준으로 보면 다음 호출의 간접 결과다.
```text
POST /reuseFc/pubDetailList/{year}/{sggOrCity}/{page}/{10}
```
## 5. 우측 저장 API 호출 분석
### 5.1 저장 버튼 호출 경로
우측 `저장` 버튼 클릭 시 `onSave()`가 실행된다.
- 버튼 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:282)
- 저장 함수 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:93)
`onSave()`는 화면 편집 데이터 `inputData`를 `excelToPbnPrfData()`로 변환한 뒤 `prfmnRegData()`에 전달한다.
- 변환 코드: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:100)
### 5.2 실제 저장 API
`prfmnRegData()`에서 실제 저장 호출을 수행한다.
- 함수 위치: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:198)
```text
POST /reuseFc/pubFcPrf
```
- 호출 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:207)
- 요청 바디:
- `PrfType`
- `excelToPbnPrfData(inputData, searchYear, pbnstRuseFcltId)` 변환 결과
### 5.3 저장 성공 후 후속 조회
저장 성공 시 `prfmnRegData()`는 종료하지 않고 다시 `hasuListFetch()`를 호출한다.
- 관련 코드: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:213)
즉 저장 직후 다음 흐름으로 재조회가 일어난다.
1. `POST /reuseFc/pubFcPrf` 저장
2. `hasuListFetch()` 재실행
3. 내부에서 다시 FO/Common + WR 조회 수행
4. `prfmncRes`, `prfmncResCount` 갱신
5. `pageKey` 증가
6. 우측 테이블 `TableBody` 재렌더링
관련 코드:
- 상태 갱신: [index.tsx](/D:/iipbs/frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx:221)
- 우측 `TableBody` key: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:360)
## 6. 파일등록 엑셀 저장도 동일 API 사용
파일등록 모달에서도 저장 시 동일한 `prfmnRegData()`를 호출한다.
- 호출 위치: [page.tsx](/D:/iipbs/frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx:381)
즉 수기 입력 저장과 엑셀 등록 저장은 최종적으로 같은 API를 사용한다.
```text
POST /reuseFc/pubFcPrf
```
## 7. 화면 이벤트별 API 요약
| 화면 이벤트 | 호출 함수 | 실제 API |
| --- | --- | --- |
| 화면 진입 | `getPrfFmcRes()` | `GET /reuseFc/pubFc` |
| 화면 진입 후 좌측 목록 초기조회 | `prfmncHasuInit()` | `POST /prcsFclt/ruseInfo/...` + `POST /reuseFc/pubDetailList/...` |
| 좌측 목록 이름검색 | `prfmncHasuNmSearch()` | `POST /prcsFclt/ruseInfo/.../{searchTxt}` + `POST /reuseFc/pubDetailList/...` |
| 좌측 행 클릭 | `cellClick()` | 별도 API 없음, `prfmncRes[index].prfList` 사용 |
| 우측 저장 | `prfmnRegData()` | `POST /reuseFc/pubFcPrf` 후 `POST /prcsFclt/ruseInfo/...` + `POST /reuseFc/pubDetailList/...` 재조회 |
| 엑셀 파일등록 저장 | `prfmnRegData()` | `POST /reuseFc/pubFcPrf` 후 재조회 |
## 8. 확인이 필요한 사항
- `POST /prcsFclt/ruseInfo/1/{filteredFclt.length + 1}` 에서 `+ 1`을 주는 이유는 코드상 명확한 설명이 없다.
- 저장 후 재조회 시 현재 페이지를 유지하지 않고, 호출부에서 전달한 페이지 값에 의존한다.
- 현재 수기 저장과 엑셀 저장 모두 `1` 페이지로 호출하고 있다.
- 우측 화면은 독립 조회 API가 없으므로, `pubDetailList` 응답 구조가 바뀌면 좌우 화면이 함께 영향을 받는다.
@@ -0,0 +1,371 @@
# 하수재이용시설 현황 API 정리 작업계획
작성일: 2026-07-01
## 1. 배경
현재 하수재이용시설 현황/상세 조회는 백엔드와 프런트 모두에서 응답 구조가 과도하게 분산되어 있다.
- 상세 조회는 `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 호출 후, 다시 `POST /reuseFc/pubDetailList/...`를 추가 호출해서 실적을 붙이고 있다.
- 현황 목록은 `GET /reuseFc/pbnstList/...` 호출 시 월별 실적 내역 `prfList`까지 함께 조회한 뒤, 서비스 레이어에서 장내/장외/총합계를 계산하고 있다.
- 프런트도 위 구조에 맞춰 상세 모달과 현황 화면에서 중복 후처리를 수행한다.
이번 작업의 목표는 아래 2가지다.
1. `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 단일 호출만으로 상세 모달에 필요한 데이터가 모두 내려오도록 정리
2. `GET /reuseFc/pbnstList/...`는 목록에 필요한 합계만 SQL에서 계산해 내려주고, 월별 실적 내역 `prfList`는 제외
추가 전제:
- 본 작업은 backend 정리 후 이에 맞춰 `frontend`도 함께 수정한다.
## 2. 현재 구조 분석
### 2.1 상세 조회 API
현재 백엔드 상세 조회 엔드포인트:
- `GET /reuseFc/pubFc/{pbnstRuseFcltId}`
현재 SQL/매핑:
- `public_fc_select.xml`의 `getFc`
- `PublicFcInsertResultMap`
현재 `getFc`는 아래 성격의 데이터만 내려준다.
- 기본 시설 정보
- 파일 정보
- 사업 정보
- 운영 정보
- 원수 취수 정보
- 공급관로 정보
- 수요처 정보
현재 `prfList`는 포함하지 않는다.
### 2.2 상세 실적 추가 조회 API
현재 추가 호출 엔드포인트:
- `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}`
현재 역할:
- 시설 목록 기준으로 `prfList`를 포함한 실적 정보를 별도 조회
- 프런트에서 `pubFc/{id}` 결과와 `pubDetailList` 결과를 다시 매핑
### 2.3 상세 모달 사용 위치 분석
실제 상세 모달 렌더링 위치는 아래 2곳이다.
- `apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx`
- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx`
추가 영향 지점은 아래와 같다.
- `apps/wr/app/store/map/ruseMap/index.tsx`
- `getRuseMaplList()`
- `getRuseMaplListReturn()`
- 현재 `pubFc/{id}` 호출 후 `pubDetailList`를 다시 호출해 `prfList`를 수동 병합
- `apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx`
- `getPbnStnDetail()`
- 현재는 `pubFc/{id}`만 조회하지만, 모달 내부는 상황에 따라 목록의 `prfList`를 참조
정리하면, 상세 모달 렌더링 지점은 2곳이 맞고, 실제 수정 범위는 스토어까지 확장된다.
### 2.4 현황 목록 API
현재 목록 엔드포인트:
- `GET /reuseFc/pbnstList/{year}/{pageNo}/{pageCnt}/{searchingCd}/{nameCd}/{fcltCpct}/{oprtngSttsCd}/{bscFctClsCd}`
현재 구조:
- SQL `getPbnstList`에서 `tb_wr_pbnst_ruse_fclt_prfmnc`를 조인
- `PublicPrfListMap`으로 월별 `prfList`까지 전부 매핑
- `PublicFcService.getPbnstList()`에서 `sumUsage()`로 장내/장외/총합 계산
- 응답 객체 `PublicPbnstList`에 `prfList`도 포함
문제점:
- 목록 조회인데 월별 실적 상세까지 매번 로딩
- 페이지 목록 응답에 비해 payload가 과다
- 서비스 레이어에서 반복 합계 계산 수행
- 프런트는 목록 화면에서 합계만 사용하지만 상세 내역까지 수신
## 3. 목표 구조
### 3.1 상세 조회 목표
`GET /reuseFc/pubFc/{pbnstRuseFcltId}` 한 번으로 아래가 모두 내려오도록 변경한다.
- 기본 시설 정보
- 파일/사업/운영/원수취수/공급관로/수요처 정보
- 해당 시설의 하수재이용실적 `prfList`
- 필요 시 장내/장외/총합도 함께 포함 여부 검토
프런트는 상세 모달 진입 시 더 이상 `POST /reuseFc/pubDetailList/...`를 호출하지 않도록 변경한다.
### 3.2 목록 조회 목표
`GET /reuseFc/pbnstList/...`는 아래만 내려오도록 정리한다.
- 시설 기본 정보
- 운영정보 1건
- 장내 합계
- 장외 합계
- 총합계
제외 대상:
- 월별 실적 상세 `prfList`
합계는 SQL에서 계산한다.
## 4. 백엔드 작업계획
### 4.1 `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 응답 확장
대상 파일:
- `wr_back_out/src/main/resources/postgres/mapper/wr/public_fc_select.xml`
- `wr_back_out/src/main/java/kr/co/mindone/wrout/object/publicFc/PublicFcInsert.java` 대신 PublicFcSelect.java 로 신규 object생성
- `wr_back_out/src/main/java/kr/co/mindone/wrout/controller/PublicFcController.java`
- `wr_back_out/src/main/java/kr/co/mindone/wrout/service/public_fc/PublicFcService.java`
계획:
1. `getFc` SQL에 `tb_wr_pbnst_ruse_fclt_prfmnc` 조인 추가
2. `year` query param을 추가하고, 해당 연도 실적만 조회하도록 수정
3. frontend에서 상세 조회 시 현재년도를 세팅해서 호출
적용안:
- `GET /reuseFc/pubFc/{pbnstRuseFcltId}?year=2026`
- frontend는 상세 모달 진입 시 `new Date().getFullYear()` 기준 현재년도를 세팅해서 호출
- backend는 전달받은 `year` 기준으로 `prfList`를 조회
이유:
- 상세 모달은 현재 연도 기준 실적을 보는 흐름이 명확하다
- 전체 연도 실적을 무제한 포함하면 payload가 다시 커진다
- 실적 조회 기준 연도가 API 계약에 명시되어야 응답이 예측 가능하다
추가 검토:
- 현재 `getFc`는 `like #{fcltPk}||'%'`라서 같은 하수처리시설 하위 재이용시설 여러 건을 반환하는 구조다.
- 호출명이 `/{pbnstRuseFcltId}` 이므로 실제 의도가
- 특정 재이용시설 1건 조회인지
- 기반 시설코드(prefix) 하위 재이용시설 다건 조회인지
를 명확히 해야 한다.
현 프런트 사용상 분석:
- `ruseDetailPopup.tsx`는 탭으로 여러 재이용시설을 보여주므로 prefix 기반 다건 조회 구조를 활용 중이다.
- `getPbnStnDetail()`는 정확한 `pbnstRuseFcltId`로 단건 선택 후 화면에서 1건 사용한다.
따라서 이번 변경에서는 기존 다건 반환 계약은 유지하되, 반환 각 항목에 `prfList`를 포함하는 쪽이 안전하다.
### 4.2 `pubDetailList` 영향 검토
현재 `pubDetailList`는 아래에서 사용된다.
- `apps/wr/app/store/map/ruseMap/index.tsx`
- `hasuListFetch()`
- `getRuseMaplList()`
- `getRuseMaplListReturn()`
- `packages/common/business_components/src/ui/map/searchBar/searchBar.tsx`
- 경로 문자열 참조 1건
- 과거 실적등록 관련 코드/문서
계획:
- 이번 작업에서 상세 모달 경로는 `pubFc/{id}` 단일 조회로 전환
- `pubDetailList`는 잔여 런타임 사용처가 확인되었으므로 즉시 제거하지 않는다
- 1차로 상세 모달 경로에서 제거하고, `ruseMap` 목록/지도 경로는 영향도 확인 후 후속 정리한다
- 작업계획 기준으로는 "상세 모달에서는 더 이상 사용하지 않음"까지를 1차 목표로 둔다
### 4.3 `GET /reuseFc/pbnstList/...` SQL 합계화
대상 파일:
- `public_fc_select.xml`
- `PublicPbnstList.java`
- `PublicPrfListMap` 및 관련 DTO
- `PublicFcService.getPbnstList()`
계획:
1. `getPbnstList` SQL에서 `prfList` 월별 내역 컬럼 제거
2. 연도 조건이 반영된 실적 집계 서브쿼리 또는 집계 조인 추가
3. SQL에서 아래 컬럼 직접 계산
- `in_usage`
- `out_usage`
- `usage_total`
4. 서비스 레이어 `sumUsage()` 기반 계산 제거
5. `PublicPbnstList`에서 `prfList` 제거
- `pbnstList`는 현황 목록 용도
- `pubFc/{id}`는 상세 용도
- 응답 목적이 달라 DTO를 분리하는 편이 유지보수에 유리
### 4.4 서비스 레이어 정리
`PublicFcService.getPbnstList()`에서는 아래를 제거할 수 있다.
- `item.setPrfList(detail.getPrfList())`
- `sumUsage(...)` 연쇄 호출
남는 역할:
- SQL 결과를 DTO로 변환
- 운영정보 1건 정리
- 기본 문자열 보정
## 5. 프런트 작업계획
### 5.1 상세 모달 경로 정리
대상 파일:
- `apps/wr/components/ui/map/detailPopup/ruseDetailPopup.tsx`
- `apps/wr/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal.tsx`
- `apps/wr/app/store/map/ruseMap/index.tsx`
- `apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx`
- `apps/wr/app/store/ruseFcltMng/fcltReg/pbnstReg/type.tsx`
계획:
1. `ruseMap.getRuseMaplList()`에서 `pubDetailList` 추가 호출 제거
2. `pubFc/{id}` 응답만으로 `ruseDetailList` 구성
3. `PbnstSttnModal`이 `isMap` 여부에 따라 목록 `prfList`를 다시 참조하는 구조 단순화
4. 상세 모달 공통 데이터 타입에 `prfList`가 항상 포함되도록 정리
5. 상세 조회 호출 시 frontend에서 현재년도를 query param으로 전달
목표:
- 상세 모달 렌더링 시 실적 데이터 원천을 `pubFc/{id}` 하나로 통일
### 5.2 현황 목록 경로 정리
대상 파일:
- `apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx`
- 관련 화면:
- `apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx`
계획:
1. `pbnstListFetch()`가 `pbnstList` 응답의 `inUsage/outUsage/usageTotal`만 사용하도록 정리
2. 목록 응답에서 `prfList`를 기대하는 코드 제거
3. 상세 버튼 클릭 시 목록 데이터가 아니라 상세 API 응답을 기준으로 모달 표시
## 6. 데이터 구조 변경 계획
### 6.1 상세 응답
현재 후보:
- `PublicFcInsert` 확장
- 또는 `PublicFcDetailWithPrf` 신규 DTO 추가
권장:
- 신규 상세 DTO 분리 검토
이유:
- 등록/수정용 DTO와 조회용 DTO 성격이 다름
- 상세 조회에 `prfList`, 합계 필드, 화면 전용 값이 추가될 가능성이 높음
### 6.2 목록 응답
현재 `PublicPbnstList`는 `prfList`를 포함한다.
변경 방향:
- 목록 DTO에서는 `prfList` 제거
- 합계 숫자 필드만 유지
프런트 타입도 이에 맞춰 변경:
- `WaterReuseFacility`
- `customPbnst`
- `FcPrfType` 사용 위치 재점검
## 7. 구현 순서
1. `pubFc/{id}` 응답에 실적 포함 설계 확정
2. 상세 모달 사용처 2곳 및 스토어를 `pubFc/{id}` 단일 호출 구조로 전환
3. `pbnstList` SQL을 합계형 목록 조회로 변경
4. 목록 DTO 및 프런트 타입 정리
5. 잔여 `pubDetailList` 사용처 영향 확인
6. 화면 수동 검증
## 8. 검증 계획
### 8.1 상세 모달 검증
- 지도 팝업 상세
- `ruseDetailPopup.tsx`
- 현황 목록 상세 모달
- `pbnstSttnModal.tsx`
확인 항목:
- `pubFc/{id}` 1회 호출만 발생하는지
- 추가 `pubDetailList` 호출이 사라졌는지
- 실적 탭의 장내/장외/합계 표가 기존과 동일하게 보이는지
### 8.2 목록 검증
- `pbnstList` 응답에서 `prfList`가 제거되었는지
- `inUsage`, `outUsage`, `usageTotal` 합계가 기존과 동일한지
- 검색/페이징/시도시군구 조건에서 정상 조회되는지
## 9. 리스크 및 확인 필요사항
### 9.1 `pubFc/{id}`의 조회 단위
현재 `getFc` SQL은 prefix 조회다.
- `WHERE pf.pbnst_ruse_fclt_id like #{fcltPk} || '%'`
이 계약을 유지할지,
정확한 ID 단건 조회로 바꿀지 먼저 결정이 필요하다.
현 프런트와의 호환성을 고려하면 1차는 prefix 다건 조회 유지가 안전하다.
### 9.2 실적 연도 파라미터
상세 응답에 실적을 포함할 때 연도 조건은 필요하다.
이번 계획에서는 아래처럼 확정한다.
- `getFc` 조회 시 `year` query param을 받는다
- frontend는 상세 실적 조회 시 현재년도를 세팅해서 호출한다
- backend는 해당 연도 실적만 `prfList`에 포함한다
### 9.3 `pubDetailList` 잔여 사용처
상세 모달 외에도 아래 스토어에서 여전히 사용 중이다.
- `apps/wr/app/store/map/ruseMap/index.tsx`
이번 작업에서 상세 경로는 정리하되, 다른 화면의 잔여 사용 여부는 후속 정리 대상으로 남을 수 있다.
## 10. 결론
이번 작업은 단순 API 이름 정리가 아니라,
- 상세 조회는 `pubFc/{id}` 중심
- 목록 조회는 `pbnstList` 중심
으로 응답 책임을 다시 나누는 작업이다.
핵심은 아래 두 줄로 요약된다.
1. 상세 모달은 `GET /reuseFc/pubFc/{pbnstRuseFcltId}` 한 번만 호출해도 되게 만든다.
2. 목록 API `GET /reuseFc/pbnstList/...`는 월별 내역을 빼고 SQL 합계만 내려주게 정리한다.
@@ -0,0 +1,285 @@
# 하수처리수재이용 실적등록 작업계획
작성일: 2026-06-30
## 1. 대상
- 화면
- `frontend/apps/wr/app/(with-layout)/ruseFcltMng/prfmncReg/pbnstPrfmncReg/page.tsx`
- 프론트 스토어
- `frontend/apps/wr/app/store/prfmncReg/pbnstPrfmncReg/index.tsx`
- 백엔드
- `D:\iipbs\wr_back_out`
- 기준문서
- `frontend/apps/wr/docs/기준문서/하수재이용실적 등록 api 호출분석_20260630.md`
## 2. 요청사항 정리
- 좌측 목록은 `tb_wr_pbnst_ruse_fclt`만 기준으로 조회한다.
- 좌측 목록은 `reuse-service` 1회 호출로 조회한다.
- 시도/시군구 명칭은 공통코드 조회값을 사용해 화면에서 표시한다.
- 우측 `하수처리수재이용 실적(m³) 등록결과`는 좌측 시설 클릭 시 별도 API를 호출해 조회한다.
- 현재처럼 좌측 목록 조회 시 실적 `prfList`까지 함께 내려받는 구조는 제거한다.
- 좌측 신규 API명은 `reuseFc/pbnstPrfRegList`로 맞춘다.
## 3. 현재 구조 확인 결과
### 3.1 프론트
- 화면 진입 시 현재 호출 흐름
- `GET /reuseFc/pubFc`
- `POST /prcsFclt/ruseInfo/...`
- `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}`
- 좌측 목록은 FO/Common 응답과 WR 응답을 프론트에서 합성한다.
- 우측 실적은 별도 API를 호출하지 않고 좌측 목록의 `prfList`를 그대로 사용한다.
- 저장 후에도 다시 좌측 전체 조회를 수행한다.
### 3.2 reuse-service
- 이미 존재하는 API
- `GET /reuseFc/pubFc`
- 현재는 `pbnst_ruse_fclt_id`의 마지막 1자리를 제거한 기초시설코드 목록만 반환한다.
- 좌측 목록 API로는 재사용 불가하다.
- `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}`
- 시설 기본정보 + `prfList`를 함께 반환한다.
- 우측 클릭 조회용으로는 과조회이며 현재 구조의 결합 원인이다.
- `GET /reuseFc/pbnstList/...`
- WR 기준 목록이지만 `prfList`, 운영정보, 합계까지 같이 내려준다.
- 좌측 단순 목록 전용 API로는 과하다.
- 결론
- 요청사항에 정확히 맞는 좌측 목록 API는 현재 없음
- `reuseFc/pbnstPrfRegList` 신규 추가가 필요함
## 4. 목표 구조
### 4.1 좌측 목록
- 조회원천
- `sm_bs_wr.tb_wr_pbnst_ruse_fclt`
- 호출
- `reuse-service` 1회
- 응답 범위
- 시설명
- 재사용시설명
- 재사용시설코드
- 시도코드
- 시군구코드
- 필요 시 페이징용 totalCount
- 화면 표시
- 시도/시군구 명칭은 공통 코드 스토어에서 코드명으로 변환
### 4.2 우측 실적
- 조회시점
- 좌측 행 클릭 시
- 조회원천
- `sm_bs_wr.tb_wr_pbnst_ruse_fclt_prfmnc`
- 응답범위
- 선택 시설의 해당 연도 월별 실적만 반환
- 저장 후 처리
- 저장 성공 시 좌측 목록 전체 재조회 대신 우측 상세만 재조회
- 좌측 목록은 변경 필요가 없으므로 불필요한 재호출 제거
## 5. 좌측 API 설계안
### 5.1 API 존재 여부 판단
- `GET /reuseFc/pubFc`: 부적합
- 코드목록만 반환
- `GET /reuseFc/pbnstList/...`: 부적합
- `prfList`와 운영정보까지 포함
- 따라서 신규 API 추가
### 5.2 API 경로
- `GET /reuseFc/pbnstPrfRegList/{pageNo}/{pageCnt}/{searchingCd}/{nameCd}`
- 이름검색이 필요하면 query parameter로 확장
- 예: `?swtrmtFcltNm=...`
### 5.3 조회조건
- `pageNo`
- `pageCnt`
- `searchingCd`
- 기본은 행정구역 기준 `A`
- `nameCd`
- `0`: 전체
- `NN`: 시도코드
- `NNNNN`: 시군구코드
- `swtrmtFcltNm`
- 시설명 검색어
### 5.4 응답 컬럼
- `swageBscFctNm`
- 시설명
- `ruseUtztnFcltNm`
- 재사용시설명
- `pbnstRuseFcltId`
- 재사용시설코드
- `sidoCd`
- `substring(swage_adst_dstr_cd, 1, 2)`
- `sggCd`
- `swage_adst_dstr_cd`
- `totalCount`
### 5.5 DB 매핑 기준
- 시설명
- `swage_bsc_fct_nm`
- 재사용시설명
- `ruse_utztn_fclt_nm`
- 재사용시설코드
- `pbnst_ruse_fclt_id`
- 시도코드
- `swage_adst_dstr_cd` 앞 2자리
- 시군구코드
- `swage_adst_dstr_cd`
### 5.6 SQL 방향
- `tb_wr_pbnst_ruse_fclt`만 조회
- `prfList`, `operList`, 합계 컬럼 조인 제거
- 정렬은 `pbnst_ruse_fclt_id` 기준
- 검색은 `swage_bsc_fct_nm like '%' || #{swtrmtFcltNm} || '%'`
## 6. 우측 API 설계안
### 6.1 기존 API 분석
- 현재 우측에 사용되는 데이터 원본은 `POST /reuseFc/pubDetailList/{year}/{sggCd}/{pageNo}/{pageCnt}` 응답의 `prfList`다.
- 즉 기존 응답에서 실제 우측 화면이 쓰는 값은 아래뿐이다.
- `pbnstRuseFcltId`
- `ym`
- `wshwtr`
- `coolUswtr`
- `cleanUswtr`
- `drwtUswtr`
- `dltnUswtr`
- `ithlRswtr`
- `ithlEtcUswtr`
- `cleanToilUswtr`
- `resn`
- `lndwtr`
- `hydUswtr`
- `rvrKeepUswtr`
- `farmUswtr`
- `ugwtChr`
- `indstUswtr`
### 6.2 신규 상세 API 방향
- 우측은 클릭 시 시설 1건만 조회하므로 시설 단건 + 연도 조건 API로 분리한다.
- 권장 경로
- `GET /reuseFc/pbnstPrfRegDtl/{year}/{pbnstRuseFcltId}`
- 최종 명칭은 구현 시 팀 네이밍 규칙에 맞춰 확정한다.
### 6.3 요청 파라미터
- `year`
- 조회연도
- 예: `2026`
- `pbnstRuseFcltId`
- 선택한 재사용시설코드
- 예: `11000SW001R1`
### 6.4 응답값
- 시설식별 정보
- `pbnstRuseFcltId`
- `ruseUtztnFcltNm`
- `swageBscFctNm`
- 실적목록
- `prfList: PublicFcPrfmnc[]`
- `prfList` 원소 구조
- `ym`
- `wshwtr`
- `coolUswtr`
- `cleanUswtr`
- `drwtUswtr`
- `dltnUswtr`
- `ithlRswtr`
- `ithlEtcUswtr`
- `cleanToilUswtr`
- `resn`
- `lndwtr`
- `hydUswtr`
- `rvrKeepUswtr`
- `farmUswtr`
- `ugwtChr`
- `indstUswtr`
### 6.5 SQL 방향
- `tb_wr_pbnst_ruse_fclt_prfmnc`를 `pbnst_ruse_fclt_id`, `ym like #{year} || '%'` 조건으로 조회
- 필요 시 시설명/재사용시설명 표시를 위해 `tb_wr_pbnst_ruse_fclt` 1회 join
- 반환 단위는 1개 시설 + 최대 12개월 실적
## 7. 프론트 변경계획
### 7.1 스토어
- `getPrfFmcRes()` 제거
- 더 이상 `GET /reuseFc/pubFc` 선조회 불필요
- `hasuListFetch()` 제거 또는 전면 교체
- FO/Common + WR 합성 로직 삭제
- 좌측 목록 조회 함수 신설
- `GET /reuseFc/pbnstPrfRegList/...`
- 우측 상세 조회 함수 신설
- 좌측 클릭 시 상세 API 호출
- 저장 후 재조회 변경
- 기존: 좌측 전체 목록 재조회
- 변경: 우측 상세 재조회
### 7.2 페이지
- 최초 진입 시 좌측 목록만 조회 + 첫시설의 우측 상세 호출. 첫시설 선택 표시.
- 좌측 클릭 시 우측 상세 호출
- `cellClick()` 내부의 `prfmncRes[index].prfList` 직접 사용 제거
- 좌측 row 데이터는 코드 중심으로 유지
- `sidoCd`, `sggCd`를 공통코드명으로 변환해 표시
## 8. 백엔드 변경계획
### 8.1 대상 파일
- `src/main/java/kr/co/mindone/wrout/controller/PublicFcController.java`
- `src/main/java/kr/co/mindone/wrout/service/public_fc/PublicFcService.java`
- `src/main/java/kr/co/mindone/wrout/mapper/wr/PublicFcMapper.java`
- `src/main/resources/postgres/mapper/wr/public_fc_select.xml`
- 필요 시 신규 DTO
- 좌측 목록 DTO
- 우측 상세 DTO
### 8.2 구현 항목
1. 좌측 목록 DTO 추가
- 실적 없는 가벼운 응답 모델
2. `pbnstPrfRegList` 컨트롤러/서비스/매퍼 추가
3. 우측 단건 실적 조회 API 추가
4. `pubDetailList`는 기존 화면 영향 범위 확인 중요함. 그 후에 유지 혹은 신규 추가.
5. Swagger 설명 추가
## 9. 검증 항목
1. 화면 진입 시 좌측 목록 API 1회만 호출되는지
2. 화면 진입 시 `POST /prcsFclt/ruseInfo/...`가 더 이상 호출되지 않는지
3. 좌측 목록 응답에 `prfList`가 포함되지 않는지
4. 좌측 클릭 시에만 우측 상세 API가 호출되는지
5. 저장 후 좌측 목록 재조회 없이 우측 데이터만 갱신되는지
6. 시도/시군구 명칭이 공통 코드 기준으로 정상 표시되는지
7. 기존 저장 API `POST /reuseFc/pubFcPrf`와 정합성 문제가 없는지
## 10. 작업 순서
1. `reuse-service`에 좌측 목록 신규 API 추가
2. `reuse-service`에 우측 상세 신규 API 추가
3. 프론트 스토어를 신규 API 기준으로 분리
4. 페이지 클릭/저장 후 갱신 흐름 수정
5. 수동 테스트로 호출 수와 데이터 정합성 확인
## 11. 결론
- 좌측 목록 전용 API `reuseFc/pbnstPrfRegList`는 현재 `reuse-service`에 없으므로 신규 추가가 필요하다.
- 우측 실적도 현재는 `pubDetailList`에 묶여 있으므로, 시설 클릭 시 조회하는 단건 상세 API로 분리하는 것이 맞다.
- 이번 작업의 핵심은 "좌측은 시설목록만, 우측은 클릭 후 실적조회"로 API 책임을 분리해 화면 구조와 호출 구조를 단순화하는 것이다.
+6 -1
View File
@@ -125,7 +125,12 @@
background: #1a1c20;
}
input[type="file"] {
background-color: #1a1a1a;
border-color: #262626;
color: #f1f1f1;
}
}
input[type="file"] {
@@ -53,13 +53,13 @@ const CustomTooltip = ({
}) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
<div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${fncNumberFormet(item.value)}`}</span>
<span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${fncNumberFormet(item.value)}`}</span>
</div>
))}
</div>
@@ -76,7 +76,7 @@ const CustomLegend = (props: any) => {
{payload.map((entry: unknown, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: (entry as {color: string}).color }}></div>
<span className="text-[12px]">{(entry as {value: string}).value}</span>
<span className="text-[12px] text-card-foreground">{(entry as {value: string}).value}</span>
</div>
))}
</div>
@@ -141,7 +141,7 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle='
radius={5}
key={key}
barSize={graphSize}
fill={colors[idx % colors.length]}
fill={colors[idx % colors.length] || "var(--foreground)"}
/>
))
)}
@@ -151,4 +151,4 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle='
</div>
</>
)
}
}
@@ -44,13 +44,13 @@ const CustomTooltip = ({
}) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
<div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span>
<span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value}`}</span>
</div>
))}
</div>
@@ -67,7 +67,7 @@ const CustomLegend = (props: any) => {
{payload.map((entry: unknown, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: (entry as {color: string}).color }}></div>
<span className="text-[12px]">{(entry as {value: string}).value}</span>
<span className="text-[12px] text-card-foreground">{(entry as {value: string}).value}</span>
</div>
))}
</div>
@@ -123,7 +123,7 @@ export default function DoubleBarChart({chartConfig=undefined, chartData=[], cha
key={key}
stackId="a"
barSize={graphSize}
fill={colors[idx]}
fill={colors[idx] || "var(--foreground)"}
/>
))
)}
@@ -133,4 +133,4 @@ export default function DoubleBarChart({chartConfig=undefined, chartData=[], cha
</div>
</>
)
}
}
@@ -69,7 +69,7 @@ const renderCustomizedLabel = ({
<text
x={x}
y={y}
fill="#333"
fill="var(--muted-foreground)"
fontSize={11}
textAnchor={x > cx ? "start" : "end"}
dominantBaseline="central"
@@ -87,12 +87,12 @@ const CustomTooltip = ({ active, payload, label }: {
}) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
<div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
<div style={{ width: "10px", height: "10px", backgroundColor: item.payload.fill, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
<span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
</div>
))}
</div>
@@ -126,7 +126,7 @@ const CustomLegend = (props: LegendProps) => {
minHeight: "14px",
}}
></div>
<span style={{ fontSize: "13px", whiteSpace: "nowrap" }}>
<span className="text-card-foreground" style={{ fontSize: "13px", whiteSpace: "nowrap" }}>
{entry.payload?.label || entry.value}
</span>
</div>
@@ -49,13 +49,13 @@ type LegendProps = {
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
<div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
<span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
</div>
))}
</div>
@@ -74,7 +74,7 @@ const CustomLegend = (props: LegendProps) => {
{payload.map((entry: LegendEntry, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div>
<span className="text-[12px]">{entry.value}</span>
<span className="text-[12px] text-card-foreground">{entry.value}</span>
</div>
))}
</div>
@@ -172,9 +172,10 @@ export default function LineBarchart({
x={x - padding} // 맨 좌측 바깥
y={y + height / 2 - 12} // 수직 중앙
textAnchor="middle"
transform={`rotate(-90, ${x - padding}, ${y + height / 2})`}
className="text-[12px] font-bold"
>
transform={`rotate(-90, ${x - padding}, ${y + height / 2})`}
className="text-[12px] font-bold"
fill="var(--muted-foreground)"
>
{fieldText}
</text>
);
@@ -184,9 +185,10 @@ export default function LineBarchart({
x={x + height / 2 } // 맨 좌측 바깥
y={y - width + padding } // 수직 중앙
textAnchor="middle"
transform={`rotate(90, ${x}, ${y})`}
className="text-[12px] font-bold"
>
transform={`rotate(90, ${x}, ${y})`}
className="text-[12px] font-bold"
fill="var(--muted-foreground)"
>
{fieldText}
</text>
);
@@ -249,7 +251,7 @@ export default function LineBarchart({
key={`bar-${key}-${index}`}
yAxisId="right"
dataKey={key}
fill={config?.color || "black"}
fill={config?.color || "var(--foreground)"}
name={String(config?.label || "Y축")}
maxBarSize={80}
/>
@@ -267,7 +269,7 @@ export default function LineBarchart({
yAxisId="left"
type="linear"
dataKey={key}
stroke={config?.color || "black"}
stroke={config?.color || "var(--foreground)"}
strokeWidth={2}
dot={{r: 0}}
name={String(config?.label || "X축")}
@@ -281,4 +283,4 @@ export default function LineBarchart({
</div>
</div>
);
}
}
@@ -46,13 +46,13 @@ type LegendProps = {
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
<div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
<span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
</div>
))}
</div>
@@ -71,7 +71,7 @@ const CustomLegend = (props: LegendProps) => {
{payload.map((entry: LegendEntry, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.color }}></div>
<span className="text-[12px]">{entry.value}</span>
<span className="text-[12px] text-card-foreground">{entry.value}</span>
</div>
))}
</div>
@@ -151,7 +151,7 @@ export default function LineBarchart2({
key={`bar-${key}-${index}`}
yAxisId="right"
dataKey={key}
fill={config?.color || "black"}
fill={config?.color || "var(--foreground)"}
name={String(config?.label || "Y축")}
maxBarSize={80}
/>
@@ -169,7 +169,7 @@ export default function LineBarchart2({
yAxisId="left"
type="linear"
dataKey={key}
stroke={config?.color || "black"}
stroke={config?.color || "var(--foreground)"}
strokeWidth={2}
dot={{r: 0}}
name={String(config?.label || "X축")}
@@ -183,4 +183,4 @@ export default function LineBarchart2({
</div>
</div>
);
}
}
@@ -63,13 +63,13 @@ type LegendProps = {
const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
<div className="w-[180px] rounded border border-border bg-accent p-2.5 text-accent-foreground shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="m-3 text-[12px] font-bold text-accent-foreground">{label}</p>
{payload.filter(item => item.value != null && String(item.value) != 'NaN' && item.value != undefined).map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(',','')}`}</span>
<span className="text-[11px] font-bold text-accent-foreground">{` ${item.name} : ${item.value.toString().replace(',','')}`}</span>
</div>
))}
</div>
@@ -122,7 +122,7 @@ const CustomLegend = (props: LegendProps) => {
minHeight: "14px",
}}
></div>
<span style={{ fontSize: "13px", whiteSpace: "nowrap" }}>{entry.value}</span>
<span className="text-card-foreground" style={{ fontSize: "13px", whiteSpace: "nowrap" }}>{entry.value}</span>
</div>
))}
</div>
@@ -141,6 +141,7 @@ const CustomAxisText : React.FC<{ viewBox: any, fieldText: string, position:stri
textAnchor="middle"
transform={`rotate(-90, ${x - padding}, ${y + height / 2})`}
className="text-[12px] font-bold"
fill="var(--muted-foreground)"
>
{fieldText}
</text>
@@ -153,6 +154,7 @@ const CustomAxisText : React.FC<{ viewBox: any, fieldText: string, position:stri
textAnchor="middle"
transform={`rotate(90, ${x}, ${y})`}
className="text-[12px] font-bold"
fill="var(--muted-foreground)"
>
{fieldText}
</text>
@@ -277,4 +279,4 @@ export default function Linechart({
</div>
</div>
);
}
}
@@ -151,7 +151,7 @@ export function UiTable<T extends object = Record<string, unknown>>({
) : (
<></>
)}
<div className={`card !p-0r !m-[1.2rem] ${className}`}>
<div className={`card !m-[1.2rem] ${className}`}>
<Table variant="secondary" className={widthFixed ? "table-fixed" : ""}>
{widthFixed ?
<colgroup>