فهرست منبع

update 2024-09-24

junggilpark 10 ماه پیش
والد
کامیت
e51e6f7dc9
32فایلهای تغییر یافته به همراه1107 افزوده شده و 104 حذف شده
  1. 1 0
      conf/tsi-sig-server.pid
  2. 2 0
      conf/tsi-sig-server.yml
  3. 1 1
      pom.xml
  4. 30 10
      src/main/java/com/tsi/sig/server/controller/MainController.java
  5. 5 1
      src/main/java/com/tsi/sig/server/mapper/MainMapper.java
  6. 11 7
      src/main/java/com/tsi/sig/server/repository/ApplicationRepository.java
  7. 2 0
      src/main/java/com/tsi/sig/server/security/WebSecurityConfig.java
  8. 32 15
      src/main/java/com/tsi/sig/server/service/MainService.java
  9. 1 1
      src/main/java/com/tsi/sig/server/vo/CvibNodeStatusVo.java
  10. 1 1
      src/main/java/com/tsi/sig/server/vo/CvibStatusVo.java
  11. 22 0
      src/main/java/com/tsi/sig/server/vo/EvpPhaseVo.java
  12. 11 0
      src/main/java/com/tsi/sig/server/vo/EvpRouteVo.java
  13. 22 0
      src/main/java/com/tsi/sig/server/vo/EvpServiceVo.java
  14. 8 0
      src/main/java/com/tsi/sig/server/vo/EvpSignalVo.java
  15. 4 1
      src/main/java/com/tsi/sig/server/websocket/kafka/AllTopicConsumerThread.java
  16. 43 34
      src/main/resources/application.yml
  17. 77 1
      src/main/resources/mybatis/mapper/main.xml
  18. 81 2
      src/main/resources/static/css/main.css
  19. 9 0
      src/main/resources/static/images/car.svg
  20. BIN
      src/main/resources/static/images/find_coord_off.png
  21. BIN
      src/main/resources/static/images/find_coord_on.png
  22. BIN
      src/main/resources/static/images/refresh.png
  23. 1 1
      src/main/resources/static/js/common/common-ajax.js
  24. 6 4
      src/main/resources/static/js/common/cvibDetail.js
  25. 9 4
      src/main/resources/static/js/common/cvibStatus.js
  26. 573 8
      src/main/resources/static/js/map.js
  27. 3 3
      src/main/resources/static/js/signal.js
  28. 40 0
      src/main/resources/static/js/worker.js
  29. 1 1
      src/main/webapp/WEB-INF/jsp/common.jsp
  30. 1 1
      src/main/webapp/WEB-INF/jsp/cvibInfoDetail.jsp
  31. 59 4
      src/main/webapp/WEB-INF/jsp/main.jsp
  32. 51 4
      src/main/webapp/WEB-INF/jsp/treeListFrame.jsp

+ 1 - 0
conf/tsi-sig-server.pid

@@ -0,0 +1 @@
+30436

+ 2 - 0
conf/tsi-sig-server.yml

@@ -0,0 +1,2 @@
+server:
+  port: 8443

+ 1 - 1
pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.3.4.RELEASE</version>
+        <version>2.4.13</version>
         <relativePath/> <!-- lookup parent from repository -->
     </parent>
 

+ 30 - 10
src/main/java/com/tsi/sig/server/controller/MainController.java

@@ -1,11 +1,11 @@
 package com.tsi.sig.server.controller;
 
+import com.tsi.sig.server.objects.UserVO;
 import com.tsi.sig.server.service.MainService;
-import com.tsi.sig.server.vo.CvibNodeStatusVo;
-import com.tsi.sig.server.vo.CvibNodeVO;
-import com.tsi.sig.server.vo.ResultVo;
+import com.tsi.sig.server.vo.*;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -13,13 +13,11 @@ import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 
+import javax.naming.AuthenticationException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
+import java.util.*;
 
 @Slf4j
 @Controller
@@ -65,13 +63,13 @@ public class MainController {
         return "userEdit";
     }
 
-    @RequestMapping(value = "getBottomListFrame.do")
+    @RequestMapping(value = "/getBottomListFrame.do")
     public String getBottomListFrame() throws Exception {
         //log.info("CALL.........................................: getBottomListFrame");
         return "bottomListFrame";
     }
 
-    @RequestMapping(value = "getTreeListFrame.do")
+    @RequestMapping(value = "/getTreeListFrame.do")
     public String getTreeListFrame(Locale locale, Model model, HttpServletRequest request, HttpServletResponse response) throws Exception {
         //log.info("CALL.........................................: getTreeListFrame");
         return "treeListFrame";
@@ -128,7 +126,19 @@ public class MainController {
     @ResponseBody
 //    public List<CvibNodeStatusVo> cvibInfoList(@RequestParam Map<String, String> paramMap) throws Exception {
     public Map<String,List<?>> cvibInfoList(@RequestParam Map<String, String> paramMap) throws Exception {
-        return mainService.getCvibNodeStatusList(paramMap);
+        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        if (principal != null && principal != "anonymousUser") {
+            UserVO user = (UserVO)principal;
+            String[] test = new String[]{"I", "B", "N", "M", "S"};
+            List<String> strList = new ArrayList<>(Arrays.asList(test));
+            if ( strList.contains(user.getUserGrade())) {
+                return mainService.getCvibNodeStatusList(paramMap);
+            }
+            else {
+                throw new AuthenticationException("사용자 권한을 확인해주세요ㅕ.");
+            }
+        }
+        throw new AuthenticationException("로그인 하지않고 API 를 사용할 수 없습니다.");
     }
     @RequestMapping(value = "/getCvibDetail.do", method = RequestMethod.POST)
     @ResponseBody
@@ -136,4 +146,14 @@ public class MainController {
         return mainService.getCvibNodeDetail(nodeId);
     }
 
+    @RequestMapping(value = "/getEvpServiceList.do", method = RequestMethod.POST)
+    @ResponseBody
+    public List<EvpServiceVo> getEvpServiceList() throws Exception {
+        return mainService.getEvpServiceList();
+    }
+    @RequestMapping(value = "/getEvpRouteList.do", method = RequestMethod.POST)
+    @ResponseBody
+    public List<EvpRouteVo> getEvpRouteList(@RequestParam HashMap<String,Object> paramMap) throws Exception {
+        return mainService.getEvpRouteList(paramMap);
+    }
 }

+ 5 - 1
src/main/java/com/tsi/sig/server/mapper/MainMapper.java

@@ -1,6 +1,6 @@
 package com.tsi.sig.server.mapper;
 
-import com.tsi.sig.server.vo.CvibNodeVO;
+import com.tsi.sig.server.vo.*;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.HashMap;
@@ -12,5 +12,9 @@ public interface MainMapper {
     List<CvibNodeVO> getNodeList();
     List<CvibNodeVO> getCvibNodeInfo(HashMap<String, Object> vo);
     List<CvibNodeVO> getCvibMapNodeList(HashMap<String, Object> vo);
+    List<EvpServiceVo> getEvpServiceList();
+    List<EvpRouteVo> getEvpRouteList(HashMap<String, Object> vo);
+    List<EvpPhaseVo> getEvpPhaseList(HashMap<String, Object> vo);
+    List<EvpSignalVo> getEvpSignalList(HashMap<String, Object> vo);
 
 }

+ 11 - 7
src/main/java/com/tsi/sig/server/repository/ApplicationRepository.java

@@ -27,27 +27,31 @@ public class ApplicationRepository {
                 CvibStatusVo newVo = new CvibStatusVo();
                 newVo.setNodeId(vo.nodeId);
                 newVo.setCommStatus(vo.commStatus);
-                newVo.setDate(vo.date);
+                newVo.setLocalDate(vo.localDate);
                 newVo.setConnDate(vo.connDate);
                 list.add(newVo);
                 Date connDate = null;
                 Date date = null;
+                Date now = new Date();
                 if (vo.getConnDate() != null) {
                     connDate = this.getStringParseToDate(vo.getConnDate());
                 }
 
-                if (vo.getDate() != null) {
-                    date = this.getStringParseToDate(vo.getDate());
+                if (vo.getLocalDate() != null) {
+                    date = this.getStringParseToDate(vo.getLocalDate());
                 }
                 if ( date != null &&
                      connDate != null &&
-                     connDate.getTime() - date.getTime() > 3000) {
-                    log.info("=========== OverTime 3 Seconds.....  nodeId : {}, OverTime : {} ms",
-                            vo.nodeId,connDate.getTime() - date.getTime());
+                     connDate.getTime() - date.getTime() > 10000) {
+                     //log.warn("=========== OverTime 3 Seconds.....  nodeId : {}, OverTime : {} ms",
+                     //       vo.nodeId,connDate.getTime() - date.getTime());
+                    //newVo.setCommStatus(false);
+                }
+                else if (date != null && connDate != null && now.getTime() - connDate.getTime() > 10000) {
                     newVo.setCommStatus(false);
                 }
                 else if(!vo.commStatus) {
-                    log.info("=========== 통신 이상...  nodeId : {}",vo.nodeId);
+                    log.warn("=========== 통신 이상...  nodeId : {}",vo.nodeId);
                 }
 
             }

+ 2 - 0
src/main/java/com/tsi/sig/server/security/WebSecurityConfig.java

@@ -28,7 +28,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     private final LoginServiceImpl loginService;
 
     protected void configure(HttpSecurity http) throws Exception {
+        //"/showlogin.do", "/login.do", "/logout.do", "/", "/getBottomListFrame.do"
         ((HttpSecurity)((FormLoginConfigurer)((FormLoginConfigurer)((HttpSecurity)((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)((HttpSecurity)http.headers().frameOptions().sameOrigin().and()).authorizeRequests().antMatchers(new String[]{"/**"})).permitAll().and()).formLogin().loginPage("/showlogin.do").failureUrl("/denied.do")).usernameParameter("userId").passwordParameter("userPswd").permitAll()).and()).logout().logoutUrl("/logout.do").invalidateHttpSession(true).logoutSuccessUrl("/showlogin.do");
+        http.authorizeRequests().anyRequest().hasAnyRole("I", "B", "N", "M", "S");
         http.csrf().disable();
     }
 

+ 32 - 15
src/main/java/com/tsi/sig/server/service/MainService.java

@@ -3,10 +3,7 @@ package com.tsi.sig.server.service;
 import com.tsi.sig.server.app.AppUtils;
 import com.tsi.sig.server.mapper.MainMapper;
 import com.tsi.sig.server.repository.ApplicationRepository;
-import com.tsi.sig.server.vo.CvibNodeStatusVo;
-import com.tsi.sig.server.vo.CvibNodeVO;
-import com.tsi.sig.server.vo.CvibStatusVo;
-import com.tsi.sig.server.vo.ResultVo;
+import com.tsi.sig.server.vo.*;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.dao.DataAccessException;
 import org.springframework.stereotype.Service;
@@ -52,6 +49,7 @@ public class MainService {
 
         List<CvibNodeVO> regionList = this.getRegionList(paramMap);
         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        Date now = new Date();
         if (regionList.size() > 0) {
             for (CvibNodeVO vo : regionList) {
                 String nodeId = vo.getNodeid();
@@ -64,14 +62,25 @@ public class MainService {
                             connDate = format.parse(statusVo.getConnDate());
                         }
 
-                        if (statusVo.getDate() != null) {
-                            date = format.parse(statusVo.getDate());
+                        if (statusVo.getLocalDate() != null) {
+                            date = format.parse(statusVo.getLocalDate());
                         }
-                        if ( date     != null &&
-                             connDate != null &&
-                             connDate.getTime() - date.getTime() <= 3000) {
-                            resultList.add(repoMap.get(nodeId));
+
+                        if (repoMap.get(nodeId).getTurns().size() == 0) {
+                            repoMap.get(nodeId).setCommStatus(false);
+                        }
+                        if (now.getTime() - connDate.getTime() > 10000) {
+                            statusVo.setCommStatus(false);
+                            statusVo.setTurns(new ArrayList<>());
                         }
+//                        if ( date     != null &&
+//                             connDate != null &&
+//                             connDate.getTime() - date.getTime() <= 10000) {
+                            resultList.add(repoMap.get(nodeId));
+//                        }
+                    }
+                    else {
+                        statusVo.setTurns(new ArrayList<>());
                     }
                 }
             }
@@ -84,7 +93,7 @@ public class MainService {
 //            resultList = new ArrayList<>(resultMap.values());
 //        }
         long end = System.currentTimeMillis();
-        log.info("================ 소요시간: {} ms", (end - start));
+        //log.info("================ 소요시간: {} ms", (end - start));
         //List<CvibNodeStatusVo> resultList = new ArrayList<>();
 
 //        for (String nodeId : nodeIdList) {
@@ -106,10 +115,10 @@ public class MainService {
             try {
                 if (resultVo != null) {
                     Date conDate = repo.getStringParseToDate(resultVo.getConnDate());
-                    Date date = repo.getStringParseToDate(resultVo.getDate());
-                    if (conDate.getTime() - date.getTime() > 3000) {
-                        resultVo.setCommStatus(false);
-                        resultVo.setTurns(new ArrayList<>());
+                    Date date = repo.getStringParseToDate(resultVo.getLocalDate());
+                    if (conDate.getTime() - date.getTime() > 10000) {
+                        //resultVo.setCommStatus(false);
+                        //resultVo.setTurns(new ArrayList<>());
                     }
                 }
             }
@@ -177,4 +186,12 @@ public class MainService {
         }
         return result;
     }
+
+    public List<EvpServiceVo> getEvpServiceList() {
+        return this.mainMapper.getEvpServiceList();
+    }
+
+    public List<EvpRouteVo> getEvpRouteList(HashMap<String, Object> paramMap) {
+        return this.mainMapper.getEvpRouteList(paramMap);
+    }
 }

+ 1 - 1
src/main/java/com/tsi/sig/server/vo/CvibNodeStatusVo.java

@@ -18,7 +18,7 @@ public class CvibNodeStatusVo {
     public int errScu;
     public int errCenter;
     public int errCont;
-    public String date;
+    public String localDate;
     public int dataCount;
     public int counter;
     public int divFlag;

+ 1 - 1
src/main/java/com/tsi/sig/server/vo/CvibStatusVo.java

@@ -7,5 +7,5 @@ public class CvibStatusVo {
     private String nodeId;
     private boolean commStatus;
     private String connDate;
-    private String date;
+    private String localDate;
 }

+ 22 - 0
src/main/java/com/tsi/sig/server/vo/EvpPhaseVo.java

@@ -0,0 +1,22 @@
+package com.tsi.sig.server.vo;
+
+import lombok.Data;
+
+@Data
+public class EvpPhaseVo {
+    private String serviceId;
+    private Long seqNo;
+    private Long nodeId;
+    private int ring;
+    private int phaseNo;
+    private int planClass;
+    private int flowNo;
+    private double headLat;
+    private double headLng;
+    private double midLat;
+    private double midLng;
+    private double endLat;
+    private double endLng;
+    private int headAngle;
+    private int endAngle;
+}

+ 11 - 0
src/main/java/com/tsi/sig/server/vo/EvpRouteVo.java

@@ -0,0 +1,11 @@
+package com.tsi.sig.server.vo;
+
+import lombok.Data;
+
+@Data
+public class EvpRouteVo {
+    private String serviceId;
+    private Long seqNo;
+    private double lat;
+    private double lng;
+}

+ 22 - 0
src/main/java/com/tsi/sig/server/vo/EvpServiceVo.java

@@ -0,0 +1,22 @@
+package com.tsi.sig.server.vo;
+
+import lombok.Data;
+
+@Data
+public class EvpServiceVo {
+    private String serviceId;
+    private String clctDt;
+    private String evNo;
+    private double curLat;
+    private double curLng;
+    private String serviceNm;
+    private double arrLat;
+    private double arrLng;
+    private long arrTm;
+    private long vehLen;
+    private String ocrNo;
+    private String ocrType;
+    private long serviceDist;
+    private int statusCd;
+    private String statusDesc;
+}

+ 8 - 0
src/main/java/com/tsi/sig/server/vo/EvpSignalVo.java

@@ -0,0 +1,8 @@
+package com.tsi.sig.server.vo;
+
+import lombok.Data;
+
+@Data
+public class EvpSignalVo {
+
+}

+ 4 - 1
src/main/java/com/tsi/sig/server/websocket/kafka/AllTopicConsumerThread.java

@@ -89,7 +89,7 @@ public class AllTopicConsumerThread implements Runnable {
         Date date = new Date(((long) value.getInt(41) & 0xffffffffL) * 1000L);
         sdf.setTimeZone(TimeZone.getTimeZone("GMT+9"));
 
-        node.date = sdf.format(date);
+        node.localDate = sdf.format(date);
         node.connDate = sdf.format(new Date());
 
         int idx = 0;
@@ -112,6 +112,9 @@ public class AllTopicConsumerThread implements Runnable {
             turns.add(stts);
         }
         node.turns = turns;
+        if (turns.size() == 0) {
+            node.commStatus = false;
+        }
     }
 
     public List<String> formatPartitions(Collection<TopicPartition> partitions) {

+ 43 - 34
src/main/resources/application.yml

@@ -1,4 +1,9 @@
 spring:
+  profiles:
+    active: seoul
+  config:
+    import:
+      - optional:file:${user.dir}/conf/tsi-sig-server.yml
   application:
     name: tsi-sig-server
   datasource:
@@ -7,17 +12,41 @@ spring:
       driver-class-name: org.mariadb.jdbc.Driver
       username: cvim
       password: 44Klctest$$
-      jpool-name: hikari-cp
+      jpool-name: tsi-sig-server
       jmaximum-pool-size: 10
       jminimum-idle: 2
   mvc:
     view:
       prefix: /WEB-INF/jsp/
       suffix: .jsp
+  aop:
+    proxy-target-class: true
+  devtools:
+    livereload:
+      enabled: true
+    restart:
+      enabled: true
+
+management:
+  endpoints:
+    prometheus:
+      enabled: true
+    web:
+      exposure:
+        include: "*"
+    health:
+      show-details: "always"
+  security:
+    enabled: false
 
 server:
   port: 8443
   shutdown: graceful
+  tomcat:
+    use-relative-redirects: true
+    remoteip:
+      protocol-header: x-forwarded-proto
+      remote-ip-header: x-forwarded-for
   error:
     whitelabel:
       enabled: true
@@ -25,7 +54,11 @@ server:
     include-stacktrace: never
   servlet:
     session:
-      timeout: 300
+      timeout: 43200m
+      cookie:
+        name: sigcookie
+    max-http-header-size: 400000
+
 
 application:
   kafka:
@@ -40,38 +73,12 @@ application:
       #  - acks: 0
       #  - retries: 0
       #  - linger.ms: 1
----
-spring:
-  profiles: default
-  datasource:
-    mybatis:
-      #      jdbc-url: jdbc:mariadb://10.4.4.20:3306/cvim_db?characterEncoding=UTF-8&serverTimezone=UTC
-      jdbc-url: jdbc:mariadb://172.30.1.99:3306/cvim_db?characterEncoding=UTF-8&serverTimezone=UTC
-
-application:
-  kafka:
-    consumer:
-      #      bootstrap-servers: 172.24.0.30:9092,172.24.0.31:9093,172.24.0.32:9094
-      # bootstrap-servers: 61.82.138.91:19091,61.82.138.91:19092,61.82.138.91:19093
-      # bootstrap-servers: 172.30.1.121:9092, 172.30.1.123:9092, 172.30.1.124:9092
-      bootstrap-servers: 61.82.138.91:19091,61.82.138.91:19092,61.82.138.91:19093
-
----
-spring:
-  profiles: wonju
-  datasource:
-    mybatis:
-      jdbc-url: jdbc:mariadb://10.4.4.20:3307/cvim_db?characterEncoding=UTF-8&serverTimezone=UTC
-
-application:
-  kafka:
-    consumer:
-      bootstrap-servers: 123.142.27.53:9092,123.142.27.53:9093,123.142.27.53:9094
-      #bootstrap-servers: 10.4.4.30:9092,10.4.4.31:9093,10.4.4.32:9094
 
 ---
 spring:
-  profiles: seoul
+  config:
+    activate:
+      on-profile: seoul
   datasource:
     mybatis:
       jdbc-url: jdbc:mariadb://10.4.4.20:3306/cvim_db?characterEncoding=UTF-8&serverTimezone=UTC
@@ -80,12 +87,14 @@ application:
   kafka:
     consumer:
       #bootstrap-server 변경
-      #bootstrap-servers: 172.24.0.30:9092,172.24.0.31:9093,172.24.0.32:9094
-      bootstrap-servers: 61.82.138.91:19091,61.82.138.91:19092,61.82.138.91:19093
+      bootstrap-servers: 172.24.0.30:9092,172.24.0.31:9093,172.24.0.32:9094
+      #bootstrap-servers: 61.82.138.91:19091,61.82.138.91:19092,61.82.138.91:19093
 
 ---
 spring:
-  profiles: dev
+  config:
+    activate:
+      on-profile: dev
   datasource:
     mybatis:
       #jdbc-url: jdbc:mariadb://59.29.208.150:23307/cvim_db?characterEncoding=UTF-8&serverTimezone=UTC

+ 77 - 1
src/main/resources/mybatis/mapper/main.xml

@@ -7,7 +7,7 @@
 		SELECT nodeid as nodeid, name as name, FORMAT(latitude, 10) as lat, FORMAT(longitude, 9) as lng, addr1, addr2, addr3, addnode, ipaddr, nodetype, region_id as regionid
           FROM tb_tsc_node
          WHERE useyn  = 'Y'
-           AND nodeyn = 'Y'	
+           AND nodeyn = 'Y'
 	</select>
 
 	<select id="getCvibNodeInfo" parameterType="java.util.HashMap" resultType="com.tsi.sig.server.vo.CvibNodeVO">
@@ -29,5 +29,81 @@
 		  AND useyn  = 'Y'
           AND nodeyn = 'Y'
 	</select>
+	<!-- 서비스 상태 코드(1:진행중-서비스 진행중,2:정상종료-모든 교차로 제어 및 해제 완료,3:취소-아직 통과하지 않은 교차로 존재,
+						4:센터강제종료-운영자가 서비스를 강제로 종료,5:비정상종료-서비스가 존재하지 않음,6:서비스시작실패-제어대상교차로가 없음,
+						8:비정상종료-앱서버에 에러 발생,9:비정상종료-일정시간 앱에서 위치 및 속도 정보가 오지 않는 경우,
+						10:자동종료-경로이탈,11:자동종료-경로진입 가능시간 초과,12:자동종료-정차가능시간 초과,13:취소-모든 교차로 제어및 해제 완료,
+						14:실패-서비스 제어 요청 실패,15:실패-서비스 가능 교차로가 존재하지 않음,16:자동종료-위치정보 수신 가능 시간 초과)	-->
+	<select id="getEvpServiceList" resultType="com.tsi.sig.server.vo.EvpServiceVo">
+		SELECT SERVICE_ID,
+			   CLCT_DT,
+			   EV_NO,
+			   CUR_LAT,
+			   CUR_LNG,
+			   SERVICE_NM,
+			   ARR_LAT,
+			   ARR_LNG,
+			   ARR_TM,
+			   VEH_LEN,
+			   OCR_NO,
+			   OCR_TYPE,
+			   SERVICE_DIST,
+			   STATUS_CD,
+			   IF(STATUS_CD = 1,  '진행중',
+			   IF(STATUS_CD = 2,  '정상종료',
+			   IF(STATUS_CD = 3,  '취소-아직 통과하지 않은 교차로 존재',
+			   IF(STATUS_CD = 4,  '센터강제종료-운영자가 서비스를 강제 종료',
+			   IF(STATUS_CD = 5,  '비정상종료-서비스가 존재하지 않음',
+			   IF(STATUS_CD = 6,  '서비스 시작 실패',
+			   IF(STATUS_CD = 7,  '비정상정료-앱서버에 에러 발생',
+			   IF(STATUS_CD = 8,  '비정상종료-일정시간 앱에서 위치 및 속도 정보가 오지 않음',
+			   IF(STATUS_CD = 9,  '자동종료-경로이탈',
+			   IF(STATUS_CD = 10, '자동종료-경로진입 가능시간 초과',
+			   IF(STATUS_CD = 11, '자동종료-정차가능시간 초과',
+			   IF(STATUS_CD = 12, '취소',
+			   IF(STATUS_CD = 13, '실패-서비스 제어 요청',
+			   IF(STATUS_CD = 14, '실패-서비스 가능 교차로가 존재하지 않음',
+			   IF(STATUS_CD = 15, '자동종료', '-'
+			   ))))))))))))))) AS STATUS_DESC
+		FROM tb_evp_service
+		WHERE DATE_FORMAT(CLCT_DT, '%Y-%m-%d') = CURDATE()
+	</select>
+	<select id="getEvpRouteList" parameterType="java.util.HashMap" resultType="com.tsi.sig.server.vo.EvpRouteVo">
+		SELECT SERVICE_ID,
+		       SEQ_NO,
+		       LAT,
+		       LNG
+		  FROM TB_EVP_ROUTE
+		WHERE SERVICE_ID = #{serviceId}
+	</select>
+	<select id="getEvpPhaseList" parameterType="java.util.HashMap" resultType="com.tsi.sig.server.vo.EvpPhaseVo">
+		SELECT SERVICE_ID,
+			   SEQ_NO,
+			   NODE_ID,
+			   RING,
+		       PHASE_NO,
+		       PLAN_CLASS,
+		       FLOW_NO,
+		       HEAD_LAT,
+		       HEAD_LNG,
+		       MID_LAT,
+		       MID_LNG,
+		       END_LAT,
+		       END_LNG,
+		       HEAD_ANGLE,
+		       END_ANGLE
+		FROM TB_EVP_PHASE
+	</select>
 
+	<select id="getEvpSignalList" parameterType="java.util.HashMap" resultType="com.tsi.sig.server.vo.EvpSignalVo">
+		SELECT SERVICE_ID,
+			   SEQ_NO,
+			   NODE_ID,
+			   STATE,
+			   PLAN_CLASS,
+			   A_RING_PHASE,
+			   B_RING_PHASE,
+			   HOLD_PHASE
+		FROM TB_EVP_SIGNAL
+	</select>
 </mapper>

+ 81 - 2
src/main/resources/static/css/main.css

@@ -402,7 +402,7 @@ table {
 .mapToggle {
     z-index: 100000;
     height: 24px;
-    width: 410px;
+    width: 430px;
     position: absolute;
     left: 10px;
     top: 3px;
@@ -1173,9 +1173,40 @@ div.popState dl.jaywork dd strong.rblue {color:#607cd4; text-align:right;}
 /* 트리리스트 */
 
 /* .leftMenu .iframeTreeList{width:95%;height:calc(100% - 45px);overflow:auto;padding:5px} */
+.leftMenu .searchBox {
+    width: 100%;
+    height: 40px;
+    border-bottom: 1px solid #dddddd;
+    display: flex;
+    gap: 5px;
+    justify-content: center;
+    align-items: center;
+}
+.leftMenu .searchBox .search-button {
+    padding: 6px;
+    border: 1px solid #eeeeee;
+    background-color: #1e65c5;
+    color: white;
+    width: 50px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 5px;
+    font-weight: bold;
+    cursor: pointer;
+}
+
+.leftMenu .searchBox .search-button:hover{
+    filter: brightness(1.1);
+}
+
+.leftMenu .searchBox input {
+    width: 210px;
+    padding: 5px;
+}
 
 .leftMenu #intTree {
-    height: calc(100% - 45px);
+    height: calc(100% - 82px);
     overflow: auto;
     padding: 5px
 }
@@ -3159,6 +3190,54 @@ cursor: pointer;border: 1px solid #ebebeb;border-bottom-color: #e2e2e2;border-ra
     margin: 0 auto
 }
 
+
+.coordBox {
+    background-color: white;
+    gap: 10px;
+    width: 470px;
+    height: 40px;
+    display: none;
+    align-items: center;
+    justify-content: center;
+    position: absolute;
+    z-index: 2;
+    left: 85px;
+    top: 28px;
+    border: 1px solid #c9c9c9;
+}
+
+.coordBox.on {
+    display: flex;
+}
+
+.coord-unit {
+    display: flex;
+    gap: 5px;
+    align-items: center;
+    font-weight: bold;
+    font-size: 12px;
+}
+
+.coord-unit input {
+    padding: 3px 5px;
+    font-weight: bold;
+    width : 110px;
+}
+
+.coord-btn {
+    border: 1px solid #1e65c5;
+    background-color: #1e65c5;
+    color: white;
+    font-weight: bold;
+    cursor: pointer;
+    border-radius: 5px;
+    padding: 3px 18px;
+}
+
+.coord-btn:hover {
+    filter:brightness(1.1);
+}
+
 @media (max-width: 1000px) {
     .crossLoadMap {
         width: 100%;

+ 9 - 0
src/main/resources/static/images/car.svg

@@ -0,0 +1,9 @@
+<svg width="32" height="39" viewBox="0 0 32 39" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="ic_&#236;&#158;&#144;&#235;&#143;&#153;&#236;&#176;&#168;1">
+<path id="Vector" d="M32 15.9545C32 28.3636 16 39 16 39C16 39 0 28.3636 0 15.9545C6.32325e-08 11.7231 1.68571 7.66504 4.68629 4.67298C7.68687 1.68092 11.7565 0 16 0C20.2435 0 24.3131 1.68092 27.3137 4.67298C30.3143 7.66504 32 11.7231 32 15.9545Z" fill="white"/>
+<path id="Vector_2" d="M30.25 15.8333C30.25 26.9166 16 36.4166 16 36.4166C16 36.4166 1.75 26.9166 1.75 15.8333C1.75 12.0539 3.25133 8.42937 5.92373 5.75698C8.59612 3.08459 12.2207 1.58325 16 1.58325C19.7793 1.58325 23.4039 3.08459 26.0763 5.75698C28.7487 8.42937 30.25 12.0539 30.25 15.8333Z" fill="#02B393"/>
+<g id="&#236;&#158;&#144;&#235;&#143;&#153;&#236;&#176;&#168;1">
+<path d="M23 17.125C23 17.6082 22.6082 18 22.125 18V21.0625C22.125 21.7782 21.7813 22.4136 21.25 22.8127V24.5625C21.25 24.8041 21.0541 25 20.8125 25H19.0625C18.8209 25 18.625 24.8041 18.625 24.5625V23.25H13.375V24.5625C13.375 24.8041 13.1791 25 12.9375 25H11.1875C10.9459 25 10.75 24.8041 10.75 24.5625V22.8127C10.2187 22.4136 9.875 21.7782 9.875 21.0625V18C9.39175 18 9 17.6082 9 17.125V15.375C9 14.8918 9.39175 14.5 9.875 14.5V13.3097C9.875 12.2702 10.6147 11.3569 11.6808 11.2344C12.6823 11.1193 14.1631 11 16 11C17.8369 11 19.3177 11.1193 20.3192 11.2344C21.3853 11.3569 22.125 12.2702 22.125 13.3097V14.5C22.6082 14.5 23 14.8918 23 15.375V17.125ZM12.1077 13.8179C13.066 13.7183 14.3924 13.625 16 13.625C17.6075 13.625 18.934 13.7183 19.8922 13.8179C20.1326 13.8429 20.3477 13.6683 20.3726 13.428C20.3976 13.1876 20.2231 12.9726 19.9827 12.9476C18.998 12.8452 17.6411 12.75 16 12.75C14.3588 12.75 13.002 12.8452 12.0172 12.9476C11.7769 12.9726 11.6023 13.1876 11.6273 13.428C11.6523 13.6683 11.8674 13.8429 12.1077 13.8179ZM16 14.5C14.3588 14.5 13.002 14.5952 12.0172 14.6976C11.7943 14.7208 11.625 14.9086 11.625 15.1327V18.2422C11.625 18.4663 11.7943 18.6542 12.0172 18.6774C13.002 18.7798 14.3588 18.875 16 18.875C17.6411 18.875 18.998 18.7798 19.9827 18.6774C20.2056 18.6542 20.375 18.4663 20.375 18.2422V15.1327C20.375 14.9086 20.2056 14.7208 19.9827 14.6976C18.998 14.5952 17.6411 14.5 16 14.5ZM13.375 20.625C13.375 20.1418 12.9832 19.75 12.5 19.75C12.0167 19.75 11.625 20.1418 11.625 20.625C11.625 21.1082 12.0167 21.5 12.5 21.5C12.9832 21.5 13.375 21.1082 13.375 20.625ZM20.375 20.625C20.375 20.1418 19.9832 19.75 19.5 19.75C19.0167 19.75 18.625 20.1418 18.625 20.625C18.625 21.1082 19.0167 21.5 19.5 21.5C19.9832 21.5 20.375 21.1082 20.375 20.625ZM14.25 20.625C14.25 21.1082 14.6417 21.5 15.125 21.5H16.875C17.3582 21.5 17.75 21.1082 17.75 20.625C17.75 20.1418 17.3582 19.75 16.875 19.75H15.125C14.6417 19.75 14.25 20.1418 14.25 20.625Z" fill="white"/>
+</g>
+</g>
+</svg>

BIN
src/main/resources/static/images/find_coord_off.png


BIN
src/main/resources/static/images/find_coord_on.png


BIN
src/main/resources/static/images/refresh.png


+ 1 - 1
src/main/resources/static/js/common/common-ajax.js

@@ -1 +1 @@
-/**
 * @param {Object} url
 * @param {Object} param
 * @param {Object} callback
 */
function requestService(url, param, callback, async) {
    /*
     * 스프링시큐리티 csrf 토큰 에러때문에  ajax 통신시 해더에 포함해줘야한다.
     */
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");

    if (async) {

        async = true;
    }

    $.ajax({
        url: _WEB_CONTEXT_NAME + "/" + url
        , data: encodeURI(param)
        , cache: false
        , async: async
        , type: 'POST'
        , success: callback
        , error: whenError
        , beforeSend: function (xhr) {
            xhr.setRequestHeader(header, token);
        }
    });
}

/**
 *
 * @param url
 * @param formName
 * @param callback
 * @param async
 */
function requestServiceForm(url, formName, callback, async) {

    if (async) {

        async = true;
    }

    $("#" + formName).ajaxSubmit({

        url: _WEB_CONTEXT_NAME + "/" + url
        , data: $(this).serialize()
        , cache: false
        , async: async
        , type: 'POST'
        , success: callback
        , error: whenError

    });
}

function whenError() {

    console.log('ajax error');

}
+/**
 * @param {Object} url
 * @param {Object} param
 * @param {Object} callback
 */
function requestService(url, param, callback, async) {
    /*
     * 스프링시큐리티 csrf 토큰 에러때문에  ajax 통신시 해더에 포함해줘야한다.
     */

    if (async) {

        async = true;
    }

    const token = $("meta[name='_csrf']").attr("content");
    const header = $("meta[name='_csrf_header']").attr("content");
    $.ajax({
        url: _WEB_CONTEXT_NAME + "/" + url
        , data: encodeURI(param)
        , cache: false
        , async: async
        , type: 'POST'
        , success: callback
        , error: whenError
        , beforeSend: function (xhr) {
            xhr.setRequestHeader(header, token);
        }
    });
}

/**
 *
 * @param url
 * @param formName
 * @param callback
 * @param async
 */
function requestServiceForm(url, formName, callback, async) {

    if (async) {

        async = true;
    }

    $("#" + formName).ajaxSubmit({

        url: _WEB_CONTEXT_NAME + "/" + url
        , data: $(this).serialize()
        , cache: false
        , async: async
        , type: 'POST'
        , success: callback
        , error: whenError

    });
}

function whenError() {

    console.log('ajax error');

}

+ 6 - 4
src/main/resources/static/js/common/cvibDetail.js

@@ -20,6 +20,9 @@ function getCvibList(paramId) {
     $.ajax({
         method: 'POST',
         url : '/getCvibDetail.do',
+        headers:
+            {'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')}
+        ,
         data : {
             nodeId : paramId
         },
@@ -72,7 +75,7 @@ function recvDetailData(recv) {
     recv.turns.sort(dirCodeSort);
 
     let {
-        nodeId, date, errCenter, errCont, errScu, oprBlink, oprInd,
+        nodeId, localDate, errCenter, errCont, errScu, oprBlink, oprInd,
         oprManual, oprTrans, oprTurnoff, dataCount, counter, commStatus
     } = recv;
     if (!commStatus) {
@@ -85,7 +88,7 @@ function recvDetailData(recv) {
         oprManual  = 2;
         oprTurnoff = 2;
         dataCount  = "-";
-        date       = "-";
+        localDate  = "-";
         counter    = "-";
     }
     errCenter  = transStatusCodeToWord(errCenter, 0);
@@ -134,7 +137,7 @@ function recvDetailData(recv) {
     // }
 
     _CvibNode = new CvibInfo(nodeId, nodeName, nodeLat, nodeLng, nodeAddr1,
-        nodeAddr2, nodeAddr3, date, errCenter, errCont, errScu, oprBlink, oprInd, oprManual, oprTrans, oprTurnoff, dataCount, counter,
+        nodeAddr2, nodeAddr3, localDate, errCenter, errCont, errScu, oprBlink, oprInd, oprManual, oprTrans, oprTurnoff, dataCount, counter,
         cvibSttsArr[10], cvibSttsArr[20], cvibSttsArr[30], cvibSttsArr[40], cvibSttsArr[50], cvibSttsArr[60], cvibSttsArr[70], cvibSttsArr[80],
         light, dirAdd, timeFlag, walker, unprotected, stts, dispTm, remainTm, dirCode);
 
@@ -157,7 +160,6 @@ function recvDetailData(recv) {
 
 function setCvibSttsFnc(unprotected, light, dirCode, stts, cvibStts) {
     if (cvibStts === undefined || cvibStts === "" || cvibStts === null) {
-        //TODO: 20230221
         cvibStts = '9_9_9_9';
     }
 

+ 9 - 4
src/main/resources/static/js/common/cvibStatus.js

@@ -15,9 +15,14 @@ function getCvibList() {
     const minY = map.getBounds().getSouthWest().getLat();
     const maxX =map.getBounds().getNorthEast().getLng();
     const maxY = map.getBounds().getNorthEast().getLat();
+    const token = $("meta[name='_csrf']").attr("content");
+    const header = $("meta[name='_csrf_header']").attr("content");
     $.ajax({
         method: 'POST',
         url : '/getCvibList.do',
+        beforeSend: (xhr) => {
+            xhr.setRequestHeader(header, token)
+        },
         data : {
             minX : minX,
             minY : minY,
@@ -29,7 +34,7 @@ function getCvibList() {
             recvCvibListData(res);
             let end = new Date().getTime();
             let differ = end - start;
-            console.log('소요시간 :',differ, 'ms');
+            // console.log('소요시간 :',differ, 'ms');
             if ((differ) >= 1000) {
                 differ = 1000;
             }
@@ -69,7 +74,7 @@ function recvCvibListData(recv) {
             let cvibSttsArr = [...cvibSttsDefault];
            // 직진_좌회전_보행_유턴
             let nodeId      = sectionList[ii].nodeId;
-            let date        = sectionList[ii].date;
+            let localDate   = sectionList[ii].localDate;
             let dataCount   = sectionList[ii].dataCount;
             let counter     = sectionList[ii].counter;
             // let connDate    = sectionList[ii].connDate;
@@ -102,7 +107,7 @@ function recvCvibListData(recv) {
             if (sigIdx >= 0) {
                 const signal = signalArr[sigIdx];
                 const cvib   = new CvibInfo(signal.nodeId, signal.name, signal.Lat, signal.Lng, signal.addr1,
-                    signal.addr2, signal.addr3, date, errCenter, errCont, errScu, oprBlink, oprInd, oprManual, oprTrans, oprTurnoff, dataCount, counter,
+                    signal.addr2, signal.addr3, localDate, errCenter, errCont, errScu, oprBlink, oprInd, oprManual, oprTrans, oprTurnoff, dataCount, counter,
                     cvibSttsArr[10], cvibSttsArr[20], cvibSttsArr[30], cvibSttsArr[40], cvibSttsArr[50], cvibSttsArr[60], cvibSttsArr[70], cvibSttsArr[80],
                     light, dirAdd, timeFlag, walker, unprotected, stts, dispTm, remainTm, dirCode);
 
@@ -183,7 +188,7 @@ function recvCvibListData(recv) {
             $nodeEl.find('#errCenter').empty().text(errCenter);
             $nodeEl.find('#errCont').empty().text(errCont);
             $nodeEl.find('#counter').empty().text(counter);
-            $nodeEl.find('#date').empty().text(date);
+            $nodeEl.find('#date').empty().text(localDate);
             // for (let ii = 0; ii < _signalInfoArr.length; ii++) {
             //     if (_signalInfoArr[ii].nodeId == nodeId) {
             //         _signalInfoArr[ii].date = date;

+ 573 - 8
src/main/resources/static/js/map.js

@@ -39,12 +39,30 @@ $(document).ready(function () {
     init();
 });
 
+let sigData =
+    {
+        service_id : 1,
+        seq_no : 1,
+        node_id : 1,
+        ring : 1,
+        head_lat : 126.5169020,
+        head_lng : 33.2475970,
+        mid_lat : 126.5170416,
+        mid_lng : 33.2475840,
+        end_lat : 126.5170395,
+        end_lng : 33.2474713
+    }
+
+
+
 function init() {
     //정보관리창 닫을때 regionCd intNo 가저와서 해당 위치로 이동..
     var masterLng = sessionStorage.getItem('masterLng');
     var masterLat = sessionStorage.getItem('masterLat');
-    var lat = 35.871595504565676;
-    var lng = 128.601612749012;
+    //var lat = 35.871595504565676;
+    //var lng = 128.601612749012;
+    var lat = '37.5630934';
+    var lng = '126.9752351';
 
     if (masterLng != null && masterLat != null) {
         lat = masterLat;
@@ -54,11 +72,12 @@ function init() {
     mapContainer = document.getElementById('map') // 지도를 표시할 div
     var mapOption = {
         center: new kakao.maps.LatLng(lat, lng),
-        level: _Level
+        level: _Level,
+        maxLevel : 11,
     };
 
     map = new kakao.maps.Map(mapContainer, mapOption); // 지도생성
-    map.setMaxLevel(11);
+   // map.setMaxLevel(11);
     // 지도와 스카이뷰 지도타입 컨트롤을 생성
     var mapTypeControl = new kakao.maps.MapTypeControl();
     // 지도에 컨트롤을 추가해야 지도위에 표시됩니다
@@ -71,6 +90,8 @@ function init() {
     // 신호현시 줌,이동 이벤트 분리
     kakao.maps.event.addListener(map, 'zoom_changed', mapZoomBound);
     kakao.maps.event.addListener(map, 'dragend', mapMoveBound);//드래그 종료시 이벤트발생
+    kakao.maps.event.addListener(map, 'rightclick', mapRightClick);//드래그 종료시 이벤트발생
+    kakao.maps.event.addListener(map, 'click', mapClick);//드래그 종료시 이벤트발생
     //kakao.maps.event.addListener(map, 'idle', mapIdle);
 
     /* // 맵클릭했을때 위도경도 찾기
@@ -79,7 +100,313 @@ function init() {
         console.log(nowlatLng);
     });*/
 
-    getSignalInfo();
+    //getSignalInfo();
+    // const worker = new Worker('/js/worker.js');
+    // worker.postMessage({message:"worker START"});
+    // worker.onmessage = (ev)=>{
+    //     const data = JSON.parse(ev.data);
+    //     console.log(data);
+    // }
+
+}
+
+let coordMarker;
+function mapRightClick(e) {
+    if (!$('.road_on')[0]) {
+        if (coordMarker) {
+            coordMarker.close();
+        }
+        const textArea = $('<textarea>');
+        let xCoordinate = e.latLng.getLng().toString();
+        let yCoordinate = e.latLng.getLat().toString();
+
+        if (xCoordinate.length > 11) {
+            xCoordinate = xCoordinate.substring(0, 11);
+        }
+
+        if (yCoordinate.length > 10) {
+            yCoordinate = yCoordinate.substring(0, 10);
+        }
+        const text = xCoordinate + "," + yCoordinate;
+        textArea.val(text);
+        $('body').append(textArea);
+        textArea[0].select();
+        document.execCommand("copy");
+        textArea.remove();
+        coordMarker = new kakao.maps.InfoWindow({
+            map: map,
+            position: e.latLng,
+            content: '<div style="width:150px; text-align: center;">' + text + '</div>',
+        });
+    }
+}
+
+function mapClick(){
+    if (coordMarker) {
+        coordMarker.close();
+    }
+}
+
+function getKakaoPosition(y, x) {
+    return new kakao.maps.LatLng(y, x);
+}
+
+let testData = [
+    {
+        service_id : 1,
+        lat : 126.5240617,
+        lng : 33.2471400,
+        seq_no : 1,
+    },
+    {
+        service_id : 1,
+        lat : 126.5229991,
+        lng : 33.2471900,
+        seq_no : 2,
+    },
+    {
+        service_id : 1,
+        lat : 126.5210876,
+        lng : 33.2474622,
+        seq_no : 3,
+    },{
+        service_id : 1,
+        lat : 126.5191443,
+        lng : 33.2476711,
+        seq_no : 4,
+    },
+    {
+        service_id : 1,
+        lat : 126.5167390,
+        lng : 33.2479593,
+        seq_no : 5,
+    },
+    {
+        service_id : 1,
+        lat : 126.5141087,
+        lng : 33.2482105,
+        seq_no : 7,
+    },
+    {
+        service_id : 2,
+        lat : 126.5141180,
+        lng : 33.2479716,
+        seq_no : 1,
+    },
+    {
+        service_id : 2,
+        lat : 126.5141180,
+        lng : 33.2479716,
+        seq_no : 2,
+    },
+    {
+        service_id : 2,
+        lat : 126.5154815,
+        lng : 33.2478417,
+        seq_no : 3,
+    },
+    {
+        service_id : 2,
+        lat : 126.5169955,
+        lng : 33.2476673,
+        seq_no : 4,
+    },
+    {
+        service_id : 2,
+        lat : 126.5187242,
+        lng : 33.2474756,
+        seq_no : 5,
+    },
+    {
+        service_id : 2,
+        lat : 126.5204636,
+        lng : 33.2472750,
+        seq_no : 6,
+    },
+    {
+        service_id : 2,
+        lat : 126.5223642,
+        lng : 33.2470298,
+        seq_no : 7,
+    },{
+        service_id : 2,
+        lat : 126.5240712,
+        lng : 33.2468741,
+        seq_no : 8,
+    },
+]
+
+function emergencyEvent() {
+
+}
+
+function test() {
+    if (testData && testData.length) {
+        testData.sort((a,b)=>{
+            return a.service_id > b.service_id ? 1 : a.service_id < b.service_id ?  -1 : a.seq_no > b.seq_no ? 1 : a.seq_no === b.seq_no ? 0 : -1
+        });
+        const objMap = new Map();
+        for (let data of testData) {
+            if (!objMap.get(data.service_id)) {
+                objMap.set(data.service_id, {service_id : data.service_id, coordinateArr : []});
+            }
+            objMap.get(data.service_id).coordinateArr.push({
+              lng : data.lng,
+              lat : data.lat,
+            })
+        }
+        objMap.forEach((obj)=>{
+            const road = new emergencyRoad(obj);
+            road.init();
+            road.drawPolyLine();
+            road.setBound();
+        })
+    }
+}
+
+function emergencySignal(data){
+    if ( data && data.length > 0 ) {
+        data.sort((a,b)=>{
+            return a.service_id > b.service_id ? 1 : a.service_id < b.service_id ?  -1 : a.seq_no > b.seq_no ? 1 : a.seq_no === b.seq_no ? 0 : -1
+        });
+        let lineMap = new Map();
+        const bounds = new kakao.maps.LatLngBounds();
+        bounds.extend(getKakaoPosition(33.2471400, 126.5240617));
+        bounds.extend(getKakaoPosition(33.2482105,126.5141087));
+        map.setBounds(bounds);
+        const markerMap = new Map();
+        for (let obj of data) {
+            const position = getKakaoPosition(obj.lng, obj.lat);
+            if (!lineMap.get(obj.service_id)) {
+                lineMap.set(obj.service_id, []);
+                let markImage = new kakao.maps.MarkerImage(
+                    '/images/car.svg',
+                    new kakao.maps.Size(32, 39), {
+                         offset: new kakao.maps.Point(16, 39)
+                    });
+
+
+                const marker = new kakao.maps.Marker({
+                    image: markImage,
+                    position: position,
+                });
+                markerMap.set(obj.service_id, marker);
+            }
+
+            lineMap.get(obj.service_id).push(position);
+        }
+        let polyMap = new Map();
+        lineMap.forEach((position, key)=>{
+            let backLine = new kakao.maps.Polyline({
+                path: position, // 선을 구성하는 좌표 배열입니다 클릭한 위치를 넣어줍니다
+                strokeWeight: 10, // 선의 두께입니다
+                strokeColor: '#ffffff', // 선의 색깔입니다
+                strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
+                strokeStyle: 'solid', // 선의 스타일입니다
+                zIndex: 1,
+            });
+
+            // 선이 그려지고 있을 때 마우스 움직임에 따라 선이 그려질 위치를 표시할 선을 생성합니다
+            let polyline = new kakao.maps.Polyline({
+                strokeWeight: 8, // 선의 두께입니다
+                path: position,
+                strokeColor: '#3386ff', // 선의 색깔입니다
+                strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
+                strokeStyle: 'solid', // 선의 스타일입니다
+                zIndex: 2,
+            });
+            backLine.setMap(map);
+            polyline.setMap(map);
+            polyMap.set(key, [backLine, polyline]);
+        });
+
+        markerMap.forEach((marker, key)=>{
+            marker.setMap(map);
+            if (lineMap.get(key)) {
+                const arr = lineMap.get(key);
+                let cnt = 0;
+                const timer = setInterval(()=>{
+                    if (arr[cnt]) {
+                        marker.setPosition(arr[cnt++])
+                    }
+                    else {
+                        marker.setMap(null);
+                        const polyArr = polyMap.get(key);
+                        if (polyArr && polyArr.length === 2) {
+                            polyArr[0].setMap(null);
+                            polyArr[1].setMap(null);
+                        }
+                        clearInterval(timer);
+                    }
+                }, 1000)
+            }
+        })
+
+    }
+}
+
+function drawEmergencyRoad(serviceId) {
+    requestService('/getEvpList.do', {serviceId : serviceId}, (data)=>{
+        data.sort((a,b)=>{
+            return a.service_id > b.service_id ? 1 : a.service_id < b.service_id ?  -1 : a.seq_no > b.seq_no ? 1 : a.seq_no === b.seq_no ? 0 : -1
+        });
+        let lineMap = new Map();
+        const bounds = new kakao.maps.LatLngBounds();
+        bounds.extend(getKakaoPosition(33.2471400, 126.5240617));
+        bounds.extend(getKakaoPosition(33.2482105,126.5141087));
+        map.setBounds(bounds);
+        const markerMap = new Map();
+        for (let obj of data) {
+            const position = getKakaoPosition(obj.lng, obj.lat);
+            if (!lineMap.get(obj.service_id)) {
+                lineMap.set(obj.service_id, []);
+                let markImage = new kakao.maps.MarkerImage(
+                    '/images/car.svg',
+                    new kakao.maps.Size(32, 39), {
+                        //마커의 좌표에 해당하는 이미지의 위치를 설정합니다.
+                        //이미지의 모양에 따라 값은 다를 수 있으나, 보통 width/2, height를 주면 좌표에 이미지의 하단 중앙이 올라가게 됩니다.
+                        offset: new kakao.maps.Point(16, 39)
+                    });
+
+
+                const marker = new kakao.maps.Marker({
+                    image: markImage,
+                    position: position,
+                });
+                markerMap.set(obj.service_id, marker);
+            }
+
+            lineMap.get(obj.service_id).push(position);
+        }
+        let polyMap = new Map();
+        lineMap.forEach((position, key)=>{
+            let backLine = new kakao.maps.Polyline({
+                path: position, // 선을 구성하는 좌표 배열입니다 클릭한 위치를 넣어줍니다
+                strokeWeight: 10, // 선의 두께입니다
+                strokeColor: '#ffffff', // 선의 색깔입니다
+                strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
+                strokeStyle: 'solid', // 선의 스타일입니다
+                zIndex: 1,
+            });
+
+            // 선이 그려지고 있을 때 마우스 움직임에 따라 선이 그려질 위치를 표시할 선을 생성합니다
+            let polyline = new kakao.maps.Polyline({
+                strokeWeight: 8, // 선의 두께입니다
+                path: position,
+                strokeColor: '#3386ff', // 선의 색깔입니다
+                strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
+                strokeStyle: 'solid', // 선의 스타일입니다
+                zIndex: 2,
+            });
+            backLine.setMap(map);
+            polyline.setMap(map);
+            polyMap.set(key, [backLine, polyline]);
+        });
+    })
+}
+
+function moveMarker(position, marker) {
+    marker.setCenter(position);
 }
 
 function customOverlayFnc(intNm, xCoord, yCoord) {
@@ -303,13 +630,13 @@ function getRoadView() {
 
 //로드뷰에서 X버튼을 눌렀을 때 로드뷰를 지도 뒤로 숨기는 함수입니다
 function closeRoadview() {
-    //$('#mapWrapper').css('width','100%');
-    var width = wideFlag ? 'calc(100% - 10px)':'calc(100% - 338px)';
+    const width = wideFlag ? 'calc(100% - 10px)':'calc(100% - 338px)';
     $('#mapWrapper').css('width', width);
-    //$('#rvWrapper').css('width','0px');
     $('#rvWrapper').css('display','none');
+
     // 지도의 크기가 변경되었기 때문에 relayout 함수를 호출합니다
     map.relayout();
+
     // 현재 마커가 놓인 자리의 좌표입니다
     if(roadviewMarker != null)
     {
@@ -1200,4 +1527,242 @@ function toggleSiginfo() {
         if (customOverlay != null) customOverlay.setMap(null);
     } else _ToggleSiginfo = false;
     getSignalInfo();
+}
+
+
+/****************************************************************************
+ * 좌표검색
+ ****************************************************************************/
+function moveToPosition() {
+    const $lctnX = $('#lctn_x_coord');
+    const $lctnY = $('#lctn_y_coord');
+    var xCrdn = $lctnX.val();
+    var yCrdn = $lctnY.val();
+    if (!xCrdn) {
+        alert("X 좌표 값을 입력해주세요.");
+        return $lctnX.focus();
+    }
+
+    if (!yCrdn) {
+        alert("Y 좌표 값을 입력해주세요.");
+        return $lctnY.focus();
+    }
+
+    if (isNaN(Number(xCrdn)) || xCrdn.length < 3) {
+        alert("X 좌표 : "+xCrdn+"\n부적절한 좌표값입니다. 다시 입력해주세요.");
+        return $lctnX.focus();
+    }
+
+    if (isNaN(Number(yCrdn)) || yCrdn.lenth < 2) {
+        alert("Y 좌표 : "+yCrdn+"\n부적절한 좌표값입니다. 다시 입력해주세요.");
+        return $lctnY.focus();
+    }
+
+    if (!xCrdn.includes('.')) {
+        xCrdn = xCrdn.substring(0, 3) + '.' + xCrdn.substring(3, xCrdn.length);
+    }
+
+    if (!yCrdn.includes('.')) {
+        yCrdn = yCrdn.substring(0, 2) + '.' + yCrdn.substring(2, yCrdn.length);
+    }
+    const position = new kakao.maps.LatLng(yCrdn, xCrdn);
+    map.setCenter(position);
+    map.setLevel(3);
+    mapZoomBound();
+}
+
+class emergencyRoad{
+    constructor({service_id, coordinateArr}) {
+        this.service_id = service_id;
+        this.coordinateArr = coordinateArr;
+        this.backPoly = null;
+        this.polyLine = null;
+    }
+
+    init() {
+        if (this.coordinateArr && this.coordinateArr.length > 0) {
+            let lineArr = [];
+            for (let coordinate of this.coordinateArr) {
+                lineArr.push(getKakaoPosition(coordinate.lng, coordinate.lat));
+            }
+            this.backPoly = new kakao.maps.Polyline({
+                path: lineArr, // 선을 구성하는 좌표 배열입니다 클릭한 위치를 넣어줍니다
+                strokeWeight: 10, // 선의 두께입니다
+                strokeColor: '#ffffff', // 선의 색깔입니다
+                strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
+                strokeStyle: 'solid', // 선의 스타일입니다
+                zIndex: 1,
+            });
+
+            // 선이 그려지고 있을 때 마우스 움직임에 따라 선이 그려질 위치를 표시할 선을 생성합니다
+            this.polyLine = new kakao.maps.Polyline({
+                strokeWeight: 8, // 선의 두께입니다
+                path: lineArr,
+                strokeColor: '#3386ff', // 선의 색깔입니다
+                strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
+                strokeStyle: 'solid', // 선의 스타일입니다
+                zIndex: 2,
+            });
+        }
+    }
+
+    drawPolyLine() {
+        if (this.polyLine && this.backPoly) {
+            this.polyLine.setMap(map);
+            this.backPoly.setMap(map);
+        }
+    }
+
+    hidePolyLine() {
+        if (this.polyLine && this.backPoly) {
+            this.polyLine.setMap(null);
+            this.backPoly.setMap(null);
+        }
+    }
+
+    setBound() {
+        const bounds = new kakao.maps.LatLngBounds();
+        const lastIdx = this.coordinateArr.length - 1;
+        bounds.extend(getKakaoPosition( this.coordinateArr[0].lng,  this.coordinateArr[0].lat));
+        bounds.extend(getKakaoPosition(this.coordinateArr[lastIdx].lng,  this.coordinateArr[lastIdx].lat));
+        map.setBounds(bounds);
+    }
+}
+
+const stateMap = new Map();
+stateMap.set(0, '#FF0000');
+stateMap.set(1, '#00FF00');
+stateMap.set(2, '#FFFF00');
+stateMap.set(3, '#808080');
+stateMap.set(4, '#CC6600');
+stateMap.set(5, '#9933FF');
+
+// 1~5 레발 까지만 보임
+// 통신 이상 : 0 (#FF0000, 빨강), 정상 : 1 (#00FF00, 초록), 점멸 : 2 (#FFFF00, 노랑), 소등 : 3 (#808080, 회색), 수동진행 : 4 (#CC6600, 갈색), 현시 유지 : 5 (#9933FF, 보라)
+class Ring{
+    constructor({service_id, seq_no, node_id, ring, phase_no, plan_class, flow_no, head_lat, head_lng, mid_lat, mid_lng, end_lat, end_lng, head_angle, end_angle, state}) {
+        this.service_id = service_id;
+        this.seq_no = seq_no;
+        this.node_id = node_id;
+        this.ring = ring;
+        this.phase_no = phase_no;
+        this.plan_class = plan_class;
+        this.flow_no = flow_no;
+        this.head_lat = head_lat;
+        this.head_lng = head_lng;
+        this.mid_lat = mid_lat;
+        this.mid_lng = mid_lng;
+        this.end_lat = end_lat;
+        this.end_lng = end_lng;
+        this.head_angle = head_angle;
+        this.end_angle = end_angle;
+        this.state = state;
+    }
+
+    init() {
+            const headPosition = getKakaoPosition(this.head_lng, this.head_lat);
+            const midPosition = getKakaoPosition(this.mid_lng, this.mid_lat);
+            const endPosition = getKakaoPosition(this.end_lng, this.end_lat);
+            let stateColor = '#FF0000';
+            if (!isNaN(Number(this.state))) {
+                stateColor = stateMap.get(Number(this.state));
+            }
+            // 선이 그려지고 있을 때 마우스 움직임에 따라 선이 그려질 위치를 표시할 선을 생성합니다
+            this.polyLine = new kakao.maps.Polyline({
+                strokeWeight: 5, // 선의 두께입니다
+                path: [headPosition, midPosition, endPosition],
+                strokeColor: stateColor, // 선의 색깔입니다
+                strokeOpacity: 1, // 선의 불투명도입니다 0에서 1 사이값이며 0에 가까울수록 투명합니다
+                strokeStyle: 'solid', // 선의 스타일입니다
+                endArrow : true,
+                zIndex: 2,
+            });
+    }
+
+    drawLine() {
+        if (this.polyLine) {
+            this.polyLine.setMap(map);
+        }
+    }
+
+    hideLine() {
+        if (this.polyLine) {
+            this.polyLine.setMap(null);
+        }
+    }
+
+    setPath(pathArr) {
+        if (this.polyLine) {
+            this.polyLine.setPath(pathArr);
+        }
+    }
+
+    setOptions(option) {
+        if (this.polyLine) {
+            this.polyLine.setOptions(option);
+        }
+    }
+}
+
+class Sig{
+    constructor({clct_dt, service_id, node_id, node_nm, lat, lng, seq_no, rem_dist, state, plan_class, a_ring_phase, b_ring_phase, hold_phase}) {
+        this.clct_dt = clct_dt;
+        this.service_id = service_id;
+        this.node_id = node_id;
+        this.node_nm = node_nm;
+        this.lat = lat;
+        this.lng = lng;
+        this.seq_no = seq_no;
+        this.rem_dist = rem_dist;
+        this.state = state;
+        this.plan_class = plan_class;
+        this.a_ring_phase = a_ring_phase;
+        this.b_ring_phase = b_ring_phase;
+        this.hold_phase = hold_phase;
+        this.a_ring = null;
+        this.b_ring = null;
+        this.marker = null;
+    }
+
+    init() {
+        let stateColor = '#FF0000';
+        if (!isNaN(Number(this.state))) {
+            stateColor = stateMap.get(Number(this.state));
+        }
+        this.marker = new kakao.maps.Circle({
+            center : getKakaoPosition(this.lng, this.lat),  // 원의 중심좌표 입니다
+            radius: 50, // 미터 단위의 원의 반지름입니다
+            strokeWeight: 5, // 선의 두께입니다
+            strokeColor: stateColor, // 선의 색깔입니다
+            strokeOpacity: 1, // 선의 불투명도 입니다 1에서 0 사이의 값이며 0에 가까울수록 투명합니다
+            strokeStyle: 'solid', // 선의 스타일 입니다
+            fillColor: stateColor, // 채우기 색깔입니다
+            fillOpacity: 0.7, // 채우기 불투명도 입니다
+            zIndex: 1
+        });
+    }
+
+    drawMarker() {
+        this.marker.setMap(map);
+        if (this.a_ring) {
+            this.a_ring.setMap(map);
+        }
+        if (this.b_ring) {
+            this.b_ring.setMap(map);
+        }
+    }
+
+    hideMarker() {
+        this.marker.setMap(null);
+        if (this.a_ring) {
+            this.a_ring.setMap(null);
+        }
+        if (this.b_ring) {
+            this.b_ring.setMap(null);
+        }
+    }
+
+    setCenter() {
+        map.setCenter(getKakaoPosition(this.lng, this.lat));
+    }
 }

+ 3 - 3
src/main/resources/static/js/signal.js

@@ -21,7 +21,7 @@ function mapZoomBound() {
 
     _Level = map.getLevel();
     getSignalInfo();
-    if (_Level == 0 || _Level == 1) {
+    if (_Level === 0 || _Level === 1) {
         // _CvibNodeArr.forEach(function (el) {
         //     el.allDrawCircle();
         //     el.allDrawCvib();
@@ -193,7 +193,7 @@ function getSignalInfoCallback(json) {
 
     // 하단 리스트 클리어
     // 하단 리스트 초기작업
-    $('#iframeBottomList').contents().find('#cvibBottomInfo').empty().html(bottomCvibInfoList);
+    $('#iframeBottomList').contents().find('#cvibBottomInfo').html(bottomCvibInfoList);
 }
 
 /*function drawSignal(signal) {
@@ -295,7 +295,7 @@ Signal.prototype = {
 
         kakao.maps.event.addListener(this.mapCircle, 'mouseout', function () {
             // 신호 이미지 마우스아웃시 [이름 사라짐]
-            customOverlay.setMap(null);
+            if (customOverlay) customOverlay.setMap(null);
         });
 
         kakao.maps.event.addListener(this.mapCircle, 'click', function () {

+ 40 - 0
src/main/resources/static/js/worker.js

@@ -0,0 +1,40 @@
+"use strict"
+self.onmessage = (e)=> {
+
+    console.log(e.data.message);
+    let time = 1000;
+    let timer = null;
+
+    callEvpServiceList();
+    function callEvpServiceList() {
+        let start = new Date().getTime();
+
+        if (timer) clearTimeout(timer);
+
+        const xhr = new XMLHttpRequest();
+        xhr.onreadystatechange = (e)=> {
+            if(xhr.readyState === 4){
+                if(xhr.status === 200){
+                    const resultData = xhr.responseText;
+                    console.log(resultData);
+                    if (resultData) {
+                        postMessage(resultData);
+                    }
+                    let end  = new Date().getTime();
+                    let between  = end - start;
+                    if (between >= 1000) {
+                        between = 1000;
+                    }
+
+                    timer = setTimeout(()=>{
+                        callEvpServiceList();
+                    }, time - between)
+                }
+            }
+        }
+        xhr.open('POST', '/getEvpServiceList.do', true);
+        xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+        xhr.responseType = 'text';
+
+    }
+}

+ 1 - 1
src/main/webapp/WEB-INF/jsp/common.jsp

@@ -1 +1 @@
-<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>

<%
    //Context Root
    String contextRoot = request.getContextPath().equals("/") ? "" : request.getContextPath();
%>

<!-- 기본변수 -->
<c:set var="contextRoot" value="<%=contextRoot%>"/>

<script type="text/javascript">
    var CONTEXTROOT = '${contextRoot }';
    var _CONTEXTROOT = '${contextRoot }'.replace('/', '');
    var BTN_IMAGE_PATH = CONTEXTROOT + "/images/";
    var _signalInfoArr = [];
</script>
<base href="/">
<!--  서울/원주 키 -->
<!--
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=8d9fdadc20c00319444ac31b9a2b9a3b&libraries=services,clusterer,drawing"></script>
-->
<!--  localhost 키 -->

<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=00c3f7d32c1f1822dc98ad72e35e0223&libraries=services,clusterer,drawing"></script>

+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>

<%
    //Context Root
    String contextRoot = request.getContextPath().equals("/") ? "" : request.getContextPath();
%>

<!-- 기본변수 -->
<c:set var="contextRoot" value="<%=contextRoot%>"/>

<script type="text/javascript">
    var CONTEXTROOT = '${contextRoot }';
    var _CONTEXTROOT = '${contextRoot }'.replace('/', '');
    var BTN_IMAGE_PATH = CONTEXTROOT + "/images/";
    var _signalInfoArr = [];
</script>
<base href="/">
<!--  서울/원주 키 -->
<!--
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=8d9fdadc20c00319444ac31b9a2b9a3b&libraries=services,clusterer,drawing"></script>
-->
<!--  localhost 키 -->

<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=00c3f7d32c1f1822dc98ad72e35e0223&libraries=services,clusterer,drawing"></script>

+ 1 - 1
src/main/webapp/WEB-INF/jsp/cvibInfoDetail.jsp

@@ -236,7 +236,7 @@
             else if (_CvibNode.light[ii] === 2) outputTxt = '좌회전';
             else if (_CvibNode.light[ii] === 3) outputTxt = '보행';
             else if (_CvibNode.light[ii] === 4) outputTxt = '자전거';
-            else if (_CvibNode.light[ii] === 5) outputTxt = '자전거';
+            else if (_CvibNode.light[ii] === 5) outputTxt = '우회전';
             else if (_CvibNode.light[ii] === 6) outputTxt = '버스';
             else if (_CvibNode.light[ii] === 7) outputTxt = '유턴';
             else outputTxt = '';

+ 59 - 4
src/main/webapp/WEB-INF/jsp/main.jsp

@@ -25,8 +25,8 @@
     <meta name="Description" content="신호관제"/>
     <meta name="Keywords" content="신호관제"/>
     <link rel="icon" href="data:;base64,iVBORw0KGgo=">
-    <link rel="stylesheet" type="text/css" href="${contextRoot }/css/common.css"/>
-    <link rel="stylesheet" type="text/css" href="${contextRoot }/css/main.css"/>
+    <link rel="stylesheet" type="text/css" href="${contextRoot }/css/common.css?v=1"/>
+    <link rel="stylesheet" type="text/css" href="${contextRoot }/css/main.css?v=1"/>
     <link rel="stylesheet" type=iframe"text/css" href="${contextRoot }/css/themes/default/style.css"/>
     <link rel="icon" type="text/css" href="data:;base64,iVBORw0KGgo="/>
 
@@ -109,7 +109,7 @@
                     <p></p>
                     키워드 :
                     <input id="keyword" type="text" size="15" maxlength="50" value="">
-                    <button type="submit" class="searchBtn" onclick="searchPlaces();">검색하기</button>
+                    <button type="submit" class="searchBtn" onclick="searchPlaces();" autocomplete="false">검색하기</button>
                     <p></p>
                 </div>
                 <hr>
@@ -124,6 +124,11 @@
                                                                                                               src="${contextRoot}/images/find_location_off.png"
                                                                                                               alt="위치찾기"/></a>
                     </li>
+                    <li class="toggleBtn" style="margin-left: 5px;float:left;"><a href="javascript:findLocation()" class="coord" id="coordControl"><img id="coord"
+                                                                                                              class="coord_off"
+                                                                                                              src="${contextRoot}/images/find_coord_off.png"
+                                                                                                              alt="좌표검색"/></a>
+                    </li>
                     <li class="toggleBtn" style="margin-left:5px;float:left;"><a href="javascript:getRoadView();"
                                                                                  class="road" id="roadviewControl"><img
                             id="road" class="road_off" src="${contextRoot}/images/road_view.gif" alt="로드뷰오프"/></a></li>
@@ -149,9 +154,26 @@
                                                                                                           src="${contextRoot}/images/position.png"
                                                                                                           alt="현재위치"/></a>
                     </li>
+                    <li class="toggleBtn" style="margin-left:5px; float:left;"><a href="javascript:refreshNodeList();"
+                                                                                  class="refresh"><img
+                            class="refresh"
+                            src="${contextRoot}/images/refresh.png"
+                            alt="새로고침"/></a>
+                    </li>
                 </ul>
             </div>
             <!-- <img src="images/bg_traffic04.png" alt="범례" class="bumImage" /> -->
+            <div class="coordBox">
+                <div class="coord-unit">
+                    <div>X 좌표 : </div>
+                    <input type="text" id="lctn_x_coord" name="x_coord" autocomplete="false">
+                </div>
+                <div class="coord-unit">
+                    <div>Y 좌표 : </div>
+                    <input type="text" id="lctn_y_coord" name="y_coord" autocomplete="false">
+                </div>
+                <div class="coord-btn" onclick="moveToPosition()">이동</div>
+            </div>
         </div>
     </div>
     <!-- 로드뷰 -->
@@ -161,7 +183,7 @@
         </div>
     </div>
     <!-- //로드뷰-->
-    <iframe src="/getBottomListFrame.do" id="iframeBottomList" class="iframeBottomList"></iframe>
+    <iframe src="/getBottomListFrame.do" id="iframeBottomList" class="iframeBottomList" onload="getSignalInfo()"></iframe>
     <!-- 모바일인경우 현재위치버튼 변경 -->
     <div class="mylocationMob" style="display:none;position:absolute;bottom:calc(25% + 35px);left:335px;z-index:10;">
         <a href="javascript:getMyLocation();" class="mylocationMobA">
@@ -179,6 +201,24 @@
     var eventPopId = null;
     var signalPopId = null;
 
+    /**
+     * 좌표 검색
+     */
+    function findLocation() {
+        $('#lctn_x_coord').val('');
+        $('#lctn_y_coord').val('');
+        const $coordBox = $('.coordBox');
+        $coordBox.toggleClass('on');
+        var coordImage = "find_coord_";
+        var onOff = 'off';
+        if ($coordBox.hasClass('on')) {
+            onOff = 'on';
+        }
+        coordImage += onOff;
+        coordImage += ".png";
+
+        $('#coord').attr('src', BTN_IMAGE_PATH + coordImage);
+    }
 
     /********************************************************************************
      * 위치검색
@@ -445,6 +485,21 @@
         if (signalPopId != null) signalPopId.close();
     });
 
+    function refreshNodeList() {
+        requestService('refreshNodeList.do', null, (res)=>{
+            console.log(res);
+            if (res) {
+                if (res.success) {
+                    alert('리스트 목록이 새로고침 되었습니다.');
+                    location.reload(true);
+                }
+                else if(res.errorMsg) {
+                    alert('리스트 목록 새로고침 중 오류가 발생하였습니다.<br>' + res.errorMsg);
+                }
+            }
+        }, true)
+    }
+
 </script>
 </body>
 </html>

+ 51 - 4
src/main/webapp/WEB-INF/jsp/treeListFrame.jsp

@@ -19,6 +19,10 @@
         <!-- <li><a href="javascript:goGroupMenu()" class="tab" id="tab2"><img src="images/tab02_off.png" alt="신호그룹메뉴" /></a></li> -->
     </ul>
     <div id="onOffTree"></div>
+    <div class="searchBox">
+        <input type="text" id="searchText" placeholder="검색어를 입력해주세요" autocomplete="false" onkeyup="searchTreeData(event)">
+        <div class="search-button" onclick="searchTreeData()">검색</div>
+    </div>
     <div id="intTree"></div>
     <div id="groupTree"></div>
     <div class="treeLoading"><img src="/css/themes/classic/throbber.gif"/></div>
@@ -48,8 +52,10 @@
     var onlineIntStatusData;
     var offlineIntStatusData;
     let _regionMap = new Map();
+    var _searchText = "";
 
     function setAddrMap(data) {
+
         _regionMap.clear();
         for (let ii = 0; ii < data.length; ii++) {
             if (data[ii].addr1 == null || data[ii].addr1 == '-') {
@@ -75,6 +81,21 @@
 
     var timeout = setInterval(controllerOnOffFuc, 1000);
 
+    function searchTreeData(el) {
+        if (el && el.code !== "Enter") {
+            return;
+        }
+
+        const $searchText = $('#searchText');
+        _searchText = $searchText.val();
+        const oldTree = $('#intTree').jstree(true);
+        if (oldTree) {
+            oldTree.destroy();
+        }
+        setTreeList();
+        getIntTreeList();
+        $('#intTree').jstree("refresh");
+    }
     /*
     * 신호 제어기 상태 정보
     * 1 : 정상, 2: 교차로 시각 오류 0: 마지막통신 시간 3초이상 지연시
@@ -246,9 +267,12 @@
         requestService(url, '', getIntTreeListCallback);
     }
     function nameSort(a, b) {
-        var x = a.name.toLowerCase();
-		var y = b.name.toLowerCase();
-		return x < y ? -1 : x > y ? 1 : 0;
+        const aA = a.addr1;
+        const bA = b.addr1;
+        const x = a.name.toLowerCase();
+		const y = b.name.toLowerCase();
+        return aA > bA ? 1 : aA === bA ? (x > y ? 1 : x === y ? 0 : -1) : -1;
+        //return x < y ? -1 : x > y ? 1 : 0;
     }
 
 
@@ -269,7 +293,7 @@
 
         //TODO: 20230221
         setAddrMap(data);
-
+        window.parent._signalInfoArr = [];
         for (var i = 0; i < data.length; i++) {
             if (data[i].addr1 == null || data[i].addr1 == '-') {
                 continue;
@@ -311,6 +335,29 @@
 
             }
 
+            let searchX = null;
+            let searchY = null;
+            let searchText = null;
+            if (_searchText) {
+                if (_searchText.indexOf(',') > -1) {
+                    const coordArr = _searchText.split(',');
+                    if (coordArr.length === 2) {
+                        searchX = coordArr[0].trim();
+                        searchY = coordArr[1].trim();
+                        if (lat.toString() !== searchX.toString() || lng.toString() !== searchY.toString()) {
+                            continue;
+                        }
+                    }
+                }
+                else {
+                    searchText = _searchText;
+                    if (nodeId.indexOf(searchText) === -1 &&
+                        name.indexOf(searchText) === -1
+                    ) {
+                        continue;
+                    }
+                }
+            }
             treeCtrl.set([nameCntOn], {
                 name: name+" ("+nodeId+")",
                 id: nodeId,