Merge remote-tracking branch 'origin/iipbs/dm/dev'

This commit is contained in:
yschoi
2025-07-30 09:45:42 +09:00
24 changed files with 488 additions and 485 deletions
+5 -6
View File
@@ -1,7 +1,8 @@
NEXT_PUBLIC_SITE_ID = DM
NEXT_PUBLIC_PORT = 3000
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_PORT = 11035
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
@@ -9,9 +10,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
+7 -5
View File
@@ -3,17 +3,19 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_PORT = 11035
NEXT_PUBLIC_PORT = 3005
#NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000/
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_PROXY_HOST = http://localhost:3005/
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
#NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
+1 -1
View File
@@ -11,7 +11,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
@@ -74,7 +74,7 @@ export type FormData = z.infer<typeof sdmInfoFormSchema>;
function SdmInfoInsertContent() {
const searchParams = useSearchParams();
const {map, insertSdmInfoData, Sdm } = useDmStore((state) => state);
const {insertSdmInfoData, Sdm ,setSubsInfo} = useDmStore((state) => state);
const { successLogin } = Sdm;
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
const formSchema = sdmInfoFormSchema;
@@ -127,7 +127,9 @@ function SdmInfoInsertContent() {
if (local) {
setIsLogin(true);
}
}, []);
setSubsInfo([]); // subsInfo 초기화 후보지 처음 화면 들어올때 초기화(스토어내 객체만)
}, [setSubsInfo]);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
@@ -135,12 +137,27 @@ function SdmInfoInsertContent() {
mode: "onChange",
});
const subsInfo = useDmStore((state) => state.map.subsInfo);
useEffect(()=>{
if(map.subsInfo.length>0){
form.setValue('candidateTitle', map.subsInfo)
//map.subsInfo 스토어 객체 값이 변경되면 해당 값이 폼에 자동으로 들어가도록 수정 대신 값이 있어야함
if(subsInfo.length>0){
form.setValue('candidateTitle', subsInfo)
}
},[map.subsInfo, form])
},[subsInfo, form])
//후보지 팝업 값 셋팅
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'SET_SUBS_INFO') {
setSubsInfo(event.data.payload); // zustand로 값 설정
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [setSubsInfo]);
// 로그인 성공 시 모달 닫기 및 폼 업데이트
useEffect(() => {
@@ -154,6 +171,7 @@ function SdmInfoInsertContent() {
setFacilityName(userData.dmdCoNm);
form.setValue('sdmNm', userData.dmdCoNm);
}
} catch (error) {
console.error('Error parsing user data:', error);
}
@@ -208,9 +226,10 @@ const router = useRouter();
await insertSdmInfoData(apiData);
setTimeout(() => {
router.push("/");
}, 1000);
} catch (error) {
console.error('Error submitting form:', error);
}
@@ -47,6 +47,7 @@ export default function MyForm() {
dmdInfoSn: '',
dmdInfoFk: '',
status: '',
splyCndtList: []
};
formDataRef.current = convertedData;
insertSdmData(convertedData).catch(error => {
@@ -53,14 +53,16 @@ export default function WrAboutPage() {
}, observerOptions);
// 각 카드 요소를 관찰
cardRefs.current.forEach((card) => {
const cardsSnapshot = [...cardRefs.current];
cardsSnapshot.forEach((card) => {
if (card) {
observer.observe(card);
}
});
return () => {
cardRefs.current.forEach((card) => {
cardsSnapshot.forEach((card) => {
if (card) {
observer.unobserve(card);
}
@@ -1,7 +1,7 @@
'use client';
import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
interface RegionData {
name: string;
@@ -193,16 +193,16 @@ const WrStatusPage: React.FC = () => {
};
const tableData: TableData[] = [
{ region: '전국', sewage: '717(1,137,538)', rainwater: '3544(13,945)', reclaimed: '1018(222,476)', thermal: '123' },
{ region: '전국', sewage: '717(1,137,538)', rainwater: '3544(13,945)', reclaimed: '1018(222,476)', thermal: '123(492)' },
{ region: '서울특별시', sewage: '4(114,908)', rainwater: '816(430)', reclaimed: '371(341)', thermal: '0' },
{ region: '부산광역시', sewage: '13(146,197)', rainwater: '68(335)', reclaimed: '20(512)', thermal: '1' },
{ region: '대구광역시', sewage: '9(102,380)', rainwater: '294(133)', reclaimed: '14(44)', thermal: '2' },
{ region: '인천광역시', sewage: '17(57,691)', rainwater: '165(642)', reclaimed: '35(7,859)', thermal: '15' },
{ region: '광주광역시', sewage: '3(28,738)', rainwater: '74(23)', reclaimed: '15(450)', thermal: '15' },
{ region: '대전광역시', sewage: '2(11,966)', rainwater: '52(2)', reclaimed: '11(492)', thermal: '20' },
{ region: '울산광역시', sewage: '8(36,076)', rainwater: '111(39)', reclaimed: '19(28,904)', thermal: '30' },
{ region: '부산광역시', sewage: '13(146,197)', rainwater: '68(335)', reclaimed: '20(512)', thermal: '1(18)' },
{ region: '대구광역시', sewage: '9(102,380)', rainwater: '294(133)', reclaimed: '14(44)', thermal: '2(12)' },
{ region: '인천광역시', sewage: '17(57,691)', rainwater: '165(642)', reclaimed: '35(7,859)', thermal: '15(32)' },
{ region: '광주광역시', sewage: '3(28,738)', rainwater: '74(23)', reclaimed: '15(450)', thermal: '15(6)' },
{ region: '대전광역시', sewage: '2(11,966)', rainwater: '52(2)', reclaimed: '11(492)', thermal: '20(100)' },
{ region: '울산광역시', sewage: '8(36,076)', rainwater: '111(39)', reclaimed: '19(28,904)', thermal: '30(130)' },
{ region: '세종특별자치시', sewage: '9(2,555)', rainwater: '65(16)', reclaimed: '0(0)', thermal: '0' },
{ region: '경기도', sewage: '176(313,298)', rainwater: '1009(4,735)', reclaimed: '252(77,096)', thermal: '40' },
{ region: '경기도', sewage: '176(313,298)', rainwater: '1009(4,735)', reclaimed: '252(77,096)', thermal: '40(143)' },
{ region: '강원특별자치도', sewage: '69(19,677)', rainwater: '69(4)', reclaimed: '22(447)', thermal: '0' },
{ region: '충청북도', sewage: '38(15,607)', rainwater: '17(91)', reclaimed: '91(2,211)', thermal: '0' },
{ region: '충청남도', sewage: '67(41,368)', rainwater: '54(77)', reclaimed: '188(50,434)', thermal: '0' },
@@ -213,7 +213,7 @@ const WrStatusPage: React.FC = () => {
{ region: '제주특별자치도', sewage: '8(1,666)', rainwater: '47(5,746)', reclaimed: '64(660)', thermal: '0' },
];
const regionData: RegionData[] = [
const regionData: RegionData[] = useMemo(() => [
{ name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] },
@@ -231,7 +231,7 @@ const WrStatusPage: React.FC = () => {
{ name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] },
];
], []);
const handleMarkerClick = (regionId: string) => {
if (selectedRegion === regionId) {
@@ -242,7 +242,7 @@ const WrStatusPage: React.FC = () => {
};
const createDonutMarker = (svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
const createDonutMarker = useCallback((svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
const radius = 26;
const innerRadius = 18;
@@ -334,7 +334,7 @@ const WrStatusPage: React.FC = () => {
});
svg.appendChild(g);
};
}, [handleMarkerClick, regionIdMap]);
useEffect(() => {
const loadSVG = async () => {
@@ -374,7 +374,7 @@ const WrStatusPage: React.FC = () => {
};
loadSVG();
}, []);
}, [createDonutMarker, regionData]);
useEffect(() => {
const svg = svgContainerRef.current?.querySelector('svg');
@@ -493,10 +493,10 @@ const WrStatusPage: React.FC = () => {
<tr>
<th className="text-[12px] font-semibold px-4 py-2">번호</th>
<th className="text-[12px] font-semibold px-4 py-2">시도</th>
<th className="text-[12px] font-semibold px-4 py-2">하수(이용량,천m³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">빗물(이용량,천m³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">중수도(이용량,천m³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">온배수(이용량,천m³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">하수(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">빗물(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">중수도(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">온배수(이용량,천𝓂³/년)</th>
</tr>
</thead>
<tbody className="divide-y">
+184 -84
View File
@@ -1,42 +1,46 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState, useCallback } from "react";
import { useEffect, useRef, useState } from "react";
import { mapService } from "../service";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { SearchBar } from "@/components/ui/map/searchBar/searchBar";
import { useDmStore } from "@/app/store";
import SelectedFcltList from "./selectedFcltList";
import type { SubsInfo } from "@/app/store/map/type";
// export type SubsInfo={
// ruseUtztnFcltNm: string; // 재이용시설명
// ruseUtztnFcltCd: string; // 재이용시설 코드
// }
const Map = () => {
//수요처 저장하는 함수 setSubsInfo
//setSubsInfo(subsInfo:SubsInfo[]):void
const { map,setSubsInfo } = useDmStore(
(state) => state
);
//저장된 수요처 리스트
const { subsInfo } = 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 [checkedLayers, setCheckedLayers] = useState<string[]>([]);
const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]);
const [selectDataPk, setSelectDataPk] = useState<string>("");
const [subs, setSubs] = useState<object[]>([]);
//적용 버튼 클릭 이벤트
const [selectDataPk, setSelectDataPk] = useState<Record<string, any>>({});
const [selectedFclts, setSelectedFclts] = useState<any[]>([]);
//적용 버튼 클릭 이벤트
const onApply = () => {
//setSubs([{'41110PB001R1':'수원제1,2 재이용1'},{'41110PB001R2':'수원제1,2 재이용2'}]);
console.log(subs)
if (selectedFclts.length > 3) {
alert("재이용시설 후보지는 최대 3개까지만 선택할 수 있습니다.");
return;
}
const subsInfoList: SubsInfo[] = selectedFclts.map((fclt) => ({
ruseUtztnFcltNm: fclt.ruseUtztnFcltNm,
ruseUtztnFcltCd: fclt.pbnstRuseFcltId,
}));
window.opener.postMessage(
{
type: "SET_SUBS_INFO",
payload: subsInfoList, // 또는 필요한 데이터
},
"*"
);
window.close();
};
//트리 레이어 데이터 조회
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
@@ -46,76 +50,167 @@ const Map = () => {
}
};
// 팝업 닫기 핸들러를 useCallback으로 메모이제이션
const handlePopupClose = useCallback(() => {
setIsOpen(false);
setFeatureData(null);
}, []);
//하수처리시설코드로 재이용시설 목록 조회
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(() => {
fetchData();
}, []);
useEffect(() => {
console.log("Selected Data Pk: ", selectDataPk);
}, [selectDataPk]);
// 레이어 생성 후 초기화 처리
const onCheckedLayersChange = () => {
//수요처 ID
const urlParams = new URLSearchParams(window.location.search);
const dmdInfoId = urlParams.get("dmdInfoId");
// 체크된 레이어 정보를 받는 핸들러
const handleCheckedLayersChange = (
checkedIds: string[],
checkedNodesData: any[]
) => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
};
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: any) => {
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
//수요처 위치로 화면 이동
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;
if (
data &&
typeof data.getGeometry === "function" &&
typeof data.setStyle === "function"
) {
// 이미 Feature 인스턴스라면 그대로 사용
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
map
.getLayers()
.getArray()
.forEach((layer: any) => {
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;
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 }
);
}
// 3. 피처 선택
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();
map.getView().fit(extent, {
maps.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
@@ -123,21 +218,23 @@ const Map = () => {
nearest: true,
});
}
select.selectFeature(foundFeature, true);
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
console.warn("OpenLayers Feature를 찾을 수 없습니다:", selectDataPk);
}
}, [selectDataPk]);
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
// 재이용시설 클릭 시 선택 시설목록에 추가
const onFeatureSelect = (data: any) => {
// 처리시설코드
const swtrmtFcltCd = data.get("swtrmt_fclt_cd");
reuseData(swtrmtFcltCd);
};
return (
<>
<SearchBar
setHasuPk={(hasuPk: string) => {
if (hasuPk !== "") {
setHasuPk={(hasuPk: any) => {
if (hasuPk) {
setSelectDataPk(hasuPk);
}
}}
@@ -150,7 +247,7 @@ const Map = () => {
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={null}
detailView={undefined}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
@@ -158,11 +255,14 @@ const Map = () => {
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
onCheckedLayersChange={onCheckedLayersChange}
imgVisble={false}
setData={setSubs}
/>
</div>
<SelectedFcltList
data={selectedFclts}
onRemove={(updatedList) => setSelectedFclts(updatedList)}
/>
</>
);
};
@@ -1,147 +0,0 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState, useCallback } from "react";
import { mapService } from "../service";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { SearchBar } from "@/components/ui/map/searchBar/searchBar";
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 [checkedLayers, setCheckedLayers] = useState<string[]>([]);
const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]);
const [selectDataPk, setSelectDataPk] = useState<string>("");
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
setTreeData(response); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 팝업 닫기 핸들러를 useCallback으로 메모이제이션
const handlePopupClose = useCallback(() => {
setIsOpen(false);
setFeatureData(null);
}, []);
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
console.log("Selected Data Pk: ", selectDataPk);
}, [selectDataPk]);
// 체크된 레이어 정보를 받는 핸들러
const handleCheckedLayersChange = (
checkedIds: string[],
checkedNodesData: any[]
) => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
};
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: any) => {
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature: any = null;
if (
data &&
typeof data.getGeometry === "function" &&
typeof data.setStyle === "function"
) {
// 이미 Feature 인스턴스라면 그대로 사용
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
map
.getLayers()
.getArray()
.forEach((layer: any) => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
}
});
}
// 3. 피처 선택
if (foundFeature) {
const geometry = foundFeature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
select.selectFeature(foundFeature, true);
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
}
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
};
return (
<>
<SearchBar
setHasuPk={(hasuPk: string) => {
if (hasuPk !== "") {
setSelectDataPk(hasuPk);
}
}}
/>
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={null}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
imgVisble={false}
/>
</div>
</>
);
};
export default Map;
@@ -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>
);
}
@@ -1,94 +0,0 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수
const copyStyles = (sourceDoc: Document, targetDoc: Document) => {
Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
try {
if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS
const newStyleEl = sourceDoc.createElement('style');
Array.from(styleSheet.cssRules).forEach(cssRule => {
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
});
targetDoc.head.appendChild(newStyleEl);
} else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일
const newLinkEl = sourceDoc.createElement('link');
newLinkEl.rel = 'stylesheet';
newLinkEl.href = styleSheet.href;
targetDoc.head.appendChild(newLinkEl);
}
} catch (e) {
console.warn('스타일시트를 복사할 수 없습니다:', e);
}
});
};
interface NewWindowPortalProps {
children: React.ReactNode;
onClose: () => void;
title?: string;
width?: number;
height?: number;
resizable?: string;
scrollbars?: string;
}
const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "" , width = 900, height = 800, resizable = "yes", scrollbars= "yes" }) => {
const [container, setContainer] = useState<HTMLElement | null>(null);
const newWindowRef = useRef<Window | null>(null);
const onCloseRef = useRef(onClose);
// onClose 함수를 ref로 저장하여 의존성 문제 해결
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
const handleUnload = useCallback(() => {
onCloseRef.current();
}, []);
useEffect(() => {
// 이미 창이 열려있으면 새로 열지 않음
if (newWindowRef.current && !newWindowRef.current.closed) return;
// 1. 새 창을 열고 참조를 저장합니다.
const popupOptions = `width=${width},height=${height},resizable=${resizable},scrollbars=${scrollbars}`;
newWindowRef.current = window.open('', title, popupOptions);
if (newWindowRef.current) {
newWindowRef.current.document.title = title;
// 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다.
const div = newWindowRef.current.document.createElement('div');
newWindowRef.current.document.body.appendChild(div);
setContainer(div);
// 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요)
copyStyles(document, newWindowRef.current.document);
// 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다.
// const handleUnload = () => {
// onClose();
// };
newWindowRef.current.addEventListener('unload', handleUnload);
// 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다.
return () => {
if (newWindowRef.current) {
newWindowRef.current.removeEventListener('unload', handleUnload);
if (!newWindowRef.current.closed) {
newWindowRef.current.close();
}
}
};
} else {
alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.');
onCloseRef.current();
}
}, []);
// `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다.
return container ? createPortal(children, container) : null;
};
export default NewWindowPortal;
@@ -3,6 +3,7 @@
* 관망도 API
*/
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`
const REUSE_API_URL = `${process.env.NEXT_PUBLIC_REUSE_API_URL}`
export type LyrStngProps = {
lyr_sn: string
@@ -41,6 +42,20 @@ export const mapService = {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
},
getPbnstRuseFclts: async (ruseFcltPk:any) => {
const response = await fetch(`${REUSE_API_URL}/reuseFc/pubFc/${ruseFcltPk}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
+1 -2
View File
@@ -1,9 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import path from "path";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || "";
const SERVER_API_URL = process.env.NEXT_PUBLIC_DM_SERVER_API_URL || "";
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
-1
View File
@@ -16,7 +16,6 @@ const getRuseFclt = async (
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`;
console.log(" checkWrRegListRes ", checkWrRegListRes);
const ruseList = checkWrRegListRes.data.filter((item: string) => item !== "");
if (searchTxt) {
+4 -2
View File
@@ -13,8 +13,10 @@ export const StoreZus = (initState: DmState= {
...dmMapState,
}) => {
return createStore<DmStore>()((set) => {
tokenManager.setToken("site-id-key", 'DM');
tokenManager.setToken("site-theme-key", 'green');
if (typeof window !== "undefined") {
tokenManager.setToken("site-id-key", 'DM');
tokenManager.setToken("site-theme-key", 'green');
}
return {
@@ -1,53 +1,76 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
type SearchType = {
searchNm: string;
searchPk: string;
bscFctNm: string;
bscFctCd: string;
};
type thisProps = {
label: string | undefined;
initValue: string | undefined;
setValue: (searchNm?: string) => void;
setRtnData: (rtnData: any) => void;
searchList?: any[];
searchList?: SearchType[];
};
export default function SearchInputText({
label = undefined,
initValue = undefined,
setValue = () => {},
setRtnData = () => {},
searchList = undefined,
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);
if (value && value!=='') {
setIsOpen(true);
} else {
setIsOpen(false);
}
setHighlightIndex(-1);
setIsOpen(value !== "");
},
[setValue]
);
const handleSuggestionClick = useCallback(
(searchNm: string) => {
// Do not update textValue to keep input unchanged on suggestion click
setValue(searchNm);
setTextValue(searchNm);
setIsOpen(false);
setRtnData(searchList?.find((item) => item.searchNm === searchNm) || {});
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 (
@@ -68,31 +91,39 @@ export default function SearchInputText({
<div ref={wrapperRef} className="relative">
<input
type="text"
value={textValue ||''}
value={textValue || ""}
onChange={handleInputChange}
/* onFocus={() => setIsOpen(true)} */
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 && searchList.length > 0 ? (
searchList.map((item: SearchType, index: number) => (
{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 hover:bg-gray-100"
onClick={() => handleSuggestionClick(item.searchNm)}
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.searchNm);
handleSuggestionClick(item.bscFctNm);
}
}}
>
{item.searchPk?.substring(0, 10)}&nbsp;
<span className="float-right">{item.searchNm}</span>
<span className="float-left">{item.bscFctNm}</span>
</div>
))
) : (
@@ -3,7 +3,6 @@ import { useDmStore } from "@/app/store";
import {
SearchDiv,
SearchFormLeft,
SearchFormRight,
SelectBox,
type SelectDataType,
} from "@common/business_components/ui";
@@ -13,9 +12,9 @@ import SearchInputText from "../SearchInputText/SearchInputText";
export function SearchBar({
setHasuPk,
onApply
onApply,
}: {
setHasuPk: (hasuPk: string) => void;
setHasuPk: (hasuPk: any) => void;
onApply: () => void;
}) {
const { map, Common, initializeCommonData, findRuseFclt } = useDmStore(
@@ -26,6 +25,7 @@ export function SearchBar({
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([
{ text: "전체", val: "0" },
]);
useEffect(() => {
initializeCommonData();
}, [initializeCommonData]);
@@ -62,9 +62,9 @@ export function SearchBar({
[initializeSigunData]
);
useEffect(() => {
console.log("map.ruseFcltList ", map.ruseFcltList);
}, [map.ruseFcltList]);
// useEffect(() => {
// console.log("map.ruseFcltList ", map.ruseFcltList);
// }, [map.ruseFcltList]);
return (
<>
@@ -112,25 +112,19 @@ export function SearchBar({
}
}}
setRtnData={(rtnData: any) => {
setHasuPk(rtnData.bscFctCd || "");
setHasuPk(rtnData);
}}
searchList={map.ruseFcltList || []}
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="red"
className="mx-2 mt-2"
size={"sm"}
onClick={() => {}}
>
후보지선택
</Button>
<Button
color="sky"
className="mt-2"
size={"sm"}
onClick={() => {onApply();}}
onClick={() => {
onApply();
}}
>
적용
</Button>
@@ -14,7 +14,6 @@ interface AccessibleFormFieldProps<T extends FieldValues> {
export function AccessibleFormField<T extends FieldValues>({
label,
required = false,
error,
inputComponent,
}: AccessibleFormFieldProps<T>) {
const labelText = required ? `${label} *` : label;
@@ -23,11 +22,11 @@ export function AccessibleFormField<T extends FieldValues>({
<FormUi label={labelText}>
<div>
{inputComponent}
{error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
{/* {error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
<p key={idx} className="text-red-500 text-sm mt-1" role="alert">
{msg}
</p>
))}
))} */}
</div>
</FormUi>
);
@@ -15,11 +15,9 @@ import { Button } from "@common/components/ui";
import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { AccessibleFormField } from "./AccessibleFormField";
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
import type { ControllerRenderProps, UseFormReturn, FieldError } from "react-hook-form";
import SearchInputText from "../common/searhDemandInput/component";
import type { DemandCompType } from "@/app/store/sdmMng";
import NewWindowPortal from "@/app/(popup-layout)/map/popup/windowPotal";
import Map from "@/app/(popup-layout)/map/popup/sdmInfoMap";
export interface SdmInfoRegFormProps {
form: UseFormReturn<FormData>;
@@ -47,7 +45,6 @@ export function SdmInfoRegForm({
const [isSimilarCase, setIsSimilarCase] = useState(false);
const [key, setKey] = useState(0);
// 후보지 팝업 오픈 상태
const [isCandidatePopup, setIsCandidatePopup] = useState(false);
//모달 조회에 필요한 데이터
const [sply, setSply] = useState<number>(0);
@@ -58,18 +55,16 @@ export function SdmInfoRegForm({
initializeCommonData();
}, [initializeCommonData]);
useEffect(() =>{
console.log('candidateTitle ', form.getValues("candidateTitle"))
},[form.getValues("candidateTitle") ])
useEffect(() => {
console.log("candidateTitle ", form.getValues("candidateTitle"));
}, [form.getValues]);
useEffect(() => {
if (useTypesData && useTypesData.length > 0) {
setTimeout(() => {
form.setValue("usePurpose", useTypesData[0]?.val?.toString() || "");
}, 500);
}
}, [useTypesData, form]);
}, [useTypesData, form.setValue, form]);
useEffect(() => {
if (facilityName) {
@@ -77,7 +72,7 @@ export function SdmInfoRegForm({
form.setValue("sdmNm", facilityName);
setKey((prev) => prev + 1);
}
}, [facilityName, form]);
}, [facilityName, form.setValue, form]);
useEffect(() => {
if (searchParams.get("dmdCoNm")) {
@@ -85,19 +80,22 @@ export function SdmInfoRegForm({
setSdmNmValue(searchParams.get("dmdCoNm") || "");
setKey((prev) => prev + 1);
}
}, [searchParams, form]);
const handleCandidateClose = useCallback(
() => setIsCandidatePopup(false),
[]
);
}, [searchParams, form.setValue, form]);
const searchCandidate = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
//setIsCandidatePopup(true);
const popupOptions = `width=1920,height=1080,resizable='y',scrollbars='y'`;
window.open("/map/popup", "candidatePopup", popupOptions);
const dmdInfoId = JSON.parse(
sessionStorage.getItem("user") || "{}"
)?.dmdInfoId?.toString();
const url = `/map/popup?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);
};
const searchSimilarCase = (e: React.MouseEvent) => {
@@ -219,32 +217,24 @@ export function SdmInfoRegForm({
formIndex: 0,
required: true,
component: (field) => (
<AccessibleFormField
field={field}
label="희망단가(원/㎥)"
required={true}
error={form.formState.errors.money}
inputComponent={
<FormUi
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
<Input
placeholder="희망단가(원/㎥)을 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
onChange={(e) => {
field.onChange(e);
setMoney(Number(e.target.value));
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormUi>
}
/>
<FormUi
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
<Input
placeholder="희망단가(원/㎥)을 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
onChange={(e) => {
field.onChange(e);
setMoney(Number(e.target.value));
}}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
</FormUi>
),
},
{
@@ -484,16 +474,18 @@ export function SdmInfoRegForm({
</Button>
</div>
<div className="flex justify-center gap-10 mt-5">
{(form.getValues("candidateTitle") || []).map((candidate: any, index: number) => (
<Input
key={index}
placeholder={`후보지${index + 1}`}
type="text"
className="!text-xl w-[130px] text-gray-600"
value={candidate.ruseUtztnFcltNm}
readOnly
/>
))}
{(form.getValues("candidateTitle") || []).map(
(candidate: any, index: number) => (
<Input
key={index}
placeholder={`후보지${index + 1}`}
type="text"
className="!text-xl w-[130px] text-gray-600"
value={candidate.ruseUtztnFcltNm}
readOnly
/>
)
)}
</div>
</div>
),
@@ -529,7 +521,11 @@ export function SdmInfoRegForm({
field={field}
label={label}
required={required}
error={form.formState.errors[name]}
error={
typeof form.formState.errors[name]?.message === 'string'
? (form.formState.errors[name] as FieldError)
: undefined
}
inputComponent={component(field)}
/>
)
@@ -555,7 +551,11 @@ export function SdmInfoRegForm({
field={field}
label={label}
required={required}
error={form.formState.errors[name]}
error={
typeof form.formState.errors[name]?.message === 'string'
? (form.formState.errors[name] as FieldError)
: undefined
}
inputComponent={component?.(field)}
/>
)}
@@ -580,7 +580,11 @@ export function SdmInfoRegForm({
field={field}
label={label}
required={required}
error={form.formState.errors[name]}
error={
typeof form.formState.errors[name]?.message === 'string'
? (form.formState.errors[name] as FieldError)
: undefined
}
inputComponent={component?.(field)}
/>
)}
@@ -600,7 +604,6 @@ export function SdmInfoRegForm({
noFooterYn="Y"
/>
)}
</>
);
}
+2 -2
View File
@@ -3,9 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev -p 3005",
"build": "next build",
"start": "next start",
"start:dm": "next start -p 3005",
"lint": "next lint"
},
"dependencies": {
+1 -1
View File
@@ -1,7 +1,7 @@
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DM";
export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL ||'http://con.mind-one.co.kr:11035/api/v1';
export const SERVER_API_DM_URL = process.env.NEXT_PUBLIC_DM_SERVER_API_URL ||'http://con.mind-one.co.kr:11035/api/v1';
export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1';
export const SERVER_API_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL||' http://con.mind-one.co.kr:11080/api/v1';
export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000';
@@ -13,7 +13,6 @@ import GisToolBar from './GisToolBar';
import GisTree from './GisTree';
import './lib/css/gis.common.css';
import GisApp from './lib/gis.core';
// import FacilityInfoPopup from './FacilityInfoPopup';
export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null });
export const useGisContext = () => useContext(GisContext);
@@ -84,7 +83,6 @@ export const searchUpperPpls = async (fcltMngNo) => {
}
};
function Gis({
isOpen, //우측 상세 레이어 팝업
gisMaps,
@@ -101,7 +99,6 @@ function Gis({
onFeatureSelect,
setIsSearchModalOpen,
onCheckedLayersChange,
setData = () => {},
}) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
@@ -130,6 +127,7 @@ function Gis({
// Feature Click Event
const handleFeatureSelect = useCallback((data) => {
console.log(data);
// gisMap = mapInstanceRef.current;
// gisMap = mapInstanceRef.current;
@@ -223,9 +221,8 @@ function Gis({
// setIsOpen(true);
// setIsSearchModalOpen(false);
const layerName = layerNmRef.current
data.layerNm = layerName;
onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출
console.log(data)
}, [mapInstanceRef]);
// 지도 생성 및 초기화
@@ -291,6 +288,7 @@ function Gis({
gisCore.changeBaseLayer('base');
setMapObject({ map: map });
mapInstanceRef.current = map; // 생성 완료 플래그 설정
}, []);
// treeData 변경 시 레이어 코드 생성
@@ -456,7 +454,7 @@ function Gis({
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
onCheckedLayersChange?.(checkedIds, checkedNodesData);
}, 0);
}, 1000);
}, [onCheckedLayersChange]);
return (
@@ -31,7 +31,6 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
// 기본으로 체크된 노드들 찾기
const defaultChecked = getDefaultCheckedNodes(nodes);
setChecked(defaultChecked);
console.log('defaultChecked : ', defaultChecked);
// 기본으로 확장된 노드들 설정
const defaultExpanded = Array.from(new Set([
@@ -88,7 +88,7 @@ GisOlStyle.prototype = {
}
let baseResolution = 2;
let minScale = 0.4;
let minScale = 0.8;
let maxScale = 1.5;
let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution;
scale = Math.max(minScale, Math.min(maxScale, scale));
@@ -176,7 +176,7 @@ GisOlStyle.prototype = {
const properties = feature.getProperties();
const overLayId = feature?.get('overLayId');
const overLayMinZoom = layer.get('overLayMinZoom') || '12';
const overLayMinZoom = layer.get('overLayMinZoom') || '10';
const overLayOffset = layer?.get('overLayOffset');
let findoOverlay = this.overlays[overLayId];
if (findoOverlay) {
@@ -193,7 +193,8 @@ GisOlStyle.prototype = {
if (typeof overlayHtml === 'string') {
//const sanitizedHtml = DOMPurify.sanitize(overlayHtml);
overlayContainer.innerHTML = sanitizedHtml;
//overlayContainer.innerHTML = sanitizedHtml;
overlayContainer.innerHTML = overlayHtml;
}
// React 컴포넌트일 경우 createPortal() 사용
else if (React.isValidElement(overlayHtml)) {
@@ -205,7 +206,7 @@ GisOlStyle.prototype = {
element: overlayContainer,
positioning: 'bottom-center',
stopEvent: false,
offset: overLayOffset ? overLayOffset : [0, 0], // 오프셋 설정 (위치 조정)
offset: overLayOffset ? overLayOffset : [80, 0], // 오프셋 설정 (위치 조정)
});
this.overlays[overLayId] = overlay;