공급처 선정 지도 팝업 추가

This commit is contained in:
yschoi
2025-08-10 02:05:04 +09:00
parent fef6ac864b
commit 4903684bce
11 changed files with 920 additions and 1 deletions
@@ -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<number>(-1);
const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
<div ref={wrapperRef} className="relative">
<input
type="text"
value={textValue || ""}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`${label}을(를) 입력하세요`}
onClick={() => {
setTextValue("");
setValue("");
setIsOpen(false);
}}
/>
{isOpen && (
<div className="absolute top-full left-0 z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
<div className="max-h-72">
{searchList.length > 0 ? (
searchList.map((item, index) => (
<div
key={index}
className={`w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl ${
highlightIndex === index
? "bg-blue-100"
: "hover:bg-gray-100"
}`}
onClick={() => handleSuggestionClick(item.bscFctNm)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleSuggestionClick(item.bscFctNm);
}
}}
>
<span className="float-left">{item.bscFctNm}</span>
</div>
))
) : (
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
검색 결과가 없습니다.
</div>
)}
</div>
</div>
)}
</div>
);
}
@@ -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<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [selectDataPk, setSelectDataPk] = useState<Record<string, any>>({});
const [selectedFclts, setSelectedFclts] = useState<any[]>([]);
//적용 버튼 클릭 이벤트
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 = `
<div class="rounded-md border border-gray-300 bg-white shadow-md text-sm w-[120px]">
<!-- 헤더 -->
<div class="bg-cyan-700 text-white px-3 py-2 text-[11px] font-semibold text-center rounded-t-md">
${feature.get("swtrmt_fclt_nm") || "-"}
</div>
<!-- 테이블 -->
<table class="w-full text-sm text-gray-800 border-t border-gray-200">
<tbody>
<tr class="border-b border-gray-200">
<th class="bg-gray-100 text-gray-600 font-semibold text-[10px] text-left">용량(m³/일)</th>
<td class="text-right text-[10px] font-bold">${formatNumber(feature.get("fclt_cpct"))}</td>
</tr>
</tbody>
</table>
</div>
`;
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 (
<>
<div className="bg-container">
<Gis
height='h-screen'
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={undefined}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={onCheckedLayersChange}
imgVisble={false}
searchBar={
<SearchBar
setHasuPk={(hasuPk: any) => {
if (hasuPk) setSelectDataPk(hasuPk);
}}
onApply={onApply}
/>
}
waterDepth={undefined}
/>
</div>
<SelectedFcltList
data={selectedFclts}
onRemove={(updatedList) => setSelectedFclts(updatedList)}
/>
</>
);
};
export default Map;
@@ -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<string>("");
const [sigunValue, setSigunValue] = useState<string>("");
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([
{ 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 (
<div className="flex justify-center w-full px-4 py-3">
<div className="flex items-center w-full max-w-screen-lg rounded-xl bg-white shadow-md px-6 py-4 space-x-3">
<SelectBox
labelClass="w-[40px]"
className="w-[200px]"
label="시도"
selectClass="rounded-lg bg-white"
selectArray={code.cityCd || []}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue}
/>
<SelectBox
labelClass="w-[65px]"
className="w-[200px]"
label="시군구"
selectClass="rounded-lg bg-white"
selectArray={filteredSigunData || []}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<Label className="text-gray-600">하수재이용시설</Label>
<SearchInputText
label="시설명"
initValue={undefined}
setValue={(searchNm?: string) => {
let sggOrCity = "";
if (searchNm) {
if (sigunValue === "0" || sigunValue === "") {
sggOrCity = sidoValue;
} else {
sggOrCity = sigunValue;
}
findRuseFclt(sggOrCity, searchNm);
}
}}
setRtnData={(rtnData: any) => {
setHasuPk(rtnData);
}}
searchList={map.ruseFcltList || []}
/>
<Button
color="sky"
className="ml-2 text-white"
size="sm"
onClick={() => {
onApply();
}}
>
적용
</Button>
</div>
</div>
);
}
@@ -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 = (
<>
<thead className="bg-gray-100 text-gray-700 text-[13px]">
<tr>
<th className="px-3 py-2 text-left border-b">시설명</th>
<th className="px-3 py-2 text-right border-b">
하수처리량
<br />
(㎥/일)
</th>
<th className="px-3 py-2 text-right border-b">
재이용량
<br />
(㎥/일)
</th>
<th className="px-3 py-2 border-b">삭제</th>
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={item.pbnstRuseFcltId} className="border-b last:border-none">
<td className="px-3 py-2 text-[12px]">{item.ruseUtztnFcltNm}</td>
<td className="px-3 py-2 text-right text-[12px]">
{Number(item.splyExtraCpct || 0).toLocaleString()}
</td>
<td className="px-3 py-2 text-right text-[12px]">
{Number(item.ruseCpct || 0).toLocaleString()}
</td>
<td className="px-3 py-2 text-center">
<button
onClick={() => handleDelete(item.pbnstRuseFcltId)}
className="text-red-500 hover:text-red-700 text-sm font-bold"
aria-label="삭제"
title="삭제"
>
✕
</button>
</td>
</tr>
))}
</tbody>
</>
);
if (data.length === 0) return null;
return (
<div className="fixed right-40 top-[8rem] w-[40rem] max-h-[130rem] overflow-auto bg-white shadow-lg rounded p-4 border border-gray-300 z-50">
<h3 className="bg-cyan-700 text-white text-xl font-bold mb-2 p-2">
선택된 시설
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm text-gray-800 border border-gray-200">
{TableContent}
</table>
</div>
</div>
);
}
@@ -0,0 +1 @@
export * from './map'
@@ -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();
}
}
+93
View File
@@ -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,
},
}));
},
};
};
+39
View File
@@ -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; // 재이용시설 코드
}
+10
View File
@@ -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<WrStore>()((set) => {
@@ -371,6 +380,7 @@ export const StoreZus = (
...ruseMapActionsExport(set),
...coSttnMapActionsExport(set),
...ldrmntMapActionsExport(set),
...DmMapReducer(set),
};
});
};
+2
View File
@@ -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;
@@ -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 (
<>
<hr className="mt-8" />
@@ -516,6 +575,8 @@ export default function ConCostForm({
console.log(e);
setLastSub(null) // 빌드오류때문에 넣은거 삭제필요
searchCandidate(e);
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
}}