Merge remote-tracking branch 'origin/iipbs/dm/dev'
This commit is contained in:
@@ -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
@@ -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 # 박이정
|
||||
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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 || "";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)}
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user