Merge remote-tracking branch 'origin/main' into iipbs/sn/dev
This commit is contained in:
@@ -15,9 +15,13 @@ import {
|
||||
getWrLayerNm,
|
||||
} from "@/components/lib/func/mapUtil";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import createOverlayManager from "@common/business_components/ui/map/GisOverlay";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay";
|
||||
|
||||
const Map = () => {
|
||||
const { comMap } = useWrStore((state) => state);
|
||||
|
||||
const {code, comMap ,rainMapDetile ,rswtMapDetile,getRuseMaplListReturn,pbnMapDetaile,ldrmntMapDetile,htdfMapDetile,coSttnMapDetail} = useWrStore((state) => state);
|
||||
const { isFeatured } = comMap;
|
||||
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
@@ -79,11 +83,10 @@ const Map = () => {
|
||||
|
||||
// SearchModal, GisContent 등에서 호출
|
||||
const onFeatureFocus = (data: any) => {
|
||||
console.log('data',data)
|
||||
console.log("data", data);
|
||||
};
|
||||
|
||||
const onFeatureSelect = (data: any ,popUpClick?:boolean) => {
|
||||
|
||||
const onFeatureSelect = (data: any, popUpClick?: boolean) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) {
|
||||
console.warn("지도가 초기화되지 않았습니다.");
|
||||
@@ -100,21 +103,22 @@ const Map = () => {
|
||||
// 2. data가 OpenLayers Feature 인스턴스인지 확인
|
||||
let foundFeature: any = null;
|
||||
let featureId = data.fid;
|
||||
|
||||
if(popUpClick){
|
||||
|
||||
if (popUpClick) {
|
||||
const layerName = getWrLayerNm(data);
|
||||
const featurePk = getWrFeaturePk(layerName ?? "");
|
||||
// 재이용시설
|
||||
if (featurePk) {
|
||||
|
||||
const lastPk = layerName==='tb_wr_ruse_fclt_dmd_co'? data[featurePk].replaceAll("-", "") : data[featurePk]
|
||||
const lastPk =
|
||||
layerName === "tb_wr_ruse_fclt_dmd_co"
|
||||
? data[featurePk].replaceAll("-", "")
|
||||
: data[featurePk];
|
||||
featureId = `${layerName}.${lastPk}`;
|
||||
} else {
|
||||
console.warn("해당 피처는 선택된 레이어에서 찾을수 없습니다.");
|
||||
}
|
||||
}else{
|
||||
featureId=getClickFeatureId(data)
|
||||
|
||||
} else {
|
||||
featureId = getClickFeatureId(data);
|
||||
}
|
||||
map
|
||||
.getLayers()
|
||||
@@ -143,9 +147,9 @@ const Map = () => {
|
||||
}
|
||||
select.selectFeature(foundFeature, true);
|
||||
} else {
|
||||
// toast.error('위치정보를 찾을수 없습니다')
|
||||
// toast.error('위치정보를 찾을수 없습니다')
|
||||
console.warn("OpenLayers Feature를 찾을 수 없습니다:");
|
||||
console.log("foundFeature :", foundFeature );
|
||||
console.log("foundFeature :", foundFeature);
|
||||
console.warn("featureId : ", featureId);
|
||||
console.warn("data : ", data);
|
||||
console.warn(": 끝");
|
||||
@@ -157,14 +161,21 @@ const Map = () => {
|
||||
setIsOpen(true);
|
||||
setIsSearchModalOpen(false);
|
||||
|
||||
if (popUpClick && foundFeature&&!(
|
||||
Object.keys(foundFeature?.getProperties()).length === 0 &&
|
||||
foundFeature.constructor === Object
|
||||
)){
|
||||
const layerName = getWrLayerNm(data);
|
||||
const isColumn = getFeatureCd(foundFeature.getProperties(), String(layerName));
|
||||
if(layerName&&isColumn)
|
||||
featurePopUpOpen(layerName,isColumn,popupDetailRef)
|
||||
if (
|
||||
popUpClick &&
|
||||
foundFeature &&
|
||||
!(
|
||||
Object.keys(foundFeature?.getProperties()).length === 0 &&
|
||||
foundFeature.constructor === Object
|
||||
)
|
||||
) {
|
||||
const layerName = getWrLayerNm(data);
|
||||
const isColumn = getFeatureCd(
|
||||
foundFeature.getProperties(),
|
||||
String(layerName)
|
||||
);
|
||||
if (layerName && isColumn)
|
||||
featurePopUpOpen(layerName, isColumn, popupDetailRef);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,14 +189,12 @@ const Map = () => {
|
||||
const bc = new BroadcastChannel("mapDetailPopUp");
|
||||
bc.onmessage = (event) => {
|
||||
if (event.data.type === "SELECT_FEATURE") {
|
||||
onFeatureSelect(event.data.feature,true);
|
||||
onFeatureSelect(event.data.feature, true);
|
||||
closePopup();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const renderDetailView = useCallback(
|
||||
(props: any) => {
|
||||
const map = gisMaps.current;
|
||||
@@ -202,11 +211,10 @@ const Map = () => {
|
||||
isFeature.constructor === Object
|
||||
)
|
||||
) {
|
||||
const layerNm = getSelectedLayers(isFeature,gisMaps);
|
||||
const safeProps = isFeature.getProperties();
|
||||
const layerNm = getSelectedLayers(isFeature, gisMaps);
|
||||
const safeProps = isFeature.getProperties();
|
||||
const featureCd = getFeatureCd(safeProps, String(layerNm));
|
||||
if(layerNm)
|
||||
featurePopUpOpen(layerNm,featureCd,popupDetailRef)
|
||||
if (layerNm) featurePopUpOpen(layerNm, featureCd, popupDetailRef);
|
||||
}
|
||||
} else {
|
||||
console.log(" data확인 필요");
|
||||
@@ -239,6 +247,139 @@ const Map = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const OVERLAY_MIN_ZOOM = 16;
|
||||
const OVERLAY_COORD_PRECISION = 5;
|
||||
const OVERLAY_MAX_PER_VIEW = 800;
|
||||
|
||||
const overlayManagersRef = useRef<any[]>([]);
|
||||
const overlayActiveRef = useRef(false);
|
||||
const moveEndDebounceRef = useRef<number | null>(null);
|
||||
|
||||
// 반환: Promise<HTMLDivElement | null>
|
||||
const overlayFormatter = async (feature: any, props: any) => {
|
||||
const p = props || {};
|
||||
const featureId =
|
||||
feature.getId?.() ?? p?.id ?? p?.fid ?? p?.ftrIdn ?? '';
|
||||
if (!featureId) return null;
|
||||
|
||||
const tableId = String(featureId).split('.')[0]??'';
|
||||
const fcltCd = getFeatureCd(props, tableId);
|
||||
if (!fcltCd) return null;
|
||||
|
||||
let data: any = null;
|
||||
let title = '';
|
||||
|
||||
if (tableId === 'tb_il_swage_prcs_fclt') { data = await pbnMapDetaile(fcltCd); title = '하수처리시설'; }
|
||||
else if (tableId === 'tb_wr_bdst_ruse_fclt') { data = await getRuseMaplListReturn(fcltCd); title = '하수재이용시설'; }
|
||||
else if (tableId === 'tb_wr_rnwtr_utztn_fclt'){ data = await rainMapDetile(fcltCd); title = '빗물이용시설'; }
|
||||
else if (tableId === 'tb_wr_rswtr_fclt') { data = await rswtMapDetile(fcltCd); title = '중수도시설'; }
|
||||
else if (tableId === 'tb_wr_htdfl_fclt') { data = await htdfMapDetile(fcltCd); title = '온배수시설'; }
|
||||
else if (tableId === 'tb_wr_ruse_fclt_dmd_co'){ data = await coSttnMapDetail(fcltCd); title = '수요처'; }
|
||||
else if (tableId === 'tb_wr_tmpr_dmd_co') { data = await ldrmntMapDetile(fcltCd); title = '잠재수요처'; }
|
||||
|
||||
const empty =
|
||||
data == null ||
|
||||
(Array.isArray(data) && data.length === 0) ||
|
||||
(typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0);
|
||||
|
||||
if (empty) return null;
|
||||
|
||||
const div = document.createElement('div');
|
||||
const root = createRoot(div);
|
||||
(div as any).__dispose = () => { try { root.unmount(); } catch {} };
|
||||
root.render(<WrOverLay title={title} data={data} code={code} />);
|
||||
return div; // ← 데이터 있을 때만 HTMLElement 반환
|
||||
};
|
||||
const destroyAllManagers = () => {
|
||||
overlayManagersRef.current.forEach(m => { try { m.destroy(); } catch {} });
|
||||
overlayManagersRef.current = [];
|
||||
overlayActiveRef.current = false;
|
||||
};
|
||||
|
||||
const mountOverlayManagers = () => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
// 이전 것 정리
|
||||
destroyAllManagers();
|
||||
|
||||
// 체크된 벡터 레이어만 골라 매니저 생성
|
||||
const checkedSet = new Set(checkedLayers.map(String));
|
||||
const layers: any[] = map.getLayers().getArray();
|
||||
const targets = layers.filter((layer: any) => {
|
||||
const src = layer.getSource?.();
|
||||
const isVector = src && typeof src.getFeatures === "function";
|
||||
const id = String(layer.get?.("lyrSn") ?? layer.get?.("id") ?? "");
|
||||
return isVector && layer.getVisible?.() && checkedSet.has(id);
|
||||
});
|
||||
|
||||
targets.forEach((layer: any) => {
|
||||
const mgr = createOverlayManager(map, layer, overlayFormatter, {
|
||||
coordPrecision: OVERLAY_COORD_PRECISION,
|
||||
maxOverlaysPerViewport: OVERLAY_MAX_PER_VIEW,
|
||||
});
|
||||
mgr.mount(); // 리스너 1회 등록
|
||||
mgr.enable(); // 표시 + 1회 sync
|
||||
overlayManagersRef.current.push(mgr);
|
||||
});
|
||||
|
||||
overlayActiveRef.current = overlayManagersRef.current.length > 0;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
const z = map.getView()?.getZoom?.() ?? 0;
|
||||
if (z < OVERLAY_MIN_ZOOM) {
|
||||
// 저줌에선 그냥 안 보이게만
|
||||
overlayManagersRef.current.forEach(m => { try { m.disable({ clear:true, delayed:true }); } catch {} });
|
||||
return;
|
||||
}
|
||||
mountOverlayManagers();
|
||||
|
||||
return () => { destroyAllManagers(); };
|
||||
}, [checkedLayers]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
const onMoveStart = () => {
|
||||
// 숨기기만 (리스너/DOM 대량 제거 X)
|
||||
overlayManagersRef.current.forEach(m => { try { m.disable({ clear:false }); } catch {} });
|
||||
if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; }
|
||||
};
|
||||
|
||||
const onMoveEnd = () => {
|
||||
const z = map.getView()?.getZoom?.() ?? 0;
|
||||
const active = overlayActiveRef.current;
|
||||
|
||||
if (moveEndDebounceRef.current) clearTimeout(moveEndDebounceRef.current);
|
||||
moveEndDebounceRef.current = window.setTimeout(() => {
|
||||
if (z < OVERLAY_MIN_ZOOM) {
|
||||
if (active) overlayManagersRef.current.forEach(m => { try { m.disable({ clear:true, delayed:true }); } catch {} });
|
||||
return;
|
||||
}
|
||||
if (!active) {
|
||||
mountOverlayManagers();
|
||||
} else {
|
||||
// 다시 보이고 1회만 동기화
|
||||
overlayManagersRef.current.forEach(m => { try { m.enable(); m.syncOverlays(); } catch {} });
|
||||
}
|
||||
}, 180) as unknown as number;
|
||||
};
|
||||
|
||||
map.on("movestart", onMoveStart);
|
||||
map.on("moveend", onMoveEnd);
|
||||
return () => {
|
||||
map.un("movestart", onMoveStart);
|
||||
map.un("moveend", onMoveEnd);
|
||||
if (moveEndDebounceRef.current) { clearTimeout(moveEndDebounceRef.current); moveEndDebounceRef.current = null; }
|
||||
};
|
||||
}, [checkedLayers]);
|
||||
return (
|
||||
<div className="bg-container">
|
||||
<Gis
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function MapFeaturePopup() {
|
||||
if (layerNm === "tb_wr_bdst_ruse_fclt") {
|
||||
setIsDetail(isFeatureCd);
|
||||
} else if (layerNm === "tb_il_swage_prcs_fclt") {
|
||||
pbnMapDetaile(isFeatureCd, isFeatureCd).then((res) => {
|
||||
pbnMapDetaile(isFeatureCd).then((res) => {
|
||||
setIsDetail(res);
|
||||
});
|
||||
} else if (layerNm === "tb_wr_rnwtr_utztn_fclt") {
|
||||
|
||||
@@ -211,6 +211,7 @@ export const rswtPrscCdMthdCd = [
|
||||
},
|
||||
{ val: "소독(염소, UV 등)", text: "소독(염소, UV 등)" },
|
||||
{ val: "기타", text: "기타" },
|
||||
{ val: "확인불가", text: "확인불가" },
|
||||
];
|
||||
|
||||
export const htdPowerMthdCd= [
|
||||
|
||||
@@ -77,7 +77,6 @@ type pbnMapActions = {
|
||||
) => void;
|
||||
pbnMapDetaile: (
|
||||
featureCd: string,
|
||||
data: string
|
||||
) => Promise<pubHasuType | undefined>;
|
||||
};
|
||||
|
||||
@@ -117,11 +116,11 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
pbnMapDetaile: async (featureCd: string, data: string) => {
|
||||
pbnMapDetaile: async ( featureCd: string) => {
|
||||
try {
|
||||
const hasuRes = await postFectch(
|
||||
`/prcsFclt/ruseInfo/1/20/A/0/0/1/SW/${featureCd}`,
|
||||
[data],
|
||||
`/prcsFclt/ruseInfo/1/1/A/0/0/1/SW/`,
|
||||
[featureCd],
|
||||
fetchType.COMMON
|
||||
);
|
||||
const filteredData = hasuRes.data.map((item: any) => {
|
||||
@@ -131,7 +130,7 @@ export const pbnMapActionsExport: (set: any) => pbnMapActions = (set: any) => {
|
||||
ruse: item.ruse === false ? "N" : "Y",
|
||||
};
|
||||
});
|
||||
return filteredData;
|
||||
return filteredData[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error; // 호출부에서 catch 가능하도록
|
||||
|
||||
@@ -114,8 +114,6 @@ export const rainMapActionsExport: (set: any) => rainMapActions = (
|
||||
},
|
||||
rainMapDetile: async (featureCd: string) => {
|
||||
try {
|
||||
console.log('featureCd ', featureCd)
|
||||
|
||||
const rainDetail = await getFetch(fetchType.WR,
|
||||
`/reuseFc/rainyInfoList/1/3/0/${featureCd}` )
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ type ruseMapActions = {
|
||||
fclts: string[],
|
||||
fcltNm: string
|
||||
) => void;
|
||||
ruseMapDetail: (ruseFcltId: string) => Promise<customPbnst | undefined>;
|
||||
getRuseMaplList: (bscFctCd: string) => void;
|
||||
getRuseMaplListReturn: (bscFctCd: string) => Promise<customPbnst | undefined>;
|
||||
};
|
||||
|
||||
export type ruseMapType = ruseMapState & ruseMapActions;
|
||||
@@ -106,21 +106,7 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = (
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
ruseMapDetail: async (ruseFcltId: string) => {
|
||||
try {
|
||||
let ruseDetail = await getFetch("wr", `/reuseFc/pubFc/${ruseFcltId}`);
|
||||
if (ruseDetail.length >= 0) {
|
||||
ruseDetail = ruseDetail.find(
|
||||
(item: customPbnst) => item.pbnstRuseFcltId === ruseFcltId
|
||||
);
|
||||
}
|
||||
// 🔑 스토어 업데이트 대신 리턴
|
||||
return ruseDetail;
|
||||
} catch (errorHasu) {
|
||||
console.log(errorHasu);
|
||||
throw errorHasu; // 호출부에서 catch 가능하도록
|
||||
}
|
||||
},
|
||||
|
||||
getRuseMaplList: async (bscFctCd: string) => {
|
||||
try {
|
||||
const ruseDetailList = await getFetch(
|
||||
@@ -204,5 +190,66 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = (
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
|
||||
getRuseMaplListReturn: async (bscFctCd: string) => {
|
||||
try {
|
||||
const ruseDetailList = await getFetch(
|
||||
fetchType.WR,
|
||||
`/reuseFc/pubFc/${bscFctCd}`
|
||||
);
|
||||
const year = new Date().getFullYear();
|
||||
const data = [bscFctCd];
|
||||
const rusePrfList = await postFectch(
|
||||
`/reuseFc/pubDetailList/${year}/0/1/10`,
|
||||
data,
|
||||
fetchType.WR
|
||||
);
|
||||
const dataList = ruseDetailList.map((ruse) => {
|
||||
const matchRuse = rusePrfList?.data.find(
|
||||
(prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId
|
||||
);
|
||||
|
||||
|
||||
const usage = matchRuse.prfList.reduce(
|
||||
(acc, cur) => {
|
||||
acc.inUsage +=
|
||||
cur.wshwtr +
|
||||
cur.coolUswtr +
|
||||
cur.cleanUswtr +
|
||||
cur.drwtUswtr +
|
||||
cur.dltnUswtr +
|
||||
cur.ithlRswtr +
|
||||
cur.ithlEtcUswtr +
|
||||
cur.cleanToilUswtr +
|
||||
cur.resn;
|
||||
acc.outUsage +=
|
||||
cur.lndwtr +
|
||||
cur.hydUswtr +
|
||||
cur.ugwtChr +
|
||||
cur.farmUswtr +
|
||||
cur.rvrKeepUswtr +
|
||||
cur.indstUswtr;
|
||||
acc.total = acc.inUsage + acc.outUsage;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ total: 0, inUsage: 0, outUsage: 0 }
|
||||
);
|
||||
|
||||
|
||||
return {
|
||||
...ruse,
|
||||
prfList: matchRuse.prfList,
|
||||
inUsage: usage?.inUsage,
|
||||
outUsage: usage?.outUsage,
|
||||
|
||||
};
|
||||
});
|
||||
return dataList
|
||||
} catch (errorHasu) {
|
||||
console.log(errorHasu);
|
||||
throw errorHasu; // 호출부에서 catch 가능하도록
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,8 +18,8 @@ const rswtrObjSchema = z.object({
|
||||
filtrTrtmfctProcs: z.string({ required_error: validationMessages.required("주요처리공정") }).min(1, { message: validationMessages.required("주요처리공정") }),
|
||||
filtrTrtmfctUsgCd: z.string().default("").nullish(),
|
||||
rswtrUsqty: z.number().default(0),
|
||||
altUswtrUtztnYn: z.enum(["Y", "N"]).nullish(), //안쓰는값
|
||||
altUswtrKndCd: z.string().default("").nullish(), //안쓰는값
|
||||
//altUswtrUtztnYn: z.enum(["Y", "N"]).nullish(), //안쓰는값
|
||||
// altUswtrKndCd: z.string().default("").nullish(), //안쓰는값
|
||||
altUswtrUtztnQnt: z.number().default(0), //안쓰는값
|
||||
altUswtrIdntyMthdCd: z.string().default("").nullish(), //안쓰는값
|
||||
rgtr: z.string().nullish().default(null),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { formatCommaNumber, formatPhoneNumber } from "./commom";
|
||||
|
||||
const wrLayerList = [
|
||||
{ text: "tb_il_swage_prcs_fclt", val: "bscFctCd" },
|
||||
{ text: "tb_wr_bdst_ruse_fclt", val: "pbnstRuseFcltId" },
|
||||
@@ -26,8 +28,6 @@ const wrFeaturePK = [
|
||||
// { text: "잠재수요처", val: "tb_wr_tmpr_dmd_co" },
|
||||
|
||||
export const getFeatureCd = (featurePropertise: any, layerNm: string) => {
|
||||
|
||||
console.log('featurePropertise ', featurePropertise)
|
||||
const featureCd =
|
||||
layerNm === "tb_wr_bdst_ruse_fclt"
|
||||
? featurePropertise?.swtrmt_fclt_cd
|
||||
@@ -39,32 +39,30 @@ export const getFeatureCd = (featurePropertise: any, layerNm: string) => {
|
||||
].includes(layerNm)
|
||||
? featurePropertise?.bdst_nm
|
||||
: layerNm === "tb_il_swage_prcs_fclt"
|
||||
? featurePropertise?.swtrmt_fclt_nm
|
||||
: layerNm === "tb_wr_ruse_fclt_dmd_co"?
|
||||
featurePropertise?.bdst_info_sn
|
||||
: null;
|
||||
? featurePropertise?.swtrmt_fclt_cd
|
||||
: layerNm === "tb_wr_ruse_fclt_dmd_co"
|
||||
? featurePropertise?.bdst_info_sn
|
||||
: null;
|
||||
|
||||
return featureCd;
|
||||
};
|
||||
|
||||
export const getWrLayerNm = (featurePropertise: any) => {
|
||||
const result = wrLayerList
|
||||
.filter(item => item.val in featurePropertise) // 키 존재 여부 체크
|
||||
.map(item => item.text);
|
||||
|
||||
.filter((item) => item.val in featurePropertise) // 키 존재 여부 체크
|
||||
.map((item) => item.text);
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
|
||||
export const getWrFeaturePk = (layerName :string) => {
|
||||
export const getWrFeaturePk = (layerName: string) => {
|
||||
const result = wrFeaturePK
|
||||
.filter(item => item.val === layerName) // 키 존재 여부 체크
|
||||
.map(item => item.text);
|
||||
.filter((item) => item.val === layerName) // 키 존재 여부 체크
|
||||
.map((item) => item.text);
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const getSelectedLayers = (isFeature: any, gisMaps:any) => {
|
||||
export const getSelectedLayers = (isFeature: any, gisMaps: any) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) {
|
||||
console.warn("지도가 초기화되지 않았습니다.");
|
||||
@@ -97,11 +95,11 @@ export const getSelectedLayers = (isFeature: any, gisMaps:any) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getClickFeatureId = (data: any) => {
|
||||
let featureId =''
|
||||
export const getClickFeatureId = (data: any) => {
|
||||
let featureId = "";
|
||||
const layerName = data.layerNm;
|
||||
if (layerName === "하수재이용시설") {
|
||||
// 재이용시설
|
||||
// 재이용시설
|
||||
featureId = `${layerName}.${data.searchPk}`;
|
||||
} else if (
|
||||
//빗물이용시설, 중수도시설
|
||||
@@ -124,7 +122,11 @@ export const getClickFeatureId = (data: any) => {
|
||||
return featureId;
|
||||
};
|
||||
|
||||
export function featurePopUpOpen(layerNm:string,featureCd:string,popupDetailRef){
|
||||
export function featurePopUpOpen(
|
||||
layerNm: string,
|
||||
featureCd: string,
|
||||
popupDetailRef
|
||||
) {
|
||||
const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
|
||||
const width =
|
||||
String(layerNm) === "tb_wr_bdst_ruse_fclt"
|
||||
@@ -134,8 +136,8 @@ export function featurePopUpOpen(layerNm:string,featureCd:string,popupDetailRef)
|
||||
String(layerNm) === "tb_wr_bdst_ruse_fclt"
|
||||
? Math.floor(window.innerHeight - 100)
|
||||
: String(layerNm) === "tb_wr_ruse_fclt_dmd_co"
|
||||
?Math.floor(window.innerHeight - 430)
|
||||
: Math.floor(window.innerHeight - 300); // 150px 여백 확보; // calc(100% - 70px)
|
||||
? Math.floor(window.innerHeight - 430)
|
||||
: Math.floor(window.innerHeight - 300); // 150px 여백 확보; // calc(100% - 70px)
|
||||
const left = (window.screen.availWidth - width) / 2;
|
||||
const top = (window.screen.availHeight - height) / 2;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes `;
|
||||
@@ -145,4 +147,76 @@ export function featurePopUpOpen(layerNm:string,featureCd:string,popupDetailRef)
|
||||
"지도 시설 상세정보 팝업",
|
||||
popupOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const getOverLayObj = (fclType: string, data: any,code?:any) => {
|
||||
let newObj = {};
|
||||
let title = "";
|
||||
|
||||
|
||||
|
||||
|
||||
if (fclType === "하수재이용시설") {
|
||||
newObj = {
|
||||
"시설용량(톤/일)": formatCommaNumber(data?.fcltCpct) ??'확인불가',
|
||||
"재이용률(%)": (data?.fcltCpct / data?.bscFctCpc)||0,
|
||||
"장내용수(m³/년)": formatCommaNumber(data?.inUsage) ?? '확인불가',
|
||||
"장외용수(m3/년)":formatCommaNumber(data?.outUsage) ?? '확인불가',
|
||||
// 수요처: data?.reuseBuilding[0]?.rainyObj.pntfColArea ?? '확인불가',
|
||||
};
|
||||
title = data?.bdstNm ?? '확인불가';
|
||||
} else if (fclType === "하수처리시설") {
|
||||
newObj = {
|
||||
"시설용량(톤/일)": formatCommaNumber(data?.fctCpc) ?? '확인불가',
|
||||
지역구분: data?.efflWqltStndrdNm ?? '확인불가',
|
||||
재이용여부: data?.rswtrYn ?? '확인불가',
|
||||
"재이용시설 등록여부": data?.ruse ?? '확인불가',
|
||||
};
|
||||
title = data?.bscFctNm ?? '확인불가';
|
||||
} else if (fclType === "빗물이용시설" ) {
|
||||
newObj = {
|
||||
건물용도: code?.bdstUsgCd.find((item) => item.val=== String(data?.reuseBuilding[0].bdstUsgCd) )?.text ?? '확인불가',
|
||||
설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? '확인불가',
|
||||
처리시설유무: data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn ?? '확인불가',
|
||||
"처리시설용량(m³/일)": formatCommaNumber(data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct) ?? '확인불가',
|
||||
"저류조용량(m³)": formatCommaNumber(data?.reuseBuilding[0]?.rainyObj.pntfColArea) ?? '확인불가',
|
||||
};
|
||||
title = data?.bdstNm ?? '확인불가';
|
||||
} else if (fclType === "중수도시설") {
|
||||
newObj = {
|
||||
건물용도: code?.bdstUsgCd.find((item) => item.val===String(data?.reuseBuilding[0].bdstUsgCd) )?.text ?? '확인불가',
|
||||
설치완료일: data?.reuseBuilding[0]?.instlCmptnDt ?? '확인불가',
|
||||
"주요 처리공정": code?.rswtPrscCdMthdCd.find((item) => item.val===data?.reuseBuilding[0]?.rswtrObj.filtrTrtmfctProcs)?.text ?? '확인불가',
|
||||
원수종류: code?.orgwtKndCd.find((item) => item.val === data?.reuseBuilding[0]?.rswtrObj.orgwtKndCd)?.text ?? '확인불가',
|
||||
//이용용도: isCode.filtrTrtmfctUsgCd data?.reuseBuilding[0]?.rswtrObj.altUswtrKndCd ?? '확인불가',
|
||||
};
|
||||
title = data?.bdstNm ?? '확인불가';
|
||||
} else if (fclType === "온배수시설") {
|
||||
newObj = {
|
||||
미정: '확인불가',
|
||||
// '설치완료일': data?.reuseBuilding[0]?.instlCmptnDt??'',
|
||||
// '처리시설유무': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn??'',
|
||||
// '처리시설용량(m³/일)': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct??'',
|
||||
// '저류조용량(m³)': data?.reuseBuilding[0]?.rainyObj.pntfColArea??'',
|
||||
};
|
||||
title = data?.bdstNm ?? '확인불가';
|
||||
} else if (fclType === "수요처") {
|
||||
newObj = {
|
||||
'사업자등록번호': data?.dmdInfoId??'',
|
||||
'주소': data?.daddr??'',
|
||||
'전화번호': formatPhoneNumber(data?.telno)??'',
|
||||
};
|
||||
title = data?.dmdCoNm ?? '확인불가';
|
||||
|
||||
} else if (fclType === "잠재수요처") {
|
||||
newObj = {
|
||||
공장구분: data?.fctrySe ?? '확인불가',
|
||||
설립구분: data?.fndnSe ?? '확인불가',
|
||||
업종명: data?.tpbizNm ?? '확인불가',
|
||||
생산폼목: data?.prdctnItem ?? '확인불가',
|
||||
};
|
||||
title = data?.dmdCoNm ?? '확인불가';
|
||||
}
|
||||
|
||||
return { title, newObj };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import WrOverLayTable from "../WrOverLayTable/WrOverLayTable";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@common/components/ui";
|
||||
import { getOverLayObj } from "@/components/lib/func/mapUtil";
|
||||
|
||||
|
||||
type RuseTabData = {
|
||||
data: Record<string, string>;
|
||||
tabData:{
|
||||
pbnstRuseFcltId: string;
|
||||
ruseUtztnFcltNm: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default function WrOverLayRuse({ overLayData }: { overLayData: any }) {
|
||||
const [fcltTab, setFcltTab] = useState<string>("");
|
||||
const [ruseData, setRuseData] = useState<RuseTabData[]>([]as RuseTabData[]);
|
||||
// useEffect(() => {
|
||||
// const detail = overLayData.find((list) => list.pbnstRuseFcltId === fcltTab);
|
||||
// if (detail) setIsDetail(detail);
|
||||
// }, [fcltTab, overLayData]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (overLayData && overLayData[0]) {
|
||||
setFcltTab(overLayData[0].pbnstRuseFcltId);
|
||||
|
||||
const newRuseData= overLayData.map((ruse) =>{
|
||||
const isData = getOverLayObj( '하수재이용시설',ruse);
|
||||
return {
|
||||
data: isData.newObj,
|
||||
tabData:{
|
||||
pbnstRuseFcltId: ruse.pbnstRuseFcltId,
|
||||
ruseUtztnFcltNm: ruse.ruseUtztnFcltNm
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setRuseData(newRuseData);
|
||||
}
|
||||
}, [overLayData]);
|
||||
|
||||
if (!(overLayData && overLayData[0])) return;
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="bg-sky-600 text-white text-center font-semibold py-2 text-2xl">
|
||||
</div>
|
||||
<Tabs value={fcltTab} onValueChange={setFcltTab}>
|
||||
<TabsList className="mt-2 mb-2 gap-6 flex flex-wrap">
|
||||
{ruseData?.map((item, index) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
|
||||
key={item.tabData.pbnstRuseFcltId + index}
|
||||
className="text-center !text-base size-3/12"
|
||||
value={item.tabData.pbnstRuseFcltId}
|
||||
>
|
||||
{item.tabData.ruseUtztnFcltNm}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
|
||||
{ruseData?.map((item) => (
|
||||
<TabsContent
|
||||
key={item.tabData.pbnstRuseFcltId}
|
||||
className="flex-1 px-4 py-3"
|
||||
value={item.tabData.pbnstRuseFcltId}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<WrOverLayTable overLayData={item.data} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Table, TableBody, TableCell, TableRow } from "@common/components/ui";
|
||||
|
||||
export default function WrOverLayTable({overLayData}:{overLayData:any,}){
|
||||
|
||||
|
||||
|
||||
return(
|
||||
|
||||
<>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{Object.entries(overLayData).map(([key, value]) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell className=" w-1/3 bg-slate-100 text-slate-800 font-medium text-sm align-middle border border-slate-300">
|
||||
{" "}
|
||||
{key}{" "}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
className={
|
||||
"w-1/3 bg-white text-right pr-3 text-sm align-middle border border-slate-300"
|
||||
}
|
||||
>
|
||||
{" "}
|
||||
{String(value)}{" "}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
import { getOverLayObj } from "@/components/lib/func/mapUtil";
|
||||
import { Card } from "@common/business_components/ui/chart/lib/card";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import WrOverLayTable from "./WrOverLayTable/WrOverLayTable";
|
||||
import WrOverLayRuse from "./WrOverLayRuse/WrOverLayRuse";
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
export default function WrOverLay({
|
||||
title,
|
||||
data,
|
||||
code,
|
||||
}: {
|
||||
title: string;
|
||||
data: any;
|
||||
code: any;
|
||||
}) {
|
||||
const [newOverLayData, setNewOverLayData] = useState<Record<string, string>>(
|
||||
{}
|
||||
);
|
||||
const [typeTitle, setTypeTitle] = useState<string>("");
|
||||
const [pos, setPos] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
const bubbleLabel = title?.[0] ?? "•";
|
||||
|
||||
const dragRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (title !== "하수재이용시설" && data) {
|
||||
const overRayData = getOverLayObj(title, data, code);
|
||||
setTypeTitle(overRayData?.title ?? "");
|
||||
setNewOverLayData(overRayData.newObj ?? {});
|
||||
}
|
||||
}, [title, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{open ? (
|
||||
<Draggable
|
||||
position={pos}
|
||||
nodeRef={dragRef}
|
||||
onDrag={(e, data) => setPos({ x: data.x, y: data.y })}
|
||||
>
|
||||
<div ref={dragRef}>
|
||||
<Card className="w-[320px] p-0 shadow-md rounded-xl overflow-hidden border border-slate-300 relative">
|
||||
{/* 헤더: 제목 바 + 우측 정렬 버튼 (절대배치 제거, 타이틀 중앙 정렬 유지) */}
|
||||
<div className="bg-sky-600 text-white font-semibold py-2 text-xl">
|
||||
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-2 px-2">
|
||||
{/* 좌측 스페이서: 우측 버튼과 동일한 너비를 차지하도록 동일 버튼을 투명하게 복제 */}
|
||||
<div className="opacity-0 pointer-events-none">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setPos({ x: 0, y: 0 })}
|
||||
className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition"
|
||||
title="원위치"
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="축소"
|
||||
title="축소"
|
||||
className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition"
|
||||
>
|
||||
–
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 중앙 타이틀 */}
|
||||
<div className="text-2xl text-center truncate">
|
||||
{typeTitle + "(" + title + ")"}
|
||||
</div>
|
||||
|
||||
{/* 우측 실제 버튼 영역 */}
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
onClick={() => setPos({ x: 0, y: 0 })}
|
||||
className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition"
|
||||
title="원위치"
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="축소"
|
||||
title="축소"
|
||||
className="p-2 bg-gray-200 text-gray-700 shadow hover:bg-gray-300 hover:shadow-md transition"
|
||||
>
|
||||
–
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 본문 */}
|
||||
{title === "하수재이용시설" ? (
|
||||
<WrOverLayRuse overLayData={data ?? []} />
|
||||
) : (
|
||||
<WrOverLayTable overLayData={newOverLayData} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</Draggable>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="확장"
|
||||
title={title || "정보 보기"}
|
||||
className="w-[30px] h-[30px] bg-white font-bold shadow flex items-center justify-center border border-slate-300"
|
||||
>
|
||||
{bubbleLabel}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
"next": "15.1.7",
|
||||
"ol": "^7.3.0",
|
||||
"ol-ext": "^4.0.11",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-i18next": "^15.6.0",
|
||||
|
||||
@@ -23,4 +23,5 @@ export * from "./title";
|
||||
export * from "./sdmInsertForm";
|
||||
export * from "./popover_ui";
|
||||
export * from "./date_month_picker";
|
||||
export * from "./date_picker_range"
|
||||
export * from "./date_picker_range"
|
||||
export * from "./map/GisOverlay";
|
||||
@@ -0,0 +1,242 @@
|
||||
// Overlay manager (no React here)
|
||||
import Overlay from 'ol/Overlay';
|
||||
import { getFeatureLabelCoordinate } from '../map/lib/util';
|
||||
import { buildCoordinateKeyFactory } from '../map/lib/util';
|
||||
|
||||
// ---- main factory ----
|
||||
export default function createOverlayManager(map, vectorLayer, overlayFormatter, options = {}) {
|
||||
const coordPrecision = Math.max(0, Math.min(12, Number.isFinite(options.coordPrecision) ? options.coordPrecision : 6));
|
||||
const maxOverlaysPerViewport = Number.isFinite(options.maxOverlaysPerViewport) ? options.maxOverlaysPerViewport : Infinity;
|
||||
const getCoordinateKey = buildCoordinateKeyFactory(coordPrecision);
|
||||
|
||||
let currentFormatter = overlayFormatter;
|
||||
const overlayMap = new Map(); // coordKey -> Overlay
|
||||
const overlayElementMap = new Map(); // coordKey -> HTMLElement
|
||||
const vectorSource = vectorLayer?.getSource?.();
|
||||
|
||||
const SYNC_MIN_INTERVAL = 120;
|
||||
let lastSyncAt = 0;
|
||||
|
||||
let isMounted = false;
|
||||
let isActive = false; // when false, event handlers no-op
|
||||
let rafId = null;
|
||||
let idleClearId = null;
|
||||
|
||||
|
||||
|
||||
const clearAllOverlaysAsync = (batch = 150) => {
|
||||
const keys = Array.from(overlayMap.keys());
|
||||
if (!keys.length) return;
|
||||
|
||||
// 재귀로 사용하는 이유 한번에 다지운는ㄴ것이아닌 150개 이상 더있을경우를 대비해서 다시 호출하기 위해(지도가 계속 멈춤)
|
||||
const step = () => {
|
||||
let iNum = 0;
|
||||
const end = Math.min(iNum + batch, keys.length);
|
||||
for (; iNum < end; iNum++) {
|
||||
const k = keys[iNum];
|
||||
const ov = overlayMap.get(k);
|
||||
try { if (ov) map.removeOverlay(ov); } catch {}
|
||||
overlayMap.delete(k);
|
||||
const el = overlayElementMap.get(k);
|
||||
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); }
|
||||
}
|
||||
if (iNum < keys.length) {
|
||||
(typeof requestAnimationFrame === 'function' ? requestAnimationFrame : setTimeout)(step, 0);
|
||||
}
|
||||
};
|
||||
|
||||
step();
|
||||
};
|
||||
|
||||
const scheduleSync = () => {
|
||||
if (!isActive) return;
|
||||
const now = Date.now();
|
||||
const since = now - lastSyncAt;
|
||||
const run = () => {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(() => {
|
||||
try { syncOverlays(); } finally { lastSyncAt = Date.now(); }
|
||||
});
|
||||
};
|
||||
if (since >= SYNC_MIN_INTERVAL) run(); else run();
|
||||
};
|
||||
|
||||
const onVisibleChange = () => { if (!isActive) return; scheduleSync(); };
|
||||
const onAddFeature = () => { if (!isActive) return; scheduleSync(); };
|
||||
const onChangeFeature = () => { if (!isActive) return; scheduleSync(); };
|
||||
const onRemoveFeature = () => { if (!isActive) return; scheduleSync(); };
|
||||
|
||||
const clearAllOverlays = () => {
|
||||
overlayMap.forEach((ov) => { try { map.removeOverlay(ov); } catch {} });
|
||||
overlayMap.clear();
|
||||
overlayElementMap.clear();
|
||||
};
|
||||
|
||||
const ensureOverlay = (feature, properties) => {
|
||||
const coord = getFeatureLabelCoordinate(feature);
|
||||
if (!Array.isArray(coord)) return null;
|
||||
const key = getCoordinateKey(coord);
|
||||
|
||||
let el = overlayElementMap.get(key);
|
||||
if (!el) {
|
||||
if (!currentFormatter || typeof currentFormatter !== 'function') return null;
|
||||
|
||||
const maybeEl = currentFormatter(feature, properties);
|
||||
|
||||
// 1) 동기 HTMLElement
|
||||
if (maybeEl && typeof maybeEl === 'object' && (maybeEl.nodeType === 1 || maybeEl.tagName)) {
|
||||
el = maybeEl;
|
||||
overlayElementMap.set(key, el);
|
||||
}
|
||||
// 2) 비동기 Promise<HTMLDivElement|null>
|
||||
else if (maybeEl && typeof maybeEl.then === 'function') {
|
||||
maybeEl.then((resolvedEl) => {
|
||||
if (!resolvedEl || !(resolvedEl.nodeType === 1 || resolvedEl.tagName)) return;
|
||||
// 이미 다른 사이클에서 만들어졌으면 덮어쓰지 않음
|
||||
if (!overlayElementMap.has(key)) overlayElementMap.set(key, resolvedEl);
|
||||
|
||||
let ov = overlayMap.get(key);
|
||||
if (!ov) {
|
||||
ov = new Overlay({ element: resolvedEl, positioning: 'bottom-center', offset: [0, -18], stopEvent: true });
|
||||
overlayMap.set(key, ov);
|
||||
try { map.addOverlay(ov); } catch {}
|
||||
} else {
|
||||
try { ov.setElement(resolvedEl); } catch {}
|
||||
}
|
||||
ov.setPosition(coord);
|
||||
if (resolvedEl.style) resolvedEl.style.display = 'block';
|
||||
}).catch(() => {});
|
||||
return null; // 지금은 안 만듦
|
||||
}
|
||||
// 3) 그 외(null/undefined 등) → 안 만듦
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 여기까지 왔으면 el은 HTMLElement
|
||||
let ov = overlayMap.get(key);
|
||||
if (!ov) {
|
||||
ov = new Overlay({ element: el, positioning: 'bottom-center', offset: [0, -18], stopEvent: true });
|
||||
overlayMap.set(key, ov);
|
||||
map.addOverlay(ov);
|
||||
}
|
||||
ov.setPosition(coord);
|
||||
if (el.style) el.style.display = 'block';
|
||||
return ov;
|
||||
};
|
||||
|
||||
const syncOverlays = () => {
|
||||
if (!vectorLayer?.getVisible?.()) { clearAllOverlays(); return; }
|
||||
const view = map.getView?.();
|
||||
const size = map.getSize?.();
|
||||
const viewExtent = view && size ? view.calculateExtent(size) : null;
|
||||
|
||||
// gather features in viewport only and group by coordinate
|
||||
const groups = new Map(); // coordKey -> feature[]
|
||||
const addFeature = (f) => {
|
||||
const c = getFeatureLabelCoordinate(f);
|
||||
if (!Array.isArray(c)) return;
|
||||
const k = getCoordinateKey(c);
|
||||
const arr = groups.get(k);
|
||||
if (arr) arr.push(f); else groups.set(k, [f]);
|
||||
};
|
||||
|
||||
if (vectorSource?.forEachFeatureInExtent && viewExtent) {
|
||||
vectorSource.forEachFeatureInExtent(viewExtent, addFeature);
|
||||
} else {
|
||||
vectorSource?.forEachFeature(addFeature);
|
||||
}
|
||||
|
||||
// cap overlays per viewport with simple uniform sampling
|
||||
const keys = Array.from(groups.keys());
|
||||
let selectedKeys;
|
||||
if (!isFinite(maxOverlaysPerViewport) || keys.length <= maxOverlaysPerViewport) {
|
||||
selectedKeys = keys;
|
||||
} else {
|
||||
const step = Math.ceil(keys.length / maxOverlaysPerViewport);
|
||||
selectedKeys = [];
|
||||
for (let i = 0; i < keys.length && selectedKeys.length < maxOverlaysPerViewport; i += step) {
|
||||
selectedKeys.push(keys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ensure selected overlays
|
||||
for (const k of selectedKeys) {
|
||||
const featuresAtKey = groups.get(k);
|
||||
const rep = featuresAtKey[0];
|
||||
const props = rep.getProperties?.() ?? {};
|
||||
if (featuresAtKey.length > 1) props.__groupCount = featuresAtKey.length;
|
||||
ensureOverlay(rep, props);
|
||||
}
|
||||
|
||||
// remove non-selected overlays
|
||||
const keep = new Set(selectedKeys);
|
||||
for (const [k, ov] of overlayMap.entries()) {
|
||||
if (!keep.has(k)) {
|
||||
try { map.removeOverlay(ov); } catch {}
|
||||
overlayMap.delete(k);
|
||||
const el = overlayElementMap.get(k);
|
||||
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mount = () => {
|
||||
if (isMounted) return;
|
||||
vectorLayer?.on?.('change:visible', onVisibleChange);
|
||||
vectorSource?.on?.('addfeature', onAddFeature);
|
||||
vectorSource?.on?.('changefeature', onChangeFeature);
|
||||
vectorSource?.on?.('removefeature', onRemoveFeature);
|
||||
isMounted = true;
|
||||
};
|
||||
|
||||
// Backward-compat: make dispose lightweight so it doesn't freeze the map
|
||||
const dispose = () => {
|
||||
isActive = false; // pause reactions
|
||||
setVisible(false); // hide our labels only
|
||||
const run = () => { clearAllOverlaysAsync(150); idleClearId = null; };
|
||||
if (typeof requestIdleCallback === 'function') idleClearId = requestIdleCallback(run, { timeout: 250 });
|
||||
else idleClearId = setTimeout(run, 180);
|
||||
};
|
||||
|
||||
// Explicit lifecycle controls
|
||||
const enable = () => {
|
||||
isActive = true;
|
||||
setVisible(true);
|
||||
scheduleSync();
|
||||
};
|
||||
|
||||
const disable = ({ clear = false, delayed = true } = {}) => {
|
||||
isActive = false;
|
||||
setVisible(false);
|
||||
if (clear) {
|
||||
const run = () => { clearAllOverlaysAsync(150); idleClearId = null; };
|
||||
if (delayed && typeof requestIdleCallback === 'function') idleClearId = requestIdleCallback(run, { timeout: 250 });
|
||||
else idleClearId = setTimeout(run, delayed ? 180 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
if (!isMounted) return;
|
||||
vectorLayer?.un?.('change:visible', onVisibleChange);
|
||||
vectorSource?.un?.('addfeature', onAddFeature);
|
||||
vectorSource?.un?.('changefeature', onChangeFeature);
|
||||
vectorSource?.un?.('removefeature', onRemoveFeature);
|
||||
isMounted = false;
|
||||
if (rafId) cancelAnimationFrame(rafId), rafId = null;
|
||||
if (idleClearId) { (typeof cancelIdleCallback === 'function' ? cancelIdleCallback : clearTimeout)(idleClearId); idleClearId = null; }
|
||||
clearAllOverlaysAsync(200);
|
||||
};
|
||||
|
||||
const setFormatter = (nextFormatter) => { currentFormatter = nextFormatter; syncOverlays(); };
|
||||
|
||||
const setVisible = (visible) => {
|
||||
const display = visible ? 'block' : 'none';
|
||||
for (const el of overlayElementMap.values()) {
|
||||
try { if (el && el.style) el.style.display = display; } catch {}
|
||||
}
|
||||
};
|
||||
|
||||
return { mount, dispose, enable, disable, destroy, setFormatter, syncOverlays, setVisible };
|
||||
}
|
||||
@@ -594,3 +594,50 @@ export const isMobile = () => {
|
||||
|
||||
return width < GLOBAL.MOBILE_SIZE;
|
||||
};
|
||||
|
||||
|
||||
export const getFeatureLabelCoordinate = (feature) => {
|
||||
const geometry = feature?.getGeometry?.();
|
||||
if (!geometry || typeof geometry.getType !== 'function') return null;
|
||||
|
||||
const type = geometry.getType();
|
||||
|
||||
// 1) 기본 타입
|
||||
if (type === 'Point') return geometry.getCoordinates();
|
||||
if (type === 'Polygon') return geometry.getInteriorPoint().getCoordinates();
|
||||
if (type === 'LineString') return geometry.getCoordinateAt(0.5);
|
||||
|
||||
// 2) 멀티 타입들: 가능한 한 대표 좌표를 추출
|
||||
if (type === 'MultiPoint') {
|
||||
const coords = geometry.getCoordinates?.();
|
||||
return (coords && coords[0]) ? coords[0] : null;
|
||||
}
|
||||
if (type === 'MultiLineString') {
|
||||
try { return geometry.getLineString?.(0)?.getCoordinateAt?.(0.5) ?? null; } catch {}
|
||||
}
|
||||
if (type === 'MultiPolygon') {
|
||||
try { return geometry.getInteriorPoints?.()?.getFirstCoordinate?.() ?? null; } catch {}
|
||||
}
|
||||
|
||||
// 3) 기타: extent 중심점
|
||||
try {
|
||||
const extent = geometry.getExtent?.();
|
||||
if (extent) return [
|
||||
(extent[0] + extent[2]) / 2,
|
||||
(extent[1] + extent[3]) / 2,
|
||||
];
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 수는 “좌표를 문자열 키로 변환하는 함수”를
|
||||
//좌표 정밀도(소수점 자리수)를 조절해서 근사한 좌표를 같은 그룹으로 취급할 수 있게 함.
|
||||
export const buildCoordinateKeyFactory =(precision)=> {
|
||||
const p = Math.max(0, Math.min(12, Number.isFinite(precision) ? precision : 6));
|
||||
return function getCoordinateKey(coordinate) {
|
||||
const x = coordinate[0];
|
||||
const y = coordinate[1];
|
||||
return `${x.toFixed(p)}_${y.toFixed(p)}`;
|
||||
};
|
||||
}
|
||||
@@ -87,3 +87,41 @@ export const findAddress = async (search: string) => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const searchFilter=(search:string)=>{
|
||||
|
||||
|
||||
if(search.length >0){
|
||||
//특수문자 제거
|
||||
const expText = /[%=><]/ ;
|
||||
if(expText.test(search) == true){
|
||||
alert("특수문자를 입력 할수 없습니다.") ;
|
||||
search = search.split(expText).join("");
|
||||
return false;
|
||||
}
|
||||
|
||||
//특정문자열(sql예약어의 앞뒤공백포함) 제거
|
||||
const sqlArray = [
|
||||
//sql 예약어
|
||||
"OR", "SELECT", "INSERT", "DELETE", "UPDATE", "CREATE", "DROP", "EXEC",
|
||||
"UNION", "FETCH", "DECLARE", "TRUNCATE"
|
||||
]
|
||||
|
||||
let regex;
|
||||
for(let i=0; i<sqlArray.length; i++){
|
||||
regex = new RegExp( sqlArray[i] as string,"gi") ;
|
||||
|
||||
if (regex.test(search) ) {
|
||||
alert("\"" + sqlArray[i]+"\"와(과) 같은 특정문자로 검색할 수 없습니다.");
|
||||
search =search.replace(regex, "");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true ;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Input } from "@common/components/ui";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { findAddress, type addressType } from "./findAddress";
|
||||
import { findAddress, searchFilter, type addressType } from "./findAddress";
|
||||
|
||||
type thisProps = {
|
||||
initValue: string | undefined;
|
||||
@@ -9,52 +9,63 @@ type thisProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param initValue : 초기값 ?
|
||||
* @returns setRtnData => 선택된 주소 데이터 반환
|
||||
*/
|
||||
export default function SearchInputAddress({
|
||||
initValue ,
|
||||
initValue,
|
||||
setRtnData = () => {},
|
||||
readonly=false
|
||||
readonly = false,
|
||||
}: thisProps) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [textValue, setTextValue] = useState(initValue || "");
|
||||
const [searchList, setSearchList] = useState<addressType[]>([]);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
|
||||
if(initValue !== textValue && initValue !== undefined && initValue !== null && initValue !== ''){
|
||||
setTextValue(initValue)
|
||||
}
|
||||
|
||||
},[initValue])
|
||||
useEffect(() => {
|
||||
if (
|
||||
initValue !== textValue &&
|
||||
initValue !== undefined &&
|
||||
initValue !== null &&
|
||||
initValue !== ""
|
||||
) {
|
||||
setTextValue(initValue);
|
||||
}
|
||||
}, [initValue]);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setTextValue(value);
|
||||
findAddress(value).then((data) => {
|
||||
setSearchList(data);
|
||||
})
|
||||
|
||||
if (searchFilter(value)) {
|
||||
setTextValue(value);
|
||||
findAddress(value).then((data) => {
|
||||
setSearchList(data);
|
||||
});
|
||||
}else{
|
||||
setTextValue("");
|
||||
setSearchList([]);
|
||||
}
|
||||
if (value) {
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
[setSearchList]
|
||||
[setSearchList],
|
||||
);
|
||||
|
||||
const handleSuggestionClick = useCallback(
|
||||
(roadAddrPart1: string) => {
|
||||
setTextValue(roadAddrPart1);
|
||||
setIsOpen(false);
|
||||
setRtnData(searchList?.find((item) => item.roadAddrPart1 === roadAddrPart1) || ({} as addressType));
|
||||
setRtnData(
|
||||
searchList?.find((item) => item.roadAddrPart1 === roadAddrPart1) ||
|
||||
({} as addressType),
|
||||
);
|
||||
},
|
||||
[searchList, setRtnData]
|
||||
[searchList, setRtnData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -80,18 +91,18 @@ export default function SearchInputAddress({
|
||||
value={textValue}
|
||||
onChange={handleInputChange}
|
||||
/* onFocus={() => setIsOpen(true)} */
|
||||
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"
|
||||
className="h-[3.2rem] w-full rounded-md border border-gray-300 pl-3 !text-xl text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={`주소를 입력하세요`}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
{isOpen && (
|
||||
<div className="absolute left-0 right-0 mt-2 bg-white border border-gray-300 rounded-md shadow-lg max-h-72 overflow-auto z-50">
|
||||
<div className="absolute left-0 right-0 z-50 mt-2 max-h-72 overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
|
||||
<div className="max-h-72">
|
||||
{searchList && searchList.length > 0 ? (
|
||||
searchList.map((item: addressType, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"
|
||||
className="w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl hover:bg-gray-100"
|
||||
onClick={() => handleSuggestionClick(item.roadAddrPart1)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -106,7 +117,7 @@ export default function SearchInputAddress({
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
|
||||
<div className="w-full cursor-pointer overflow-auto px-5 py-3 !text-xl hover:bg-gray-100">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user