junggilpark пре 1 година
родитељ
комит
2b204cc167
26 измењених фајлова са 1147 додато и 46 уклоњено
  1. 32 4
      src/main/java/com/its/web/controller/admin/AdminController.java
  2. 31 0
      src/main/java/com/its/web/controller/login/LoginController.java
  3. 0 21
      src/main/java/com/its/web/controller/main/MainController.java
  4. 43 0
      src/main/java/com/its/web/controller/popup/popupController.java
  5. 2 1
      src/main/java/com/its/web/controller/view/ViewController.java
  6. 1 1
      src/main/java/com/its/web/dto/admin/TbWwwMemberDto.java
  7. 54 0
      src/main/java/com/its/web/dto/admin/TbWwwPopupDto.java
  8. 7 0
      src/main/java/com/its/web/mapper/its/admin/AdminMapper.java
  9. 23 0
      src/main/java/com/its/web/mapper/its/popup/PopupMapper.java
  10. 1 2
      src/main/java/com/its/web/pagination/Pagination.java
  11. 3 2
      src/main/java/com/its/web/security/WebPasswordEncoder.java
  12. 11 0
      src/main/java/com/its/web/service/admin/AdminService.java
  13. 44 0
      src/main/java/com/its/web/service/admin/LoginService.java
  14. 4 1
      src/main/java/com/its/web/service/notice/NoticeService.java
  15. 183 0
      src/main/java/com/its/web/service/popup/PopupService.java
  16. 20 0
      src/main/resources/mybatis/mapper/its/admin/AdminMapper.xml
  17. 0 1
      src/main/resources/mybatis/mapper/its/notice/NoticeMapper.xml
  18. 112 0
      src/main/resources/mybatis/mapper/its/popup/PopupMapper.xml
  19. 43 0
      src/main/resources/static/css/login.css
  20. 22 0
      src/main/resources/static/css/notice.css
  21. 37 6
      src/main/resources/templates/admin/login.html
  22. 12 1
      src/main/resources/templates/admin/main.html
  23. 64 0
      src/main/resources/templates/admin/popup-list.html
  24. 273 0
      src/main/resources/templates/admin/popup-view.html
  25. 115 0
      src/main/resources/templates/admin/popup-write.html
  26. 10 6
      src/main/resources/templates/include/admin-header.html

+ 32 - 4
src/main/java/com/its/web/controller/admin/AdminController.java

@@ -1,6 +1,7 @@
 package com.its.web.controller.admin;
 
 import com.its.web.service.notice.NoticeService;
+import com.its.web.service.popup.PopupService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.RequiredArgsConstructor;
@@ -24,6 +25,7 @@ import java.io.IOException;
 @Api(tags = "99-2.운영자 화면 이동")
 public class AdminController {
     private final NoticeService noticeService;
+    private final PopupService popupService;
 
     @ApiOperation(value = "00.로그인")
     @GetMapping("")
@@ -39,29 +41,55 @@ public class AdminController {
     @GetMapping("/main")
     public String adminMain(Model model) {
         model.addAttribute("notice", this.noticeService.findMainNotice(5));
+        model.addAttribute("popup", this.popupService.findMainPopup(5));
         return "admin/main";
     }
 
-    @ApiOperation(value = "06.운영자 - 01.공지사항 - 01.공지사항 목록")
+    @ApiOperation(value = "01.공지사항 - 01.공지사항 목록")
     @GetMapping("/notice-list")
     public String adminNoticeList(Model model, @Nullable @Param("page") String page,
                                   @Nullable @Param("searchType") String searchType,
                                   @Nullable @Param("searchText")String searchText) {
+        model.addAttribute("selected", "notice");
         model.addAttribute("list", this.noticeService.findAllList(page, searchType, searchText));
         return "admin/notice-list";
     }
 
-    @ApiOperation(value = "06.운영자 - 01.공지사항 - 02.공지사항 상세내용")
+    @ApiOperation(value = "01.공지사항 - 02.공지사항 상세내용")
     @GetMapping("/notice-view/{boardNo}")
     public String adminNoticeView(Model model, @PathVariable("boardNo")String boardNo) {
+        model.addAttribute("selected", "notice");
         model.addAttribute("notice", this.noticeService.findNotice(boardNo));
         return "admin/notice-view";
     }
 
-    @ApiOperation(value = "06.운영자 - 01.공지사항 - 03.공지사항 글쓰기")
+    @ApiOperation(value = "01.공지사항 - 03.공지사항 글쓰기")
     @GetMapping("/notice-write")
     public String adminNoticeWrite(Model model) {
-        model.addAttribute("notice", this.noticeService.findMainNotice(5));
+        model.addAttribute("selected", "notice");
         return "admin/notice-write";
     }
+
+    @ApiOperation(value = "03.팝업공지 - 02.팝업 상세내용")
+    @GetMapping(value= {"/popup-list","/popup-list/{page}"})
+    public String adminPopupList(Model model, @Nullable @PathVariable("page") String page) {
+        model.addAttribute("selected", "popup");
+        model.addAttribute("list", this.popupService.findAllPopup(page));
+        return "admin/popup-list";
+    }
+
+    @ApiOperation(value = "03.팝업공지 - 02.팝업 상세내용")
+    @GetMapping("/popup-view/{popupId}")
+    public String adminPopupView(Model model, @PathVariable("popupId") String popupId) {
+        model.addAttribute("selected", "popup");
+        model.addAttribute("popup", this.popupService.findPopup(popupId));
+        return "admin/popup-view";
+    }
+
+    @ApiOperation(value = "03.팝업공지 - 03.팝업 글쓰기")
+    @GetMapping("/popup-write")
+    public String adminPopupWrite(Model model) {
+        model.addAttribute("selected", "popup");
+        return "admin/popup-write";
+    }
 }

+ 31 - 0
src/main/java/com/its/web/controller/login/LoginController.java

@@ -0,0 +1,31 @@
+package com.its.web.controller.login;
+
+import com.its.web.service.admin.LoginService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/login")
+@Api(tags = "00.로그인 컨트롤러")
+public class LoginController {
+
+    private final LoginService service;
+
+    @ApiOperation(value = "00.로그인 - 로그인 정보 확인")
+    @PostMapping("/loginCheck")
+    @ResponseBody
+    public Map<String, String> loginCheck(@Param("id") String id, @Param("pwd") String pwd) {
+        return this.service.loginCheck(id, pwd);
+    }
+}

+ 0 - 21
src/main/java/com/its/web/controller/main/MainController.java

@@ -1,21 +0,0 @@
-package com.its.web.controller.main;
-
-import io.swagger.annotations.Api;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@Slf4j
-@RequiredArgsConstructor
-@RestController
-@RequestMapping("/api/traffic")
-@Api(tags = "00.메인화면")
-public class MainController {
-
-//    @GetMapping("/")
-//    public String Main(Model model) {
-//        model.addAttribute("selected", "");
-//        return "main/main";
-//    }
-}

+ 43 - 0
src/main/java/com/its/web/controller/popup/popupController.java

@@ -0,0 +1,43 @@
+package com.its.web.controller.popup;
+
+import com.its.web.dto.notice.AttachFileDto;
+import com.its.web.service.popup.PopupService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Map;
+
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/popup")
+@Api(tags = "00.팝업")
+public class popupController {
+    private final PopupService service;
+
+    @ApiOperation(value = "01.팝업 글쓰기")
+    @PostMapping(value= {"/writePopup", }, produces = {"application/json; charset=utf8"})
+    @ResponseBody
+    public Map<String, String> writePopup(@RequestParam Map<String, String> paramMap, MultipartFile attachFile) {
+        return this.service.writePopup(paramMap, attachFile);
+    }
+
+    @ApiOperation(value = "02.팝업 이미지 다운")
+    @PostMapping(value = "/attach", produces = {"application/json; charset=utf8"})
+    @ResponseBody
+    public AttachFileDto downloadAttach(@Param("imgId") String imgId, @Param("imgName") String imgName) {
+        return this.service.downloadAttach(imgId, imgName);
+    }
+
+    @ApiOperation(value= "03.팝업 글수정")
+    @PostMapping(value = "/updatePopup", produces = {"application/json; charset=utf8"})
+    @ResponseBody
+    public Map<String, String> updatePopup(@RequestParam Map<String, String> paramMap, MultipartFile attachFile) {
+        return this.service.updatePopup(paramMap, attachFile);
+    }
+}

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

@@ -130,7 +130,7 @@ public class ViewController {
     public String noticeList(Model model, @Nullable @Param("page") String page,
                          @Nullable @Param("searchType") String searchType,
                          @Nullable @Param("searchText")String searchText) {
-
+        model.addAttribute("selected", "notice");
         model.addAttribute("list", this.noticeService.findAllList(page, searchType, searchText));
         return "notice/list";
     }
@@ -138,6 +138,7 @@ public class ViewController {
     @ApiOperation(value = "05.공지사항 - 02.공지사항 상세보기")
     @GetMapping("/notice/view/{boardNo}" )
     public String noticeView(Model model, @PathVariable("boardNo")String boardNo) {
+        model.addAttribute("selected", "notice");
         model.addAttribute("notice", this.noticeService.findNotice(boardNo));
         return "notice/view";
     }

+ 1 - 1
src/main/java/com/its/web/dto/admin/TbWwwMemberDto.java

@@ -43,7 +43,7 @@ public class TbWwwMemberDto{
 
     @ApiModelProperty("로그인 실패 횟수")
     @JsonProperty("login_fail_count")
-    private String loginFailCount;	//NUMBER(2)
+    private int loginFailCount;	//NUMBER(2)
 
     @ApiModelProperty("계정잠금여부(Y:계정잠김)")
     @JsonProperty("is_account_lock")

+ 54 - 0
src/main/java/com/its/web/dto/admin/TbWwwPopupDto.java

@@ -0,0 +1,54 @@
+package com.its.web.dto.admin;
+
+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("TB_WWW_POPUP DTO(홈페이지 팝업 DTO)")
+public class TbWwwPopupDto {
+    @ApiModelProperty("팝업 ID")
+    @JsonProperty("popupid")
+    private Long popupid;
+
+    @ApiModelProperty("제목")
+    @JsonProperty("title")
+    private String title;
+
+    @ApiModelProperty("URL 주소")
+    @JsonProperty("url")
+    private String url;
+
+    @ApiModelProperty("팝업 시작일")
+    @JsonProperty("post_from")
+    private String postFrom;
+
+    @ApiModelProperty("팝업 종료일")
+    @JsonProperty("post_to")
+    private String postTo;
+
+    @ApiModelProperty("이미지 ID")
+    @JsonProperty("img_id")
+    private String imgId;
+
+    @ApiModelProperty("이미지 명칭")
+    @JsonProperty("img_name")
+    private String imgName;
+
+    @ApiModelProperty("등록일")
+    @JsonProperty("regdate")
+    private String regdate;
+
+    @ApiModelProperty("사용여부")
+    @JsonProperty("use_yn")
+    private String useYn;
+
+    @ApiModelProperty("삭제여부")
+    @JsonProperty("del_yn")
+    private String delYn;
+}

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

@@ -3,7 +3,14 @@ package com.its.web.mapper.its.admin;
 import com.its.web.dto.admin.TbWwwMemberDto;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.Map;
+
 @Mapper
 public interface AdminMapper {
     TbWwwMemberDto findMemberWidthId(String userId);
+
+    int findMemberWidthIdPwd(Map<String, String> paramMap);
+
+    void updateFailCount(Map<String, String> paramMap);
+
 }

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

@@ -0,0 +1,23 @@
+package com.its.web.mapper.its.popup;
+
+import com.its.web.dto.admin.TbWwwPopupDto;
+import com.its.web.pagination.Pagination;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface PopupMapper {
+    List<TbWwwPopupDto> findAllPopup(Pagination pagination);
+
+    int findPopupTotalPage();
+
+    List<TbWwwPopupDto> findMainPopup(int num);
+
+    TbWwwPopupDto findPopup(String popupId);
+
+    int insertPopup(Map<String, String> paramMap);
+
+    int updatePopup(Map<String, String> paramMap);
+}

+ 1 - 2
src/main/java/com/its/web/pagination/Pagination.java

@@ -1,6 +1,5 @@
 package com.its.web.pagination;
 
-import com.its.web.dto.notice.NoticeDto;
 import lombok.Data;
 
 import java.util.List;
@@ -18,7 +17,7 @@ public class Pagination {
     private int nextPage;
     private String searchType;
     private String searchText;
-    private List<NoticeDto> list;
+    private List<?> list;
 
     public Pagination (String page, int totalCount, String searchType, String searchText) {
         this.currentPage = 1;

+ 3 - 2
src/main/java/com/its/web/security/WebPasswordEncoder.java

@@ -14,7 +14,8 @@ public class WebPasswordEncoder implements PasswordEncoder {
         if (charSequence == null) {
             throw new IllegalArgumentException("Encode Password Cannot be null");
         }
-        return charSequence.toString();
+        return Hashing.sha256().hashString(charSequence, StandardCharsets.UTF_8).toString();
+//        return charSequence.toString();
     }
 
     @Override
@@ -29,7 +30,7 @@ public class WebPasswordEncoder implements PasswordEncoder {
         }
         // 사용자가 입력한 비밀번호는 암호화가 되어있지 않기 때문에 암호화를 한 후
         // 데이터베이스의 비밀번호와 비교해야 함.
-        String rawEncodedPassword = Hashing.sha256().hashString(charSequence, StandardCharsets.UTF_8).toString();
+        String rawEncodedPassword = this.encode(charSequence);
 
         return StringUtils.equals(rawEncodedPassword, s);
     }

+ 11 - 0
src/main/java/com/its/web/service/admin/AdminService.java

@@ -0,0 +1,11 @@
+package com.its.web.service.admin;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class AdminService {
+}

+ 44 - 0
src/main/java/com/its/web/service/admin/LoginService.java

@@ -3,6 +3,7 @@ package com.its.web.service.admin;
 import com.its.web.dto.admin.PrincipalDetail;
 import com.its.web.dto.admin.TbWwwMemberDto;
 import com.its.web.mapper.its.admin.AdminMapper;
+import com.its.web.security.WebPasswordEncoder;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.core.userdetails.UserDetails;
@@ -10,11 +11,15 @@ import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class LoginService implements UserDetailsService {
     private final AdminMapper adminMapper;
+    private final WebPasswordEncoder encoder = new WebPasswordEncoder();
 
     @Override
     public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
@@ -45,4 +50,43 @@ public class LoginService implements UserDetailsService {
             throw new UsernameNotFoundException(userId + " 을(를) 찾을 수 없습니다.");
         }
     }
+
+    public Map<String, String> loginCheck(String id, String pwd) {
+        Map<String, String> resultMap = new HashMap<>();
+        String message = "";
+        String success = "S";
+        Map<String, String> paramMap = new HashMap<>();
+        paramMap.put("id", id);
+        paramMap.put("pwd", encoder.encode(pwd));
+        TbWwwMemberDto dto = this.adminMapper.findMemberWidthId(id);
+        if (dto != null) {
+            if (this.adminMapper.findMemberWidthIdPwd(paramMap) <= 0) {
+                success = "F";
+                if (dto.getLoginFailCount() == 5 || dto.getIsAccountLock().equals("Y"))  {
+                    message = "5회 이상 로그인 실패로 계정 이용 불가\n관리자에게 문의하세요.";
+                }
+                else {
+                    message = "ID 또는 비밀번호를 확인해주세요";
+                    paramMap.put("loginFailCount", (dto.getLoginFailCount() + 1) + "");
+                    if (dto.getLoginFailCount() == 4) {
+                        paramMap.put("isAccountLock", "Y");
+                    }
+                    this.adminMapper.updateFailCount(paramMap);
+                }
+            }
+        }
+        else {
+            message = "ID 또는 비밀번호를 확인해주세요";
+            success = "F";
+        }
+
+        if (success.equals("S")) {
+            paramMap.put("isAccountLock", "N");
+            paramMap.put("loginFailCount", "0");
+            this.adminMapper.updateFailCount(paramMap);
+        }
+        resultMap.put("message", message);
+        resultMap.put("success", success);
+        return resultMap;
+    }
 }

+ 4 - 1
src/main/java/com/its/web/service/notice/NoticeService.java

@@ -11,7 +11,9 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.*;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -210,6 +212,7 @@ public class NoticeService {
     public int updateNoticeReadCount(String boardNo) {
         return this.mapper.updateNoticeReadCount(boardNo);
     }
+
 }
 
 

+ 183 - 0
src/main/java/com/its/web/service/popup/PopupService.java

@@ -0,0 +1,183 @@
+package com.its.web.service.popup;
+
+import com.its.web.dto.admin.TbWwwPopupDto;
+import com.its.web.dto.notice.AttachFileDto;
+import com.its.web.mapper.its.popup.PopupMapper;
+import com.its.web.pagination.Pagination;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class PopupService {
+    private final PopupMapper mapper;
+
+    @Value("${popup-location}")
+    String popupLocation;
+
+    public Pagination findAllPopup(String page) {
+        int totalCount = this.mapper.findPopupTotalPage();
+        Pagination pagination = new Pagination( page, totalCount, null, null);
+        pagination.setList(this.mapper.findAllPopup(pagination));
+        return pagination;
+    }
+
+    public List<TbWwwPopupDto> findMainPopup(int num) {
+        return this.mapper.findMainPopup(num);
+    }
+
+    public TbWwwPopupDto findPopup(String popupId) {
+        return this.mapper.findPopup(popupId);
+    }
+
+    public Map<String, String> writePopup(Map<String, String> paramMap, MultipartFile attachFile) {
+        Map<String, String> resultMap = new HashMap<>();
+        String message = "작성하신 팝업 공지가 등록 되었습니다.";
+        String success = "S";
+        if (attachFile != null && !attachFile.isEmpty()) {
+
+            if (attachFile.getOriginalFilename() != null) {
+                String imgName = attachFile.getOriginalFilename();
+                String ext = imgName.substring(imgName.lastIndexOf("."), imgName.length());
+                String fileId = UUID.randomUUID().toString().replaceAll("-", "");
+                fileId += ext;
+                paramMap.put("imgName", imgName);
+                paramMap.put("imgId", fileId);
+                File file = new File(popupLocation);
+
+                if (!file.isDirectory()) {
+                    file.mkdirs();
+                }
+
+                File transFerFile = new File(popupLocation + fileId);
+                try {
+                    attachFile.transferTo(transFerFile);
+                } catch (IOException e) {
+                    message = "이미지 업로드 중 오류가 발생했습니다.";
+                    success = "F";
+                    log.error("TransferTo IOException: {}", e.getMessage());
+                }
+            }
+            else {
+                message = "팝업 공지 이미지 정보가 없습니다.";
+                success = "F";
+            }
+        }
+        else {
+            message = "팝업 공지 이미지 정보가 없습니다.";
+            success = "F";
+        }
+
+        if (success.equals("S")) {
+            int affectedRow = this.mapper.insertPopup(paramMap);
+            if (affectedRow <= 0) {
+                success = "F";
+                message = "작성하신 팝업 공지가 등록되지 않았습니다.";
+            }
+        }
+        resultMap.put("message", message);
+        resultMap.put("success", success);
+        return resultMap;
+    }
+
+    public Map<String, String> updatePopup(Map<String, String> paramMap, MultipartFile attachFile) {
+        Map<String, String> resultMap = new HashMap<>();
+        String message = "작성하신 팝업 공지가 수정 되었습니다.";
+        String success = "S";
+        TbWwwPopupDto dto = this.mapper.findPopup(paramMap.get("popupId"));
+        String imgId = dto.getImgId();
+        String imgName = dto.getImgName();
+        if (attachFile != null && !attachFile.isEmpty()) {
+            if (attachFile.getOriginalFilename() != null) {
+                String fileName = attachFile.getOriginalFilename();
+                if (!imgName.equals(fileName)) {
+                    String ext = fileName.substring(fileName.lastIndexOf("."), fileName.length());
+                    String fileId = UUID.randomUUID().toString().replaceAll("-", "");
+                    fileId += ext;
+                    imgId = fileId;
+                    imgName = fileName;
+                    File file = new File(popupLocation);
+
+                    if (!file.isDirectory()) {
+                        file.mkdirs();
+                    }
+
+                    File transFerFile = new File(popupLocation + imgId);
+                    try {
+                        attachFile.transferTo(transFerFile);
+                    } catch (IOException e) {
+                        message = "이미지 업로드 중 오류가 발생했습니다.";
+                        success = "F";
+                        log.error("TransferTo IOException: {}", e.getMessage());
+                    }
+                }
+            }
+        }
+
+        if (success.equals("S")) {
+            paramMap.put("imgId", imgId);
+            paramMap.put("imgName", imgName);
+            int affectedRow = this.mapper.updatePopup(paramMap);
+            if (affectedRow <= 0) {
+                success = "F";
+                message = "작성하신 팝업 공지가 수정되지 않았습니다.";
+            }
+        }
+
+        resultMap.put("message", message);
+        resultMap.put("success", success);
+        return resultMap;
+    }
+
+    public AttachFileDto downloadAttach(String imgId, String imgName) {
+        AttachFileDto dto = new AttachFileDto();
+        String message = null;
+        if (imgId != null && imgName != null) {
+            File file = new File(popupLocation, imgId);
+            int fileSize = (int) file.length();
+
+            if (file.exists() && fileSize > 0) {
+                try {
+                    byte[] byteArray = FileUtils.readFileToByteArray(file);
+                    if (byteArray.length > 0) {
+                        int i = 0;
+                        dto.setAttachFile(new byte[byteArray.length]);
+                        for (byte b : byteArray) {
+                            dto.getAttachFile()[i++] = b;
+                        }
+                        message = "파일 전송 완료.";
+                    }
+                    else {
+                        message = "파일을 찾을 수 없습니다.";
+                    }
+                }
+                catch (FileNotFoundException e) {
+                    log.error("File Not FoundException");
+                    message = "파일을 찾을 수 없습니다.";
+                } catch (IOException e) {
+                    log.error("IOException");
+                    message = "파일을 읽는 중 오류가 발생하였습니다.";
+                }
+            }
+        }
+        else {
+            message = "파일을 찾일 수 없습니다.";
+        }
+        dto.setMessage(message);
+        return dto;
+    }
+
+}

+ 20 - 0
src/main/resources/mybatis/mapper/its/admin/AdminMapper.xml

@@ -22,4 +22,24 @@
           AND del_yn = 'N'
     </select>
 
+    <select id="findMemberWidthIdPwd" parameterType="java.util.HashMap" resultType="java.lang.Integer">
+        SELECT
+            count(*)
+         FROM
+            TB_WWW_MEMBER
+        WHERE email = #{id}
+          AND pwd = #{pwd}
+          AND del_yn = 'N'
+    </select>
+
+    <update id="updateFailCount" parameterType="java.util.HashMap">
+        UPDATE TB_WWW_MEMBER
+           SET login_fail_count = #{loginFailCount}
+        <if test="isAccountLock != null">
+            , is_account_lock = #{isAccountLock}
+        </if>
+         WHERE EMAIL = #{id}
+    </update>
+
+
 </mapper>

+ 0 - 1
src/main/resources/mybatis/mapper/its/notice/NoticeMapper.xml

@@ -21,7 +21,6 @@
             TB_WWW_BOARD
          WHERE boardid = 1
            AND boardno = #{boardNo}
-
     </select>
 
     <select id="findAllNotice" parameterType="com.its.web.pagination.Pagination" resultType="com.its.web.dto.notice.NoticeDto">

+ 112 - 0
src/main/resources/mybatis/mapper/its/popup/PopupMapper.xml

@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.its.web.mapper.its.popup.PopupMapper">
+    <select id="findAllPopup" resultType="com.its.web.dto.admin.TbWwwPopupDto" parameterType="com.its.web.pagination.Pagination">
+        SELECT
+            popupid
+            , title
+            , url
+            , TO_CHAR(post_from, 'YYYY-MM-DD') as post_from
+            , TO_CHAR(post_to, 'YYYY-MM-DD') as post_to
+            , img_id
+            , img_name
+            , TO_CHAR(regdate, 'YYYY-MM-DD') as regdate
+            , use_yn
+            , del_yn
+        FROM (SELECT A.*,
+                     ROWNUM AS RNUM
+                FROM (SELECT *
+                        FROM TB_WWW_POPUP
+                       ORDER BY popupid DESC, regdate DESC
+                ) A
+        )
+        <![CDATA[
+        WHERE RNUM >= #{startRow}
+          AND RNUM <= #{endRow}
+          AND del_yn = 'N'
+        ]]>
+    </select>
+
+    <select id="findMainPopup" resultType="com.its.web.dto.admin.TbWwwPopupDto" parameterType="java.lang.Integer">
+        SELECT
+             popupid,
+             title
+            , post_from
+            , post_to
+          FROM (SELECT A.*,
+                  ROWNUM AS RNUM
+                FROM (SELECT
+                           popupid,
+                           title,
+                           DECODE(post_from, NULL, '-', TO_CHAR(TO_DATE(post_from, 'YYYY-MM-DD'), 'YYYY-MM-DD')) post_from,
+                           DECODE(post_to, NULL, '-', TO_CHAR(TO_DATE(post_to, 'YYYY-MM-DD'), 'YYYY-MM-DD')) post_to
+                        FROM TB_WWW_POPUP
+                        WHERE use_yn = 'Y'
+                          AND del_yn = 'N'
+                       ORDER BY popupid DESC, regdate DESC
+                ) A
+              )
+        WHERE ROWNUM BETWEEN 1 AND #{num}
+    </select>
+    <select id="findPopupTotalPage" resultType="java.lang.Integer">
+        SELECT
+            count(*)
+          FROM TB_WWW_POPUP
+         <![CDATA[
+        WHERE del_yn = 'N'
+        ]]>
+    </select>
+
+    <select id="findPopup" parameterType="java.lang.String" resultType="com.its.web.dto.admin.TbWwwPopupDto">
+        SELECT
+             popupid
+             , title
+             , url
+             , TO_CHAR(post_from, 'YYYY-MM-DD') AS post_from
+             , TO_CHAR(post_to, 'YYYY-MM-DD') AS post_to
+             , img_id
+             , img_name
+             , TO_CHAR(regdate, 'YYYY-MM-DD') AS regdate
+             , use_yn
+             , del_yn
+          FROM TB_WWW_POPUP
+         WHERE popupid = #{popupId}
+           AND del_yn = 'N'
+    </select>
+
+    <insert id="insertPopup" parameterType="java.util.HashMap">
+        INSERT INTO TB_WWW_POPUP (popupid
+                                 , title
+                                 , url
+                                 , post_from
+                                 , post_to
+                                 , img_id
+                                 , img_name
+                                 , regdate
+                                 , use_yn
+                                 , del_yn)
+              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'),
+                      #{imgId},
+                      #{imgName},
+                      TO_DATE(TO_CHAR(sysdate, 'YYYY-MM-DD'), 'YYYY-MM-DD')
+                      'Y',
+                      'N'
+                      )
+    </insert>
+
+    <update id="updatePopup" parameterType="java.util.HashMap">
+        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'),
+               img_id    = #{imgId},
+               img_name  = #{imgName}
+         WHERE popupid   = #{popupId}
+    </update>
+</mapper>

+ 43 - 0
src/main/resources/static/css/login.css

@@ -80,6 +80,49 @@
     cursor: pointer;
 }
 
+.modal.login > div {
+    /*width: 300px;*/
+    height: 200px;
+    z-index: 1000;
+}
+
+.login-modal > div:nth-child(1) {
+    width: 100%;
+    height: 40px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #8c9db4;
+    color: white;
+    font-weight: bold;
+}
+
+.login-modal > div:nth-child(2) {
+    width: 100%;
+    height: 160px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+    background-color: white;
+    padding: 20px;
+}
+
+.login-modal .error-content {
+    height: 60px;
+    width: 100%;
+    align-items: center;
+    justify-content: center;
+    display: flex;
+}
+
+.login-modal .button {
+    margin: 10px 0;
+    height: 40px;
+    line-height: 15px;
+}
+
+
 @media (max-width: 720px) {
     .loginWrap {
         height: calc(100% - 106px);

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

@@ -138,7 +138,29 @@
     cursor: pointer;
     background-color: #eeeeee;
 }
+#post {
+    outline: none;
+    border: none;
+    text-align: center;
+    width: 170px;
+}
+#post.modify {
+    cursor: pointer;
+}
 
+.post-box {
+    height: 40px;
+    padding: 8px 16px;
+    border: 1px solid rgb(204, 234, 234);
+    box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px;
+    background: 0px 0px no-repeat rgb(255, 255, 255);
+    display: flex;
+    margin-bottom: 10px;
+    gap: 10px;
+}
+.post-box > div:nth-child(1) {
+    margin-right: 20px;
+}
 .noticeWrap .content.admin-view:hover {
     box-shadow: rgba(0, 0, 0, 0.19) 0px 6px 12px;
 }

+ 37 - 6
src/main/resources/templates/admin/login.html

@@ -22,13 +22,24 @@
         <input id="pwd" type="password" name="password" placeholder="비밀번호">
         <div onclick="login()" class="button">로그인</div>
     </form>
+    <div class="modal login">
+        <div class="login-modal">
+            <div>로그인 오류</div>
+            <div class="content">
+                <div class="error-content">ID가 잘못되었습니다.</div>
+                <div class="button" onclick="loginModalClose()">확인</div>
+            </div>
+        </div>
+    </div>
 </div>
 <th:block th:include="/include/footer.html"></th:block>
 </body>
 </html>
 
 <script>
+
     let loginFail = "";
+    let errorInput = null;
     loginFail = "[[${param.LoginFail}]]";
     if (loginFail) {
         alert(loginFail);
@@ -36,20 +47,40 @@
     const $id  = $('#id');
     const $pwd = $('#pwd');
     const form = $('.container');
+    const $modal = $('.modal.login');
+    const $error = $('.error-content');
+
+    function loginModalClose() {
+        $modal.css('display', 'none');
+        $error.text("");
+        errorInput.focus();
+    }
 
     function login() {
         if (isNull($id.val())) {
-            alert("ID를 입력해주세요");
-            $id.focus();
+            $error.text("ID를 입력해주세요");
+            errorInput = $id;
+            $modal.css('display', 'flex');
             return
         }
 
         if (isNull($pwd.val())) {
-            alert("비밀번호를 입력해주세요");
-            $pwd.focus();
+            $error.text("비밀번호를 입력해주세요");
+            errorInput = $pwd;
+            $modal.css('display', 'flex');
             return
         }
-        form.submit();
+
+        getDataAsync('/api/login/loginCheck', 'POST', {id : $id.val(), pwd: $pwd.val()},  null, (jsonData)=>{
+            if (jsonData && jsonData.success === "F") {
+                $error.text(jsonData.message);
+                $modal.css('display', 'flex');
+            }
+            else {
+                form.submit();
+            }
+        }, null)
+
     }
 
     $id.on('keydown', (event)=>enterLogin(event));
@@ -64,7 +95,7 @@
             if (isNull($pwd.val())) {
                 return $pwd.focus();
             }
-            form.submit();
+            login();
         }
     }
 </script>

+ 12 - 1
src/main/resources/templates/admin/main.html

@@ -27,11 +27,22 @@
                         <div th:title="${item.getBSubject()}" th:text="${item.getBSubject()}"></div>
                         <span th:text="${item.getRegDate()}"></span>
                     </div>
+                    <div th:if="${notice == null || notice.size() == 0}">
+                        데이터 없음
+                    </div>
                 </div>
             </div>
             <div>
                 <div class="sub-title">팝업공지</div>
-                <div></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>
+                        <span th:text="${item.getPostFrom() + '~' + item.getPostTo()}"></span>
+                    </div>
+                    <div th:if="${notice == null || notice.size() == 0}">
+                        데이터 없음
+                    </div>
+                </div>
             </div>
         </div>
         <div class="bottom">

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

@@ -0,0 +1,64 @@
+<!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>
+    <link rel="stylesheet" th:href="@{/css/notice.css}">
+</head>
+<body id="body">
+<th:block th:include="/include/admin-header.html"></th:block>
+<div class="noticeWrap">
+    <div class="container">
+        <h2 class="admin-header">팝업공지</h2>
+        <div class="admin-content">
+            <div class="title">
+                <a th:href="@{/phits/popup-write}">글쓰기</a>
+            </div>
+            <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 th:text="${item.getRegdate()}"></div>
+                    </div>
+                </a>
+            </div>
+            <div class="pagination">
+                <div class="previous">
+                    <img th:if="${list.getPrevPage() == 0}" src="/images/icon/previous.png" alt="이전">
+                    <img th:if="${list.getPrevPage() > 0}" class="active" src="/images/icon/previous_active.png" alt="이전">
+                </div>
+                <div class="pages">
+                    <a th:class="${m == list.getCurrentPage() ? 'active' : ''}"
+                       th:each="m : ${#numbers.sequence(list.getStartPage(), list.getEndPage())}"
+                       th:onclick="moveNoticePage([[${m}]])"
+                       th:text="${m}"></a>
+                </div>
+                <div class="next">
+                    <img th:if="${list.getNextPage() == 0}" src="/images/icon/next.png">
+                    <img th:if="${list.getNextPage() > 0}" class="active" src="/images/icon/next_active.png" alt="다음">
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<th:block th:include="/include/footer.html"></th:block>
+</body>
+</html>
+
+<script>
+    function moveNoticePage(page) {
+        const activePage = $(".pages > a.active");
+        if (activePage[0] && activePage.text() == page ) {
+            return;
+        }
+        let searchText = '';
+        let searchType = '';
+        window.location.href = '/phits/notice-list?page=' + page + '&searchText='+searchText+'&searchType=' + searchType;
+    }
+
+</script>

+ 273 - 0
src/main/resources/templates/admin/popup-view.html

@@ -0,0 +1,273 @@
+<!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>
+    <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" />
+    <link rel="stylesheet" th:href="@{/css/notice.css}">
+</head>
+<body id="body">
+<th:block th:include="/include/admin-header.html"></th:block>
+<div class="noticeWrap">
+    <div class="container view">
+        <h2 class="admin-header">팝업공지</h2>
+        <div class="content admin-view">
+            <div class="button-box">
+                <div class="bl-button" onclick="movePath('/phits/popup-list')">목록</div>
+                <div style="display: flex;">
+                    <div class="bl-button edit-btn" onclick="edit()">편집</div>
+                    <div class="bl-button off save-btn" onclick="save()">저장</div>
+                    <div class="wt-button del-btn" onclick="delEvent()">삭제</div>
+                    <div class="wt-button off x-btn" onclick="cancel()">취소</div>
+                </div>
+            </div>
+            <div class="view-box">
+                <div>
+                    <input class="title" name="b_subject" th:value="${popup.getTitle()}" readonly>
+                    <textarea  class="b_content" rows="15" th:text="${popup.getUrl()}" readonly></textarea>
+                    <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">
+                            <div th:if="${popup.getImgId() == null or #strings.isEmpty(popup.getImgId())}">첨부파일 없음</div>
+                            <div class="attach-file" th:if="${popup.getImgName() != null and not #strings.isEmpty(popup.getImgName())}"
+                                 th:text="${popup.getImgName()}" th:title="${popup.getImgName() + ' 다운로드'}"
+                                 th:onclick="attachFileDownload([[${popup.getImgId()}]], [[${popup.getImgName()}]])"
+                            ></div>
+                        </div>
+                        <input type="file" name="attachFile" id="attach-file" accept="image/*">
+                        <div class="bl-button off attach-btn" onclick="attachFile()">파일첨부</div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<th:block th:include="/include/footer.html"></th:block>
+</body>
+</html>
+<script th:inline="javascript">
+    const $delete     = $('.del-btn');
+    const $save       = $('.save-btn');
+    const $edit       = $('.edit-btn');
+    const $cancel     = $('.x-btn');
+    const $title      = $('.title');
+    const $content    = $('.b_content');
+    const $attach     = $('.attach');
+    const $attachBtn  = $('.attach-btn');
+    const $attachFile = $('#attach-file');
+    const popup       = [[${popup}]];
+    const popupId     = popup.popupid;
+    const popupTitle  = popup.title;
+    const $post       = $('#post');
+    const post = $post.daterangepicker({
+        timePicker: false,
+        startDate: new Date(popup.post_from),
+        endDate: new Date(popup.post_to),
+        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);
+
+    if (isMobile) {
+        $content.attr('rows', 10);
+    }
+
+    function attachFileDownload(attachFileId, fileName) {
+        const param = {
+            imgId : attachFileId,
+            imgName : fileName,
+        }
+        getDataAsync('/api/popup/attach', 'POST', param, null, (jsonData)=>{
+            const attachFile = jsonData.attach_file;
+
+            if (attachFile && attachFile.length > 0) {
+                const file = b64ToBlob(attachFile);
+                const link = document.createElement('a');
+                link.href = file;
+                link.download = fileName;
+                link.click();
+            }
+            else {
+                alert(jsonData.message);
+            }
+
+        }, null);
+    }
+
+    const b64ToBlob = (byteData) => {
+        const byteCharacters = atob(byteData);
+        const byteNumbers = new Array(byteCharacters.length);
+        for (let i = 0; i < byteCharacters.length; i++) {
+            byteNumbers[i] = byteCharacters.charCodeAt(i);
+        }
+        const byteArray = new Uint8Array(byteNumbers);
+        const blob = new Blob([byteArray]);
+        const blobUrl = window.URL.createObjectURL(blob);
+
+        return blobUrl;
+    };
+
+    function attachFile() {
+        $attachFile.click();
+    }
+
+    $attachFile.on("change", function() {
+        const changeFile = $(this)[0].files[0];
+        if (changeFile) {
+            const fileName = changeFile.name;
+            $attach.html('<div><span class="attach-delete" title="첨부파일 제거" onclick="deleteAttach()"></span>' + fileName + '</div>');
+        }
+    })
+
+    function edit() {
+        $delete.addClass('off');
+        $edit.addClass('off');
+        $save.removeClass('off');
+        $cancel.removeClass('off');
+        $title.addClass('modify');
+        $content.addClass('modify');
+        $attach.addClass('modify');
+        $attachBtn.removeClass('off');
+        $post.prop("disabled", false);
+        $title.attr('readonly', false);
+        $content.attr('readonly', false);
+        $title.prop('placeholder', '팝업 제목을 입력해주세요.');
+        $content.prop('placeholder', '링크 주소가 있다면 URL을 입력해주세요. ex) https://www.naver.com');
+        $post.addClass("modify");
+
+        const fileName = $attach.children().eq(0).text();
+        if (fileName !== '첨부파일 없음') {
+            $attach.html('<div><span class="attach-delete" title="첨부파일 제거" onclick="deleteAttach()"></span>' + fileName + '</div>')
+        }
+
+
+    }
+
+    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)=>{
+                if (jsonData) {
+                    alert(jsonData.message);
+                    if (jsonData.success === "S") {
+                        window.location.href = '/phits/popup-list';
+                    }
+                }
+            }, (error)=>{
+                console.log(error);
+            });
+        }
+    }
+
+    function save() {
+        const file     = $attachFile[0].files[0];
+        const title    = $title.val();
+        const url      = $content.val();
+        const attachNm = popup.img_name;
+        const postVal  = $post.val();
+        let postFrom   = "";
+        let postTo     = "";
+        if (isNull(postVal)) {
+            const postArray = postVal.split("~");
+            if (postArray.length === 2) {
+                postFrom = postArray[0].trim();
+                postTo   = postArray[1].trim();
+            }
+        }
+
+        if (popup.title === title && popup.url === url) {
+            if (file) {
+               if (file.name === attachNm) {
+                   return alert("수정하신 내용이 없습니다. 내용을 확인 해 주세요");
+               }
+            }
+            else {
+                const attachFileName = $attach.children().eq(0).text();
+                if ( (attachFileName === "첨부파일 없음" && attachNm === "") || attachFileName === attachNm) {
+                    return alert("수정하신 내용이 없습니다. 내용을 확인 해 주세요");
+                }
+            }
+        }
+
+        const formData = new FormData();
+        formData.append("popupId", popupId);
+        formData.append("title", title);
+        formData.append("url", url);
+        formData.append("attachFile", file);
+        $.ajax({
+            url: '/api/popup/modifyPopup',
+            processData : false,
+            contentType: false,
+            data: formData,
+            type: 'POST',
+            success: function(jsonData) {
+                if (jsonData) {
+                    alert(jsonData.message);
+                    if (jsonData.success === "S") {
+                        window.location.href = "/phits/popup-view/" + popupId;
+                    }
+                }
+            },
+            error: function(error) {
+                alert(error.responseJSON.message);
+            }
+        });
+
+    }
+
+    function cancel() {
+        $delete.removeClass('off');
+        $edit.removeClass('off');
+        $save.addClass('off');
+        $cancel.addClass('off');
+        $title.removeClass('modify');
+        $content.removeClass('modify');
+        $attach.removeClass('modify');
+        $attachBtn.addClass('off');
+        $title.attr('readonly', true);
+        $content.attr('readonly', true);
+        $title.prop('placeholder', '');
+        $content.prop('placeholder', '');
+        $post.prop("disabled", true);
+        $post.removeClass("modify");
+
+        const attachFileName = popup.img_name;
+
+        let attachText = "첨부파일 없음";
+        if (attachFileName) {
+            attachText = `<div class="attach-file" title="${attachFileName} 다운로드"
+                            onclick="attachFileDownload('${popup.img_id}','${attachFileName}')">
+                            ${attachFileName}
+                         </div>`;
+            $attach.html(attachText);
+        }
+    }
+</script>

+ 115 - 0
src/main/resources/templates/admin/popup-write.html

@@ -0,0 +1,115 @@
+<!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>
+    <link rel="stylesheet" th:href="@{/css/notice.css}">
+</head>
+<body id="body">
+<th:block th:include="/include/admin-header.html"></th:block>
+<div class="noticeWrap">
+    <div class="container view">
+        <h2 class="admin-header">팝업 공지</h2>
+        <div class="content admin-view">
+            <div class="button-box">
+                <div class="bl-button" onclick="movePath('/phits/popup-list')">목록</div>
+                <div style="display: flex;">
+                    <div class="bl-button save-btn" onclick="save()">저장</div>
+                </div>
+            </div>
+            <div class="view-box">
+                <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="attach-box admin">
+                        <div class="attach modify">
+                            <div>첨부파일 없음</div>
+                        </div>
+                        <input type="file" name="attachFile" id="attach-file">
+                        <div class="bl-button attach-btn" onclick="attachFile()">파일첨부</div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<th:block th:include="/include/footer.html"></th:block>
+</body>
+</html>
+<script th:inline="javascript">
+    const $title      = $('.title');
+    const $content    = $('.b_content');
+    const $attach     = $('.attach');
+    const $attachFile = $('#attach-file');
+
+    let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
+
+    if (isMobile) {
+        $content.attr('rows', 10);
+    }
+
+    function attachFile() {
+        $attachFile.click();
+    }
+
+    $attachFile.on("change", function() {
+        const changeFile = $(this)[0].files[0];
+        if (changeFile) {
+            const fileName = changeFile.name;
+            $attach.html('<div><span class="attach-delete" title="첨부파일 제거" onclick="deleteAttach()"></span>'+fileName+'</div>');
+        }
+    })
+
+    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 file    = $attachFile[0].files[0];
+        const formData = new FormData();
+        if (isNull(title)) {
+            $title.focus();
+            return alert("공지사항 제목을 입력해주세요.");
+        }
+
+        if (isNull(content)) {
+            $content.focus();
+            return alert("공지사항 내용을 입력해주세요.");
+        }
+
+        formData.append("bSubject", title);
+        formData.append("bContent", content);
+
+        if (file) {
+            formData.append("attachFile", file);
+        }
+
+        $.ajax({
+            url: '/api/popup/writePopup',
+            processData : false,
+            contentType: false,
+            data: formData,
+            type: 'POST',
+            success: function(jsonData) {
+                if (jsonData) {
+                    alert(jsonData.message);
+                    if (jsonData.success === "S") {
+                        window.location.href = "/phits/popup-list";
+                    }
+                }
+            },
+            error: function(error) {
+                alert(error.responseJSON.message);
+            }
+        });
+    }
+</script>

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

@@ -7,19 +7,19 @@
             <p>포항시교통정보센터</p>
         </div>
         <div class="top-menu">
-            <div class="top-menu-cont" th:classappend="${selected == 'traffic'} ? ' on' : ''">
+            <div class="top-menu-cont" th:classappend="${selected == 'notice'} ? ' on' : ''">
                 <a th:href="@{/phits/notice-list}">공지사항</a>
             </div>
             <div class="top-menu-cont" th:classappend="${selected == 'statistics'} ? ' on' : ''">
                 <a th:href="@{/phits/statistics}">접속자통계</a>
             </div>
-            <div class="top-menu-cont">
-                <a th:href="@{/phits/popup}">팝업공지</a>
+            <div class="top-menu-cont" th:classappend="${selected == 'popup'} ? ' on' : ''">
+                <a th:href="@{/phits/popup-list}">팝업공지</a>
             </div>
-            <div class="top-menu-cont">
-                <a th:href="@{/phits/cctv}">CCTV 관리</a>
+            <div class="top-menu-cont" th:classappend="${selected == 'cctv'} ? ' on' : ''">
+                <a th:href="@{/phits/cctv-list}">CCTV 관리</a>
             </div>
-            <span tabindex="0" id="menu" onclick="menuOpen()">
+            <span id="menu" onclick="logout()">
                 <img src="/images/icon/menu.png" alt="menu">
             </span>
         </div>
@@ -41,4 +41,8 @@
         $(target + " .content").scrollTop(0);
     }
 
+    function logout() {
+        window.location.href = '/phits/logout';
+    }
+
 </script>