From b422380f16d17a6174afad85f5d1be1a9f49a62b Mon Sep 17 00:00:00 2001 From: thkim Date: Tue, 25 Nov 2025 18:44:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B4=91=EC=82=B0=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=20=EC=8B=9C=20=EA=B4=91=EC=82=B0=20=EC=A0=95=EB=B3=B4=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A7=80=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../geoinfo/map/main/MapMainController.java | 116 ++++++ src/main/webapp/js/map/main/map.js | 333 +++++++++++++++--- 2 files changed, 391 insertions(+), 58 deletions(-) diff --git a/src/main/java/geoinfo/map/main/MapMainController.java b/src/main/java/geoinfo/map/main/MapMainController.java index 10e998c1..df46276f 100644 --- a/src/main/java/geoinfo/map/main/MapMainController.java +++ b/src/main/java/geoinfo/map/main/MapMainController.java @@ -11,6 +11,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.io.OutputStream; + import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -158,4 +165,113 @@ public class MapMainController { return "map/left/include/comMove"; } + + /** + * 광산 정보 WFS 프록시 (CORS 해결용) + * @param request + * @param response + * @param params + * @throws Exception + */ + @RequestMapping(value = "/map/getMineWFS.do") + public void getMineWFS(HttpServletRequest request, HttpServletResponse response, @RequestParam HashMap params) throws Exception { + + // 1. 공공데이터포털 서비스 키 (Decoding 된 키 사용) + String serviceKey = "L1z0zEpxNLB0Sqwv97WAIyL1lB+shPemDLNaG9hy9g3BzbkXRVG2/aSTZ7PiAAivgaCYn9p1tLmq2keiC4yFZA=="; + + // 2. 요청 파라미터 받기 (OpenLayers에서 bbox를 보냄) + String bbox = request.getParameter("bbox"); + + // 3. API URL 생성 + StringBuilder urlBuilder = new StringBuilder("https://apis.data.go.kr/1480523/GeologicalService/getMineWFS"); + urlBuilder.append("?" + URLEncoder.encode("ServiceKey", "UTF-8") + "=" + URLEncoder.encode(serviceKey, "UTF-8")); + urlBuilder.append("&" + URLEncoder.encode("srsName", "UTF-8") + "=" + URLEncoder.encode("EPSG:3857", "UTF-8")); + urlBuilder.append("&" + URLEncoder.encode("maxFeatures", "UTF-8") + "=" + URLEncoder.encode("100", "UTF-8")); + urlBuilder.append("&" + URLEncoder.encode("resultType", "UTF-8") + "=" + URLEncoder.encode("results", "UTF-8")); + + // 버전을 1.0.0으로 명시 (GML 2 포맷 요청) + urlBuilder.append("&" + URLEncoder.encode("version", "UTF-8") + "=" + URLEncoder.encode("1.0.0", "UTF-8")); + + // bbox가 있을 경우에만 추가 (OpenLayers Strategy.BBOX가 자동으로 보냄) + if (bbox != null && !bbox.isEmpty()) { + urlBuilder.append("&" + URLEncoder.encode("bbox", "UTF-8") + "=" + URLEncoder.encode(bbox, "UTF-8")); + } + + // 4. HTTP 연결 설정 + URL url = new URL(urlBuilder.toString()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Content-type", "application/xml"); // WFS는 XML 응답 + + // 5. 응답 코드 확인 및 스트림 연결 + // 응답을 읽어서 브라우저로 바로 쏘아줍니다 (Pass-through) + response.setContentType("text/xml;charset=UTF-8"); // 브라우저에게 XML임을 알림 + + BufferedReader rd; + if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) { + rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + } else { + rd = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "UTF-8")); + } + + // 6. 데이터 읽기 및 쓰기 + StringBuilder sb = new StringBuilder(); + String line; + while ((line = rd.readLine()) != null) { + sb.append(line); + } + rd.close(); + conn.disconnect(); + + // 클라이언트로 응답 전송 + response.getWriter().write(sb.toString()); + } + + /** + * 광산 상세 정보 조회 (getInfo API 프록시) + * @param request + * @param response + * @param params + * @throws Exception + */ + @RequestMapping(value = "/map/getMineInfo.do") + public void getMineInfo(HttpServletRequest request, HttpServletResponse response, @RequestParam HashMap params) throws Exception { + + // 1. 서비스 키 & 파라미터 설정 + String serviceKey = "L1z0zEpxNLB0Sqwv97WAIyL1lB+shPemDLNaG9hy9g3BzbkXRVG2/aSTZ7PiAAivgaCYn, 9p1tLmq2keiC4yFZA=="; + String mgtNo = request.getParameter("mgtNo"); + + // 2. API URL 생성 (JSON 요청) + StringBuilder urlBuilder = new StringBuilder("https://apis.data.go.kr/1480523/GeologicalService/getMine"); + urlBuilder.append("?" + URLEncoder.encode("ServiceKey", "UTF-8") + "=" + URLEncoder.encode(serviceKey, "UTF-8")); + urlBuilder.append("&" + URLEncoder.encode("mgtNo", "UTF-8") + "=" + URLEncoder.encode(mgtNo, "UTF-8")); + urlBuilder.append("&" + URLEncoder.encode("type", "UTF-8") + "=" + URLEncoder.encode("json", "UTF-8")); // JSON 응답 요청 + + // 3. API 호출 + URL url = new URL(urlBuilder.toString()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Content-type", "application/json"); + + // 4. 응답 전달 + response.setContentType("application/json;charset=UTF-8"); + + BufferedReader rd; + if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) { + rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + } else { + rd = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "UTF-8")); + } + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = rd.readLine()) != null) { + sb.append(line); + } + rd.close(); + conn.disconnect(); + + // 클라이언트로 JSON 문자열 전송 + response.getWriter().write(sb.toString()); + } } diff --git a/src/main/webapp/js/map/main/map.js b/src/main/webapp/js/map/main/map.js index a4d93998..82c12105 100644 --- a/src/main/webapp/js/map/main/map.js +++ b/src/main/webapp/js/map/main/map.js @@ -356,6 +356,7 @@ var HOLE_LAYER_M; // 시추공 레이어2 var GEOLOGY_LAYER; // 지질도/광물 레이어 var MINERAL_LAYER; // 광물 레이어 var MINE_LAYER; // 광산 레이어 +var CTL_SELECT_MINE; // 광산 선택 var WELL_LAYER; // 관정 레이어 var STEEP_SLOPE_LAYER; // 급경사지 레이어 var CTL_SELECT_SLOPE; // 급경사지 선택 @@ -876,49 +877,236 @@ function initApp(param){ transparent: true }; + // ▼▼▼ 광산정보 (WFS Custom Parsing & Selection) ▼▼▼ + var mineStyle = new OpenLayers.Style({ + pointRadius: 6, + fillColor: "#ff0000", + strokeColor: "#ffffff", + strokeWidth: 1, + graphicName: "square", + fillOpacity: 0.7, + cursor: "pointer" + }); - MINE_LAYER = new OpenLayers.Layer.Grid( - "Mine Map", - mineBaseUrl, // Base URL - mineParams, // 정적 파라미터 - { - isBaseLayer: false, - visibility: false, - opacity: 0.7, - singleTile: true, // 단일 타일로 요청 - - /** - * getURL 함수를 재정의(override)하여 - * 원하는 URL 형식을 직접 만듭니다. - */ - getURL: function(bounds) { - // 1. 맵의 현재 영역(bounds)을 BBOX 문자열로 변환 - var bbox = bounds.toBBOX(); - - // 2. 맵의 현재 픽셀 크기(width, height) - var size = this.map.getSize(); - - // 3. 기본 URL (mineBaseUrl) - var url = this.url; - - // 4. 정적 파라미터 (ServiceKey, srs, format 등) 복사 - // OpenLayers.Util.extend는 객체를 복사합니다. - var params = OpenLayers.Util.extend({}, this.params); - - // 5. 동적 파라미터 (bbox, width, height) 추가 - params.bbox = bbox; - params.width = size.w; - params.height = size.h; - - // 6. 모든 파라미터를 URL에 조합하여 최종 요청 URL 반환 - // OpenLayers.Util.urlAppend가 ? 와 & 를 알아서 처리해줍니다. - return OpenLayers.Util.urlAppend(url, OpenLayers.Util.getParameterString(params)); + MINE_LAYER = new OpenLayers.Layer.Vector("Mine Map", { + strategies: [new OpenLayers.Strategy.BBOX({ + ratio: 1.1, + resFactor: 1 + })], + protocol: new OpenLayers.Protocol.HTTP({ + url: "/map/getMineWFS.do", + headers: { "Content-Type": "plain/text" }, + params: {}, + format: new OpenLayers.Format.XML({ + read: function(data) { + if (typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + + var features = []; + var elements = data.getElementsByTagName("*"); + + for (var i = 0; i < elements.length; i++) { + var el = elements[i]; + var tagName = el.localName || el.nodeName.split(":").pop(); + + if (tagName === "MINE_POINT") { + try { + // 좌표 추출 + var posNodes = el.getElementsByTagName("pos"); + if(posNodes.length === 0) posNodes = el.getElementsByTagName("gml:pos"); + + if (posNodes.length > 0) { + var posText = OpenLayers.String.trim(posNodes[0].textContent || posNodes[0].text); + var coords = posText.split(/\s+/); + var x = parseFloat(coords[0]); + var y = parseFloat(coords[1]); + + // 속성(MGTNO) 추출 로직 활성화 + var attributes = {}; + + // 태그명이 MGTNO 또는 openAPI:MGTNO 인 것을 찾음 + var mgtNodes = el.getElementsByTagName("MGTNO"); + if(mgtNodes.length === 0) mgtNodes = el.getElementsByTagName("openAPI:MGTNO"); + + if(mgtNodes.length > 0) { + attributes.MGTNO = mgtNodes[0].textContent || mgtNodes[0].text; + } else { + attributes.MGTNO = "정보없음"; + } + + + var objectIdNodes = el.getElementsByTagName("OBJECTID"); + if(objectIdNodes.length === 0) objectIdNodes = el.getElementsByTagName("openAPI:OBJECTID"); + + if(objectIdNodes.length > 0) { + attributes.OBJECTID = objectIdNodes[0].textContent || objectIdNodes[0].text; + } else { + attributes.OBJECTID = "정보없음"; + } + + var geometry = new OpenLayers.Geometry.Point(x, y); + var feature = new OpenLayers.Feature.Vector(geometry, attributes); + features.push(feature); + } + } catch(err) { + console.error("광산 데이터 파싱 에러:", err); + } + } + } + return features; + } + }), + readWithRequest: true + }), + styleMap: new OpenLayers.StyleMap({ + "default": mineStyle, + "select": { // 선택되었을 때 스타일 (노란색) + pointRadius: 8, + fillColor: "#ffff00", + strokeColor: "#ff0000", + fillOpacity: 1 } - } - ); + }), + visibility: false + }); BASE_MAP.addLayer(MINE_LAYER); - // ▲▲▲ 광산정보 ▲▲▲ - + + // 광산 선택(클릭) 컨트롤 생성 + CTL_SELECT_MINE = new OpenLayers.Control.SelectFeature(MINE_LAYER, { + clickout: true, toggle: true, + multiple: false, hover: false, + onSelect: function(feature) { + var mgtNo = feature.attributes.MGTNO; + var objectId = feature.attributes.OBJECTID; + + // 관리번호가 없는 경우 처리 + if (!mgtNo || mgtNo === "정보없음") { + alert("관리번호 정보가 없습니다."); + this.unselect(feature); + return; + } + + // 백엔드 프록시('/map/getMineInfo.do')를 통해 API 호출 + $.ajax({ + url: "/map/getMineInfo.do", + type: "GET", + data: { mgtNo: mgtNo }, + dataType: "json", // JSON 문자열을 그대로 받기 위해 text 사용 + success: function(data) { + + //mgtNo가 같아도 다른 광산이 많이 존재한다... 필요시 하드 코딩을 하기 위해, 아래 전국 mineList를 JSON으로 정리해서 특정 광산과 연결하도록 구현하였다. + var mineList = { + "ME2010A006": [ + 1 + ], + "ND2009A002": [ + 2 // 2 일광광산 부산광역시 기장군 일광면 원리 190-1 일원 + ], + "ME2015A013": [ + 3, + 4 + ], + "ME2009E003": [ + 5 + ], + "WJ2016E001": [ + 6, + 7 + ], + "HG2008E012": [ + 8, + 9, + 10 + ], + "JJ2005E006": [ + 11, + 12 + ], + "ME2010E001": [ + 20, + 14, + 15, + 16, + 17, + 18, + 19, // 19 일광광산 부산광역시 기장군 일광면 원리 + 13, // 13 동래군 기장면 + 24 + ], + "ND2014E002": [ + 21, + 22, + 23 + ], + "ND2016B003": [ + 25 // (가)궁근정 울산 울주군 + ], + "WJ2005E011": [ + 26, + 27, + 28, + 29 + ], + "HG2017B004": [ + 30 + ], + "HG2022G001": [ + 32 // 광장 서울특별시 강동구 암사동 100-3 + ], + "ND2022E003": [ + 31 // (가)궁근정 울산광역시 울주군 상북면 궁근정리 + ] + }; + for( idx in mineList[data.response.body.mgtno] ) { + if( Number(mineList[data.response.body.mgtno][idx]) === Number(objectId) ) { + item = data.response.body.mines[idx]; + console.log( '%o', item); + objectId = Number(objectId); + var contents = ''; + if( item.mineAdres ) { + contents += '주소: ' + item.mineAdres + '\n'; + } + if( item.mineKnd ) { + contents += '광물: ' + item.mineKnd + '\n'; + } + if( item.mineNm ) { + contents += '광산 식별명: ' + item.mineNm + '\n'; + } + + if( item.mineTy ) { + contents += '기타: ' + item.mineTy + '\n'; + } + + if( objectId ) { + contents += '식별 번호: ' + objectId + '\n'; + } + var validationTailler = ' 미확인'; + if( objectId === 31 || objectId === 32 || objectId === 25 || + objectId === 2 || + objectId === 13 || + objectId === 19 + ) { + validationTailler = ' 검증됨'; + } + console.log( contents + validationTailler ); + alert( contents ); + } + } + }, + error: function(xhr, status, error) { + console.error("API 호출 에러:", error); + alert("상세 정보를 가져오는데 실패했습니다."); + }, + complete: function() { + // 알림창 닫은 후 선택 상태 해제 (다시 클릭 가능하도록) + CTL_SELECT_MINE.unselect(feature); + } + }); + } + }); + BASE_MAP.addControl(CTL_SELECT_MINE); + // ▲▲▲ 광산정보 (WFS Custom Parsing & Selection) ▲▲▲ // ▼▼▼ 관정정보 ▼▼▼ @@ -3833,6 +4021,7 @@ function initControl(){ HOLE_SELECT2.removeAllFeatures(); CTL_SELECT.deactivate(); if(CTL_SELECT_SLOPE) CTL_SELECT_SLOPE.deactivate(); // 급경사지 선택 비활성화 + if(CTL_SELECT_MINE) CTL_SELECT_MINE.deactivate(); // 광산 선택 비활성화 CTL_SELECT_PROJECT.deactivate(); CTL_SELECT_JIBAN.deactivate(); CTL_TOOLTIP.activate(); @@ -5423,41 +5612,69 @@ function geologyMineral() { } function geologyMine() { - initControl(); // 다른 컨트롤 상태 초기화 - - // MINE_LAYER가 정상적으로 생성되었는지 확인 + initControl(); // 다른 컨트롤 초기화 + if (!MINE_LAYER) { console.error("광산 레이어가 초기화되지 않았습니다."); + alert("오류: 광산 레이어가 없습니다."); return; } - // 현재 레이어의 표시 상태를 가져옵니다. var isVisible = MINE_LAYER.getVisibility(); - if (isVisible ) { - // 레이어가 현재 보이고 있다면, 숨깁니다. - MINE_LAYER.setVisibility(false); - CTL_INFO.setText("지질 Off"); - CTL_INFO.deactivate(); // 정보창도 비활성화 + if (isVisible) { + // [OFF] 기능 끄기 + MINE_LAYER.setVisibility(false); + if(CTL_SELECT_MINE) CTL_SELECT_MINE.deactivate(); - removeGeologyLegend(); // 끌 때 범례도 닫습니다. + CTL_INFO.setText("지질 Off"); + CTL_INFO.deactivate(); + removeGeologyLegend(); + console.log("[광산] 레이어 OFF"); } else { - - // 레이어를 보이게 설정하고 강제로 다시 그립니다. + // [ON] 기능 켜기 MINE_LAYER.setVisibility(true); - MINE_LAYER.redraw(true); - - // CTL_INFO의 infoDiv가 내부 요소를 absolute 포지셔닝할 수 있도록 'relative'로 설정 - $(CTL_INFO.infoDiv).css("position", "relative"); - // CTL_INFO 텍스트 설정 시 버튼 HTML을 함께 삽입 + // 레이어 Z-Index를 최상위로 설정 + // 다른 레이어(WMS, 툴팁 등)보다 무조건 위에 오도록 값을 높게 설정합니다. + var maxZ = 0; + for(var i=0; i maxZ) { + maxZ = BASE_MAP.layers[i].getZIndex(); + } + } + MINE_LAYER.setZIndex(maxZ + 500); // 가장 높은 값보다 500 더 높게 + MINE_LAYER.redraw(true); + + // 선택 컨트롤 재활성화 및 로그 출력 + if(CTL_SELECT_MINE) { + CTL_SELECT_MINE.deactivate(); + var activated = CTL_SELECT_MINE.activate(); + + // 현재 로드된 피처 개수 확인 + var featureCount = MINE_LAYER.features.length; + console.log("[광산] 컨트롤 활성화 성공여부: " + activated); + console.log("[광산] 현재 지도에 표시된 광산 개수: " + featureCount); + + if(featureCount > 0) { + console.log("[광산] 첫번째 광산 속성 예시:", MINE_LAYER.features[0].attributes); + } + + } else { + console.error("[광산] CTL_SELECT_MINE 컨트롤이 없습니다! initApp에서 생성되었는지 확인하세요."); + alert("오류: 광산 선택 컨트롤이 생성되지 않았습니다."); + } + + + $(CTL_INFO.infoDiv).css("position", "relative"); CTL_INFO.setText("광산 On"); CTL_INFO.activate(); $("#CTL_INFO").css("bottom", "65px"); $("#CTL_INFO").css("left", "20px"); + alert('광산은 공개된 데이터가 많지 않습니다. 빨간색 사각형으로 표시됩니다.'); - } + } }