Bläddra i källkod

update 2024-03-18

junggilpark 1 år sedan
förälder
incheckning
031e46178a

+ 51 - 0
src/main/java/com/its/web/dto/traffic/FacilityDto.java

@@ -0,0 +1,51 @@
+package com.its.web.dto.traffic;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ApiModel("FacilityDto(시설물 DTO)")
+public class FacilityDto {
+    @ApiModelProperty("ID")
+    @JsonProperty("ID")
+    private String id;
+
+    @ApiModelProperty("명칭")
+    @JsonProperty("NAME")
+    private String name;
+
+    @ApiModelProperty("x좌표")
+    @JsonProperty("X_CRDN")
+    private Double xCrdn;
+
+    @ApiModelProperty("y좌표")
+    @JsonProperty("Y_CRDN")
+    private Double yCrdn;
+
+    @ApiModelProperty("시설물 유형")
+    @JsonProperty("TYPE")
+    private String type;
+
+    @ApiModelProperty("이미지 경로")
+    @JsonProperty("IMAGE")
+    private String image;
+
+    @ApiModelProperty("이미지 유형")
+    @JsonProperty("IMAGE_TYPE")
+    private String imageType;
+
+    @ApiModelProperty("스트리밍 주소")
+    @JsonProperty("URL")
+    private String url;
+
+    @ApiModelProperty("시설물 속성")
+    @JsonProperty("prop")
+    private Object prop;
+
+}

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

@@ -34,7 +34,7 @@ public class ConnectHistory implements HandlerInterceptor {
                 service.insertConnHs();
             }
             catch (DataAccessException e) {
-                log.error("접속 이력 등록에 실패하였습니다.");
+                log.error("방문자 카운팅에 실패하였습니다.");
             }
         }
 

+ 2 - 1
src/main/java/com/its/web/service/traffic/TrafficService.java

@@ -34,7 +34,8 @@ public class TrafficService {
      * @return
      */
     public List<TbCctvCtlrDto> findCctvList() {
-        return this.mapper.findCctvList();
+        List<TbCctvCtlrDto> list = this.mapper.findCctvList();
+        return list;
     }
 
     /**

+ 40 - 21
src/main/resources/static/css/main.css

@@ -8,7 +8,8 @@
     -webkit-box-pack: center;
     justify-content: center;
     /*background-image: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3)), url(/images/background/main_bg.gif);*/
-    background-image: url(/images/background/main_bg.gif);
+    /*background-image: url(/images/background/main_bg.gif);*/
+    background-image: url("/images/background/bg_main3.jpg");
     background-size: cover;
     /*z-index: 1;*/
 }
@@ -180,7 +181,7 @@ body {
 /*}*/
 
 .mainWrap .bottom {
-    /*margin-top: 10px;*/
+    margin-top: 10px;
     margin-bottom: 10px;
     font-size: 16px;
     font-weight: bold;
@@ -193,18 +194,26 @@ body {
     width: 100%;
     height: 350px;
     background-size: 100% 350px;
-    background-image: url("/images/background/bg_main3.jpg");
+    /*background : white;*/
+    /*background-image: url("/images/background/bg_main3.jpg");*/
 }
 .mainWrap .bottom > div {
-    width: 50%;
+    width: calc(50% - 5px);
     /*width: 100%;*/
     height: 100%;
     padding: 1.5rem;
     /*box-shadow: 2px 2px 2px 2px #eeeeee;*/
 }
+.mainWrap .bottom > div:first-child {
+    margin-right: 5px;
+}
+.mainWrap .bottom > div:nth-child(2) {
+    margin-left: 5px;
+    background-color: #cceaea;
+}
 .mainWrap .bottom .first-box {
     text-align: right;
-    color: white;
+    color: black;
 }
 .mainWrap .bottom .first-box h1,
 .mainWrap .bottom .first-box div {
@@ -221,19 +230,20 @@ body {
 }
 .mainWrap .bottom > div:nth-child(2) > div:first-child{
     width: 80%;
-    height: 100%;
+    height: 30px;
     border-radius: 5%;
 }
 .mainWrap .bottom video {
-    width: 100%;
-    /*height: calc(100% - 30px);*/
-    height: 100%;
+    width: 85%;
+    height: calc(100% - 40px);
+    margin: auto;
+    /*height: 100%;*/
     border-radius: 15px;
     background-color: black;
 }
 .mainWrap .mid > div {
-    width: calc(33.3333%);
-    /*width: 50%;*/
+    /*width: calc(33.3333%);*/
+    width: 50%;
     height: 220px;
 }
 
@@ -268,7 +278,7 @@ body {
 .mainWrap .incd {
     padding: 1.5rem;
     background-color: rgb(255, 255, 255);
-    box-shadow: 2px 2px 2px 2px #eeeeee;
+    /*box-shadow: 2px 2px 2px 2px #eeeeee;*/
 }
 
 .mainWrap .incd > div:first-child {
@@ -451,22 +461,30 @@ body {
     }
     .mainWrap .mid {
         flex-direction: column;
-        height: 600px;
+        height: 400px;
     }
     .mainWrap .bottom {
         flex-direction: column;
-        height: 350px;
+        height: 400px;
     }
     .mainWrap .bottom .first-box {
         padding: 1.5rem 1.5rem 0.5rem 1.5rem;
         height: 135px;
     }
-    .mainWrap .bottom > div:nth-child(2) > div:first-child{
-        width: 100%;
-    }
+    /*.mainWrap .bottom > div:nth-child(2) > div:first-child{*/
+    /*    width: 100%;*/
+    /*    height: 200px;*/
+    /*    margin-right: 0;*/
+    /*}*/
     .mainWrap .bottom > div:nth-child(2) {
-        height: calc(100% - 135px);
-        padding: 0 1rem 1rem 1rem;
+        height: 200px;
+        /*padding: 0 1rem 1rem 1rem;*/
+        padding : 1rem;
+        margin-left: 0;
+    }
+    .mainWrap .bottom video {
+        width: 100%;
+        height: calc(100% - 30px);
     }
     .mainWrap .bottom > div,
     .mainWrap .mid > div {
@@ -475,8 +493,9 @@ body {
         height: 200px;
     }
 
-    .mainWrap .notice > div:first-child {
-        margin-bottom: 0.7rem;
+    .mainWrap .bottom > div:first-child {
+        /*margin-bottom: 0.7rem;*/
+        margin-right: 0;
     }
 
     .mainWrap .notice > div:first-child img,

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

@@ -115,44 +115,6 @@ function alertMessage(type, AMessage, ATitle, id, confirmMethod, isConfirm) {
         }
     })
 }
-function confirmMessage(AMessage, ATitle) {
-    const title = ATitle ?? document.title;
-    const message = AMessage;
-    const ok = new $.Deferred();
-    $.alert({
-        useBootstrap: false,
-        title: title,
-        titleClass: "dx-theme-accent-as-text-color",
-        content: message, //theme: "dark",
-        type: "blue dx-theme-accent-as-border-color",
-        typeAnimated: true,
-        columnClass: "small",
-        icon: "fa fa-check dx-theme-accent-as-text-color",
-        // closeIcon: true,
-        closeIcon: "cancel",
-        closeIconClass: "fa fa-close dx-theme-accent-as-text-color",
-        draggable: true,
-        escapeKey: false,
-        backgroundDismiss: false,
-        backgroundDismissAnimation: "glow",
-        buttons: {
-            close: {
-                text: " 확 인 ",
-                btnClass: "btn-blue dx-theme-accent-as-background-color",
-                action: function () {
-                    ok.resolve(true);
-                },
-            },
-            cancel: {
-                text: " 취 소 ", //btnClass: "btn-blue",
-                action: function () {
-                    ok.resolve(false);
-                },
-            },
-        },
-    });
-    return ok.promise();
-}
 
 
 function textFormat(text) {
@@ -164,10 +126,7 @@ function isNull(value) {
         return true;
     }
 
-    if (value.toString().trim() === "") {
-        return true;
-    }
-    return false;
+    return value.toString().trim() === "";
 }
 
 function moveNoticeView(boarNo, isAdmin) {

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 739 - 298
src/main/resources/static/js/traffic/traffic.js


+ 1878 - 0
src/main/resources/static/js/traffic/traffic_org.js

@@ -0,0 +1,1878 @@
+let _size  = [48, 48, 48, 48, 40, 32, 24, 22, 20, 18, 18];
+let _AtrdData = [];
+let _AtrdMap = new Map();
+let selectIncidentId = null;
+
+//시설물 유형
+const _FacilityArray = ['cctv', 'vms', 'intersection', 'incident', 'traffic', 'parking', 'intersectionCamera'];
+
+const g_color = new Map();
+g_color.set('LTC0', '#888888;');
+g_color.set('LTC1', '#2fba2c;');
+g_color.set('LTC2', '#ffc500;');
+g_color.set('LTC3', '#ee0000;');
+
+const CCTV_DISPLAY_TIME = 30 * 1000; // 영상 표출 시간
+const VMS_DISPLAY_TIME  = 4 * 1000; // VMS 이미지 표출 시간
+
+let _MapHandler; //카카오 맵 핸들러
+let _Level = 6; //현재 줌레벨
+
+let _methodMap = new Map();
+_methodMap.set('cctv', getCctv);
+_methodMap.set('vms', getVms);
+_methodMap.set('incident', getIncident);
+_methodMap.set('intersection', getIntersection);
+_methodMap.set('traffic', getAtrd);
+_methodMap.set('parking', getParking);
+
+$(()=>{
+    // Map 요소 ID로 생성
+    _MapHandler = new MapHandler('map');
+    _MapHandler.init();
+
+    // 시설물 유형 정보로 flag 값 세팅
+    if (_type) {
+        _MapHandler[_type + 'Flag'] = true;
+        _MapHandler[_type + 'ListFlag'] = true;
+        _methodMap.get(_type)();
+    }
+
+    /**
+     * 범례 클릭 이벤트 및 색 변환
+     * */
+    $('.legend-bottom button').on('click', function (){
+        const target     = $(this);
+        const legendIcon = target.children().eq(0);
+        const isShow     = !legendIcon.hasClass('active');
+
+        const id = target.attr('id');
+        const type = id.replace('-legend', '');
+
+        let subType = null;
+
+        if (type === 'intersection') {
+            subType = type + 'Camera';
+        }
+
+        if (type === 'traffic') {
+            subType = 'atrd';
+            getVertex();
+        }
+        else {
+            if (_MapHandler[type] && _MapHandler[type].length <= 0) {
+                _methodMap.get(type)();
+            }
+        }
+
+        let toggle = 'hide';
+        let classToggle = 'removeClass';
+        if (isShow === true) {
+            toggle = 'show';
+            classToggle = 'addClass';
+        }
+
+        legendIcon[classToggle]('active');
+        target[classToggle]('active');
+
+        _MapHandler[toggle](type);
+        if (subType) {
+            _MapHandler[toggle](subType);
+        }
+
+    })
+
+    /**
+     * 범례 호버 색 변환
+     * */
+    $('.legend-bottom ul li').hover((event)=>{
+        const iconDiv = $(event.currentTarget).children().eq(0);
+        const childDiv = iconDiv.children().eq(0);
+        if (!iconDiv.hasClass('active')) {
+            childDiv.addClass('hover');
+        }
+        else {
+            if (childDiv.hasClass('hover')) {
+                childDiv.removeClass('hover');
+            }
+        }
+    },
+    (event)=>{
+        const iconDiv = $(event.currentTarget).children().eq(0);
+        const childDiv = iconDiv.children().eq(0);
+
+        if (!iconDiv.hasClass('active')) {
+            childDiv.removeClass('hover');
+        }
+        else {
+            if (childDiv.hasClass('hover')) {
+                childDiv.removeClass('hover');
+            }
+        }
+    });
+
+    /**
+     * 탭 호버 색 변환
+     * */
+    $('.left-list-area .list-tab > li').hover((event)=>{
+        const iconDiv  = $(event.currentTarget);
+        const childDiv = iconDiv.children().eq(0);
+        if (!iconDiv.hasClass('active')) {
+            childDiv.addClass('hover');
+        }
+        else {
+            if (childDiv.hasClass('hover')) {
+                childDiv.removeClass('hover');
+            }
+        }
+    },
+    (event)=>{
+        const iconDiv = $(event.currentTarget);
+        const childDiv = iconDiv.children().eq(0);
+        if (!iconDiv.hasClass('active')) {
+            childDiv.removeClass('hover');
+        }
+        else {
+            if (childDiv.hasClass('hover')) {
+                childDiv.removeClass('hover');
+            }
+        }
+    });
+
+
+    let _MarkerArr = [];
+    $('.tab-title > div').on('click', function(){
+        const $spot = $('.list-content.spot');
+        const $list = $('.list-content.list');
+        let searchType = $('.tab-title > div.active').text();
+        if (searchType === $(this).text()) return;
+        $('.tab-title > div').toggleClass("active");
+        searchType = $(this).text();
+        let list;
+        let spot;
+        if (searchType === '지점 검색') {
+            list = 'none';
+            spot = 'block';
+            $spot.html("");
+        }
+        else {
+            list = 'block';
+            spot = 'none';
+            $('.list-content.list > li').css('display', 'block');
+        }
+
+        $('#search-box').val("");
+        $list.css('display', list);
+        $spot.css('display', spot);
+        if (_MarkerArr.length > 0) {
+            _MarkerArr.forEach((obj)=>{
+                obj.setMap(null);
+            });
+            _MarkerArr = [];
+        }
+    })
+
+
+    $('#search-box').on('keyup', function () {
+        const searchType = $('.tab-title > div.active').text();
+        const searchText = $(this).val();
+
+        if (searchType === '리스트') {
+            const $list = $('.list-content.list > li');
+            if (!$list.length) return;
+
+            for (let ii = 0; ii < $list.length; ii++) {
+                const li = $list.eq(ii);
+                if ($(this).val().length === 0) {
+                    li.css('display', 'block');
+                }
+                else {
+                    let text = $('.list-content.list > li').eq(ii).text().toLowerCase();
+                    if (text.includes(searchText.toLowerCase())) {
+                        li.css('display', 'block');
+                    }
+                    else {
+                        li.css('display', 'none');
+                    }
+                }
+            }
+        }
+        else {
+            if (_MarkerArr.length > 0) {
+                _MarkerArr.forEach((obj)=>{
+                    obj.setMap(null);
+                });
+                _MarkerArr = [];
+            }
+            $('.list-content.spot').html("");
+            if (!searchText || !searchText.trim()) return;
+            $.ajax({
+                url: 'https://dapi.kakao.com/v2/local/search/address.json',
+                headers: { 'Authorization': 'KakaoAK 4896b94398b96949d349881d004835cf'},
+                data :{
+                    query : searchText,
+                },
+                type: 'GET'
+            }).done(function(data) {
+                if (data && data.documents.length > 0) {
+                    let str = "";
+                    const setMap = new Map();
+
+                    const bounds = new kakao.maps.LatLngBounds();
+                    let minX;
+                    let minY;
+                    let maxX;
+                    let maxY;
+                    let idx = 0;
+                    data.documents.forEach((obj)=>{
+                        if (!setMap.get(obj.address_name) && obj.address_name.includes("경북 포항시")) {
+                            idx++;
+                            let addr = obj.address_name;
+                            if (obj.x && obj.y) {
+                                const position = getKakaoPosition(obj.y, obj.x);
+                                let marker = new kakao.maps.Marker({
+                                    position : position,
+                                    title : addr ,
+                                    content : addr
+                                });
+
+                                new kakao.maps.event.addListener(marker, 'click', function() {
+                                    moveLocation(obj.x, obj.y, idx);
+                                })
+                                if (!minX) {
+                                    minX = obj.x;
+                                }
+
+                                if (!maxX) {
+                                    maxX = obj.x;
+                                }
+                                if (!minY) {
+                                    minY = obj.y;
+                                }
+
+                                if (!maxY) {
+                                    maxY = obj.y;
+                                }
+
+                                minX = minX <= obj.x ? minX : obj.x;
+                                maxX = maxX >= obj.x ? maxX : obj.x;
+                                minY = minY <= obj.y ? minY : obj.y;
+                                maxY = maxY >= obj.y ? maxY : obj.y;
+
+                                marker.setMap(_MapHandler.map);
+                                _MarkerArr.push(marker);
+                                setMap.set(addr, obj);
+
+                                str+= `<li id="spot-${idx}" onclick="moveLocation(${obj.x}, ${obj.y}, ${idx})">
+                                            ${obj.address_name}
+                                       </li>`;
+                            }
+                        }
+                    });
+
+                    if (minX && minY && maxX && maxY) {
+                        bounds.extend(new kakao.maps.LatLng(minY, minX));
+                        bounds.extend(new kakao.maps.LatLng(maxY, maxX));
+                        _MapHandler.map.setBounds(bounds);
+                        $('.list-content.spot').html(str);
+                        getVertex();
+                    }
+                }
+            });
+        }
+    })
+});
+
+/**
+ * 간선도로 마커 레벨별 이미지 보이기 감추기
+ */
+function atrdMarkerResize() {
+    let level = _Level;
+
+    let atrdArr = _MapHandler['atrd'];
+    atrdArr.forEach((atrd)=>{
+        setMarkerImage(atrd, atrd.obj.drct_cd, true);
+    });
+
+    if (level === 3) {
+        level = 4;
+    }
+    if (level >= 8) {
+        level = 7;
+    }
+    else if (level >= 6) {
+        level = 6;
+    }
+
+    const atrdInfo = _AtrdMap.get(level.toString());
+    if (atrdInfo) {
+        _AtrdData = [];
+        let upHillArr = [];
+        let downHillArr = [];
+        const $selectedLi = $(".left-list-area .list-content.list > li.click");
+        let id;
+        let idArr;
+        if ($selectedLi[0]) {
+            id = $selectedLi.attr('id');
+        }
+        if (id) {
+            idArr = id.split("_");
+        }
+        atrdInfo.forEach((atrd)=>{
+            if (atrd.atrd_id.toString() === idArr[1] || atrd.atrd_id.toString() === idArr[2]) {
+                _AtrdData.push(atrd);
+
+                if (atrd.atrd_id.toString() === idArr[1]) {
+                    upHillArr.push(atrd);
+                }
+                else {
+                    downHillArr.push(atrd);
+                }
+            }
+        });
+
+        if (upHillArr[0]) {
+            let upHillX = upHillArr[0].x_crdn_arr.split(',')[0];
+            let upHillY = upHillArr[0].y_crdn_arr.split(',')[0];
+            atrdArr[0].marker.setPosition(getKakaoPosition(upHillY, upHillX));
+        }
+
+        if (downHillArr[0]) {
+            let downHillX = downHillArr[0].x_crdn_arr.split(',')[0];
+            let downHillY = downHillArr[0].y_crdn_arr.split(',')[0];
+            atrdArr[1].marker.setPosition(getKakaoPosition(downHillY, downHillX));
+        }
+    }
+}
+
+/**
+ * 스마트교차로 카메라 레벨별 이미지 보이기 감추기
+ */
+function intersectionCameraChangeWidthZoomLevel() {
+    const level = _Level;
+    const size = [48, 48, 48]
+    const levelSize = size[level];
+    const type = 'intersectionCamera';
+    const markerArr = _MapHandler[type];
+    const map = _MapHandler.map;
+    if (markerArr.length > 0) {
+        markerArr.forEach((obj) => {
+            const content = $('<div title='+obj.NAME+'>');
+            content.css(
+                {
+                    width: levelSize+ 'px',
+                    height: levelSize+ 'px',
+                    backgroundImage:'url(/images/icon/intersection-cctv.png)',
+                    backgroundSize : levelSize+'px '+levelSize+'px',
+                    backgroundRepeat: 'no-repeat',
+                    backgroundPosition : 'center',
+                    transform : 'rotate(' + obj.obj.cmra_angn +'deg)',
+                    cursor    : 'pointer'
+                });
+
+            obj.marker.setContent(content[0]);
+            content.on('click', ()=> {
+                _MapHandler.click(type, obj.ID);
+            });
+
+            if (level <= 2 && _MapHandler['intersectionFlag']) {
+                obj.marker.setMap(map);
+                obj.polyline.setMap(map);
+            } else {
+                obj.marker.setMap(null);
+                obj.polyline.setMap(null);
+            }
+        })
+    }
+}
+
+/**
+ * 스마트교차로 레벨별 이미지 보이기 감추기
+ */
+function intersectionMarkerChangeWithZoomLevel() {
+    const level = _Level;
+    if (_MapHandler['intersection'].length > 0) {
+        _MapHandler['intersection'].forEach((obj)=>{
+            setMarkerImage(obj, "", false);
+            if (level > 2 && _MapHandler['intersectionFlag']) {
+                obj.marker.setVisible(true);
+            }
+            else {
+                obj.marker.setVisible(false);
+            }
+        })
+    }
+}
+
+/**
+ * 맵 레벨별 이미지 변환
+ * @param array
+ */
+function markerSizeChangeWithZoomLevel(array) {
+    if (array && array.length > 0) {
+        array.forEach((obj)=>{
+            if (obj.isClick) {
+                setMarkerImage(obj, 2, false)
+            }
+            else {
+                setMarkerImage(obj, 1, false);
+            }
+        });
+    }
+}
+
+/**
+ * 지점 클릭 위치 이동
+ * @param xCoordinate x좌표
+ * @param yCoordinate y좌표
+ * @param listIndex 지점 선택 인덱스
+ */
+function moveLocation(xCoordinate, yCoordinate, listIndex) {
+    const position = getKakaoPosition(yCoordinate, xCoordinate);
+
+    _MapHandler.map.setCenter(position);
+    _MapHandler.map.setLevel(3);
+    $('.list-content.spot > li.click').removeClass('click');
+    $('#spot-' +listIndex).addClass('click');
+    getVertex();
+}
+
+/**
+ * cctv 데이터 가져오기
+ */
+function getCctv() {
+    getDataAsync('/api/traffic/cctv-list', 'POST', null, null, (jsonData)=>{
+        if (jsonData && jsonData.length > 0) {
+            receiveFacilityData(jsonData, TbCCtvObj,'cctv');
+        }
+    }, null);
+}
+
+/**
+ * vms 데이터 가져오기
+ */
+function getVms() {
+    getDataAsync('/api/traffic/vms-list', 'POST', null, null, (jsonData)=>{
+        if (jsonData && jsonData.length > 0) {
+            jsonData.sort((a, b)=>{
+                return a.vms_nm > b.vms_nm ? 1 : a.vms_nm < b.vms_nm ? -1 : 0;
+            })
+            receiveFacilityData(jsonData, TbVmsObj,'vms');
+        }
+    }, null);
+}
+
+/**
+ * 돌발정보
+ */
+function getIncident () {
+    getDataAsync('/api/traffic/incident-list', 'POST', null, null, (jsonData)=>{
+        if (jsonData && jsonData.length > 0) {
+            receiveFacilityData(jsonData, TbIncdObj, 'incident');
+            if (selectIncidentId) {
+                _MapHandler.click('incident', selectIncidentId);
+            }
+        }
+    }, null);
+}
+
+/**
+ * 스마트교차로
+ */
+function getIntersection() {
+    getDataAsync('/api/itcs/list', 'POST', null, null, (jsonData)=>{
+        if (jsonData && jsonData.length > 0) {
+            jsonData.sort((a, b)=>{
+                return a.ixr_nm > b.ixr_nm ? 1 : a.ixr_nm < b.ixr_nm ? -1 : 0;
+            });
+            let data = [];
+            jsonData.forEach((obj)=>{
+                if(obj.detail.length > 0) {
+                    data.push(obj);
+                }
+            })
+            receiveFacilityData(data, IntersectionObj, 'intersection');
+        }
+    }, null);
+}
+
+/**
+ * 주차정보
+ */
+function getParking() {
+    getDataAsync('/api/traffic/parking-list', 'POST', null, null, (jsonData)=>{
+        if (jsonData && jsonData.length > 0) {
+            receiveFacilityData(jsonData, TbParkingObj, 'parking');
+        }
+    }, null);
+}
+
+/**
+ * 간선도로 정보
+ */
+function getAtrd() {
+    getDataAsync("/api/traffic/atrd-vertex-all", "POST", null, null, (jsonData)=>{
+        if (jsonData && jsonData.length > 0) {
+            _AtrdMap = new Map();
+            jsonData.forEach((obj)=>{
+                _AtrdMap.set(obj.level, obj.list);
+            });
+        }
+    }, null);
+
+    getDataAsync('/api/traffic/atrd-list', 'POST', null, null, (jsonData)=>{
+        if (jsonData) {
+            let listStr = "";
+            let mobileStr = "";
+            let atrdData = [];
+
+            for (let key in jsonData){
+                atrdData.push({
+                    name : key,
+                    list : jsonData[key]
+                });
+            }
+
+            atrdData.sort((a,b)=>{
+                return a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
+            });
+
+            atrdData.forEach((obj)=>{
+                let upHillId = null;
+                let downHillId = null;
+                const list = obj.list;
+                if (list && list.length === 2) {
+                    upHillId   = list[0].atrd_id;
+                    downHillId = list[1].atrd_id;
+
+                    if (list[0].drct_cd === "1") {
+                        upHillId   = list[1].atrd_id;
+                        downHillId = list[0].atrd_id;
+                    }
+                }
+                listStr += `<li id="atrd_${upHillId}_${downHillId}" onclick="atrdClickEvent('${upHillId}', '${downHillId}')">${obj.name}</li>`
+                mobileStr += `<option value="${upHillId}_${downHillId}">${obj.name}</option>`
+            });
+
+            const $mobileSelect = $('.mobile-select');
+            $mobileSelect.append($(mobileStr));
+            $mobileSelect.on('change', function(){
+                const ids = $(this).val();
+                if (ids && ids !== '-') {
+                    const idArr = ids.split('_');
+                    atrdClickEvent(idArr[0], idArr[1]);
+                }
+                else {
+                    const array = _MapHandler['atrd'];
+                    if (array.length > 0) {
+                        array.forEach((atrd)=>{
+                            atrd.close();
+                        })
+                        _MapHandler['atrd'] = [];
+                        getVertex();
+                    }
+                }
+            })
+            const listSection = $('.left-list-area .list-content.list');
+            listSection.empty();
+            listSection.html(listStr);
+        }
+    });
+
+    getVertex();
+}
+
+/**
+ * 소통정보 가져오기
+ */
+function getVertex() {
+    const array = _MapHandler['traffic'];
+    if (array.length > 0) {
+        array.forEach((obj) => {
+            obj.setVisibleMarker(false);
+            if (obj.infoWindow) {
+                obj.infoWindow.setMap(null);
+            }
+        })
+        _MapHandler['traffic'] = [];
+    }
+
+    let level = _MapHandler.map.getLevel();
+    const bounds = _MapHandler.map.getBounds();
+    const swLatLng = bounds.getSouthWest();
+    const neLatLng = bounds.getNorthEast();
+    const data = {
+        levl : level,
+        swLat : swLatLng.getLat(),
+        neLat : neLatLng.getLat(),
+        swLng : swLatLng.getLng(),
+        neLng : neLatLng.getLng(),
+    }
+    if (_MapHandler.atrd && _MapHandler.atrd.length > 0) {
+        _MapHandler.atrd.forEach((atrd)=>{
+            atrd.cnt = 0;
+            atrd.trvl_hh = 0;
+            atrd.sped = 0;
+        })
+    }
+
+    getDataAsync('/api/traffic/vertex-list', 'POST', data, null, (jsonData)=>{
+        if (jsonData && jsonData.length > 0) {
+            jsonData.forEach((obj)=>{
+                const trafficObj = new TrafficObj(obj);
+                if (_AtrdData.length > 0) {
+
+                    _AtrdData.forEach((atrd)=>{
+                        if (atrd.road_id.toString() === trafficObj.ID.toString()) {
+                            let sped = Number(obj.sped);
+                            let trvl_hh = Number(obj.trvl_hh);
+                            const atrdObj = _MapHandler.getSelectObj(_MapHandler['atrd'], atrd.atrd_id);
+                            if (!isNaN(sped)) {
+                                atrdObj.sped += sped;
+                            }
+                            if (!isNaN(trvl_hh)) {
+                                atrdObj.trvl_hh += trvl_hh;
+                            }
+                            atrdObj.cnt += 1;
+                            trafficObj.polyBackLine.setOptions({
+                                strokeColor : 'black',
+                            });
+                        }
+                    })
+                }
+                _MapHandler['traffic'].push(trafficObj);
+            })
+        }
+    });
+}
+
+
+/**
+ * 간선도로 클릭 이벤트
+ * @param AupHillId 상행 ID
+ * @param AdownHillId 하행 ID
+ */
+function atrdClickEvent(AupHillId, AdownHillId) {
+    let atrdArr = _MapHandler['atrd'];
+    if (atrdArr.length > 0) {
+        atrdArr.forEach((atrd)=>{
+            atrd.close();
+        });
+        _MapHandler['atrd'] = [];
+    }
+    const selectedObj = _MapHandler.selectedObj;
+    if (selectedObj) {
+        _MapHandler.close(selectedObj.type, selectedObj.ID);
+    }
+    _AtrdData = [];
+    const $selectedLi = $('.left-list-area .list-content.list > li.click');
+    if ($selectedLi) {
+        $selectedLi.removeClass('click');
+    }
+    const $selectLi = $('#atrd_' + AupHillId  + "_" + AdownHillId);
+    $selectLi.addClass('click');
+    $selectLi.focus();
+
+    //상행, 하행 ID 가 있다면 실행
+    if (AupHillId && AdownHillId) {
+        const upHillArr = [];
+        const downHillArr = [];
+        const bounds = new kakao.maps.LatLngBounds();
+
+        //전체 간선도로 목록을 담은 Map 에서 기준값을 5레벨로 잡아 포커스 좌표를 설정
+        if (_AtrdMap.get("5")) {
+            const basicAtrd = _AtrdMap.get("5");
+            for (let idx in basicAtrd) {
+                const obj = basicAtrd[idx];
+                if (obj.atrd_id.toString() === AupHillId || obj.atrd_id.toString() === AdownHillId) {
+                    bounds.extend(new kakao.maps.LatLng(obj.y_crdn_min, obj.x_crdn_min));
+                    bounds.extend(new kakao.maps.LatLng(obj.y_crdn_max, obj.x_crdn_max));
+                }
+            }
+            _MapHandler.map.setBounds(bounds);
+
+            //전체 레벨이 다있는게 아니므로 로드 레벨은 재설정
+            let level = _Level;
+            if (level >= 8) {
+                level = 7;
+            }
+            else if (level >= 6) {
+                level = 6;
+            }
+
+            /**
+             * 범위를 포커스 하게 되면 레벨이 변경되므로 변경 된 레벨의 간선도로 목록에서
+             * 상행, 하행 ID가 포함된 데이터 갖고오며 상행, 하행 데이터 따로 구분
+             */
+            const atrdInfo = _AtrdMap.get(level.toString());
+            if (atrdInfo) {
+                atrdInfo.forEach((atrd)=>{
+                    if (atrd.atrd_id.toString() === AupHillId.toString() || atrd.atrd_id.toString() === AdownHillId.toString()) {
+                        _AtrdData.push(atrd);
+
+                        if (atrd.atrd_id === AupHillId) {
+                            upHillArr.push(atrd);
+                        }
+                        else {
+                            downHillArr.push(atrd);
+                        }
+                    }
+                })
+            }
+            //레벨이 변경됐으므로 다시한번 버텍스를 초기화해준다
+            getVertex();
+
+            //각 상행 하행의 첫번째 좌표를 가지고 상행, 하행 아이콘을 생성 하며 객체는 array에 보관해둠
+            if (upHillArr[0]) {
+                const upHillObj = new TbAtrdObj(upHillArr[0]);
+                _MapHandler['atrd'].push(upHillObj);
+            }
+
+            if (downHillArr[0]) {
+                const downHillObj = new TbAtrdObj(downHillArr[0]);
+                _MapHandler['atrd'].push(downHillObj);
+            }
+        }
+    }
+}
+
+/**
+ * 간선도로 상,하행 마커 그리기
+ * @param src 상, 하행 코드
+ * @param lat X 좌표
+ * @param lng Y 좌표
+ * @param name 명칭
+ * @returns {daum.maps.Marker} 생성 마커
+ */
+function drawAtrdMakrer(src, lng, lat, name) {
+    let imageSize;
+    let imageOption;
+    let imageSrc = '/images/icon/atrd' + src + '.png';
+
+    //레벨별 사이즈를 다르게 표출
+    const level = _Level;
+    let size = 48;
+    if (level >= 7) {
+        size = 24;
+    }
+
+    imageSize = new kakao.maps.Size(size, size);
+    imageOption = {
+        offset: new kakao.maps.Point(size/2, size),
+    };
+
+    let markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imageOption);
+    let markerPosition = new kakao.maps.LatLng(lng, lat);
+
+    let atrdSideMarker = new kakao.maps.Marker({
+        position: markerPosition,
+        image: markerImage,
+        zIndex: 5,
+        title: name,
+    });
+    return atrdSideMarker;
+}
+
+/**
+ * 소통정보 Object
+ */
+class TrafficObj {
+    constructor(obj) {
+        this.ID = obj.roadway_id;
+        this.NAME = obj.roadway_nm;
+        this.obj = obj;
+        this.infoWindow = null;
+        this.polyLine = null;
+        this.polyBackLine = null;
+        this.init();
+    }
+
+    init() {
+        const _self = this;
+        const trafficObj = this.obj;
+        const xArray = trafficObj.x_crdn.split(",");
+        const yArray = trafficObj.y_crdn.split(",");
+        const linePath = [];
+        for (let ii = 0; ii < xArray.length; ii++) {
+            const x_crdn = Number(xArray[ii]);
+            const y_crdn = Number(yArray[ii]);
+            const coordinates = getKakaoPosition(y_crdn, x_crdn);
+            linePath.push(coordinates);
+        }
+
+        let strokeWeight = 5;
+        let strokeWeightBack = 7;
+        const level = _Level;
+
+        if (level === 3) {
+            strokeWeightBack = 6;
+        } else if (level === 5 || level === 4) {
+            strokeWeight = 3;
+            strokeWeightBack = 5;
+        } else if (level >= 6) {
+            strokeWeight = 2;
+            strokeWeightBack = 4;
+        }
+        this.polyBackLine = new kakao.maps.Polyline({
+            path: linePath, // 선을 구성하는 좌표배열 입니다
+            strokeWeight: strokeWeightBack, // 선의 두께 입니다
+            strokeColor: '#eeeeee', // 선의 색깔입니다
+            strokeOpacity: 1, // 선의 불투명도 입니다 1에서 0 사이의 값이며 0에 가까울수록 투명합니다
+            strokeStyle: 'solid', // 선의 스타일입니다
+            zIndex: 1
+        });
+        this.polyLine = new kakao.maps.Polyline({
+            path: linePath, // 선을 구성하는 좌표배열 입니다
+            strokeWeight: strokeWeight, // 선의 두께 입니다
+            strokeColor: g_color.get(trafficObj.cmtr_grad_cd), // 선의 색깔입니다
+            strokeOpacity: 1, // 선의 불투명도 입니다 1에서 0 사이의 값이며 0에 가까울수록 투명합니다
+            strokeStyle: 'solid', // 선의 스타일입니다
+            zIndex: 2
+        });
+
+        this.setVisibleMarker(_MapHandler['trafficFlag']);
+
+        new kakao.maps.event.addListener(this.polyLine, 'mouseover', function (event) {
+
+            this.setOptions({strokeColor: '#0000FF'});
+            const iwContent =
+                `<div class="trafficPop">
+                            <div class="traffic-speed ${trafficObj.cmtr_grad_cd}">
+                                <span class="traffic-name">${_self.NAME}</span>
+                                <span class="traffic-speed-info border-back ${trafficObj.cmtr_grad_cd}">${trafficObj.grad_nm}</span>
+                            </div>
+                            <div class="traffic-info">
+                                <span>${trafficObj.strt_nm_node} → ${trafficObj.end_nm_node}</span>
+                                <br>
+                                <span>소요시간 : </span>
+                                <span class="${trafficObj.cmtr_grad_cd}">약 ${textFormat(trafficObj.trvl_hh)}분 </span>
+                                <span>&nbsp; 속도 : </span>
+                                <span class="${trafficObj.cmtr_grad_cd}">약 ${textFormat(trafficObj.sped)}km/h</span>
+                            </div>
+                         </div>`;
+            _self.infoWindow = new kakao.maps.CustomOverlay({
+                map: _MapHandler.map,
+                clickable: true,
+                position: event.latLng,
+                content: iwContent,
+                xAnchor: -0.1,
+                yAnchor: 1.1,
+                zIndex: 4
+            });
+
+        });
+
+        daum.maps.event.addListener(this.polyLine, 'mouseout', function () {
+            this.setOptions({strokeColor:  g_color.get(trafficObj.cmtr_grad_cd)});
+            if (_self.infoWindow != null) _self.infoWindow.setMap(null);
+        });
+    }
+
+    setVisibleMarker(isVisible) {
+        const isShow = isVisible ? _MapHandler.map : null;
+        this.polyBackLine.setMap(isShow);
+        this.polyLine.setMap(isShow);
+    }
+}
+
+/**
+ * 간선도로 Object
+ */
+class TbAtrdObj {
+    constructor(obj) {
+        this.ID = obj.atrd_id;
+        this.NAME = obj.atrd_nm;
+        this.X_CRDN = null;
+        this.Y_CRDN = null;
+        this.obj = obj;
+        this.marker = null;
+        this.imgSrc = '/images/icon/atrd';
+        this.isClick = false;
+        this.sped = 0;
+        this.trvl_hh = 0;
+        this.cnt = 0;
+        this.infoWindow = null;
+        this.init();
+    }
+
+    init() {
+        if (this.obj.x_crdn_arr) {
+            this.X_CRDN = this.obj.x_crdn_arr.split(",")[0];
+        }
+
+        if (this.obj.y_crdn_arr) {
+            this.Y_CRDN = this.obj.y_crdn_arr.split(",")[0];
+        }
+        const name = this.NAME + " [" + this.obj.drct_nm + "]";
+        this.marker = drawAtrdMakrer(this.obj.drct_cd, this.Y_CRDN, this.X_CRDN, name);
+        this.click();
+        // let _self = this;
+        // new kakao.maps.event.addListener(this.marker, 'mouseover', function (event) {
+        //     let position = getKakaoPosition(_self.Y_CRDN, _self.X_CRDN);
+        //     const iwContent =
+        //         `<div class="trafficPop">
+        //             <div>
+        //                 <span class="traffic-name">${_self.NAME}</span>
+        //                 <span>[${_self.obj.drct_nm}]</span>
+        //             </div>
+        //             <div class="traffic-info">
+        //                 <span>평균속도 : ${Math.round(_self.sped/_self.cnt)} km/h</span><br>
+        //                 <span>통행시간 : ${textFormat(Math.round(_self.trvl_hh/_self.trvl_hh))}분 </span>
+        //             </div>
+        //          </div>`;
+        //     _self.infoWindow = new kakao.maps.CustomOverlay({
+        //         map: _MapHandler.map,
+        //         clickable: true,
+        //         position: position,
+        //         content: iwContent,
+        //         xAnchor: -0.1,
+        //         yAnchor: 1.1,
+        //         zIndex: 4
+        //     });
+        // });
+        // new kakao.maps.event.addListener(this.marker, 'mouseout', function (event) {
+        //     _self.infoWindow.setMap(null);
+        // })
+    }
+
+    click() {
+        this.setVisibleMarker(true);
+    }
+
+    setVisibleMarker(isVisible) {
+        let visible = isVisible ? _MapHandler.map : null;
+        this.marker.setMap(visible);
+        this.isClick = isVisible;
+    }
+
+    close() {
+        this.setVisibleMarker(false);
+    }
+}
+
+/**
+ * CCTV Object
+ */
+class TbCCtvObj {
+    constructor(obj) {
+        this.ID     = obj.cctv_mngm_nmbr;
+        this.NAME   = obj.istl_lctn_nm;
+        this.X_CRDN = obj.x_crdn;
+        this.Y_CRDN = obj.y_crdn;
+        this.URL    = obj.strm_http_addr;
+        this.type   = 'cctv';
+        this.obj = obj;
+        this.marker = null;
+        this.infoWindow = null;
+        this.iwContent  = null;
+        this.isClick = false;
+        this.imgSrc = '/images/icon/cctv';
+        this.timer = null;
+    }
+
+    init() {
+        this.marker = createMarker(this, 'cctv');
+        this.iwContent  = `<div class="cctv-info-window">
+                    <div class="title">
+                        <div class="cctv-name-${this.ID}">${this.NAME}</div>
+                        <div onclick="infoWindowEvent('cctv', '${this.ID}', 'close')">X</div>
+                    </div>
+                    <div class="content">
+                        <div>
+                            <video id="video-${this.ID}" class="video-js" playsinline style="width: 100%; height: 100%;"></video>
+                        </div>
+                        <div>
+                            <div>※ CCTV영상은 30초간 제공됩니다.</div>
+                            <div class="continue-play">계속재생</div>
+                        </div>
+                    </div>
+                </div>`;
+    }
+
+    setVisibleMarker(isVisible) {
+        this.marker.setVisible(isVisible);
+    }
+
+}
+/**
+ * Parking Object
+ */
+class TbParkingObj {
+    constructor(obj) {
+        this.ID = obj.parking_id;
+        this.NAME = obj.parking_nm;
+        this.X_CRDN = obj.x_crdn;
+        this.Y_CRDN = obj.y_crdn;
+        this.obj = obj;
+        this.marker = null;
+        this.infoWindow = null;
+        this.iwContent = null;
+        this.isClick = false;
+        this.imgSrc = '/images/icon/parking';
+        this.timer = null;
+        this.type = 'parking';
+    }
+
+    init() {
+        this.marker = createMarker(this, 'parking');
+        this.iwContent = `<div class="parking-info-window">
+                    <div class="title">
+                        <div class="parking-name-${this.ID}">${this.NAME}</div>
+                        <div onclick="infoWindowEvent('parking', '${this.ID}', 'close')"></div>
+                    </div>
+                    <div class="content">
+                        <div class="row">
+                            <div>주차면수</div>
+                            <div title="주차면수 : ${this.obj.parking_num} 대">${this.obj.parking_num} 대</div>
+                        </div>
+                        <div class="row">
+                            <div>구분</div>
+                            <div title="구분 : ${this.obj.parking_type_desc}">${this.obj.parking_type_desc}</div>
+                        </div>
+                        <div class="row">
+                            <div>기본요금</div>
+                            <div title="기본요금 : ${this.obj.parking_fee_type_desc}">${this.obj.parking_fee_type_desc}</div>
+                        </div>
+                        <div class="row">
+                            <div>주소</div>
+                            <div title="주소 : ${this.obj.parking_addr}">${this.obj.parking_addr}</div>
+                        </div>
+                    </div>
+                </div>`;
+    }
+
+    setVisibleMarker(isVisible) {
+        this.marker.setVisible(isVisible);
+    }
+
+}
+
+/**
+ * VMS Object
+ */
+class TbVmsObj {
+    constructor(obj) {
+        this.ID = obj.vms_ctlr_nmbr;
+        this.NAME = obj.vms_nm;
+        this.X_CRDN = obj.x_crdn;
+        this.Y_CRDN = obj.y_crdn;
+        this.obj    = obj;
+        this.marker = null;
+        this.infoWindow = null;
+        this.iwContent = null;
+        this.isClick = false;
+        this.imgSrc = '/images/icon/vms';
+        this.timer = null;
+        this.phaseArray = [];
+        this.type = 'vms';
+    }
+
+    init() {
+        this.marker = createMarker(this, 'vms');
+        this.phaseArray = [];
+        let width;
+        let height;
+        let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
+        switch (this.obj.vms_type_cd) {
+            case 'VMC1':
+                width  = 400;
+                height = 64;
+                break;
+            case 'VMC2':
+                width  = 384;
+                height = 64;
+                break;
+            case 'VMC3':
+                width  = 288;
+                height = 160;
+                break;
+            case 'VMC4':
+                width  = 256;
+                height = 192;
+                break;
+        }
+        const windowHeight = $(window).height();
+        if (isMobile || windowHeight < 900) {
+            width = width/2;
+            height = height/2;
+        }
+        width = width +'px';
+        height = height + 'px';
+
+        const msg = this.obj.msg;
+
+        if (!msg || msg.length === 0) {
+            width = '256px';
+            height = '50px';
+        }
+
+        let iwContent =
+            `<div class="vms-info-window" style="width: calc(${width} + 10px); height: calc(${height} + 50px);">
+                    <div class="title">
+                        <div class="vms-name-${this.ID}">${this.NAME}</div>
+                        <div onclick="infoWindowEvent('vms', ${this.ID}, 'close')">X</div>
+                    </div>
+                    <div class="content" style="width: ${width}; height: ${height};">`;
+        if (msg && msg.length > 0) {
+            for (let idx in msg) {
+                let msgObj = msg[idx];
+                let className = '';
+                if (idx === "0") {
+                    className = 'active'
+                }
+                iwContent += `<img id="phase-${msgObj.phase}" class="${className}" style="width: ${width}; height: ${height};" src="/api/traffic/vms-dspl-image/${this.ID}/${msgObj.phase}">`;
+                this.phaseArray.push(msgObj.phase);
+            }
+        }
+        else {
+            iwContent += '표출 이미지 데이터가 없습니다.';
+        }
+        iwContent += `</div></div>`;
+        this.iwContent = iwContent;
+
+    }
+
+    setVisibleMarker(isVisible) {
+        this.marker.setVisible(isVisible);
+    }
+}
+
+/**
+ * 돌발정보 Object
+ */
+class TbIncdObj {
+    constructor(obj) {
+        this.ID         = obj.incd_ocrr_id;
+        this.NAME       = obj.incd_titl;
+        this.X_CRDN     = obj.x_crdn;
+        this.Y_CRDN     = obj.y_crdn;
+        this.obj        = obj;
+        this.marker     = null;
+        this.infoWindow = null;
+        this.isClick    = false;
+        this.imgSrc     = '/images/icon/incd';
+        this.iwContent  = null;
+        this.type       = 'incident';
+    }
+
+    init() {
+        this.marker = createMarker(this, 'incident');
+        this.iwContent = `<div class="incident-info-window">
+                    <div class="title">
+                        <div class="incident-name-${this.ID}">${this.NAME}</div>
+                        <div onclick="infoWindowEvent('incident', '${this.ID}', 'close')">X</div>
+                    </div>
+                    <div class="content">
+                        <div>
+                            위치 : ${this.obj.road_nm}
+                        </div>
+                        <div>
+                            설명 : ${this.obj.incd_expl}
+                        </div>
+                        <div>
+                            기간 : ${this.obj.incd_strt_dt} ~ ${this.obj.incd_end_prar_dt}
+                        </div>
+                    </div>
+            </div>`;
+    }
+
+    setVisibleMarker(isVisible) {
+        this.marker.setVisible(isVisible);
+    }
+}
+
+/**
+ * 스마트 교차로 Object
+ */
+class IntersectionObj {
+    constructor(obj) {
+        this.ID         = obj.ixr_id;
+        this.NAME       = obj.ixr_nm;
+        this.X_CRDN     = obj.x_crdn;
+        this.Y_CRDN     = obj.y_crdn;
+        this.obj        = obj;
+        this.marker     = null;
+        this.infoWindow = null;
+        this.isClick    = false;
+        this.imgSrc     = '/images/icon/intersection';
+        this.detail     = [];
+        this.type       = 'intersection';
+    }
+
+    init() {
+        let imageSrc    = this.imgSrc + '.png', // 마커이미지의 주소입니다
+            size        = _size[_Level],
+            imageSize   = new kakao.maps.Size(size, size), // 마커이미지의 크기입니다
+            imageOption = {
+                offset: new kakao.maps.Point(size/2, size/2),
+                alt: this.NAME,
+            };
+
+        // 마커의 이미지정보를 가지고 있는 마커이미지를 생성합니다
+        let markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imageOption),
+            markerPosition = getKakaoPosition(this.Y_CRDN, this.X_CRDN); // 마커가 표시될 위치입니다
+
+        this.marker = new kakao.maps.Marker(
+            {map: _MapHandler.map, position: markerPosition, image: markerImage, zindex: 10, clickable: true, title : this.NAME}
+        );
+        const level     = _Level;
+        const isVisible = (level > 2 && _MapHandler['intersectionFlag']);
+        this.marker.setVisible(isVisible);
+        let _self = this;
+        new kakao.maps.event.addListener(this.marker, 'click', ()=> _MapHandler.click('intersection', _self.ID));
+    }
+
+    setVisibleMarker(isVisible) {
+        if (_Level <= 2) isVisible = false;
+        this.marker.setVisible(isVisible);
+    }
+
+}
+
+/**
+ * 스마트교차로 카메라 Object
+ */
+class IntersectionCameraObj {
+    constructor(obj) {
+        this.ID = obj.cmra_id + '_' + obj.drct_dvsn_cd;
+        this.NAME = obj.drct_lctn;
+        this.X_CRDN = obj.cmra_x_crdn;
+        this.Y_CRDN = obj.cmra_y_crdn;
+        this.URL    = obj.hmpg_cmra_url;
+        this.obj = obj;
+        this.marker = null;
+        this.infoWindow = null;
+        this.iwContent  = null;
+        this.isClick = false;
+        this.timer = null;
+        this.polyline = null;
+        this.type = 'intersectionCamera';
+    }
+
+    init() {
+        this.marker = createIntersectionCameraMarker(this);
+        this.iwContent =
+            `<div class="cctv-info-window">
+                    <div class="title">
+                        <div class="intersectionCamera-name-${this.ID}">${this.NAME}</div>
+                        <div class="close-window" onclick="infoWindowEvent('intersectionCamera', '${this.ID}', 'close')">X</div>
+                    </div>
+                    <div class="content">
+                        <div>
+                            <video id="video-${this.ID}" class="video-js" style="width: 100%; height: 100%;"></video>
+                        </div>
+                        <div>
+                            <div>※ CCTV영상은 30초간 제공됩니다.</div>
+                            <div class="continue-play">계속재생</div>
+                        </div>
+                    </div>
+                </div>`;
+    }
+
+    setVisibleMarker(isVisible) {
+        const visible = isVisible && _Level <= 2 ? _MapHandler.map : null;
+        this.marker.setMap(visible);
+        this.polyline.setMap(visible);
+    }
+
+}
+
+/**
+ * 인포윈도우 이벤트
+ * @param type 시설물 유형
+ * @param id 요소 ID
+ * @param event 이벤트 종류 (click, close)
+ */
+function infoWindowEvent(type, id, event) {
+    _MapHandler[event](type, id);
+}
+
+
+let isHide = false
+
+/**
+ * 좌측 목록 토글 이벤트
+ */
+function toggleEvent() {
+    const $listArea = $('.left-list-area');
+    const $toggleButton = $('.toggle-button');
+
+    if (!isHide) {
+        $toggleButton.animate({
+            left: 0
+        }, 'slow');
+        $listArea .animate({
+            left: -$listArea.width()
+        }, 'slow');
+        $toggleButton.text('>');
+    }
+    else {
+        $toggleButton.animate({
+            left: $listArea.width()
+        }, 'slow');
+        $listArea .animate({
+            left: 0
+        }, 'slow');
+        $toggleButton.text('<');
+    }
+    isHide = !isHide;
+}
+
+window.addEventListener('resize', function(event) {
+    if ($(this).width() > 450) {
+        const $toggleButton = $('.toggle-button');
+        const $listArea = $('.left-list-area');
+        const left = $toggleButton.offset().left;
+        const listLeft = $listArea.offset().left;
+        if ($(this).width() >= 920) {
+            if (left > 0 && left < 400) {
+                $toggleButton.css('left', 400);
+            }
+
+            if (listLeft > -400 && listLeft < 0) {
+                $listArea.css('left', -400);
+            }
+        }
+        else {
+            if (left > 0 && left > 275) {
+                $toggleButton.css('left', 275);
+            }
+
+            if (listLeft < -273) {
+                $listArea.css('left', -273);
+            }
+        }
+    }
+})
+/**
+ * 시설물 마커 초기화
+ * @param obj 시설물 객체
+ * @returns {kakao.maps.Marker}
+ */
+function createMarker(obj, type) {
+    let map = _MapHandler.map;
+    let imageSrc    = obj.imgSrc + '1.png', // 마커이미지의 주소입니다
+        size        = _size[map.getLevel()],
+        imageSize   = new kakao.maps.Size(size, size), // 마커이미지의 크기입니다
+        imageOption = {
+            offset: new kakao.maps.Point(size/2, size/2),
+            alt: obj.NAME,
+        };
+
+    // 마커의 이미지정보를 가지고 있는 마커이미지를 생성합니다
+    let markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imageOption),
+        markerPosition = getKakaoPosition(obj.Y_CRDN, obj.X_CRDN); // 마커가 표시될 위치입니다
+
+    let marker = new kakao.maps.Marker(
+        {map: map, position: markerPosition, image: markerImage, zindex: 10, clickable: true, title : obj.NAME}
+    );
+    let flag = _MapHandler[type+'Flag'];
+    marker.setVisible(flag);
+
+    kakao.maps.event.addListener(marker, 'mouseover', function () {
+        if (!obj.isClick) {
+            setMarkerImage(obj, 2, false);
+        }
+    });
+
+    kakao.maps.event.addListener(marker, 'mouseout', function () {
+        if (!obj.isClick) {
+            setMarkerImage(obj, 1, false)
+        }
+    });
+
+    kakao.maps.event.addListener(marker, 'click', ()=> _MapHandler.click(type, obj.ID));
+
+    return marker;
+}
+
+/**
+ * 스마트 교차로 카메라 마커 초기화
+ * @param obj 스마트 교차로 카메라 객체
+ * @returns {kakao.maps.Marker}
+ */
+function createIntersectionCameraMarker(obj) {
+    const position =  getKakaoPosition(obj.Y_CRDN, obj.X_CRDN);
+    const content = $('<div id="camera_'+obj.ID+'" title="'+obj.NAME+'">');
+    const angle = Number(obj.obj.cmra_angn);
+    content.css(
+        {
+            width: '38.45px',
+            height: '38.45px',
+            backgroundImage:'url(/images/icon/intersection-cctv.png)',
+            backgroundSize : '38.45px 38.45px',
+            backgroundRepeat: 'no-repeat',
+            backgroundPosition : 'center',
+            transform : 'rotate(' + angle +'deg)'
+        });
+
+    const marker = new kakao.maps.CustomOverlay({
+        content: content[0],
+        position: position,
+        zindex: 15,
+    });
+
+    const linePath1 = [obj.obj.start_x, obj.obj.start_y]
+    const linePath2 = [obj.obj.end_x, obj.obj.end_y];
+    const color = ['#888888', '#15B337', '#15B337', '#15B337', '#FFAA00', '#FFAA00', '#EB260C', '#EB260C', '#EB260C'];
+    obj.polyline = new kakao.maps.Polyline({
+        path: [
+            getKakaoPosition(linePath1[1], linePath1[0]),
+            getKakaoPosition(linePath2[1], linePath2[0]),
+        ],
+        strokeWeight: 10,
+        strokeColor: color[obj.obj.acrd_los],
+        strokeOpacity: 1,
+        strokeStyle: 'solid',
+        endArrow: true,
+        name : obj.NAME,
+    });
+
+    if (_Level <= 2 && _MapHandler['intersectionFlag']){
+        marker.setMap(_MapHandler.map);
+        obj.polyline.setMap(_MapHandler.map);
+    }
+
+    content.on('click', ()=> {
+        obj.click();
+    });
+
+    return marker;
+}
+
+/**
+ * 시설물 이미지 유형 변경 이벤트
+ * @param obj 시설물 객체
+ * @param type 시설물 이미지 유형
+ */
+function setMarkerImage(obj, type, isAtrd) {
+    const currentLevel = _Level;
+    let size = _size[currentLevel];
+    let point1 = size/2;
+    let point2 = size/2;
+    if (isAtrd) {
+        size = ( currentLevel >= 7 ) ? 24 : 48;
+        point1 = size/2;
+        point2 = size;
+    }
+
+    const imageSize = new kakao.maps.Size(size, size);
+    const imageSrc  = obj.imgSrc + type +'.png';
+    const imageOption = {
+        offset: new kakao.maps.Point(point1, point2),
+        alt: obj.NAME
+    };
+    const markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imageOption);
+    obj.marker.setImage(markerImage);
+}
+
+/**
+ * 선택 리스트 스크롤 높이 반환 이벤트
+ * @param selectIndex 선택 인덱스
+ * @returns {number} 스크롤 높이
+ */
+function getScrollTop(selectIndex) {
+    let scrollTop = 0;
+    for (let ii=0; ii < selectIndex; ii++) {
+        let height = $('.left-list-area .list-content.list').children().eq(ii).css('height');
+        if (height) {
+            height = Number(height.replace('px', ''));
+            if (!isNaN(height)) {
+                scrollTop += height;
+            }
+        }
+    }
+    return scrollTop;
+}
+
+/**
+ * 좌측 리스트 선택 시 스크롤 변경 이벤트
+ * @param flag
+ * @param array
+ * @param id
+ */
+function moveToScroll(flag, array, id) {
+    if (flag) {
+        let selectIndex = array.findIndex((obj)=> obj.ID == id);
+        let scrollTop = getScrollTop(selectIndex);
+        $('.left-list-area .list-content.list').animate({
+            scrollTop : scrollTop + 'px'
+        });
+        $('.mobile-select').val(id);
+    }
+}
+
+/**
+ * 시설물 데이터 처리 메서드
+ * @param jsonData 수신 데이터
+ * @param array 시설물 Object Array List
+ * @param facilityClass 시설물 Object Class
+ * @param listFlag 시설물 리스트 목록 표출 플래그
+ * @param type 시설물 유형
+ * @returns {*[]} 시설물 Object Array List
+ */
+function receiveFacilityData(jsonData, facilityClass, type) {
+    if (_MapHandler[type].length > 0) {
+        _MapHandler[type].forEach((obj)=>{
+            if (obj.marker) {
+                obj.marker.setMap(null);
+            }
+            if (obj.polyline) {
+                obj.polyline.setMap(null);
+            }
+        });
+        _MapHandler[type] = [];
+    }
+    let listStr = "";
+    let mobileStr = "";
+    if (jsonData && jsonData.length > 0) {
+        jsonData.forEach((obj)=>{
+            const marker = new facilityClass(obj);
+            marker.init();
+            if (type === 'intersection') {
+                if (obj.detail && obj.detail.length > 0) {
+                    obj.detail.forEach((cameraObj)=>{
+                        const camera = new IntersectionCameraObj(cameraObj);
+                        camera.init();
+                        _MapHandler[type + 'Camera'].push(camera);
+                    })
+                }
+            }
+
+            listStr += `<li id="${type}-${marker.ID}" onclick="infoWindowEvent('${type}', '${marker.ID}' , 'click')">${marker.NAME}</li>`
+            mobileStr += `<option value="${marker.ID}">${marker.NAME}</option>`;
+            _MapHandler[type].push(marker);
+        });
+
+        if (_MapHandler[type + 'Flag']) {
+            _MapHandler.show(type);
+        }
+    }
+
+    if (_MapHandler[type + 'ListFlag'] === true) {
+        const listSection  = $('.left-list-area .list-content.list');
+        listSection.empty();
+        listSection.html(listStr);
+        const $mobileSelect = $('.mobile-select');
+        $mobileSelect.append($(mobileStr));
+        $mobileSelect.on('change', function(){
+            const id = $(this).val();
+            if (id && id !== "-") {
+                infoWindowEvent(type, id , 'click');
+            }
+        })
+    }
+}
+
+/**
+ * 맵 중앙 아이콘 위 위치 좌표
+ * @param infoWindow
+ * @returns {number[]}
+ */
+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[_Level];
+    let top = mapTop + mapHalfH - height - iconH;
+    return [top, left];
+}
+
+/**
+ * 맵 드래그 적용
+ */
+function setInfoWindowPositionWidthDraggable(markerObj, type) {
+    const {infoWindow, ID} = markerObj;
+    const position = getInfoWidowPosition(infoWindow);
+    let top = position[0];
+    let left = position[1];
+    infoWindow.css({
+        top : top + 'px',
+        left : left + 'px',
+        position : 'absolute',
+        zIndex : 999,
+    });
+    infoWindow.draggable({containment : 'body', handle: '.'+ type + '-name-' + ID});
+}
+
+
+/**
+ * 카카오 포지션 지정
+ */
+function getKakaoPosition(yCoordinate, xCoordinate) {
+    return new kakao.maps.LatLng(Number(yCoordinate), Number(xCoordinate));
+}
+
+/**
+ * videoJs 객체 생성
+ */
+function createVideoJs(id, url) {
+    let video = videojs("video-" + id, {
+        sources: [
+            {
+                src: url,
+                type: "application/x-mpegURL",
+                crossorigin: "anonymous",
+            },
+        ],
+        responsive: false,
+        autoplay: true,
+        muted: true,
+        preload: "metadata",
+    });
+
+    video.on('error', ()=>{
+        if (video.error().code === 4) {
+            video.pause();
+            video.dispose();
+            const $errorBox = $('.content > div:nth-child(1)');
+            $errorBox.append($('<img src="/images/icon/error.png" alt="스트리밍 오류 이미지">'));
+            $errorBox.css({
+                display : 'flex',
+                alignItems : 'center',
+                justifyContent : 'center',
+            });
+            video = null;
+        }
+    });
+
+    return video;
+}
+
+
+class MapHandler {
+    constructor(id) {
+        this.selectedObj = null;
+        this.map = null;
+        this.mapElement = id;
+        this.atrd = [];
+    }
+    init () {
+        //시설물별 배열, 토글 플래그, 리스트 플래그 생성
+        _FacilityArray.forEach((type)=>{
+            this[type] = [];
+            this[type + 'Flag'] = false;
+            this[type + 'ListFlag'] = false;
+        });
+
+        const container = document.getElementById(this.mapElement); //지도를 담을 영역의 DOM 레퍼런스
+        const options = { //지도를 생성할 때 필요한 기본 옵션
+            center: getKakaoPosition(36.0191816, 129.3432983), //지도의 중심좌표.
+            level: _Level,
+            maxLevel: 9,
+            minLevel: 1,
+            disableDoubleClickZoom: true
+        };
+        this.map = new kakao.maps.Map(container, options);
+        const mapTypeControl = new kakao.maps.MapTypeControl();
+        this.map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);
+        const zoomControl = new kakao.maps.ZoomControl();
+        this.map.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);
+
+        const handler = this;
+
+        /**
+         * Map Zoom Level Change 이벤트 (이미지 사이즈, 스마트 교차로 이미지 토글, 소통정보 정보 변경)
+         */
+        kakao.maps.event.addListener(this.map, 'zoom_changed', function () {
+            _Level = this.getLevel();
+            const zoomChangeArray = ['cctv', 'vms', 'parking', 'incident'];
+            zoomChangeArray.forEach((type)=>{
+                const markerArr = handler[type];
+                const markerFlag = handler[type + 'Flag'];
+                markerSizeChangeWithZoomLevel(markerArr, markerFlag);
+            });
+
+            intersectionMarkerChangeWithZoomLevel();
+            intersectionCameraChangeWidthZoomLevel();
+            if (handler['atrd'].length > 0) {
+                atrdMarkerResize();
+            }
+            getVertex();
+        });
+
+        /**
+         * 좌표 데이터가의 양이 많으므로 이동 할때마다 영역별 소통정보를 새로 그려줌
+         */
+        kakao.maps.event.addListener(this.map, 'dragend', function() {
+            getVertex();
+        });
+
+    }
+
+    //마커 클릭 이벤트
+    click(type, id) {
+        const markerArr      = this[type]; // 시설물 배열
+        const markerFlag     = this[ type + 'Flag']; // 시설물 토글 플래그
+        const markerListFlag = this[ type + 'ListFlag']; // 시설물 토글 플래그
+        const atrdArr        = this['atrd'];
+        if (atrdArr && atrdArr.length > 0) {
+            atrdArr.forEach((atrd)=>{
+                atrd.close();
+            });
+            this['atrd'] = [];
+            _AtrdData = [];
+            const $selectedLi = $('.left-list-area .list-content.list > li.click');
+            if ($selectedLi) {
+                $selectedLi.removeClass('click');
+            }
+        }
+
+        let selectObj = this.selectedObj; // 이전 클릭 객체
+        let clickObj = this.getSelectObj(markerArr, id); // 현재 클릭 객체
+
+        if (selectObj) {
+            if (selectObj === clickObj && type !== 'intersection') {
+                return;
+            }
+            _MapHandler.close(selectObj.type, selectObj.ID);
+        }
+
+        const coordinates = getKakaoPosition(clickObj.Y_CRDN, clickObj.X_CRDN);
+
+        this.selectedObj = clickObj;
+
+        const selectedLi = $('#'+type+'-' + clickObj.ID);
+        selectedLi.addClass('click');
+        selectedLi.focus();
+
+        this.map.setCenter(coordinates);
+        if (this['trafficFlag']) {
+            getVertex();
+        }
+
+        if (clickObj.iwContent) { // 인포 윈도우가 있을때만 실행
+            clickObj.infoWindow = $(clickObj.iwContent);
+            $('body').append(clickObj.infoWindow);
+            setInfoWindowPositionWidthDraggable(clickObj, type);
+        }
+
+        if (clickObj.URL) { // Url 유무로 영상 이벤트 실행
+            this.videoEvent(clickObj);
+        }
+
+        if (type === 'vms') { // vms 일 경우 이미지 표출실행
+            this.vmsEvent(clickObj);
+        }
+
+        if (type === 'intersection') { // 스마트 교차로는 줌레벨 2로 변경
+            this.map.setLevel(2);
+        }
+        else if (clickObj.imgSrc) { // img src 값이 있는 객체만 클릭 이미지 변경
+            setMarkerImage(clickObj, 2, false);
+        }
+
+        clickObj.isClick = true;
+        if (markerListFlag) {
+            moveToScroll(markerFlag, markerArr, clickObj.ID); // 클릭 시 리스트 이동
+        }
+    }
+
+    //마커 이벤트 종료
+    close(type, id) {
+        const markerArr = this[type];
+        const listFlag  = this[type + 'ListFlag'];
+        const closeObj =  this.getSelectObj(markerArr, id);
+        if (closeObj) {
+            closeObj.isClick = false;
+            this.selectedObj = null;
+
+            if (!type.includes('intersection')) { // 스마트교차로 아닌 화면은 이미지를 원래대로 돌린다.
+                setMarkerImage(closeObj, 1, false);
+            }
+
+            let oldPlayer    = document.getElementById("video-" + closeObj.ID);
+            let selectedLi   = $("#" + type + "-" + closeObj.ID);
+
+            if (selectedLi.hasClass('click')) { // 선택 리스트가 있는지 여부
+                selectedLi.removeClass('click');
+            }
+
+            if (listFlag) { // 리스트 플래그 있는지 여부
+                $('.mobile-select').val("-");
+            }
+
+            if (oldPlayer) { // 켜져있던 영상이 있는지 여부
+                videojs(oldPlayer).dispose();
+            }
+            if (closeObj.infoWindow) { // 인포 윈도우를 화면에 올려놨는지
+                closeObj.infoWindow.remove();
+                closeObj.infoWindow = null;
+            }
+            if (closeObj.timer) { // 인터벌 된 객체가 있는지
+                clearTimeout(closeObj.timer);
+            }
+        }
+    }
+
+    //선택 객체 찾기
+    getSelectObj(array, id) {
+        let idx = array.findIndex(obj => obj.ID.toString() === id.toString());
+        return array[idx];
+    }
+
+    // 마커 보이기
+    show(type) {
+        this[type].forEach(obj => obj.setVisibleMarker(true));
+        this[type + 'Flag'] = true;
+    }
+
+    // 마커 숨기기
+    hide(type) {
+        this[type].forEach(obj => {
+            obj.setVisibleMarker(false);
+            if (obj.isClick === true) {
+                _MapHandler.close(type, obj.ID);
+            }
+        });
+        this[type + 'Flag'] = false;
+    }
+
+    //영상 이벤트
+    videoEvent(obj) {
+        let video = createVideoJs(obj.ID, obj.URL);
+
+        obj.timer = setTimeout(()=>{
+            if (video) {
+                video.pause();
+            }
+        }, CCTV_DISPLAY_TIME);
+
+        $('.continue-play').on('click', ()=>{
+            if (obj.timer) {
+                setTimeout(obj.timer);
+            }
+            if (video) {
+                video.play();
+                obj.timer = setTimeout(()=>{
+                    video.pause();
+                }, CCTV_DISPLAY_TIME);
+            }
+        });
+    }
+
+    //vms 이미지 표출 이벤트
+    vmsEvent(obj) {
+        let cnt = 1;
+        if (obj.phaseArray.length > 0) {
+            obj.timer = setInterval(()=>{
+                if (cnt === obj.phaseArray.length) {
+                    cnt = 0;
+                }
+                const activeImage = $('.vms-info-window .content img.active');
+                if (activeImage[0]) {
+                    activeImage.removeClass('active');
+                }
+                $("#phase-" + obj.phaseArray[cnt]).addClass('active');
+                cnt++;
+            }, VMS_DISPLAY_TIME);
+        }
+    }
+}
+
+

+ 35 - 21
src/main/resources/templates/main/main.html

@@ -35,20 +35,20 @@
                             </div>
                         </div>
                     </div>
-                    <div class="bottom">
-                        <div class="first-box">
-                            <h1>POHANG</h1>
-                            <div>포항시교통정보센터 홍보 영상입니다.</div>
-                        </div>
-                        <div>
-                            <div>
-                                <video id="video" muted playsinline controls>
-                                    <source src="/video/video.mp4" preload="metadata" type="video/mp4">
-                                    이 문장은 여러분의 브라우저가 video 태그를 지원하지 않을 때 화면에 표시됩니다!
-                                </video>
-                            </div>
-                        </div>
-                    </div>
+<!--                    <div class="bottom">-->
+<!--                        <div class="first-box">-->
+<!--                            <h1>POHANG</h1>-->
+<!--                            <div>포항시교통정보센터 홍보 영상입니다.</div>-->
+<!--                        </div>-->
+<!--                        <div>-->
+<!--                            <div>-->
+<!--                                <video id="video" muted playsinline controls>-->
+<!--                                    <source src="/video/video.mp4" preload="metadata" type="video/mp4">-->
+<!--                                    이 문장은 여러분의 브라우저가 video 태그를 지원하지 않을 때 화면에 표시됩니다!-->
+<!--                                </video>-->
+<!--                            </div>-->
+<!--                        </div>-->
+<!--                    </div>-->
                     <div class="mid">
                         <div class="notice">
                             <div>
@@ -78,20 +78,34 @@
                                 <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><img src="/images/icon/video.png" alt="동영상 아이콘">홍보 동영상</div>
+                            <video id="video" muted playsinline controls>
+                                <source src="/video/video.mp4" preload="metadata" type="video/mp4">
+                                이 문장은 여러분의 브라우저가 video 태그를 지원하지 않을 때 화면에 표시됩니다.
+                            </video>
+                        </div>
                     </div>
 <!--                    <div class="bottom">-->
-<!--                        <div class="center" onclick="movePath('/center/center')">-->
-<!--                            <div>교통정보센터 소개 ></div>-->
+<!--                        <div class="first-box">-->
+<!--                            <h1>POHANG</h1>-->
+<!--                            <div>포항시교통정보센터 홍보 영상입니다.</div>-->
 <!--                        </div>-->
 <!--                        <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>-->
+<!--                                <video id="video" muted playsinline controls>-->
+<!--                                    <source src="/video/video.mp4" preload="metadata" type="video/mp4">-->
+<!--                                    이 문장은 여러분의 브라우저가 video 태그를 지원하지 않을 때 화면에 표시됩니다!-->
+<!--                                </video>-->
+<!--                            </div>-->
 <!--                        </div>-->
 <!--                    </div>-->
 

Vissa filer visades inte eftersom för många filer har ändrats