From 4903684bceecf4d430c78b1d3b55dea93400dd01 Mon Sep 17 00:00:00 2001 From: yschoi Date: Sun, 10 Aug 2025 02:05:04 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=EA=B8=89=EC=B2=98=20=EC=84=A0?= =?UTF-8?q?=EC=A0=95=20=EC=A7=80=EB=8F=84=20=ED=8C=9D=EC=97=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchInputText/SearchInputText.tsx | 139 ++++++++ apps/wr/app/(popUp-layout)/splyPopup/page.tsx | 315 ++++++++++++++++++ .../splyPopup/searchBar/searchBar.tsx | 118 +++++++ .../splyPopup/selectedFcltList.tsx | 81 +++++ .../(popUp-layout)/splyPopup/service/index.ts | 1 + .../(popUp-layout)/splyPopup/service/map.ts | 60 ++++ apps/wr/app/store/map/splyPop/index.tsx | 93 ++++++ apps/wr/app/store/map/splyPop/type.tsx | 39 +++ apps/wr/app/store/store.tsx | 10 + apps/wr/components/lib/config/index.ts | 2 + .../ui/form/coMat/conCostForm/conCostForm.tsx | 63 +++- 11 files changed, 920 insertions(+), 1 deletion(-) create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/page.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/service/index.ts create mode 100644 apps/wr/app/(popUp-layout)/splyPopup/service/map.ts create mode 100644 apps/wr/app/store/map/splyPop/index.tsx create mode 100644 apps/wr/app/store/map/splyPop/type.tsx diff --git a/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx new file mode 100644 index 000000000..05f41d09b --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/SearchInputText/SearchInputText.tsx @@ -0,0 +1,139 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; + +type SearchType = { + bscFctNm: string; + bscFctCd: string; +}; + +type thisProps = { + label: string | undefined; + initValue: string | undefined; + setValue: (searchNm?: string) => void; + setRtnData: (rtnData: any) => void; + searchList?: SearchType[]; +}; + +export default function SearchInputText({ + label = undefined, + initValue = undefined, + setValue = () => {}, + setRtnData = () => {}, + searchList = [], +}: thisProps) { + const [isOpen, setIsOpen] = useState(false); + const [textValue, setTextValue] = useState(initValue || ""); + const [highlightIndex, setHighlightIndex] = useState(-1); + + const wrapperRef = useRef(null); + + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setTextValue(value); + setValue(value); + setHighlightIndex(-1); + setIsOpen(value !== ""); + }, + [setValue] + ); + + const handleSuggestionClick = useCallback( + (searchNm: string) => { + setValue(searchNm); + setTextValue(searchNm); + setIsOpen(false); + setRtnData(searchList?.find((item) => item.bscFctNm === searchNm) || {}); + }, + [searchList, setRtnData, setValue] + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (!isOpen || searchList.length === 0) return; + + if (e.key === "ArrowDown") { + e.preventDefault(); + setHighlightIndex((prev) => (prev + 1) % searchList.length); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setHighlightIndex((prev) => + prev <= 0 ? searchList.length - 1 : prev - 1 + ); + } else if (e.key === "Enter") { + e.preventDefault(); + if (searchList.length === 1 && searchList[0]) { + handleSuggestionClick(searchList[0].bscFctNm); + } else if (highlightIndex >= 0 && highlightIndex < searchList.length && searchList[highlightIndex]) { + handleSuggestionClick(searchList[highlightIndex].bscFctNm); + } + } + }, + [highlightIndex, isOpen, searchList, handleSuggestionClick] + ); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + return ( +
+ { + setTextValue(""); + setValue(""); + setIsOpen(false); + }} + /> + {isOpen && ( +
+
+ {searchList.length > 0 ? ( + searchList.map((item, index) => ( +
handleSuggestionClick(item.bscFctNm)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + handleSuggestionClick(item.bscFctNm); + } + }} + > + {item.bscFctNm} +
+ )) + ) : ( +
+ 검색 결과가 없습니다. +
+ )} +
+
+ )} +
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/page.tsx b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx new file mode 100644 index 000000000..51cf73064 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/page.tsx @@ -0,0 +1,315 @@ +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { useEffect, useRef, useState } from "react"; +import { mapService } from "./service"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { SearchBar } from "./searchBar/searchBar"; +import SelectedFcltList from "./selectedFcltList"; + +export type SubsInfo = { + ruseUtztnFcltNm: string; // 재이용시설명 + ruseUtztnFcltCd: string; // 재이용시설 코드 +} + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [selectDataPk, setSelectDataPk] = useState>({}); + const [selectedFclts, setSelectedFclts] = useState([]); + + //적용 버튼 클릭 이벤트 + const onApply = () => { + if (selectedFclts.length > 1) { + alert("재이용시설 공급처는 1곳만 선택할 수 있습니다."); + return; + } + + const subsInfoList: SubsInfo[] = selectedFclts.map((fclt) => ({ + ruseUtztnFcltNm: fclt.ruseUtztnFcltNm, + ruseUtztnFcltCd: fclt.pbnstRuseFcltId, + })); + + if (!window.opener || typeof window.opener.postMessage !== "function") { + alert("부모 창을 찾을 수 없습니다. 팝업이 'noopener'로 열렸거나, 보안 정책으로 차단되었을 수 있어요."); + return; + } + + window.opener.postMessage( + { + type: "SET_SUBS_INFO", + payload: subsInfoList[0], // 또는 필요한 데이터 + }, + "*" + ); + + window.close(); + }; + + //트리 레이어 데이터 조회 + const fetchTreeData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + //하수처리시설코드로 재이용시설 목록 조회 + const reuseData = async (ruseFcltPk: any) => { + try { + const response = await mapService.getPbnstRuseFclts(ruseFcltPk); + const data = response.data || []; + setSelectedFclts((prev) => { + const newItems = data.filter( + (item: any) => + item && + Object.keys(item).length > 0 && + !prev.some((f) => f.pbnstRuseFcltId === item.pbnstRuseFcltId) + ); + console.log(newItems); + return [...prev, ...newItems]; + }); + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchTreeData(); + }, []); + + // 팝업 Map.tsx useEffect 안 + useEffect(() => { + const origin = window.location.origin; + if (window.opener) { + window.opener.postMessage({ type: "REQUEST_INIT_SUBS_INFO" }, origin); + } + }, []); + + useEffect(() => { + const origin = window.location.origin; + + const handler = (event: MessageEvent) => { + // 동일 출처만 허용 (개발 중엔 임시로 주석처리하고 *로 테스트해도 됨) + if (event.origin !== origin) return; + + const data = event.data; + if (!data || typeof data !== "object") return; + + if (data.type === "INIT_SUBS_INFO") { + // payload: [{ ruseUtztnFcltNm, ruseUtztnFcltCd }, ...] + const list = Array.isArray(data.payload) ? data.payload : []; + + // selectedFclts 형태에 맞게 변환해서 반영 + // (지금 onApply에서 pbnstRuseFcltId를 쓰므로 매핑 맞춰줍니다) + setSelectedFclts(list.map((x) => ({ + ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "", + pbnstRuseFcltId: x.ruseUtztnFcltCd ?? "", // <- 기존 코드가 기대하는 키 + }))); + } + }; + + window.addEventListener("message", handler); + return () => window.removeEventListener("message", handler); + }, []); + + // 레이어 생성 후 초기화 처리 + const onCheckedLayersChange = () => { + //수요처 ID + const urlParams = new URLSearchParams(window.location.search); + const dmdInfoId = urlParams.get("dmdInfoId"); + + //수요처 위치로 화면 이동 + const maps = gisMaps.current; + const layer = GisApp.Module.layer as any; + const layerNm = "tb_wr_ruse_fclt_dmd_co"; + const featureId = `${layerNm}.${dmdInfoId}`; + let foundFeature: any = null; + + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) { + foundFeature = feature; + return; + } + } + } + }); + + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + + if (geometry) { + const extent = geometry.getExtent(); + + if (geometry.getType() === "Point") { + maps.getView().setCenter(geometry.getCoordinates()); + maps.getView().setZoom(11); + } else { + maps.getView().fit(extent, { + duration: 300, + padding: [30, 30, 30, 30], + maxZoom: 16, + }); + } + } + + layer.highLightFeature( + [foundFeature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 } + ); + } + + const formatNumber = (val: any) => { + const num = Number(val); + return isNaN(num) ? "-" : num.toLocaleString(); + }; + + // 재이용시설 오버레이 + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") === "tb_wr_bdst_ruse_fclt") { + const source = layer.getSource?.(); + if (source?.getFeatures) { + const features = source.getFeatures(); + + features.forEach((feature: any) => { + // 필요한 고유 ID 또는 속성 가져오기 (예: ftrIdn 또는 feature.getId()) + const featureId = + feature.getId() || feature.get("ftrIdn") || Math.random(); + + // HTML 또는 React 오버레이 컴포넌트 구성 + const overlayContent = ` +
+ +
+ ${feature.get("swtrmt_fclt_nm") || "-"} +
+ + + + + + + + + +
용량(m³/일)${formatNumber(feature.get("fclt_cpct"))}
+
+ `; + feature.set("html", overlayContent); + feature.set("overLayId", `overLay.${featureId}`); + }); + } + } + }); + }; + + // 검색해서 재이용시설 선택하면 해당 시설로 화면 이동 처리 + useEffect(() => { + if (!selectDataPk || Object.keys(selectDataPk).length === 0) return; + const maps = gisMaps.current; + let foundFeature: any = null; + const layerNm = "tb_wr_bdst_ruse_fclt"; + const featureId = `${layerNm}.${selectDataPk.bscFctCd}`; + maps + .getLayers() + .getArray() + .forEach((layer: any) => { + if (layer.get("lyrTyNm") == layerNm) { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) { + foundFeature = feature; + return; + } + } + } + }); + + if (foundFeature) { + const geometry = foundFeature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + + maps.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", selectDataPk); + } + }, [selectDataPk]); + + // 재이용시설 클릭 시 선택 시설목록에 추가 + const onFeatureSelect = (data: any) => { + // 처리시설코드 + const swtrmtFcltCd = data.get("swtrmt_fclt_cd"); + reuseData(swtrmtFcltCd); + }; + + return ( + <> +
+ { + if (hasuPk) setSelectDataPk(hasuPk); + }} + onApply={onApply} + /> + } + waterDepth={undefined} + /> +
+ setSelectedFclts(updatedList)} + /> + + ); +}; + +export default Map; diff --git a/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx new file mode 100644 index 000000000..ce5c6d1ae --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/searchBar/searchBar.tsx @@ -0,0 +1,118 @@ +"use client"; +import { useWrStore } from "@/app/store"; +import { + SelectBox, + type SelectDataType, +} from "@common/business_components/ui"; +import { Button, Label } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import SearchInputText from "../SearchInputText/SearchInputText"; + +export function SearchBar({ + setHasuPk, + onApply, +}: { + setHasuPk: (hasuPk: any) => void; + onApply: () => void; +}) { + const { map, code, codeInit, findRuseFclt } = useWrStore( + (state) => state + ); + const [sidoValue, setSidoValue] = useState(""); + const [sigunValue, setSigunValue] = useState(""); + const [filteredSigunData, setFilteredSigunData] = useState([ + { text: "전체", val: "0" }, + ]); + + useEffect(() => { + //codeInit(); + }, [codeInit]); + + const initializeSigunData = useCallback( + (sidoCd: string) => { + if (sidoCd === "0") { + setSigunValue("0"); + setFilteredSigunData([{ text: "전체", val: "0" }]); + } else { + const newFilteredData = [ + { text: "전체", val: "0" }, + ...code.sggCd.filter( + (item: SelectDataType) => item.val.substring(0, 2) === sidoCd + ), + ]; + setSigunValue("0"); + setFilteredSigunData(newFilteredData); + } + }, + [code.sggCd] + ); + + const onSelectValue = useCallback( + (val: string, type: string) => { + switch (type) { + case "ctpvCd": + initializeSigunData(val); + setSidoValue(val); + break; + case "sggCd": + setSigunValue(val); + } + }, + [initializeSigunData] + ); + + return ( +
+
+ onSelectValue(val.toString(), "ctpvCd")} + value={sidoValue} + /> + onSelectValue(val.toString(), "sggCd")} + value={sigunValue} + /> + + { + let sggOrCity = ""; + if (searchNm) { + if (sigunValue === "0" || sigunValue === "") { + sggOrCity = sidoValue; + } else { + sggOrCity = sigunValue; + } + findRuseFclt(sggOrCity, searchNm); + } + }} + setRtnData={(rtnData: any) => { + setHasuPk(rtnData); + }} + searchList={map.ruseFcltList || []} + /> + +
+
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx b/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx new file mode 100644 index 000000000..bdf4d7e2c --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/selectedFcltList.tsx @@ -0,0 +1,81 @@ +import React from "react"; + +type FcltItem = { + pbnstRuseFcltId: string; + ruseUtztnFcltNm: string; + splyExtraCpct?: number; + ruseCpct?: number; + [key: string]: any; +}; + +export default function SelectedFcltList({ + data = [], + onRemove, +}: { + data: FcltItem[]; + onRemove: (updatedList: FcltItem[]) => void; +}) { + const handleDelete = (code: string) => { + const updatedList = data.filter((item) => item.pbnstRuseFcltId !== code); + onRemove(updatedList); + }; + + const TableContent = ( + <> + + + 시설명 + + 하수처리량 +
+ (㎥/일) + + + 재이용량 +
+ (㎥/일) + + 삭제 + + + + {data.map((item) => ( + + {item.ruseUtztnFcltNm} + + {Number(item.splyExtraCpct || 0).toLocaleString()} + + + {Number(item.ruseCpct || 0).toLocaleString()} + + + + + + ))} + + + ); + + if (data.length === 0) return null; + + return ( +
+

+ 선택된 시설 +

+
+ + {TableContent} +
+
+
+ ); +} diff --git a/apps/wr/app/(popUp-layout)/splyPopup/service/index.ts b/apps/wr/app/(popUp-layout)/splyPopup/service/index.ts new file mode 100644 index 000000000..c6bfcff72 --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/service/index.ts @@ -0,0 +1 @@ +export * from './map' diff --git a/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts new file mode 100644 index 000000000..b7afe1deb --- /dev/null +++ b/apps/wr/app/(popUp-layout)/splyPopup/service/map.ts @@ -0,0 +1,60 @@ + +/** + * 관망도 API + */ + import { DEV_URL_FO, DEV_URL_WR, DEV_URL_SN } from "@/components/lib/config"; + +export type LyrStngProps = { + lyr_sn: string + lyr_nm: string + upper_lyr_sn: string + drawOrdr: string + flterSql: string + gobjTy: string + lyrTpNm: string + useYn: string + minViewSize: string + iconNm: string + lblAtrbNm: string + fontNm: string + fontIntnColor: string + fontExtnColor: string + fontExtnWdth: string + lnWdth: string + lnColor: string + polgnColor: string + initEprssYn: string + clickPsblYn: string +} + +export const mapService = { + //레이어 설정 조회 + getLyrStngs: async () => { + const response = await fetch(`${DEV_URL_SN}/v1/map/lyrStngs/DM_NETWORK`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + }, + getPbnstRuseFclts: async (ruseFcltPk:any) => { + const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + } +} diff --git a/apps/wr/app/store/map/splyPop/index.tsx b/apps/wr/app/store/map/splyPop/index.tsx new file mode 100644 index 000000000..42dc850d3 --- /dev/null +++ b/apps/wr/app/store/map/splyPop/index.tsx @@ -0,0 +1,93 @@ +import { fetchType, getFetch } from "@/components/lib/api"; +import type { FacilityInfo, SubsInfo } from "./type"; +import { DEV_URL_FO } from "@/components/lib/config"; + +const checkWrRegList = async () => { + const wrRegList = await getFetch("wr", `/reuseFc/pubFc`); + return wrRegList; +}; + +const getRuseFclt = async ( + page: string, + sggOrCity: string, + searchTxt?: string +) => { + const checkWrRegListRes = await checkWrRegList(); + + let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`; + + const ruseList = checkWrRegListRes.filter((item: string) => item !== ""); + + if (searchTxt) { + url = url + `/${searchTxt}`; + } + + const response = await fetch(DEV_URL_FO + url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(ruseList), + }); + + const hasuList = await response.json().then((res: any) => { + const data = res.data + .map((item: any) => { + return { + ...item, + fctCpc: Number(item.fctCpc) || 0, + searchNm: item.fctNm, + searchPk: item.bscFctNm, + }; + }) + .filter((hasu: any) => ruseList.find((ruse: any) => hasu.bscFctCd === ruse)); + return { data: data, count: res.count }; + }); + + return hasuList; +}; + +export type DmMapActions = { + findRuseFclt(sggOrCity: string, searchTxt: string): void; + setSubsInfo(subsInfo: SubsInfo[]): void; +}; + +export type DmMapType = { + map: { + ruseFcltList: FacilityInfo[]; + subsInfo: SubsInfo[]; + }; +}; + +export const dmMapState: DmMapType = { + map: { + ruseFcltList: [], + subsInfo: [] + }, +}; +export type DmMapStore = DmMapType & DmMapActions; + +export const DmMapReducer = (set: any): DmMapActions => { + return { + findRuseFclt: async (sggOrCity: string, searchTxt: string) => { + const ruseFcltList = await getRuseFclt("1", sggOrCity, searchTxt); + + set((state: DmMapType) => ({ + ...state, + map: { + ...state.map, + ruseFcltList: ruseFcltList.data, + }, + })); + }, + setSubsInfo: async (subsInfo: SubsInfo[]) => { + set((state: DmMapType) => ({ + ...state, + map: { + ...state.map, + subsInfo: subsInfo, + }, + })); + }, + }; +}; diff --git a/apps/wr/app/store/map/splyPop/type.tsx b/apps/wr/app/store/map/splyPop/type.tsx new file mode 100644 index 000000000..adcaf7f37 --- /dev/null +++ b/apps/wr/app/store/map/splyPop/type.tsx @@ -0,0 +1,39 @@ +export type FacilityInfo= { + bscFctCd: string; // 기본시설 코드 + bscFctNm: string; // 기본시설명 + rvbsnCd: string; // 유역 코드 + rvbsn: string; // 유역명 + lgbsn: string; // 대권역명 + mdmbsn: string; // 중권역명 + rgnCd: string; // 관할청 코드 + rgnNm: string; // 관할청 명칭 + adstDstrCd: string; // 행정구역 코드 + sido: string; // 시도 + gugun: string; // 구군 + addr: string; // 주소 + fctCpc: string; // 시설용량 + efflWqltStndrdCd: string; // 방류수 수질기준 코드 + efflWqltStndrdNm: string; // 방류수 수질기준명 + trtFrfNm: string; // 처리공법명 + trtDstr: string; // 처리구역 + rnDt: string; // 사용개시일 (예: YYYYMMDD) + cmplDt: string; // 준공일자 (예: YYYYMMDD) + trstOprEtpNm: string; // 위탁운영기관명 + bscFctClsNm: string; // 기본시설 구분명 + sitArea: string; // 부지면적 + trtDstrArea: string; // 처리구역 면적 + swrTrtPopl: string; // 하수처리 인구 + mnstr: string; // 총괄 유역 + efflWtrrgn: string; // 방류수역 + rvNm: string; // 하천명 + bscFctClsCd: string; // 기본시설 구분 코드 + rvbsnNm: string; // 유역명 + tel: string; // 전화번호 + rswtrYn: string; // 재이용수 여부 ("Y" 또는 "N") + ruse: boolean; // 재이용 여부 (boolean) + } + + export type SubsInfo={ + ruseUtztnFcltNm: string; // 재이용시설명 + ruseUtztnFcltCd: string; // 재이용시설 코드 + } \ No newline at end of file diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index 6577f2a8f..c7dcf1ca8 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -216,6 +216,12 @@ import { type ldrmntMapState, type ldrmntMapType, } from "./map/ldrmntCoMap"; +import { + DmMapReducer, + dmMapState, + type DmMapType, + type DmMapStore, +} from "./map/splyPop"; export type WrStore = comCodeType & pubFacType & @@ -251,6 +257,7 @@ export type WrStore = comCodeType & ruseMapType & coSttnMapType & ldrmntMapType & + DmMapStore & authType; export const StoreZus = ( @@ -288,6 +295,7 @@ export const StoreZus = ( ruseMapState & coSttnMapState & ldrmntMapState & + DmMapType & authState = { ...comCodeStateExport, ...pubFacStateExport, @@ -324,6 +332,7 @@ export const StoreZus = ( ...ruseMapStateExport, ...coSttnMapStateExport, ...ldrmntMapStateExport, + ...dmMapState, } ) => { return createStore()((set) => { @@ -371,6 +380,7 @@ export const StoreZus = ( ...ruseMapActionsExport(set), ...coSttnMapActionsExport(set), ...ldrmntMapActionsExport(set), + ...DmMapReducer(set), }; }); }; diff --git a/apps/wr/components/lib/config/index.ts b/apps/wr/components/lib/config/index.ts index ff44511b4..f05a38e62 100644 --- a/apps/wr/components/lib/config/index.ts +++ b/apps/wr/components/lib/config/index.ts @@ -4,6 +4,7 @@ import { SERVER_API_CS_URL, SERVER_API_FO_URL, SERVER_API_WR_URL, + SERVER_SEWERAGE_API_URL, } from "@/src/shared/config"; //const DEV_PORT = "11080"; @@ -19,6 +20,7 @@ import { export const LOCAL_URL_WR = PROXY_HOST + `/api/v1`; export const DEV_URL_WR = SERVER_API_WR_URL; export const DEV_URL_FO = SERVER_API_FO_URL; +export const DEV_URL_SN = SERVER_SEWERAGE_API_URL; export const LOCAL_URL = PROXY_HOST; export const CS_URL = SERVER_API_CS_URL; export const CS_TUS_UPLOAD = SERVER_API_CS_TUS_URL; diff --git a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx index 423581432..515121e4e 100644 --- a/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx +++ b/apps/wr/components/ui/form/coMat/conCostForm/conCostForm.tsx @@ -69,6 +69,18 @@ export default function ConCostForm({ setCheckFinish(false); }, [selectionVersion]); + //공급처 팝업 값 셋팅 + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === "SET_SUBS_INFO") { + setLastSub(event.data.payload); + } + }; + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [setLastSub]); + const zipDowonLoad = () => { fetch(`/2024_constarctFeeProgram.zip`, {}).then(async (response) => { const blobRet = await response.blob(); @@ -219,15 +231,62 @@ export default function ConCostForm({ }, [form.formState]); useEffect(() => { - console.log("splyCndtList ", splyList); + const origin = window.location.origin; + + const handler = (event: MessageEvent) => { + // 보안: 동일 출처만 수신 + if (event.origin !== origin) return; + + // 구조 안전 체크 + const data = event.data; + if (!data || typeof data !== "object") return; + + if (data.type === "REQUEST_INIT_SUBS_INFO") { + const initSubs = + (splyList ?? []) + .filter(Boolean) + .map((x: any) => ({ + ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "", + ruseUtztnFcltCd: x.ruseUtztnFcltCd ?? "", + })); + + // 응답: 옵션 객체 형태로 전송 (TS 에러 방지) + const target = event.source as Window | null; + target?.postMessage( + { type: "INIT_SUBS_INFO", payload: initSubs }, + { targetOrigin: origin } + ); + } + }; + + window.addEventListener("message", handler); + return () => window.removeEventListener("message", handler); }, [splyList]); + useEffect(() => { form.register("dmdInfoSn"); form.register("dmdInfoId"); form.register("ruseUtztnFcltNm"); }, []); + + const searchCandidate = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const dmdInfoId = form.getValues("dmdInfoId"); + console.log(dmdInfoId) + + const url = `/splyPopup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`; + const width = window.screen.availWidth; + const height = window.screen.availHeight; + const left = 0; + const top = 0; + const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`; + window.open(url, "candidatePopup", popupOptions); + }; + return ( <>
@@ -515,6 +574,8 @@ export default function ConCostForm({ onClick={(e) => { console.log(e); setLastSub(null) // 빌드오류때문에 넣은거 삭제필요 + + searchCandidate(e); //splyList => 해당객체가 대민에서 선택한 후보처 리스트 //setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트