wr: 하수재이용실적 등록1
This commit is contained in:
+2
-2
@@ -13,8 +13,8 @@ NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
# NEXT_PUBLIC_WR_SERVER_API_URL = http://localhost:8883/reuse-service/api/v1
|
||||
# NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://localhost:8883/reuse-service/api/v1
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service # 개발 서버 API
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost:8883/sewerage-service
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 재이용실적등록 > 하수재이용실적등록
|
||||
*/
|
||||
"use client";
|
||||
@@ -10,13 +10,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 +30,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 +135,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 +221,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 +280,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 +290,7 @@ export default function PrfmncPrfmncReg() {
|
||||
<SearchInput
|
||||
className={"w-full text-2xl"}
|
||||
label=""
|
||||
textValue={searchTxt || ""}
|
||||
textValue={searchTxt}
|
||||
setText={(e) => {
|
||||
setSearchTxt(e);
|
||||
}}
|
||||
@@ -271,12 +300,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 +315,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 +341,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 +375,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 +400,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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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,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 책임을 분리해 화면 구조와 호출 구조를 단순화하는 것이다.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user