Procházet zdrojové kódy

2024-02-20 admin popup & conn-statistics page update

junggilpark před 1 rokem
rodič
revize
498d4c4e37
27 změnil soubory, kde provedl 1234 přidání a 61 odebrání
  1. 7 0
      src/main/java/com/its/web/controller/admin/AdminController.java
  2. 8 1
      src/main/java/com/its/web/controller/common/CommonController.java
  3. 10 3
      src/main/java/com/its/web/controller/popup/popupController.java
  4. 23 0
      src/main/java/com/its/web/dto/common/ConnStatisticsDto.java
  5. 59 0
      src/main/java/com/its/web/interceptor/ConnectHistory.java
  6. 1 0
      src/main/java/com/its/web/mapper/its/admin/AdminMapper.java
  7. 8 0
      src/main/java/com/its/web/mapper/its/common/CommonMapper.java
  8. 2 0
      src/main/java/com/its/web/mapper/its/popup/PopupMapper.java
  9. 0 4
      src/main/java/com/its/web/security/WebLoginSuccessHandler.java
  10. 17 3
      src/main/java/com/its/web/security/WebSecurityConfig.java
  11. 24 2
      src/main/java/com/its/web/service/common/CommonService.java
  12. 29 2
      src/main/java/com/its/web/service/popup/PopupService.java
  13. 21 0
      src/main/resources/mybatis/mapper/its/common/CommonMapper.xml
  14. 13 9
      src/main/resources/mybatis/mapper/its/popup/PopupMapper.xml
  15. 36 1
      src/main/resources/static/css/admin-main.css
  16. 242 0
      src/main/resources/static/css/conn-statistics.css
  17. 410 0
      src/main/resources/static/css/daterangepicker.css
  18. 3 0
      src/main/resources/static/css/notice.css
  19. 13 0
      src/main/resources/static/js/daterangepiker/daterangepicker.min.js
  20. 5 0
      src/main/resources/static/js/daterangepiker/moment.min.js
  21. 122 0
      src/main/resources/templates/admin/conn-statistics.html
  22. 79 9
      src/main/resources/templates/admin/main.html
  23. 1 1
      src/main/resources/templates/admin/popup-list.html
  24. 40 15
      src/main/resources/templates/admin/popup-view.html
  25. 57 10
      src/main/resources/templates/admin/popup-write.html
  26. 1 1
      src/main/resources/templates/include/admin-header.html
  27. 3 0
      src/main/resources/templates/include/daterangepicker.html

+ 7 - 0
src/main/java/com/its/web/controller/admin/AdminController.java

@@ -70,6 +70,13 @@ public class AdminController {
         return "admin/notice-write";
     }
 
+    @ApiOperation(value = "02.접속자통계")
+    @GetMapping("/conn-statistics")
+    public String adminConnStatistics(Model model) {
+        model.addAttribute("selected", "statistics");
+        return "admin/conn-statistics";
+    }
+
     @ApiOperation(value = "03.팝업공지 - 02.팝업 상세내용")
     @GetMapping(value= {"/popup-list","/popup-list/{page}"})
     public String adminPopupList(Model model, @Nullable @PathVariable("page") String page) {

+ 8 - 1
src/main/java/com/its/web/controller/common/CommonController.java

@@ -1,5 +1,6 @@
 package com.its.web.controller.common;
 
+import com.its.web.dto.common.ConnStatisticsDto;
 import com.its.web.dto.common.TbWwwOrgDto;
 import com.its.web.service.common.CommonService;
 import io.swagger.annotations.Api;
@@ -25,5 +26,11 @@ public class CommonController {
     @ApiOperation(value = "공공기관 웹사이트 조회(TB_WWW_ORGAN)", response = TbWwwOrgDto.class, responseContainer = "ArrayList")
     @PostMapping(value = "/web-organ", produces = {"application/json; charset=utf8"})
     @ResponseBody
-    public List<TbWwwOrgDto> findAllOrganization() { return service.findAllOrganization(); }
+    public List<TbWwwOrgDto> findAllOrganization() { return this.service.findAllOrganization(); }
+
+    @ApiOperation(value = "접속자 통계(TB_WWW_CONN_HS)", response = ConnStatisticsDto.class, responseContainer = "ArrayList")
+    @PostMapping(value = "/conn-statistics", produces = {"application/json; charset=utf8"})
+    @ResponseBody
+    public List<ConnStatisticsDto> getConnStatistics() {return this.service.getConnStatistics();}
+
 }

+ 10 - 3
src/main/java/com/its/web/controller/popup/popupController.java

@@ -35,9 +35,16 @@ public class popupController {
     }
 
     @ApiOperation(value= "03.팝업 글수정")
-    @PostMapping(value = "/updatePopup", produces = {"application/json; charset=utf8"})
+    @PostMapping(value = "/modifyPopup", produces = {"application/json; charset=utf8"})
     @ResponseBody
-    public Map<String, String> updatePopup(@RequestParam Map<String, String> paramMap, MultipartFile attachFile) {
-        return this.service.updatePopup(paramMap, attachFile);
+    public Map<String, String> modifyPopup(@RequestParam Map<String, String> paramMap, MultipartFile attachFile) {
+        return this.service.modifyPopup(paramMap, attachFile);
+    }
+
+    @ApiOperation(value= "03.팝업 글삭제")
+    @PostMapping(value = "/deletePopup", produces = {"application/json; charset=utf8"})
+    @ResponseBody
+    public Map<String, String> deletePopup(@Param("popupId") String popupId) {
+        return this.service.deletePopup(popupId);
     }
 }

+ 23 - 0
src/main/java/com/its/web/dto/common/ConnStatisticsDto.java

@@ -0,0 +1,23 @@
+package com.its.web.dto.common;
+
+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("TbWwwConnHsDto(접속 통계 DTO)")
+public class ConnStatisticsDto {
+
+    @ApiModelProperty("웹페이지 접속일")
+    @JsonProperty("conn_dt")
+    private String connDt;
+
+    @ApiModelProperty("웹페이지 접속자 수")
+    @JsonProperty("conn_cnt")
+    private Integer connCnt;
+}

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

@@ -0,0 +1,59 @@
+package com.its.web.interceptor;
+
+import com.its.web.service.common.CommonService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataAccessException;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+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;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@RequiredArgsConstructor
+public class ConnectHistory implements HandlerInterceptor {
+
+    private final CommonService service;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        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) {
+            log.info("Connect Ip Address : {}, UUID : {}", hostIp, session.getId());
+            session.setAttribute(hostIp, session.getId());
+            Map<String, String> paramMap = new HashMap<>();
+            paramMap.put("connIpAddr", hostIp);
+            paramMap.put("connUuid", session.getId());
+            try {
+                service.insertConnHs(paramMap);
+            }
+            catch (DataAccessException e) {
+                log.error("접속 이력 등록에 실패하였습니다.");
+            }
+        }
+        if (uri.equals("/phits")) {
+            if (Arrays.asList(ipAddressArr).contains(hostIp)) {
+                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+                if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {
+                    response.sendRedirect("/phits/main");
+                }
+                return true;
+            }
+            response.sendRedirect("/");
+        }
+
+        return true;
+    }
+}

+ 1 - 0
src/main/java/com/its/web/mapper/its/admin/AdminMapper.java

@@ -13,4 +13,5 @@ public interface AdminMapper {
 
     void updateFailCount(Map<String, String> paramMap);
 
+
 }

+ 8 - 0
src/main/java/com/its/web/mapper/its/common/CommonMapper.java

@@ -1,14 +1,22 @@
 package com.its.web.mapper.its.common;
 
 import com.its.web.dto.common.CodeDto;
+import com.its.web.dto.common.ConnStatisticsDto;
 import com.its.web.dto.common.TbWwwOrgDto;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
+import java.util.Map;
 
 @Mapper
 public interface CommonMapper {
     List<TbWwwOrgDto> findAllOrganization();
 
     List<CodeDto> findDayList();
+
+    int insertConnHs(Map<String, String> paramMap);
+
+    List<ConnStatisticsDto> getConnStatistics();
+
+    Integer getCountConnHs(Map<String, String> paramMap);
 }

+ 2 - 0
src/main/java/com/its/web/mapper/its/popup/PopupMapper.java

@@ -20,4 +20,6 @@ public interface PopupMapper {
     int insertPopup(Map<String, String> paramMap);
 
     int updatePopup(Map<String, String> paramMap);
+
+    int deletePopup(String popupId);
 }

+ 0 - 4
src/main/java/com/its/web/security/WebLoginSuccessHandler.java

@@ -2,7 +2,6 @@ package com.its.web.security;
 
 import com.its.web.dto.admin.PrincipalDetail;
 import com.its.web.dto.admin.TbWwwMemberDto;
-import com.its.web.mapper.its.admin.AdminMapper;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.core.Authentication;
@@ -27,9 +26,6 @@ import java.util.List;
 @Service
 public class WebLoginSuccessHandler implements AuthenticationSuccessHandler {
 
-//    private final TbUserCnncHsRepository cnncHsRepo;
-    private final AdminMapper mapper;
-
     @Override
     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
         SimpleDateFormat sdfDate = new SimpleDateFormat("yyyyMMddHHmmss");

+ 17 - 3
src/main/java/com/its/web/security/WebSecurityConfig.java

@@ -1,6 +1,8 @@
 package com.its.web.security;
 
+import com.its.web.interceptor.ConnectHistory;
 import com.its.web.service.admin.LoginService;
+import com.its.web.service.common.CommonService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Bean;
@@ -12,16 +14,19 @@ import org.springframework.security.core.session.SessionRegistry;
 import org.springframework.security.core.session.SessionRegistryImpl;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
 @Slf4j
 @Configuration
 @EnableWebSecurity
 @RequiredArgsConstructor
-public class WebSecurityConfig {
+public class WebSecurityConfig implements WebMvcConfigurer {
 
     private final LoginService loginService;
     private final WebLoginSuccessHandler webLoginSuccessHandler;
     private final WebLoginFailureHandler webLoginFailureHandler;
+    private final CommonService commonService;
 
     @Bean
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -30,17 +35,25 @@ public class WebSecurityConfig {
                 .antMatchers("/swagger-ui.html", "/swagger/**", "/swagger-resources/**", "/webjars/**", "/v2/api-docs").permitAll()
                 .antMatchers("/", "/trafficMap/**", "/api/**", "/notice/**", "/center/**", "/statistics/**").permitAll()
                 .antMatchers("/images/**", "/css/**", "/js/**", "/font/**").permitAll()
-                .antMatchers("/phits").hasIpAddress("127.0.0.1")
+//                .antMatchers("/phits").hasIpAddress("127.0.0.1")
+                .antMatchers("/phits").permitAll()
                 .anyRequest().authenticated();
         http.formLogin()
                 .loginPage("/phits")
                 .loginProcessingUrl("/phits/login")
-                .failureUrl("/")
+                .failureUrl("/phits")
                 .successHandler(this.webLoginSuccessHandler)
                 .failureHandler(this.webLoginFailureHandler);
         return http.build();
     }
 
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(new ConnectHistory(commonService))
+                .excludePathPatterns("/api/**", "/images/**", "/css/**", "/js/**", "/font/**"
+                        , "/swagger-ui.html", "/swagger/**", "/swagger-resources/**", "/webjars/**", "/v2/api-docs");
+    }
+
     @Bean
     public PasswordEncoder passwordEncoder() {
         return new WebPasswordEncoder();
@@ -51,6 +64,7 @@ public class WebSecurityConfig {
         return new SessionRegistryImpl();
     }
 
+    @Bean
     public DaoAuthenticationProvider daoAuthenticationProvider() {
         DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
         authenticationProvider.setUserDetailsService(this.loginService);

+ 24 - 2
src/main/java/com/its/web/service/common/CommonService.java

@@ -1,6 +1,7 @@
 package com.its.web.service.common;
 
 import com.its.web.dto.common.CodeDto;
+import com.its.web.dto.common.ConnStatisticsDto;
 import com.its.web.dto.common.TbWwwOrgDto;
 import com.its.web.mapper.its.common.CommonMapper;
 import lombok.RequiredArgsConstructor;
@@ -8,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
+import java.util.Map;
 
 @Slf4j
 @RequiredArgsConstructor
@@ -17,7 +19,7 @@ public class CommonService {
 
     /**
      * 공공기관 사이트 URL
-     * @return
+     * @return List<TbWwwOrgDto>
      */
     public List<TbWwwOrgDto> findAllOrganization() {
         return this.mapper.findAllOrganization();
@@ -25,9 +27,29 @@ public class CommonService {
 
     /**
      * Day Code 리스트
-     * @return
+     * @return List<CodeDto>
      */
     public List<CodeDto> findDayList() {
         return this.mapper.findDayList();
     }
+
+    /**
+     * 접속자 조회 통계
+     * @return List<ConnStatisticsDto>
+     */
+    public List<ConnStatisticsDto> getConnStatistics() { return this.mapper.getConnStatistics(); }
+
+    /**
+     * 접속자 이력 등록
+     * @return DB 등록 수
+     */
+    public int insertConnHs(Map<String,String> paramMap) { return this.mapper.insertConnHs(paramMap); }
+
+
+    /**
+     * 접속 이력 있는지 확인
+     * @param paramMap
+     * @return
+     */
+    public Integer getCountConnHs(Map<String,String> paramMap) {return this.mapper.getCountConnHs(paramMap);}
 }

+ 29 - 2
src/main/java/com/its/web/service/popup/PopupService.java

@@ -25,9 +25,17 @@ import java.util.UUID;
 public class PopupService {
     private final PopupMapper mapper;
 
+    /**
+     * 팝업 저장 위치
+     */
     @Value("${popup-location}")
     String popupLocation;
 
+    /**
+     * 전체 조회 (페이지네이션)
+     * @param page 현재 페이지
+     * @return Pagination Object
+     */
     public Pagination findAllPopup(String page) {
         int totalCount = this.mapper.findPopupTotalPage();
         Pagination pagination = new Pagination( page, totalCount, null, null);
@@ -35,6 +43,11 @@ public class PopupService {
         return pagination;
     }
 
+    /**
+     * 메인 화면 팝업
+     * @param num 불러올 개수
+     * @return List<TbWwwPopupDto>
+     */
     public List<TbWwwPopupDto> findMainPopup(int num) {
         return this.mapper.findMainPopup(num);
     }
@@ -47,6 +60,7 @@ public class PopupService {
         Map<String, String> resultMap = new HashMap<>();
         String message = "작성하신 팝업 공지가 등록 되었습니다.";
         String success = "S";
+        log.error("fileName : {}", attachFile.getOriginalFilename());
         if (attachFile != null && !attachFile.isEmpty()) {
 
             if (attachFile.getOriginalFilename() != null) {
@@ -62,7 +76,7 @@ public class PopupService {
                     file.mkdirs();
                 }
 
-                File transFerFile = new File(popupLocation + fileId);
+                File transFerFile = new File(popupLocation + "/" +  fileId);
                 try {
                     attachFile.transferTo(transFerFile);
                 } catch (IOException e) {
@@ -93,7 +107,7 @@ public class PopupService {
         return resultMap;
     }
 
-    public Map<String, String> updatePopup(Map<String, String> paramMap, MultipartFile attachFile) {
+    public Map<String, String> modifyPopup(Map<String, String> paramMap, MultipartFile attachFile) {
         Map<String, String> resultMap = new HashMap<>();
         String message = "작성하신 팝업 공지가 수정 되었습니다.";
         String success = "S";
@@ -180,4 +194,17 @@ public class PopupService {
         return dto;
     }
 
+    public Map<String, String> deletePopup(String popupId) {
+        Map<String, String> resultMap = new HashMap<>();
+        String success = "S";
+        String message = "선택하신 팝업 공지가 삭제되었습니다.";
+        int affectedRow = this.mapper.deletePopup(popupId);
+        if (affectedRow <= 0) {
+            success = "F";
+            message = "선택하신 팝업 공지가 삭제되지 않았습니다.";
+        }
+        resultMap.put("success", success);
+        resultMap.put("message", message);
+        return resultMap;
+    }
 }

+ 21 - 0
src/main/resources/mybatis/mapper/its/common/CommonMapper.xml

@@ -13,4 +13,25 @@
         FROM TB_CMMN_CD
         WHERE CMMN_CLSF_CD = 'DTW'
     </select>
+
+    <insert id="insertConnHs" parameterType="java.util.HashMap">
+        INSERT INTO TB_WWW_CONN_HS (CONN_DT, CONN_IP_ADDR, CONN_UUID) VALUES(TO_CHAR(sysdate, 'YYYYMMDDHH24MISS'), #{connIpAddr}, #{connUuid})
+    </insert>
+
+    <select id="getConnStatistics" resultType="com.its.web.dto.common.ConnStatisticsDto">
+        SELECT
+            TO_CHAR(TO_DATE(A.DAYS, 'YYYY-MM-DD'), 'YYYY-MM-DD') AS conn_dt,
+            count(B.CONN_IP_ADDR) AS conn_cnt
+          FROM ( SELECT TO_CHAR(SYSDATE, 'YYYYMMDD') DAYS FROM DUAL
+                 UNION ALL
+                 SELECT TO_CHAR(TRUNC(TO_DATE(SYSDATE), 'DD') - LEVEL, 'YYYYMMDD') DAYS FROM DUAL
+                 CONNECT BY LEVEL <![CDATA[<=]]> 7
+                ) A
+        LEFT OUTER JOIN TB_WWW_CONN_HS B ON A.DAYS = TO_CHAR(TO_DATE(B.conn_dt, 'YYYYMMDDHH24MISS'), 'YYYYMMDD')
+        GROUP BY A.DAYS ORDER BY A.DAYS;
+    </select>
+
+    <select id="getCountConnHs" parameterType="java.util.HashMap" resultType="java.lang.Integer">
+        SELECT COUNT(*) FROM TB_WWW_CONN_HS WHERE CONN_IP_ADDR = #{connIpAddr} AND CONN_UUID = #{connUuid}
+    </select>
 </mapper>

+ 13 - 9
src/main/resources/mybatis/mapper/its/popup/PopupMapper.xml

@@ -18,13 +18,13 @@
                      ROWNUM AS RNUM
                 FROM (SELECT *
                         FROM TB_WWW_POPUP
+                        WHERE DEL_YN = 'N'
                        ORDER BY popupid DESC, regdate DESC
                 ) A
         )
         <![CDATA[
         WHERE RNUM >= #{startRow}
           AND RNUM <= #{endRow}
-          AND del_yn = 'N'
         ]]>
     </select>
 
@@ -53,9 +53,7 @@
         SELECT
             count(*)
           FROM TB_WWW_POPUP
-         <![CDATA[
-        WHERE del_yn = 'N'
-        ]]>
+         WHERE del_yn = 'N'
     </select>
 
     <select id="findPopup" parameterType="java.lang.String" resultType="com.its.web.dto.admin.TbWwwPopupDto">
@@ -89,11 +87,11 @@
               VALUES ((SELECT NVL(MAX(popupid),0)+1 FROM TB_WWW_POPUP),
                       #{title},
                       #{url},
-                      TO_DATE(TO_CHAR(#{postForm}, 'YYYY-MM-DD'), 'YYYY-MM-DD'),
-                      TO_DATE(TO_CHAR(#{postTo}, 'YYYY-MM-DD'), 'YYYY-MM-DD'),
+                      TO_DATE(#{postForm}, 'YYYY-MM-DD'),
+                      TO_DATE(#{postTo}, 'YYYY-MM-DD'),
                       #{imgId},
                       #{imgName},
-                      TO_DATE(TO_CHAR(sysdate, 'YYYY-MM-DD'), 'YYYY-MM-DD')
+                      TO_DATE(sysdate, 'YYYY-MM-DD'),
                       'Y',
                       'N'
                       )
@@ -103,10 +101,16 @@
         UPDATE TB_WWW_POPUP
            SET title     = #{title},
                url       = #{url},
-               post_from = TO_DATE(TO_CHAR(#{postFrom}, 'YYYY-MM-DD'), 'YYYY-MM-DD'),
-               post_to   = TO_DATE(TO_CHAR(#{postTo}, 'YYYY-MM-DD'), 'YYYY-MM-DD'),
+               post_from = TO_DATE(#{postFrom}, 'YYYY-MM-DD'),
+               post_to   = TO_DATE(#{postTo}, 'YYYY-MM-DD'),
                img_id    = #{imgId},
                img_name  = #{imgName}
          WHERE popupid   = #{popupId}
     </update>
+
+    <update id="deletePopup" parameterType="java.lang.String" >
+        UPDATE TB_WWW_POPUP
+        SET DEL_YN = 'Y'
+        WHERE popupid = #{popupId}
+    </update>
 </mapper>

+ 36 - 1
src/main/resources/static/css/admin-main.css

@@ -30,6 +30,7 @@
 }
 .mainWrap .main .top > div:nth-child(1) {
     margin-right: 5px;
+    overflow: auto;
 }
 
 .mainWrap .main .top > div:nth-child(2) {
@@ -68,16 +69,42 @@
     font-weight: bold;
     justify-content: space-between;
 }
+.content > div:hover {
+    cursor: pointer;
+    background-color: #eeeeee;
+}
+
 .content > div > div {
     text-overflow: ellipsis;
     white-space: nowrap;
     overflow: hidden;
 }
+.content > div > span {
+    font-size: 14px;
+}
+
+.content > div.notice > div {
+    width: calc(100% - 80px);
+}
+.content > div.notice > span {
+    width: 80px;
+}
+.content > div.popup > div {
+    width: calc(100% - 170px);
+}
+.content > div.popup > span {
+    width: 170px;
+}
+
+#chart {
+    height: calc(100% - 34px);
+}
 
 .sub-title {
     color: #007fce;
     font-weight: bold;
     padding: 5px 0 10px 0;
+    cursor: pointer;
 }
 
 .notice-list:hover {
@@ -97,7 +124,15 @@
 @media (min-width: 765px) {
 }
 
-@media (min-width: 920px) {
+@media (max-width: 920px) {
+    .content > div.popup > div,
+    .content > div.notice > div {
+        width: 100%;
+    }
+    .content > div.popup > span,
+    .content > div.notice > span {
+        display: none;
+    }
 }
 
 

+ 242 - 0
src/main/resources/static/css/conn-statistics.css

@@ -0,0 +1,242 @@
+
+.menu {
+    max-width: 1200px;
+    width: 100%;
+    margin: 1.5rem auto 0 auto;
+    display: flex;
+    -webkit-box-pack: center;
+    justify-content: center;
+}
+.menu > div {
+    width: 100px;
+    word-break: keep-all;
+    text-align: center;
+    line-height: 1.1;
+    font-weight: bold;
+    font-size: 18px;
+}
+
+.menu > div:not(:first-child) {
+    margin-left: 30px;
+}
+
+.menu > div.active {
+    color: rgb(51, 102, 171);
+}
+
+.menu > div:hover{
+    color: rgb(51, 102, 171);
+    cursor: pointer;
+}
+
+.mobile-menu {
+    display: none;
+    padding: 0.6rem 1rem;
+    border-top: 1px solid rgb(230, 230, 230);
+    justify-content: space-around;
+    width: 100%;
+    height: 80px;
+}
+.mobile-menu > div > div {
+    filter: grayscale(1);
+    background-size: 35px 35px;
+    width: 100%;
+    height: 35px;
+    background-position: center;
+    background-repeat: no-repeat;
+}
+.mobile-menu > div {
+    font-weight: bold;
+    padding-top : 5px;
+    font-size: 11px;
+}
+.mobile-menu > div.active > div {
+    filter: grayscale(0);
+}
+.mobile-menu > div.active {
+    color: rgb(51, 102, 171);
+}
+
+.mobile-menu > div:nth-child(1) > div {
+    background-image: url("/images/icon/menu_icon5-2.png");
+}
+
+.mobile-menu > div:nth-child(2) > div {
+    background-image: url("/images/icon/way.png");
+}
+
+.statWrap {
+    width: 100%;
+    height: calc(100% - 199.8px);
+    display: flex;
+    justify-content: center;
+    overflow: auto;
+}
+.statWrap .admin-header {
+    width: 200px;
+    height: 50px;
+    margin: 24px auto;
+    color: white;
+    text-align: center;
+    font: bold 18px / 40px NanumGothic;
+    background: url('/images/background/bg_title.png') 0px 0px / contain no-repeat transparent;
+}
+
+.statWrap .container {
+    max-width: 1200px;
+    width: 95%;
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    min-height: 700px;
+}
+
+.statWrap .header {
+    padding: 2rem 0;
+    margin-bottom: 0;
+    color: rgb(51, 102, 171);
+    text-align: center;
+    border-bottom: 1px solid rgb(33, 84, 153);
+    font-size: 2rem;
+}
+
+.statWrap .content {
+    margin: 24px 0px;
+    padding: 30px 60px;
+    transition: all 0.3s ease 0s;
+    box-shadow: rgba(0, 0, 0, 0.15) 0px 3px 6px;
+    height: calc(100% - 101px);
+}
+
+.statWrap .admin-content {
+    width: 100%;
+    height: calc(100% - 150px);
+    min-height: 500px;
+    padding: 30px 60px;
+    transition: all 0.3s ease 0s;
+    background-color: rgb(255, 255, 255);
+    box-shadow: rgba(0, 0, 0, 0.15) 0px 3px 6px;
+}
+.statWrap .admin-content .title {
+    display: flex;
+    justify-content: right;
+    align-items: center;
+    gap: 10px;
+}
+
+.title input {
+    color: rgb(58, 57, 57);
+    font-size: 17px;
+    line-height: 25px;
+    width: 250px;
+    text-align: center;
+    height: 40px;
+    padding: 8px 16px;
+    border: 1px solid rgb(204, 234, 234);
+    box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 3px;
+    background: 0px 0px no-repeat rgb(255, 255, 255);
+}
+
+.title .button {
+
+}
+.chart-box {
+    width: 100%;
+    height: calc(100% - 302px);
+}
+
+.chart-box #chart {
+    width: 100%;
+    height: 100%;
+}
+
+.list {
+    width: 100%;
+    height: 262px;
+}
+
+/*
+    border-bottom: 2px solid rgba(102, 102, 102, 0.3);
+    color: rgb(51, 102, 171);
+*/
+
+@media (max-width: 920px) {
+    .statWrap {
+        height: calc(100% - 200.19px);
+    }
+
+}
+
+@media (max-height: 765px) {
+    .statWrap {
+        height: calc(100% - 200.19px);
+    }
+}
+
+
+@media (max-width: 720px) {
+    .statWrap {
+        height: calc(100% - 205.19px);
+    }
+}
+
+
+@media (max-width: 547px) {
+    .statWrap {
+        height: calc(100% - 216.19px);
+    }
+}
+
+@media (max-width: 420px) {
+    .mobile-menu {
+        display: flex;
+    }
+
+    .menu {
+        display:  none;
+    }
+
+    .statWrap {
+        height: calc(100% - 149.19px);
+        padding: 5px 0;
+    }
+
+    .statWrap .header {
+        font-size: 1.2rem;
+        padding: 1rem 0;
+    }
+
+
+    .statWrap .container {
+        min-height: 0;
+    }
+    .statWrap .container.view {
+        min-height: 450px;
+    }
+
+    .statWrap h2 {
+        font-size: 14px;
+    }
+    .statWrap .content a {
+        padding: 10px;
+        font-size: 13px;
+    }
+
+    .statWrap .content a > div:nth-child(1) {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        width: calc(100% - 135px);
+    }
+    .item-right{
+        width : 122px;
+        font-size: 12px;
+    }
+    .item-right > div:nth-child(2) {
+        display: none;
+    }
+
+    .statWrap .content.view {
+        padding: 0px 20px;
+    }
+}

+ 410 - 0
src/main/resources/static/css/daterangepicker.css

@@ -0,0 +1,410 @@
+.daterangepicker {
+    position: absolute;
+    color: inherit;
+    background-color: #fff;
+    border-radius: 4px;
+    border: 1px solid #ddd;
+    width: 278px;
+    max-width: none;
+    padding: 0;
+    margin-top: 7px;
+    top: 100px;
+    left: 20px;
+    z-index: 3001;
+    display: none;
+    font-family: arial;
+    font-size: 15px;
+    line-height: 1em;
+}
+
+.daterangepicker:before, .daterangepicker:after {
+    position: absolute;
+    display: inline-block;
+    border-bottom-color: rgba(0, 0, 0, 0.2);
+    content: '';
+}
+
+.daterangepicker:before {
+    top: -7px;
+    border-right: 7px solid transparent;
+    border-left: 7px solid transparent;
+    border-bottom: 7px solid #ccc;
+}
+
+.daterangepicker:after {
+    top: -6px;
+    border-right: 6px solid transparent;
+    border-bottom: 6px solid #fff;
+    border-left: 6px solid transparent;
+}
+
+.daterangepicker.opensleft:before {
+    right: 9px;
+}
+
+.daterangepicker.opensleft:after {
+    right: 10px;
+}
+
+.daterangepicker.openscenter:before {
+    left: 0;
+    right: 0;
+    width: 0;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.daterangepicker.openscenter:after {
+    left: 0;
+    right: 0;
+    width: 0;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.daterangepicker.opensright:before {
+    left: 9px;
+}
+
+.daterangepicker.opensright:after {
+    left: 10px;
+}
+
+.daterangepicker.drop-up {
+    margin-top: -7px;
+}
+
+.daterangepicker.drop-up:before {
+    top: initial;
+    bottom: -7px;
+    border-bottom: initial;
+    border-top: 7px solid #ccc;
+}
+
+.daterangepicker.drop-up:after {
+    top: initial;
+    bottom: -6px;
+    border-bottom: initial;
+    border-top: 6px solid #fff;
+}
+
+.daterangepicker.single .daterangepicker .ranges, .daterangepicker.single .drp-calendar {
+    float: none;
+}
+
+.daterangepicker.single .drp-selected {
+    display: none;
+}
+
+.daterangepicker.show-calendar .drp-calendar {
+    display: block;
+}
+
+.daterangepicker.show-calendar .drp-buttons {
+    display: block;
+}
+
+.daterangepicker.auto-apply .drp-buttons {
+    display: none;
+}
+
+.daterangepicker .drp-calendar {
+    display: none;
+    max-width: 270px;
+}
+
+.daterangepicker .drp-calendar.left {
+    padding: 8px 0 8px 8px;
+}
+
+.daterangepicker .drp-calendar.right {
+    padding: 8px;
+}
+
+.daterangepicker .drp-calendar.single .calendar-table {
+    border: none;
+}
+
+.daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span {
+    color: #fff;
+    border: solid black;
+    border-width: 0 2px 2px 0;
+    border-radius: 0;
+    display: inline-block;
+    padding: 3px;
+}
+
+.daterangepicker .calendar-table .next span {
+    transform: rotate(-45deg);
+    -webkit-transform: rotate(-45deg);
+}
+
+.daterangepicker .calendar-table .prev span {
+    transform: rotate(135deg);
+    -webkit-transform: rotate(135deg);
+}
+
+.daterangepicker .calendar-table th, .daterangepicker .calendar-table td {
+    white-space: nowrap;
+    text-align: center;
+    vertical-align: middle;
+    min-width: 32px;
+    width: 32px;
+    height: 24px;
+    line-height: 24px;
+    font-size: 12px;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    white-space: nowrap;
+    cursor: pointer;
+}
+
+.daterangepicker .calendar-table {
+    border: 1px solid #fff;
+    border-radius: 4px;
+    background-color: #fff;
+}
+
+.daterangepicker .calendar-table table {
+    width: 100%;
+    margin: 0;
+    border-spacing: 0;
+    border-collapse: collapse;
+}
+
+.daterangepicker td.available:hover, .daterangepicker th.available:hover {
+    background-color: #eee;
+    border-color: transparent;
+    color: inherit;
+}
+
+.daterangepicker td.week, .daterangepicker th.week {
+    font-size: 80%;
+    color: #ccc;
+}
+
+.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date {
+    background-color: #fff;
+    border-color: transparent;
+    color: #999;
+}
+
+.daterangepicker td.in-range {
+    background-color: #ebf4f8;
+    border-color: transparent;
+    color: #000;
+    border-radius: 0;
+}
+
+.daterangepicker td.start-date {
+    border-radius: 4px 0 0 4px;
+}
+
+.daterangepicker td.end-date {
+    border-radius: 0 4px 4px 0;
+}
+
+.daterangepicker td.start-date.end-date {
+    border-radius: 4px;
+}
+
+.daterangepicker td.active, .daterangepicker td.active:hover {
+    background-color: #357ebd;
+    border-color: transparent;
+    color: #fff;
+}
+
+.daterangepicker th.month {
+    width: auto;
+}
+
+.daterangepicker td.disabled, .daterangepicker option.disabled {
+    color: #999;
+    cursor: not-allowed;
+    text-decoration: line-through;
+}
+
+.daterangepicker select.monthselect, .daterangepicker select.yearselect {
+    font-size: 12px;
+    padding: 1px;
+    height: auto;
+    margin: 0;
+    cursor: default;
+}
+
+.daterangepicker select.monthselect {
+    margin-right: 2%;
+    width: 56%;
+}
+
+.daterangepicker select.yearselect {
+    width: 40%;
+}
+
+.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect {
+    width: 50px;
+    margin: 0 auto;
+    background: #eee;
+    border: 1px solid #eee;
+    padding: 2px;
+    outline: 0;
+    font-size: 12px;
+}
+
+.daterangepicker .calendar-time {
+    text-align: center;
+    margin: 4px auto 0 auto;
+    line-height: 30px;
+    position: relative;
+}
+
+.daterangepicker .calendar-time select.disabled {
+    color: #ccc;
+    cursor: not-allowed;
+}
+
+.daterangepicker .drp-buttons {
+    clear: both;
+    text-align: right;
+    padding: 8px;
+    border-top: 1px solid #ddd;
+    display: none;
+    line-height: 12px;
+    vertical-align: middle;
+}
+
+.daterangepicker .drp-selected {
+    display: inline-block;
+    font-size: 12px;
+    padding-right: 8px;
+}
+
+.daterangepicker .drp-buttons .btn {
+    margin-left: 8px;
+    font-size: 12px;
+    font-weight: bold;
+    padding: 4px 8px;
+}
+
+.daterangepicker.show-ranges.single.rtl .drp-calendar.left {
+    border-right: 1px solid #ddd;
+}
+
+.daterangepicker.show-ranges.single.ltr .drp-calendar.left {
+    border-left: 1px solid #ddd;
+}
+
+.daterangepicker.show-ranges.rtl .drp-calendar.right {
+    border-right: 1px solid #ddd;
+}
+
+.daterangepicker.show-ranges.ltr .drp-calendar.left {
+    border-left: 1px solid #ddd;
+}
+
+.daterangepicker .ranges {
+    float: none;
+    text-align: left;
+    margin: 0;
+}
+
+.daterangepicker.show-calendar .ranges {
+    margin-top: 8px;
+}
+
+.daterangepicker .ranges ul {
+    list-style: none;
+    margin: 0 auto;
+    padding: 0;
+    width: 100%;
+}
+
+.daterangepicker .ranges li {
+    font-size: 12px;
+    padding: 8px 12px;
+    cursor: pointer;
+}
+
+.daterangepicker .ranges li:hover {
+    background-color: #eee;
+}
+
+.daterangepicker .ranges li.active {
+    background-color: #08c;
+    color: #fff;
+}
+
+/*  Larger Screen Styling */
+@media (min-width: 564px) {
+    .daterangepicker {
+        width: auto;
+    }
+
+    .daterangepicker .ranges ul {
+        width: 140px;
+    }
+
+    .daterangepicker.single .ranges ul {
+        width: 100%;
+    }
+
+    .daterangepicker.single .drp-calendar.left {
+        clear: none;
+    }
+
+    .daterangepicker.single .ranges, .daterangepicker.single .drp-calendar {
+        float: left;
+    }
+
+    .daterangepicker {
+        direction: ltr;
+        text-align: left;
+    }
+
+    .daterangepicker .drp-calendar.left {
+        clear: left;
+        margin-right: 0;
+    }
+
+    .daterangepicker .drp-calendar.left .calendar-table {
+        border-right: none;
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;
+    }
+
+    .daterangepicker .drp-calendar.right {
+        margin-left: 0;
+    }
+
+    .daterangepicker .drp-calendar.right .calendar-table {
+        border-left: none;
+        border-top-left-radius: 0;
+        border-bottom-left-radius: 0;
+    }
+
+    .daterangepicker .drp-calendar.left .calendar-table {
+        padding-right: 8px;
+    }
+
+    .daterangepicker .ranges, .daterangepicker .drp-calendar {
+        float: left;
+    }
+}
+
+@media (min-width: 730px) {
+    .daterangepicker .ranges {
+        width: auto;
+    }
+
+    .daterangepicker .ranges {
+        float: left;
+    }
+
+    .daterangepicker.rtl .ranges {
+        float: right;
+    }
+
+    .daterangepicker .drp-calendar.left {
+        clear: none !important;
+    }
+}

+ 3 - 0
src/main/resources/static/css/notice.css

@@ -386,6 +386,9 @@
     justify-content: space-between;
 }
 
+.item-right.popup {
+    width: 100px;
+}
 .attach-file:hover {
     color: rgb(51, 102, 171);
     cursor: pointer;

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 13 - 0
src/main/resources/static/js/daterangepiker/daterangepicker.min.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 5 - 0
src/main/resources/static/js/daterangepiker/moment.min.js


+ 122 - 0
src/main/resources/templates/admin/conn-statistics.html

@@ -0,0 +1,122 @@
+<!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"/>
+    <title>포항시 교통정보센터</title>
+    <th:block th:include="/include/head.html"></th:block>
+    <th:block th:include="/include/daterangepicker.html"></th:block>
+    <link rel="stylesheet" th:href="@{/css/conn-statistics.css}">
+</head>
+<body id="body">
+<th:block th:include="/include/admin-header.html"></th:block>
+<div class="statWrap">
+    <div class="container">
+        <h2 class="admin-header">접속자 통계</h2>
+        <div class="admin-content">
+            <div class="title">
+                <input id="date" type="text">
+            </div>
+            <div class="chart-box">
+                <div id="chart"></div>
+            </div>
+            <div class="list">
+            </div>
+        </div>
+    </div>
+</div>
+<th:block th:include="/include/footer.html"></th:block>
+</body>
+</html>
+
+<script>
+    const $date = $('#date');
+    const date = $date.daterangepicker({
+        timePicker: false,
+        startDate: new Date(new Date().setDate(new Date().getDate() - 9)),
+        endDate: new Date(),
+        locale: {
+            format: 'YYYY-MM-DD',
+            separator: " ~ ",
+            applyLabel: "확인",
+            cancelLabel : "취소",
+            daysOfWeek: ["일", "월", "화", "수", "목", "금", "토"],
+            monthNames: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
+        },
+        disabled : true,
+        autoApply: true,
+        opens: 'center',
+        drops: 'up',
+    });
+
+    function getConnStatistics(fromDt, toDt) {
+        getDataAsync("/api/common/conn-statistics", "POST", null, null, (jsonData)=>{
+            if (jsonData && jsonData.length > 0) {
+                drawChart(jsonData);
+            }
+        }, null);
+    }
+
+    getConnStatistics();
+
+    function drawChart(jsonData) {
+        let categories = [];
+        let data = [];
+        jsonData.forEach((obj)=>{
+            categories.push(obj.conn_dt);
+            data.push(obj.conn_cnt);
+        })
+        Highcharts.chart('chart', {
+            chart: {
+                type: 'column'
+            },
+            title: {
+                text: '',
+            },
+            xAxis: {
+                categories: categories,
+                crosshair: true,
+            },
+            yAxis: {
+                min: 0,
+                title: {
+                    text: ''
+                },
+                tickInterval: 5,
+            },
+            tooltip: {
+                valueSuffix: ' 명'
+            },
+            legend: {
+                layout: 'vertical',
+                align: 'center',
+                verticalAlign: 'top',
+                floating: false,
+                visible : false,
+                borderWidth: 0,
+                shadow: false,
+            },
+            plotOptions: {
+                column: {
+                    pointPadding: 0.2,
+                    borderWidth: 0
+                }
+            },
+            series: [
+                {
+                    name: '일별접속자수',
+                    data: data
+                },
+            ],
+            exporting: {
+                enabled: false,
+            },
+            credits: {
+                enabled: false
+            },
+        });
+    }
+</script>

+ 79 - 9
src/main/resources/templates/admin/main.html

@@ -10,10 +10,10 @@
     <title>포항시 교통정보센터</title>
     <th:block th:include="/include/head.html"></th:block>
     <link rel="stylesheet" th:href="@{/css/admin-main.css}">
-    <script th:type="javascript" th:src="@{/js/highcharts.js}"></script>
-    <script th:type="javascript" th:src="@{/js/modules/accessibility.js}"></script>
-    <script th:type="javascript" th:src="@{/js/modules/export-data.js}"></script>
-    <script th:type="javascript" th:src="@{/js/modules/exporting.js}"></script>
+    <script th:src="@{/js/highchart/highcharts.js}"></script>
+    <script th:src="@{/js/highchart/modules/accessibility.js}"></script>
+    <script th:src="@{/js/highchart/modules/export-data.js}"></script>
+    <script th:src="@{/js/highchart/modules/exporting.js}"></script>
 </head>
 <body id="body">
 <th:block th:include="/include/admin-header.html"></th:block>
@@ -21,9 +21,9 @@
     <div class="main">
         <div class="top">
             <div>
-                <div class="sub-title">공지사항</div>
+                <div class="sub-title" th:onclick="movePath('/phits/notice-list')">공지사항</div>
                 <div class="content">
-                    <div th:onclick="moveNoticeView([[${item.getBoardNo()}]], true)" class="notice-list" th:each="item, i:${notice}">
+                    <div th:class="notice" th:onclick="moveNoticeView([[${item.getBoardNo()}]], true)" class="notice-list" th:each="item, i:${notice}">
                         <div th:title="${item.getBSubject()}" th:text="${item.getBSubject()}"></div>
                         <span th:text="${item.getRegDate()}"></span>
                     </div>
@@ -33,10 +33,10 @@
                 </div>
             </div>
             <div>
-                <div class="sub-title">팝업공지</div>
+                <div class="sub-title" th:onclick="movePath('/phits/popup-list')">팝업공지</div>
                 <div class="content">
-                    <div th:onclick="movePath([['/phits/popup-view/'+${item.getPopupid()}]]" class="notice-list" th:each="item, i:${popup}">
-                        <div th:title="${item.getTitle()}" th:text="${item.getTitle() + item.getTitle() +item.getTitle() +item.getTitle()}"></div>
+                    <div th:class="popup" th:onclick="movePath([['/phits/popup-view/'+${item.getPopupid()}]])" class="notice-list" th:each="item, i:${popup}">
+                        <div th:title="${item.getTitle()}" th:text="${item.getTitle()}"></div>
                         <span th:text="${item.getPostFrom() + '~' + item.getPostTo()}"></span>
                     </div>
                     <div th:if="${notice == null || notice.size() == 0}">
@@ -56,6 +56,76 @@
 </body>
 
 <script th:inline="javascript">
+
+    function getConnStatistics() {
+        getDataAsync("/api/common/conn-statistics", "POST", null, null, (jsonData)=>{
+            if (jsonData && jsonData.length > 0) {
+                drawChart(jsonData);
+            }
+        }, null);
+    }
+
+    getConnStatistics();
+    setInterval(getConnStatistics, 5 * 60 * 1000);
+
+    function drawChart(jsonData) {
+        let categories = [];
+        let data = [];
+        jsonData.forEach((obj)=>{
+            categories.push(obj.conn_dt);
+            data.push(obj.conn_cnt);
+        })
+        Highcharts.chart('chart', {
+            chart: {
+                type: 'column'
+            },
+            title: {
+                text: '',
+            },
+            xAxis: {
+                categories: categories,
+                crosshair: true,
+            },
+            yAxis: {
+                min: 0,
+                title: {
+                    text: ''
+                },
+                tickInterval: 5,
+            },
+            tooltip: {
+                valueSuffix: ' 명'
+            },
+            legend: {
+                layout: 'vertical',
+                align: 'center',
+                verticalAlign: 'top',
+                floating: false,
+                visible : false,
+                borderWidth: 0,
+                shadow: false,
+            },
+            plotOptions: {
+                column: {
+                    pointPadding: 0.2,
+                    borderWidth: 0
+                }
+            },
+            series: [
+                {
+                    name: '일별접속자수',
+                    data: data
+                },
+            ],
+            exporting: {
+                enabled: false,
+            },
+            credits: {
+                enabled: false
+            },
+        });
+    }
+
 </script>
 
 </html>

+ 1 - 1
src/main/resources/templates/admin/popup-list.html

@@ -22,7 +22,7 @@
             <div class="list">
                 <a th:each="item : ${list.getList()}" th:onclick="movePath([[ '/phits/popup-view/'+ ${item.getPopupid()}]])" style="display: flex;">
                     <div th:text="${item.getTitle()}"></div>
-                    <div class="item-right">
+                    <div class="item-right popup">
                         <div th:text="${item.getRegdate()}"></div>
                     </div>
                 </a>

+ 40 - 15
src/main/resources/templates/admin/popup-view.html

@@ -8,10 +8,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
     <title>포항시 교통정보센터</title>
     <th:block th:include="/include/head.html"></th:block>
-    <script type="text/javascript" src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
-    <script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
-    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
-    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
+    <th:block th:include="/include/daterangepicker.html"></th:block>
     <link rel="stylesheet" th:href="@{/css/notice.css}">
 </head>
 <body id="body">
@@ -36,8 +33,6 @@
                     <div class="post-box">
                         <div>공지 기간</div>
                         <input type="text" id="post" class="post_from" name="post_from" disabled="disabled">
-<!--                        <div>~</div>-->
-<!--                        <input type="date" class="post_to" name="post_to" th:value="${popup.getPostTo()}">-->
                     </div>
                     <div class="attach-box admin">
                         <div class="attach">
@@ -96,6 +91,11 @@
         $content.attr('rows', 10);
     }
 
+    /**
+     * 첨부파일 다운로드
+     * @param attachFileId 다운로드 파일 ID
+     * @param fileName 다운로드 파일 명
+     */
     function attachFileDownload(attachFileId, fileName) {
         const param = {
             imgId : attachFileId,
@@ -118,6 +118,11 @@
         }, null);
     }
 
+    /**
+     * Byte Array -> Blob Data
+     * @param byteData 변환 데이터
+     * @returns {string} Blob URL
+     */
     const b64ToBlob = (byteData) => {
         const byteCharacters = atob(byteData);
         const byteNumbers = new Array(byteCharacters.length);
@@ -126,15 +131,19 @@
         }
         const byteArray = new Uint8Array(byteNumbers);
         const blob = new Blob([byteArray]);
-        const blobUrl = window.URL.createObjectURL(blob);
-
-        return blobUrl;
+        return window.URL.createObjectURL(blob);
     };
 
+    /**
+     * Input 파일 대체 버튼 클릭
+     */
     function attachFile() {
         $attachFile.click();
     }
 
+    /**
+     * 첨부파일 변경 이벤트
+     */
     $attachFile.on("change", function() {
         const changeFile = $(this)[0].files[0];
         if (changeFile) {
@@ -143,6 +152,9 @@
         }
     })
 
+    /**
+     *  편집 모드
+     */
     function edit() {
         $delete.addClass('off');
         $edit.addClass('off');
@@ -167,14 +179,20 @@
 
     }
 
+    /**
+     * 첨부파일 제거
+     */
     function deleteAttach() {
         $attachFile.val("");
         $attach.html("<div>첨부파일 없음</div>")
     }
 
+    /**
+     * 삭제 이벤트
+     */
     function delEvent() {
         if (confirm( "번호 : " + popupId + "\n제목 : " + popupTitle + '\n게시물을 삭제하시겠습니까?')) {
-            getDataAsync('/api/popup/deletePopup', 'DELETE', {popupId : popupId}, null, (jsonData)=>{
+            getDataAsync('/api/popup/deletePopup', 'POST', {popupId : popupId}, null, (jsonData)=>{
                 if (jsonData) {
                     alert(jsonData.message);
                     if (jsonData.success === "S") {
@@ -187,6 +205,9 @@
         }
     }
 
+    /**
+     * 저장 이벤트
+     */
     function save() {
         const file     = $attachFile[0].files[0];
         const title    = $title.val();
@@ -195,15 +216,17 @@
         const postVal  = $post.val();
         let postFrom   = "";
         let postTo     = "";
-        if (isNull(postVal)) {
-            const postArray = postVal.split("~");
+
+        //첨부파일 기간값 유효성 체크
+        if (!isNull(postVal)) {
+            const postArray = postVal.split(" ~ "); // separator " ~ " 를 이용해 post_from, post_to 값을 구분
             if (postArray.length === 2) {
-                postFrom = postArray[0].trim();
-                postTo   = postArray[1].trim();
+                postFrom = postArray[0];
+                postTo   = postArray[1];
             }
         }
 
-        if (popup.title === title && popup.url === url) {
+        if (popup.title === title && popup.url === url && $post.post_from === postFrom && $post.post_to === postTo) {
             if (file) {
                if (file.name === attachNm) {
                    return alert("수정하신 내용이 없습니다. 내용을 확인 해 주세요");
@@ -222,6 +245,8 @@
         formData.append("title", title);
         formData.append("url", url);
         formData.append("attachFile", file);
+        formData.append("postFrom", postFrom);
+        formData.append("postTo", postTo);
         $.ajax({
             url: '/api/popup/modifyPopup',
             processData : false,

+ 57 - 10
src/main/resources/templates/admin/popup-write.html

@@ -8,6 +8,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
     <title>포항시 교통정보센터</title>
     <th:block th:include="/include/head.html"></th:block>
+    <th:block th:include="/include/daterangepicker.html"></th:block>
     <link rel="stylesheet" th:href="@{/css/notice.css}">
 </head>
 <body id="body">
@@ -26,6 +27,10 @@
                 <div>
                     <input class="title modify" name="b_subject" placeholder="팝업 제목을 입력해주세요.">
                     <textarea class="b_content modify" rows="16" name="b_content" placeholder="링크 주소가 있다면 URL을 입력해주세요. ex) https://www.naver.com"></textarea>
+                    <div class="post-box">
+                        <div>공지 기간</div>
+                        <input type="text" id="post" th:class="modify" name="post">
+                    </div>
                     <div class="attach-box admin">
                         <div class="attach modify">
                             <div>첨부파일 없음</div>
@@ -46,6 +51,25 @@
     const $content    = $('.b_content');
     const $attach     = $('.attach');
     const $attachFile = $('#attach-file');
+    const $post       = $('#post');
+
+    const post = $post.daterangepicker({
+        timePicker: false,
+        startDate: new Date(),
+        endDate: new Date(new Date().setDate(new Date().getDate() + 1)),
+        locale: {
+            format: 'YYYY-MM-DD',
+            separator: " ~ ",
+            applyLabel: "확인",
+            cancelLabel : "취소",
+            daysOfWeek: ["일", "월", "화", "수", "목", "금", "토"],
+            monthNames: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
+        },
+        disabled : true,
+        autoApply: true,
+        opens: 'center',
+        drops: 'up',
+    });
 
     let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
 
@@ -68,31 +92,54 @@
     function deleteAttach() {
         $attachFile.val("");
         $attach.html("<div>첨부파일 없음</div>")
-        console.log($attachFile[0].files[0]);
     }
 
     function save() {
         const title   = $title.val();
-        const content = $content.val();
+        const url     = $content.val();
         const file    = $attachFile[0].files[0];
+        const postVal = $post.val();
+
+        let postFrom  = "";
+        let postTo    = "";
+
+        //첨부파일 기간값 유효성 체크
+        if (!isNull(postVal)) {
+            const postArray = postVal.split(" ~ "); // separator " ~ " 를 이용해 post_from, post_to 값을 구분
+            if (postArray.length === 2) {
+                postFrom = postArray[0];
+                postTo   = postArray[1];
+            }
+        }
+
         const formData = new FormData();
+
+        // 유효성 체크
         if (isNull(title)) {
             $title.focus();
-            return alert("공지사항 제목을 입력해주세요.");
+            return alert("팝업 공지 제목을 입력해주세요.");
         }
 
-        if (isNull(content)) {
-            $content.focus();
-            return alert("공지사항 내용을 입력해주세요.");
+        if (isNull(postFrom)) {
+            $post.focus();
+            return alert("팝업 공지 기간 시작일을 입력해주세요.");
         }
 
-        formData.append("bSubject", title);
-        formData.append("bContent", content);
+        if (isNull(postTo)) {
+            $post.focus();
+            return alert("팝업 공지 기간 종료일을 입력해주세요.");
+        }
 
-        if (file) {
-            formData.append("attachFile", file);
+        if (!file) {
+            return alert("팝업 이미지 파일을 첨부해주세요.")
         }
 
+        formData.append("title", title);
+        formData.append("url", url);
+        formData.append("postForm", postFrom);
+        formData.append("postTo", postTo);
+        formData.append("attachFile", file);
+
         $.ajax({
             url: '/api/popup/writePopup',
             processData : false,

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

@@ -11,7 +11,7 @@
                 <a th:href="@{/phits/notice-list}">공지사항</a>
             </div>
             <div class="top-menu-cont" th:classappend="${selected == 'statistics'} ? ' on' : ''">
-                <a th:href="@{/phits/statistics}">접속자통계</a>
+                <a th:href="@{/phits/conn-statistics}">접속자통계</a>
             </div>
             <div class="top-menu-cont" th:classappend="${selected == 'popup'} ? ' on' : ''">
                 <a th:href="@{/phits/popup-list}">팝업공지</a>

+ 3 - 0
src/main/resources/templates/include/daterangepicker.html

@@ -0,0 +1,3 @@
+<script th:src="@{/js/daterangepiker/moment.min.js}"></script>
+<script th:src="@{/js/daterangepiker/daterangepicker.min.js}"></script>
+<link rel="stylesheet" th:href="@{/css/daterangepicker.css}">

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů