Explorar o código

update 2024-03-05

junggilpark hai 1 ano
pai
achega
b1401c0aae

+ 1 - 0
src/main/java/com/its/web/controller/view/ViewController.java

@@ -147,4 +147,5 @@ public class ViewController {
         model.addAttribute("notice", this.noticeService.findNotice(boardNo));
         return "notice/view";
     }
+
 }

+ 41 - 0
src/main/java/com/its/web/exception/ErrorCode.java

@@ -0,0 +1,41 @@
+package com.its.web.exception;
+
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Getter
+public enum ErrorCode {
+    SUCCESS              (0,"요청성공"),
+    UNREGISTERED_APITOKEN(1,"등록되지 않은 API TOKEN"),
+    APITOKEN_EXPIRED     (2,"API TOKEN 유효기간 오류"),
+    DENIED_IPADDR        (3,"접근불가 IP Address"),
+    NOTFOUND_BROKER_INFO (4,"접속 가능한 브로커 정보가 없음"),
+    NOTFOUND_URL         (5,"잘못된 URL 경로"),
+    ERROR_INTERNAL_DATA  (6,"내부 데이터 오류"),
+    NOT_AVAILABLE        (7,"현재 사용할수 없음"),
+    PENDING_REGISTRATION (8,"등록 대기 중");
+
+    private final int code;
+    private final String message;
+    private static final Map<Integer, ErrorCode> map;
+
+    ErrorCode(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    static {
+        map = new HashMap<>();
+        for (ErrorCode e : values())
+            map.put(e.code, e);
+    }
+
+    public static ErrorCode getValue(int code) {
+        // FOR KISA Secure Coding pass
+        //return map.get(code);
+        ErrorCode error = map.get(code);
+        return error;
+    }
+}

+ 46 - 0
src/main/java/com/its/web/exception/ErrorResponse.java

@@ -0,0 +1,46 @@
+package com.its.web.exception;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Getter
+@ToString
+public class ErrorResponse {
+
+    private final LocalDateTime timestamp;
+    private final int status;
+    private final String title;
+    private final String message;
+    private final String path;
+    private final List<String> errors;
+
+    @Builder
+    public ErrorResponse(LocalDateTime timestamp, int status, String title, String message, String path, List<String> errors) {
+        this.timestamp = timestamp;
+        this.status    = status;
+        this.title     = title;
+        this.message   = message;
+        this.path      = path;
+        // FOR KISA Secure Coding pass
+        List<String> errList = errors;
+        this.errors = errList;
+    }
+
+    @Getter
+    public static class FieldError {
+        private final String field;
+        private final String value;
+        private final String reason;
+
+        @Builder
+        public FieldError(String field, String value, String reason) {
+            this.field = field;
+            this.value = value;
+            this.reason = reason;
+        }
+    }
+}

+ 147 - 0
src/main/java/com/its/web/exception/ExceptionContollerAdvisor.java

@@ -0,0 +1,147 @@
+package com.its.web.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+@Slf4j
+@ControllerAdvice
+public class ExceptionContollerAdvisor {
+    private String fieldName2JsonName(String str){
+        boolean isFound = false;
+        StringBuilder sb = new StringBuilder();
+        for (char x : str.toCharArray()) {
+            if (Character.isUpperCase(x)) {
+                sb.append("_").append(Character.toLowerCase(x));
+                isFound = true;
+            } else {
+                sb.append(x);
+            }
+        }
+        return isFound ? sb.toString() : "";
+    }
+
+    /**
+     * Validation error
+     * @param ex
+     * @param request
+     * @return
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public ResponseEntity<ErrorResponse> methodValidException(HttpServletRequest request, MethodArgumentNotValidException ex) {
+        List<String> errorList = new ArrayList<>();
+        BindingResult bindingResult = ex.getBindingResult();
+        for (FieldError fieldError : bindingResult.getFieldErrors()) {
+            StringBuilder builder = new StringBuilder();
+            String fieldName = fieldName2JsonName(fieldError.getField());
+            if ("".equals(fieldName)) {
+                fieldName = fieldError.getField();
+            } else {
+                fieldName = fieldError.getField() + "(" + fieldName + ")";
+            }
+
+            builder.append("[");
+            builder.append(fieldName);
+            builder.append("](은)는 ");
+            builder.append(fieldError.getDefaultMessage());
+            builder.append(" 입력된 값: [");
+            builder.append(fieldError.getRejectedValue());
+            builder.append("]");
+            errorList.add(builder.toString());
+        }
+//        List<String> errorList = ex
+//                .getBindingResult()
+//                .getFieldErrors()
+//                .stream()
+//                .map(DefaultMessageSourceResolvable::getDefaultMessage)
+//                .collect(Collectors.toList());
+        ErrorResponse response = ErrorResponse.builder()
+                .timestamp(LocalDateTime.now())
+                .status(HttpStatus.BAD_REQUEST.value())
+                .title("Arguments Not Valid(Bad Request)")
+                .message("요청 데이터가 유효하지 않습니다.")
+                .errors(errorList)
+                .path(request.getRequestURI())
+                .build();
+        log.error("{}", response.toString());
+        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
+    }
+
+    /**
+     * Data not found
+     * @param request
+     * @param ex
+     * @return
+     */
+    @ExceptionHandler(NoSuchElementException.class)
+    public ResponseEntity<ErrorResponse> exceptionHandler(HttpServletRequest request, final NoSuchElementException ex) {
+        List<String> errorList = new ArrayList<>(Collections.singletonList(ex.getMessage()));
+        ErrorResponse response = ErrorResponse.builder()
+                .timestamp(LocalDateTime.now())
+                .status(HttpStatus.NOT_FOUND.value())
+                .title("Data Not Found")
+                .message(ex.getMessage())
+                .path(request.getRequestURI())
+                .errors(errorList)
+                .build();
+        log.error("{}", response.toString());
+        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
+    }
+    @ExceptionHandler(RuntimeException.class)
+    public ResponseEntity<ErrorResponse> exceptionHandler(HttpServletRequest request, final RuntimeException ex) {
+        List<String> errorList = new ArrayList<>(Collections.singletonList(ex.getMessage()));
+        ErrorResponse response = ErrorResponse.builder()
+                .timestamp(LocalDateTime.now())
+                .status(HttpStatus.EXPECTATION_FAILED.value())
+                .title("RuntimeException")
+                .message(ex.getMessage())
+                .path(request.getRequestURI())
+                .errors(errorList)
+                .build();
+        log.error("{}", response.toString());
+        return new ResponseEntity<>(response, HttpStatus.EXPECTATION_FAILED);
+    }
+
+    @ExceptionHandler(IllegalStateException.class)
+    public ResponseEntity<ErrorResponse> illegalStateExceptionHandler(HttpServletRequest request, final IllegalStateException ex) {
+        List<String> errorList = new ArrayList<>(Collections.singletonList(ex.getMessage()));
+        ErrorResponse response = ErrorResponse.builder()
+                .timestamp(LocalDateTime.now())
+                .status(HttpStatus.EXPECTATION_FAILED.value())
+                .title("IllegalStateException")
+                .message(ex.getMessage())
+                .path(request.getRequestURI())
+                .errors(errorList)
+                .build();
+        log.error("illegalStateExceptionHandler : {}", response.toString());
+        return new ResponseEntity<>(response, HttpStatus.EXPECTATION_FAILED);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ErrorResponse> exceptionHandler(HttpServletRequest request, final Exception ex) {
+        List<String> errorList = new ArrayList<>(Collections.singletonList(ex.getMessage()));
+        ErrorResponse response = ErrorResponse.builder()
+                .timestamp(LocalDateTime.now())
+                .status(HttpStatus.EXPECTATION_FAILED.value())
+                .title("Exception")
+                .message(ex.getMessage())
+                .path(request.getRequestURI())
+                .errors(errorList)
+                .build();
+        log.error("{}", response.toString());
+        return new ResponseEntity<>(response, HttpStatus.EXPECTATION_FAILED);
+    }
+
+}

+ 24 - 0
src/main/java/com/its/web/exception/ExceptionError.java

@@ -0,0 +1,24 @@
+package com.its.web.exception;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ExceptionError implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private Instant timestamp;
+    private Integer status;
+    private String error;
+    private String message;
+    private String path;
+}

+ 55 - 10
src/main/java/com/its/web/interceptor/ConnectHistory.java

@@ -12,7 +12,6 @@ import org.springframework.web.servlet.HandlerInterceptor;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
-import java.net.InetAddress;
 import java.util.Arrays;
 
 @Slf4j
@@ -23,12 +22,13 @@ public class ConnectHistory implements HandlerInterceptor {
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-        InetAddress ipAddress = InetAddress.getLocalHost();
-        String hostIp = ipAddress.getHostAddress();
+//        InetAddress ipAddress = InetAddress.getLocalHost();
+//        String hostIp = ipAddress.getHostAddress();
         String uri = request.getRequestURI();
         String[] ipAddressArr = new String[]{"127.0.0.1", "localhost", "192.168.20.46"};
         HttpSession session = request.getSession();
-        if (session.getAttribute(hostIp) == null && !uri.contains("/phits")) {
+        String hostIp = getClientIp(request);
+        if (session.getAttribute(hostIp) == null && !uri.contains("/phits") && !uri.contains("error")) {
             log.info("Connect Ip Address : {}, UUID : {}", hostIp, session.getId());
             session.setAttribute(hostIp, session.getId());
             try {
@@ -38,17 +38,62 @@ public class ConnectHistory implements HandlerInterceptor {
                 log.error("접속 이력 등록에 실패하였습니다.");
             }
         }
-        if (uri.equals("/phits")) {
-            if (Arrays.asList(ipAddressArr).contains(hostIp)) {
+
+        boolean isHostIp = Arrays.asList(ipAddressArr).contains(hostIp);
+        String redirectPath = null;
+        if (uri.contains("/phits")) {
+            if (isHostIp) {
                 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-                if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {
-                    response.sendRedirect("/phits/main");
+                boolean isLogin = (authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated());
+
+                if (isLogin && uri.equals("/phits")) {
+                    redirectPath = "/phits/main";
                 }
-                return true;
+                if (!isLogin && !uri.equals("/phits")) {
+                    redirectPath = "/phits";
+                }
+
+            }
+            else {
+                redirectPath = "/";
             }
-            response.sendRedirect("/");
         }
 
+        if (redirectPath != null) {
+            response.sendRedirect(redirectPath);
+        }
         return true;
     }
+
+    public String getClientIp(HttpServletRequest request) {
+        if (request == null) {
+            return "";
+        }
+
+        String ipAddress = request.getHeader("X-FORWARDED-FOR");
+        // proxy 환경일 경우
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        // 웹로직 서버일 경우
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        // 기타
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr() ;
+        }
+        //-Djava.net.preferIPv4Stack=true
+        if (ipAddress.equals("0:0:0:0:0:0:0:1"))   //==> ipv6 <== default
+        {
+            ipAddress = "127.0.0.1";   //==> localhost
+        }
+        return ipAddress;
+    }
 }

+ 26 - 1
src/main/java/com/its/web/security/WebSecurityConfig.java

@@ -34,10 +34,12 @@ public class WebSecurityConfig implements WebMvcConfigurer {
         http.csrf().disable();
         http.authorizeRequests()
                 .antMatchers("/swagger-ui.html", "/swagger/**", "/swagger-resources/**", "/webjars/**", "/v2/api-docs").permitAll()
-                .antMatchers("/", "/trafficMap/**", "/api/**", "/notice/**", "/center/**", "/statistics/**").permitAll()
+                .antMatchers("/", "/parking/**","/trafficMap/**", "/api/**", "/notice/**", "/center/**", "/statistics/**").permitAll()
                 .antMatchers("/images/**", "/css/**", "/js/**", "/font/**").permitAll()
 //                .antMatchers("/phits").hasIpAddress("127.0.0.1")
+                .antMatchers("/**").permitAll()
                 .antMatchers("/phits").permitAll()
+                .antMatchers("/phits/**").hasRole("ADMIN")
                 .anyRequest().authenticated();
         http.formLogin()
                 .loginPage("/phits")
@@ -45,6 +47,10 @@ public class WebSecurityConfig implements WebMvcConfigurer {
                 .failureUrl("/phits")
                 .successHandler(this.webLoginSuccessHandler)
                 .failureHandler(this.webLoginFailureHandler);
+        http.logout()
+            .logoutUrl("/phits/logout")
+            .logoutSuccessUrl("/phits");
+
         return http.build();
     }
 
@@ -75,4 +81,23 @@ public class WebSecurityConfig implements WebMvcConfigurer {
         authenticationProvider.setHideUserNotFoundExceptions(true);
         return authenticationProvider;
     }
+
+//    @Bean
+//    public AccessDeniedHandler accessDeniedHandler() {
+//        return (request, response, accessDeniedException) -> {
+//
+//            CustomException customException = new ForbiddenException("forbidden",this.getClass().toString());
+//            ErrorResponse errorResponse =
+//                    new ErrorResponse(customException.getErrorType(), customException.getMessage(), customException.getPath());
+//
+//            Map<String, Object> responseBody = new HashMap<>();
+//            responseBody.put("status", "FAILED");
+//            responseBody.put("data", errorResponse);
+//
+//            response.setStatus(200);
+//            response.setContentType("application/json;charset=UTF-8");
+//            response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody));
+//        };
+//    }
+
 }

+ 0 - 1
src/main/resources/application.yml

@@ -17,7 +17,6 @@ spring:
   output:
     ansi:
       enabled: always
-
   thymeleaf:
     cache: false
     check-template-location: true

+ 42 - 0
src/main/resources/static/css/common.css

@@ -28,6 +28,48 @@ html, body {
     width: 100%;
     height: 100%;
 }
+.error-wrap > div {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    /*justify-content: center;*/
+    flex-direction: column;
+}
+.error-wrap > div > div:nth-child(2) {
+    margin-bottom: 20px;
+    font-weight: bold;
+    font-size: 100px;
+    letter-spacing: 20px;
+    font-family: "Nanum Gothic" !important;
+    background: linear-gradient(to bottom, #a5d9ea, #0054ba);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+}
+.error-wrap > div > div:nth-child(1) {
+    background-image: url("/images/logo/logo.png");
+    width: 316px;
+    height: 100px;
+    margin-bottom: 20px;
+    margin-top: 200px;
+    cursor: pointer;
+}
+.error-wrap > div > div:nth-child(3),
+.error-wrap > div > div:nth-child(4){
+    font-weight: bold;
+    font-size: 20px;
+    margin-bottom: 20px;
+    color: #8f8e8e;
+}
+
+.error-wrap > div > div:nth-child(5) {
+    cursor: pointer;
+    font-size: 16px;
+    font-weight: bold;
+}
+.error-wrap > div > div:nth-child(5):hover {
+    color: #0054ba;
+}
 
 /* 헤더 */
 header {

+ 67 - 18
src/main/resources/static/css/main.css

@@ -87,7 +87,7 @@
 .mainWrap .bus-bg > div img {
     width: 20px;
     height: 20px;
-    margin-left: 1rem;
+    margin-left: 0;
 }
 
 .mainWrap .other-bg {
@@ -159,31 +159,63 @@ body {
     display: flex;
 }
 
-.mainWrap .bottom > div:first-child img {
+.mainWrap .bottom > div:nth-child(2) img {
     width: 30px;
     height: 30px;
     margin-right: 1rem;
 }
 
-.mainWrap .bottom > div {
-    width: 100%;
-    height: 40px;
-}
+/*.mainWrap .bottom > div:first-child {*/
+/*    width: 100%;*/
+/*    height: 40px;*/
+/*}*/
+/*.mainWrap .bottom > div:nth-child(2) {*/
+/*    width: 100%;*/
+/*    height: 300px;*/
+/*}*/
+/*.mainWrap .bottom > div:nth-child(2) > div {*/
+/*    width: 50%;*/
+/*    height: 100%;*/
+/*}*/
 
 .mainWrap .bottom {
     margin-top: 10px;
+    margin-bottom: 10px;
     font-size: 16px;
     font-weight: bold;
-    padding: 20px;
+    /*padding: 20px;*/
     background-color: white;
     display: flex;
     align-items: center;
     justify-content: center;
+    /*flex-direction: column;*/
+    width: 100%;
+    height: 350px;
+}
+.mainWrap .bottom > div {
+    width: 50%;
+    height: 100%;
+    padding: 1.5rem;
+    box-shadow: 2px 2px 2px 2px #eeeeee;
+}
+.mainWrap .bottom > div:not(:last-child) {
+    margin-right: 10px;
+}
+.mainWrap .bottom > div:nth-child(2) {
+    display: flex;
     flex-direction: column;
+    align-items: center;
+}
+.mainWrap .bottom > div:nth-child(2) > div:first-child{
+    width: 100%;
+}
+.mainWrap .bottom video {
+    width: 100%;
+    height: calc(100% - 30px);
 }
-
 .mainWrap .mid > div {
-    width: calc(33.3333%);
+    /*width: calc(33.3333%);*/
+    width: 50%;
     height: 220px;
 }
 
@@ -193,10 +225,12 @@ body {
 
 .mainWrap .center {
     background-image: url(/images/background/bg_center.jpg);
-    background-size: cover;
-    background-position-y: 100%;
+    background-size: 100% 100%;
+    /*background-position-y: 100%;*/
     position: relative;
     cursor: pointer;
+    width: 100%;
+    height: 100%;
 }
 
 .mainWrap .center > div {
@@ -204,7 +238,7 @@ body {
     color: rgb(255, 255, 255);
     position: absolute;
     top: 75%;
-    width: 100%;
+    width: calc(100% - 3rem);
     -webkit-box-pack: center;
     justify-content: center;
     font-weight: bold;
@@ -216,6 +250,7 @@ body {
 .mainWrap .incd {
     padding: 1.5rem;
     background-color: rgb(255, 255, 255);
+    box-shadow: 2px 2px 2px 2px #eeeeee;
 }
 
 .mainWrap .incd > div:first-child {
@@ -391,15 +426,26 @@ body {
         justify-content: unset;
 
     }
-
+    .mainWrap .bottom {
+        margin-top: 0;
+        margin-bottom: 0;
+    }
     .mainWrap .mid {
         flex-direction: column;
-        height: 100%;
+        height: 400px;
     }
-
+    .mainWrap .bottom {
+        flex-direction: column;
+        height: 500px;
+    }
+    .mainWrap .bottom > div:nth-child(2) {
+        height: 300px;
+    }
+    .mainWrap .bottom > div,
     .mainWrap .mid > div {
         width: 100%;
-        height: calc(100% / 3);
+        /*height: calc(100% / 3);*/
+        height: 200px;
     }
 
     .mainWrap .notice > div:first-child {
@@ -461,7 +507,7 @@ body {
     .mainWrap .center > div,
     .mainWrap .other-bg > div,
     .mainWrap .first-bg > div {
-        font-size: 20px;
+        font-size: 16px;
     }
 
     .mainWrap .bus-direction {
@@ -487,7 +533,7 @@ body {
     .mainWrap .center > div,
     .mainWrap .other-bg > div,
     .mainWrap .first-bg > div {
-        font-size: 25px;
+        font-size: 20px;
     }
 
     .mainWrap .incd > .incd-content > div > span,
@@ -504,6 +550,9 @@ body {
     .mainWrap {
         min-height: 758px;
     }
+    .mainWrap .bus-bg > div img {
+        margin-left: 0.5rem;
+    }
 }
 
 

+ 13 - 9
src/main/resources/static/css/traffic.css

@@ -370,7 +370,7 @@ ul, li {
     padding: 5px;
     background-color: white;
     box-shadow: 2px 2px 2px 2px #2b333f;
-    max-width: 875px;
+    width: 450px;
 }
 .incident-info-window .title {
     display: flex;
@@ -378,18 +378,22 @@ ul, li {
     border-bottom: 1px solid #c7c6c6;
 }
 .incident-info-window .title > div:nth-child(1) {
-    line-height: 30px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
+    /*line-height: 30px;*/
+    /*overflow: hidden;*/
+    /*text-overflow: ellipsis;*/
+    /*white-space: nowrap;*/
+    word-break: break-all;
+    white-space: normal;
 }
 
 .incident-info-window .content > div {
     padding : 5px;
-    max-width: 860px;
-    text-overflow: ellipsis;
-    overflow: hidden;
-    white-space: nowrap;
+    width: 100%;
+    /*text-overflow: ellipsis;*/
+    /*overflow: hidden;*/
+    /*white-space: nowrap;*/
+    word-break: break-all;
+    white-space: normal;
 }
 .incident-info-window .content > div:nth-child(2) {
     word-break: break-all;

+ 45 - 0
src/main/resources/static/js/common/common.js

@@ -95,4 +95,49 @@ function closePopup(popupId) {
         setCookie('p_'+ popupId , 'N', {expires : expire});
     }
     $('#popup_' + popupId).css('display', 'none');
+}
+
+function menuOpen() {
+    let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
+    let display = 'flex';
+    if (isMobile) {
+        if ($(window).width() <= 450) {
+            display = 'block';
+        }
+    }
+    $('header #menu-modal').css('display', display);
+}
+
+window.addEventListener(`resize`, function () {
+    const display = $('header #menu-modal').css('display');
+    if (display === 'flex' || display === 'block') {
+        if ($(window).width() <= 450) {
+            $('header #menu-modal').css('display', 'block');
+        } else {
+            $('header #menu-modal').css('display', 'flex');
+        }
+    }
+
+});
+
+function closeModal(target) {
+    $(target).css('display', 'none');
+}
+
+function openBusSite(ord) {
+    const urlArr = [
+        'https://www.pohang.go.kr/bis/busRoute.do',
+        'https://www.pohang.go.kr/bis/busStation.do',
+        'https://www.pohang.go.kr/dept/contents.do?mid=0505020100',
+    ];
+    window.open(urlArr[ord], '_blank', "noopener noreferrer")
+}
+
+function movePath(uri) {
+    window.location.href = uri;
+}
+
+function openModal(target) {
+    $(target).css('display', 'flex');
+    $(target + " .content").scrollTop(0);
 }

+ 37 - 18
src/main/resources/static/js/traffic/traffic.js

@@ -83,6 +83,9 @@ $(()=>{
             atrd.marker.setImage(markerImage);
         })
 
+        if (level === 3) {
+            level = 4;
+        }
         if (level >= 8) {
             level = 7;
         }
@@ -457,6 +460,7 @@ function getVertex() {
         swLng : swLatLng.getLng(),
         neLng : neLatLng.getLng(),
     }
+    console.log(level);
 
     getDataAsync('/api/traffic/vertex-list', 'POST', data, null, (jsonData)=>{
         if (jsonData && jsonData.length > 0) {
@@ -917,10 +921,11 @@ class TbCCtvObj {
                 </div>`;
         $('body').append($(iwContent));
         this.infoWindow = $('.cctv-info-window');
-        let top = $('header')[0].offsetHeight;
-        let left = (window.innerWidth/2) - (this.infoWindow.width()/2);
+        let position = getInfoWidowPosition(this.infoWindow);
+        let top = position[0];
+        let left = position[1];
         this.infoWindow.css({
-            top : (top + 30) + 'px',
+            top : top + 'px',
             left : left + 'px',
             position : 'absolute'
         });
@@ -1110,11 +1115,10 @@ class TbVmsObj {
         $('body').append($(iwContent));
 
         this.infoWindow = $('.vms-info-window');
-        const headerH = $('header').height();
-        const wrapH   = $('.trafficWrap').innerHeight();
-        let top = headerH + (wrapH/2) - this.infoWindow.innerHeight() + 10;
+        let position = getInfoWidowPosition(this.infoWindow);
+        let top = position[0];
+        let left = position[1];
 
-        let left = (window.innerWidth / 2) - (this.infoWindow.width() / 2);
         this.infoWindow.css({
             top : top + 'px',
             left : left + 'px',
@@ -1231,17 +1235,16 @@ class TbIncdObj {
         // this.infoWindow.setMap(_Map);
         $('body').append($(iwContent));
         this.infoWindow = $('.incident-info-window');
-        const headerH   = $('header').height();
-        const wrapH     = $('.trafficWrap').innerHeight();
-        let top = headerH + (wrapH/2) - this.infoWindow.innerHeight() + 10;
-        let left = (window.innerWidth / 2) - (this.infoWindow.innerWidth() / 2);
+        let position = getInfoWidowPosition(this.infoWindow);
+        let top = position[0];
+        let left = position[1];
         this.infoWindow.css({
             top : top + 'px',
             left : left + 'px',
             position: 'absolute',
             zIndex : '999',
         })
-        this.infoWindow.draggable({containment : 'body', handle: 'incident-name-'+ this.ID});
+        this.infoWindow.draggable({containment : 'body', handle: '.incident-name-'+ this.ID});
 
 
         setMarkerImage(this, 2);
@@ -1408,10 +1411,13 @@ class IntersectionCameraObj {
                 </div>`;
         $('body').append($(iwContent));
         this.infoWindow = $('.cctv-info-window');
-        const headerH   = $('header').height();
-        const wrapH     = $('.trafficWrap').innerHeight();
-        let top = headerH + (wrapH/2) - this.infoWindow.innerHeight() + 10;
-        let left = (window.innerWidth / 2) - (this.infoWindow.width() / 2);
+        // const headerH   = $('header').height();
+        // const wrapH     = $('.trafficWrap').innerHeight();
+        // let top = headerH + (wrapH/2) - this.infoWindow.innerHeight() + 10;
+        // let left = (window.innerWidth / 2) - (this.infoWindow.width() / 2);
+        let position = getInfoWidowPosition(this.infoWindow);
+        let top = position[0];
+        let left = position[1];
         this.infoWindow.css({
             top : top + 'px',
             left : left + 'px',
@@ -1419,7 +1425,7 @@ class IntersectionCameraObj {
             zIndex : '999',
         });
 
-        this.infoWindow.draggable({content : 'body', handle : 'ixr-name-' + this.ID});
+        this.infoWindow.draggable({containment : 'body', handle : '.ixr-name-' + this.ID});
 
         // this.infoWindow = new kakao.maps.CustomOverlay({
         //     position: coordinates,
@@ -1845,4 +1851,17 @@ function receiveFacilityData(jsonData, array, facilityClass, listFlag, type) {
     return array;
 }
 
-//https://map.kakao.com/link/search/카카오
+//https://map.kakao.com/link/search/카카오
+
+function getInfoWidowPosition(infoWindow) {
+    const map = $('#map');
+    let mapHalfW = map.innerWidth() / 2;
+    let mapHalfH = map.innerHeight() / 2;
+    let mapTop = map.offset().top;
+    let halfW   = infoWindow.innerWidth() / 2;
+    let height  = infoWindow.innerHeight();
+    let left = mapHalfW - halfW;
+    let iconH = _size[_Map.getLevel()];
+    let top = mapTop + mapHalfH - height - iconH;
+    return [top, left];
+}

+ 30 - 0
src/main/resources/templates/error/404.html

@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th=http://www.thymeleaf.org>
+<head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width,initial-scale=1"/>
+    <meta name="theme-color" content="#000000"/>
+    <meta name="description" content="포항시 교통정보센터입니다"/>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+    <th:block th:include="/include/head.html"></th:block>
+    <title>포항시 교통정보센터</title>
+</head>
+<body class="error-wrap">
+    <div>
+        <div onclick="moveMain()"></div>
+        <div>404</div>
+        <div>요청하신 페이지를 찾을 수 없습니다.</div>
+        <div>페이지 주소가 잘못입력되었거나 삭제되었을 수 있습니다.</div>
+        <div onclick="moveMain()">메인으로</div>
+    </div>
+</body>
+</html>
+<script>
+    function moveMain() {
+        let moveDomain = "/";
+        if (location.pathname.includes("/phits")) {
+            moveDomain = "/phits";
+        }
+        location.href = moveDomain;
+    }
+</script>

+ 31 - 0
src/main/resources/templates/error/500.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th=http://www.thymeleaf.org>
+<head>
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width,initial-scale=1"/>
+    <meta name="theme-color" content="#000000"/>
+    <meta name="description" content="포항시 교통정보센터입니다"/>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+    <th:block th:include="/include/head.html"></th:block>
+    <title>포항시 교통정보센터</title>
+</head>
+<body class="error-wrap">
+    <div>
+        <div onclick="moveMain()"></div>
+        <div>500</div>
+        <div>Internal Server Error</div>
+        <div></div>
+        <div onclick="moveMain()">메인으로</div>
+    </div>
+</body>
+</html>
+
+<script>
+    function moveMain() {
+        let moveDomain = "/";
+        if (location.pathname.includes("/phits")) {
+            moveDomain = "/phits";
+        }
+        location.href = moveDomain;
+    }
+</script>

+ 12 - 12
src/main/resources/templates/include/admin-header.html

@@ -28,18 +28,18 @@
 
 <script th:inline="javascript">
 
-    function closeModal(target) {
-        $(target).css('display', 'none');
-    }
-
-    function movePath(uri) {
-        window.location.href = uri;
-    }
-
-    function openModal(target) {
-        $(target).css('display', 'flex');
-        $(target + " .content").scrollTop(0);
-    }
+//    function closeModal(target) {
+//        $(target).css('display', 'none');
+//    }
+//
+//    function movePath(uri) {
+//        window.location.href = uri;
+//    }
+//
+//    function openModal(target) {
+//        $(target).css('display', 'flex');
+//        $(target + " .content").scrollTop(0);
+//    }
 
     function logout() {
         window.location.href = '/phits/logout';

+ 44 - 44
src/main/resources/templates/include/header.html

@@ -128,49 +128,49 @@
 
 <script th:inline="javascript">
 
-    function menuOpen() {
-        let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
-        let display = 'flex';
-        if (isMobile) {
-            if ($(window).width() <= 450) {
-                display = 'block';
-            }
-        }
-        $('header #menu-modal').css('display', display);
-    }
-
-    window.addEventListener(`resize`, function () {
-        const display = $('header #menu-modal').css('display');
-        if (display === 'flex' || display === 'block') {
-            if ($(window).width() <= 450) {
-                $('header #menu-modal').css('display', 'block');
-            } else {
-                $('header #menu-modal').css('display', 'flex');
-            }
-        }
-
-    });
-
-    function closeModal(target) {
-        $(target).css('display', 'none');
-    }
-
-    function openBusSite(ord) {
-        const urlArr = [
-            'https://www.pohang.go.kr/bis/busRoute.do',
-            'https://www.pohang.go.kr/bis/busStation.do',
-            'https://www.pohang.go.kr/dept/contents.do?mid=0505020100',
-        ];
-        window.open(urlArr[ord], '_blank', "noopener noreferrer")
-    }
-
-    function movePath(uri) {
-        window.location.href = uri;
-    }
-
-    function openModal(target) {
-        $(target).css('display', 'flex');
-        $(target + " .content").scrollTop(0);
-    }
+    // function menuOpen() {
+    //     let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
+    //     let display = 'flex';
+    //     if (isMobile) {
+    //         if ($(window).width() <= 450) {
+    //             display = 'block';
+    //         }
+    //     }
+    //     $('header #menu-modal').css('display', display);
+    // }
+    //
+    // window.addEventListener(`resize`, function () {
+    //     const display = $('header #menu-modal').css('display');
+    //     if (display === 'flex' || display === 'block') {
+    //         if ($(window).width() <= 450) {
+    //             $('header #menu-modal').css('display', 'block');
+    //         } else {
+    //             $('header #menu-modal').css('display', 'flex');
+    //         }
+    //     }
+    //
+    // });
+    //
+    // function closeModal(target) {
+    //     $(target).css('display', 'none');
+    // }
+    //
+    // function openBusSite(ord) {
+    //     const urlArr = [
+    //         'https://www.pohang.go.kr/bis/busRoute.do',
+    //         'https://www.pohang.go.kr/bis/busStation.do',
+    //         'https://www.pohang.go.kr/dept/contents.do?mid=0505020100',
+    //     ];
+    //     window.open(urlArr[ord], '_blank', "noopener noreferrer")
+    // }
+    //
+    // function movePath(uri) {
+    //     window.location.href = uri;
+    // }
+    //
+    // function openModal(target) {
+    //     $(target).css('display', 'flex');
+    //     $(target + " .content").scrollTop(0);
+    // }
 
 </script>

+ 12 - 7
src/main/resources/templates/main/main.html

@@ -64,16 +64,21 @@
                                 <div th:if="${incident == null} or ${incident.size() == 0}">데이터가 없습니다.</div>
                             </div>
                         </div>
+<!--                        <div class="center" onclick="movePath('/center/center')">-->
+<!--                            <div>교통정보센터 소개 ></div>-->
+<!--                        </div>-->
+                    </div>
+                    <div class="bottom">
                         <div class="center" onclick="movePath('/center/center')">
                             <div>교통정보센터 소개 ></div>
                         </div>
-                    </div>
-                    <div class="bottom">
-                        <div><img src="/images/icon/video.png" alt="동영상 아이콘">홍보 동영상</div>
-                        <video height="400" width="600" controls>
-                            <source src="https://wowza.pohang.go.kr/live/23.stream/playlist.m3u8" preload="metadata" type="video/mp4">
-                            이 문장은 여러분의 브라우저가 video 태그를 지원하지 않을 때 화면에 표시됩니다!
-                        </video>
+                        <div>
+                            <div><img src="/images/icon/video.png" alt="동영상 아이콘">홍보 동영상</div>
+                            <video controls>
+                                <source src="https://wowza.pohang.go.kr/live/23.stream/playlist.m3u8" preload="metadata" type="video/mp4">
+                                이 문장은 여러분의 브라우저가 video 태그를 지원하지 않을 때 화면에 표시됩니다!
+                            </video>
+                        </div>
                     </div>
                 </div>
             </div>