Prechádzať zdrojové kódy

update 2024-02-16 admin notice update

junggilpark 1 rok pred
rodič
commit
a34521d306

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

@@ -62,6 +62,6 @@ public class AdminController {
     @GetMapping("/notice-write")
     public String adminNoticeWrite(Model model) {
         model.addAttribute("notice", this.noticeService.findMainNotice(5));
-        return "admin/main";
+        return "admin/notice-write";
     }
 }

+ 39 - 7
src/main/java/com/its/web/controller/notice/NoticeController.java

@@ -7,10 +7,11 @@ 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 org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpSession;
+import java.util.Map;
 
 @Slf4j
 @RequiredArgsConstructor
@@ -25,8 +26,39 @@ public class NoticeController {
     @PostMapping(value = "/attach", produces = {"application/json; charset=utf8"})
     @ResponseBody
     public AttachFileDto getAttachFile(@Param("boardNo") String boardNo,
-                                       @Param("fileId") String fileId,
-                                       @Param("fileName") String fileName) {
-        return this.service.getAttachFile(boardNo, fileId, fileName);
+                                       @Param("fileId") String fileId) {
+        return this.service.getAttachFile(boardNo, fileId);
+    }
+
+    @ApiOperation(value = "02.공지사항 글작성")
+    @PostMapping(value = "/writeNotice")
+    @ResponseBody
+    public Map<String, String> writeNotice(@RequestParam Map<String, Object> paramMap,
+                                           @Param("attachFile") MultipartFile attachFile,
+                                           HttpSession session) {
+        paramMap.put("webUserId", session.getAttribute("UUID"));
+        return this.service.writeNotice(paramMap, attachFile);
+    }
+
+    @ApiOperation(value = "03.공지사항 글수정")
+    @PostMapping(value = "/modifyNotice")
+    @ResponseBody
+    public Map<String, String> modifyNotice(@RequestParam Map<String, Object> paramMap,
+                                           @Param("attachFile") MultipartFile attachFile) {
+        return this.service.modifyNotice(paramMap, attachFile);
+    }
+
+    @ApiOperation(value = "04.공지사항 삭제")
+    @DeleteMapping(value = "/deleteNotice", produces = {"application/json; charset=utf8"})
+    @ResponseBody
+    public Map<String, String> deleteNotice(@Param("boarNo") String boardNo) {
+        return this.service.deleteBoard(boardNo);
+    }
+
+    @ApiOperation(value = "05.공지사항 횟수 증가")
+    @PostMapping(value = "/update-read-count/{boarNo}", produces = {"application/json; charset=utf8"})
+    @ResponseBody
+    public int updateNoticeReadCount(@PathVariable("boarNo") String boardNo) {
+        return this.service.updateNoticeReadCount(boardNo);
     }
 }

+ 10 - 0
src/main/java/com/its/web/mapper/its/notice/NoticeMapper.java

@@ -19,4 +19,14 @@ public interface NoticeMapper {
     List<NoticeDto> findMainNotice(int num);
 
     int getAttachFileCount(Map<String, String> paramMap);
+
+    int deleteBoard(String boardNo);
+
+    int insertNotice(Map<String, Object> paramMap);
+
+    int checkImageName(Map<String, Object> paramMap);
+
+    int updateNotice(Map<String, Object> paramMap);
+
+    int updateNoticeReadCount(String boardNo);
 }

+ 127 - 3
src/main/java/com/its/web/service/notice/NoticeService.java

@@ -9,11 +9,13 @@ 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.*;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 @Slf4j
 @RequiredArgsConstructor
@@ -45,7 +47,7 @@ public class NoticeService {
         return this.mapper.findMainNotice(num);
     }
 
-    public AttachFileDto getAttachFile(String boardNo, String fileId, String fileName) {
+    public AttachFileDto getAttachFile(String boardNo, String fileId) {
         AttachFileDto dto = new AttachFileDto();
         dto.setBoardNo(boardNo);
         String message = null;
@@ -56,7 +58,6 @@ public class NoticeService {
             paramMap.put("fileId", fileId);
             int result = this.mapper.getAttachFileCount(paramMap);
             if (result > 0) {
-                log.info("boardLocation : {}", boardLocation);
                 File file = new File(boardLocation, fileId);
                 int fileSize = (int) file.length();
 
@@ -66,7 +67,7 @@ public class NoticeService {
                         if (byteArray.length > 0) {
                             int i = 0;
                             dto.setAttachFile(new byte[byteArray.length]);
-                            for(byte b : byteArray) {
+                            for (byte b : byteArray) {
                                 dto.getAttachFile()[i++] = b;
                             }
                             message = "파일 전송 완료.";
@@ -84,8 +85,131 @@ public class NoticeService {
                     }
                 }
             }
+            else {
+                message = "파일을 찾일 수 없습니다.";
+            }
         }
         dto.setMessage(message);
         return dto;
     }
+
+    public Map<String, String> deleteBoard(String boardNo) {
+        Map<String, String> resultMap = new HashMap<>();
+        int affectedRows = this.mapper.deleteBoard(boardNo);
+        String message = "선택하신 게시물이 삭제되지 않았습니다.";
+        String success = "F";
+        if (affectedRows > 0) {
+            message = "선택하신 게시물이 삭제되었습니다.";
+            success = "S";
+        }
+        resultMap.put("message", message);
+        resultMap.put("success", success);
+        return resultMap;
+    }
+
+    public Map<String, String> writeNotice(Map<String, Object> paramMap, MultipartFile attachFile) {
+        Map<String, String> resultMap = new HashMap<>();
+        String fileName = "";
+        String fileId = "";
+        String message = "";
+        String success = "S";
+
+        if (attachFile != null && !attachFile.isEmpty()){
+            fileName = attachFile.getOriginalFilename();
+
+            if (fileName != null) {
+                String ext = fileName.substring(fileName.lastIndexOf("."), fileName.length());
+                fileId   = UUID.randomUUID().toString().replaceAll("-", "") + ext;
+
+                boolean isSuccess = uploadAttachFile(attachFile, fileId);
+                if (!isSuccess) {
+                    message = "파일 생성 중 오류가 발생하였습니다.";
+                    success = "F";
+                }
+            }
+        }
+
+        if (success.equals("S")) {
+            paramMap.put("attachFile", fileName + "||");
+            paramMap.put("attachFileId", fileId + "||");
+            int affectedRows = this.mapper.insertNotice(paramMap);
+            message = "작성하신 게시물이 저장되었습니다.";
+            if (affectedRows <= 0) {
+                message = "작성하신 게시물이 저장되지 않았습니다.";
+                success = "F";
+            }
+        }
+
+        resultMap.put("message", message);
+        resultMap.put("success", success);
+        return resultMap;
+    }
+
+    public Map<String, String> modifyNotice(Map<String, Object> paramMap, MultipartFile attachFile) {
+        String fileName = "";
+        String fileId = "";
+        String message = "";
+        String success = "S";
+        Map<String, String> resultMap = new HashMap<>();
+
+        if (attachFile != null && !attachFile.isEmpty()){
+            fileName = attachFile.getOriginalFilename();
+
+            if (fileName != null) {
+                String ext = fileName.substring(fileName.lastIndexOf("."), fileName.length());
+                fileId   = UUID.randomUUID().toString().replaceAll("-", "") + ext;
+                paramMap.put("fileName", fileName);
+                int sameCount = this.mapper.checkImageName(paramMap);
+                if (sameCount == 0) {
+                    boolean isSuccess = uploadAttachFile(attachFile, fileId);
+                    if (!isSuccess) {
+                        message = "파일 생성 중 오류가 발생하였습니다.";
+                        success = "F";
+                    }
+                }
+
+            }
+        }
+
+        if (success.equals("S")) {
+            paramMap.put("attachFile", fileName + "||");
+            paramMap.put("attachFileId", fileId + "||");
+            int affectedRows = this.mapper.updateNotice(paramMap);
+            message = "작성하신 게시물이 수정되었습니다.";
+            if (affectedRows <= 0) {
+                message = "작성하신 게시물이 수정되지 않았습니다.";
+                success = "F";
+            }
+        }
+
+        resultMap.put("message", message);
+        resultMap.put("success", success);
+        return resultMap;
+    }
+
+    public boolean uploadAttachFile(MultipartFile attachFile, String fileId) {
+        File file = new File(boardLocation);
+        boolean isMkFile;
+        if (file.isDirectory()) {
+            file.mkdirs();
+        }
+        File transFile = new File(boardLocation + "/" + fileId);
+
+        try {
+            attachFile.transferTo(transFile);
+            isMkFile = true;
+        }
+        catch (IOException e) {
+            log.error("파일 생성 중 오류가 발생하였습니다. {}", e.getMessage());
+            isMkFile = false;
+        }
+
+        return isMkFile;
+    }
+
+    public int updateNoticeReadCount(String boardNo) {
+        return this.mapper.updateNoticeReadCount(boardNo);
+    }
 }
+
+

+ 5 - 5
src/main/resources/application.yml

@@ -28,11 +28,11 @@ spring:
     session:
       tracking-mode: cookie
       timeout: 600
-  #    multipart:
-  #      enabled: true
-  #      file-size-threshold: 2KB
-  #      max-file-size: 200MB
-  #      max-request-size: 215MB
+    multipart:
+      enabled: true
+      file-size-threshold: 2KB
+      max-file-size: 200MB
+      max-request-size: 215MB
 
   profiles:
     active: prod

+ 14 - 8
src/main/resources/mybatis/mapper/its/notice/NoticeMapper.xml

@@ -62,7 +62,7 @@
         FROM TB_WWW_BOARD
     </select>
 
-    <update id="updateNoticeReadCount" parameterType="java.util.HashMap">
+    <update id="updateNoticeReadCount" parameterType="java.lang.String">
         UPDATE TB_WWW_BOARD
            SET READCOUNT = READCOUNT+1
          WHERE BOARDNO = #{boardNo}
@@ -83,13 +83,13 @@
                                   READCOUNT)
         VALUES ('N',
                 (SELECT NVL(MAX(BOARDNO),0)+1 FROM TB_WWW_BOARD),
-                TO_DATE(TO_CHAR(SYSDATE,'yyyymmdd hh24mi'), 'YYYYMMDD HH24MI'),
+                TO_DATE(TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), 'YYYY-MM-DD HH24:MI:SS'),
                 #{bSubject},
                 #{bContent},
                 #{webUserId},
                 #{attachFile},
-                #{ATTACHFILEID},
-                #{bNotice},
+                #{attachFileId},
+                NULL,
                 '운영자',
                 1,
                 '0')
@@ -97,12 +97,11 @@
 
     <update id="updateNotice" parameterType="java.util.HashMap">
         UPDATE TB_WWW_BOARD
-        SET REGDATE      = TO_DATE(TO_CHAR(SYSDATE,'yyyymmdd hh24mi'), 'YYYYMMDD HH24MI'),
+        SET REGDATE      = TO_DATE(TO_CHAR(SYSDATE,'YYYY-MM-DD HH24:MI:SS'), 'YYYY-MM-DD HH24:MI:SS'),
             BSUBJECT     = #{bSubject},
             BCONTENT     = #{bContent},
             ATTACHFILE   = #{attachFile},
-            ATTACHFILEID = #{attachFileId},
-            BNOTICE      = #{bNotice}
+            ATTACHFILEID = #{attachFileId}
         WHERE BOARDNO    = #{boardNo}
           AND BOARDID    = 1
     </update>
@@ -126,7 +125,7 @@
                         DECODE(regdate, NULL, '-', TO_CHAR(TO_DATE(regdate, 'YYYY-MM-DD'), 'YYYY-MM-DD')) regdate,
                         bsubject
                     from TB_WWW_BOARD
-                    order by regdate desc) A
+                    order by boardno desc, regdate desc) A
              ) B
         where ROWNUM BETWEEN 1 AND #{num}
     </select>
@@ -140,4 +139,11 @@
           AND ATTACHFILEID LIKE '%'||#{fileId}||'%'
     </select>
 
+    <delete id="deleteBoard" parameterType="java.lang.String">
+        DELETE FROM TB_WWW_BOARD WHERE BOARDID = 1 AND BOARDNO = #{boardNo}
+    </delete>
+
+    <select id="checkImageName" parameterType="java.util.HashMap" resultType="int">
+        SELECT COUNT(*) FROM TB_WWW_BOARD WHERE BOARDNO = #{boarNo} AND ATTACHFILE = #{fileName} || '||'
+    </select>
 </mapper>

+ 6 - 0
src/main/resources/static/css/admin-main.css

@@ -80,6 +80,12 @@
     padding: 5px 0 10px 0;
 }
 
+.notice-list:hover {
+    cursor: pointer;
+    color: #007fce;
+    background-color: #eeeeee;
+}
+
 @media (max-width: 450px) {
 }
 

+ 42 - 1
src/main/resources/static/css/notice.css

@@ -133,6 +133,12 @@
     height: calc(100% - 126px);
     overflow: auto;
 }
+
+.noticeWrap .admin-content .list a:hover {
+    cursor: pointer;
+    background-color: #eeeeee;
+}
+
 .noticeWrap .content.admin-view:hover {
     box-shadow: rgba(0, 0, 0, 0.19) 0px 6px 12px;
 }
@@ -238,7 +244,10 @@
     color: rgb(255, 255, 255);
     cursor: pointer;
 }
-
+.wt-button.off,
+.bl-button.off {
+    display: none;
+}
 .bl-button:hover,
 .wt-button:hover{
     filter: brightness(1.1);
@@ -283,6 +292,34 @@
     background: rgba(204, 234, 234, 0.3);
 }
 
+.noticeWrap .content .view-box .attach-box.admin .attach{
+    width: 100%;
+}
+.noticeWrap .content .view-box .attach-box.admin .attach.modify {
+    width: calc(100% - 120px);
+}
+
+.noticeWrap .content .view-box .attach-box.admin .attach-btn {
+    line-height: 30px;
+}
+#attach-file {
+    display: none;
+}
+
+.attach-delete {
+    background-image: url("/images/icon/minus.png");
+    background-size: 15px 15px;
+    width: 15px;
+    height: 15px;
+    background-repeat: no-repeat;
+    background-position: center;
+    padding: 10px 15px;
+    cursor: pointer;
+}
+.attach-delete:hover{
+    filter: brightness(1.1);
+}
+
 .noticeWrap .content .view-box .attach-box .attach{
     height: 40px;
     padding: 8px 16px;
@@ -316,6 +353,10 @@
     flex-direction: column;
     margin: 16px 0px;
 }
+
+.noticeWrap .content.admin-view .view-box {
+    margin: 0px;
+}
 .item-right {
     width: 250px;
     display: flex;

BIN
src/main/resources/static/images/icon/minus.png


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

@@ -34,3 +34,16 @@ function isNull(value) {
     }
     return false;
 }
+
+function moveNoticeView(boarNo, isAdmin) {
+    getDataAsync("/api/notice/update-read-count/" +boarNo, "POST", null, null, (jsonData)=>{
+        if (jsonData) {
+            let page = '/notice/view/';
+            if (isAdmin) page = '/phits/notice-view/';
+            window.location.href = page + boarNo;
+        }
+        else {
+            moveNoticeView(boarNo);
+        }
+    }, null);
+}

+ 20 - 4
src/main/resources/templates/admin/login.html

@@ -33,9 +33,11 @@
     if (loginFail) {
         alert(loginFail);
     }
+    const $id  = $('#id');
+    const $pwd = $('#pwd');
+    const form = $('.container');
+
     function login() {
-        const $id = $('#id');
-        const $pwd = $('#pwd');
         if (isNull($id.val())) {
             alert("ID를 입력해주세요");
             $id.focus();
@@ -47,8 +49,22 @@
             $pwd.focus();
             return
         }
-
-        const form =  $('.container');
         form.submit();
     }
+
+    $id.on('keydown', (event)=>enterLogin(event));
+    $pwd.on('keydown', (event)=>enterLogin(event));
+
+    function enterLogin(event) {
+        if (event.originalEvent.key === 'Enter'){
+            if (isNull($id.val())) {
+                return $id.focus();
+            }
+
+            if (isNull($pwd.val())) {
+                return $pwd.focus();
+            }
+            form.submit();
+        }
+    }
 </script>

+ 2 - 13
src/main/resources/templates/admin/main.html

@@ -23,20 +23,10 @@
             <div>
                 <div class="sub-title">공지사항</div>
                 <div class="content">
-                    <div th:each="item, i:${notice}">
-                        <div th:onclick="movePath([['/notice/view/'+ ${item.getBoardNo()}]])" th:title="${item.getBSubject()}" th:text="${item.getBSubject()}"></div>
+                    <div 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>
-                    <div th:each="item, i:${notice}">
-                        <div th:onclick="movePath([['/notice/view/'+ ${item.getBoardNo()}]])"
-                             th:title="${item.getBSubject()}"
-                             th:text="${item.getBSubject()}"></div>
-                        <span th:text="${item.getRegDate()}"></span>
-                    </div>
-                    <div>
-                        <div th:title="${notice.get(0).getBSubject()}"  th:text="${notice.get(0).getBSubject()}"></div>
-                        <span th:text="${notice.get(0).getRegDate()}"></span>
-                    </div>
                 </div>
             </div>
             <div>
@@ -55,7 +45,6 @@
 </body>
 
 <script th:inline="javascript">
-
 </script>
 
 </html>

+ 3 - 2
src/main/resources/templates/admin/notice-list.html

@@ -20,7 +20,7 @@
                 <a th:href="@{/phits/notice-write}">글쓰기</a>
             </div>
             <div class="list">
-                <a th:each="item : ${list.getList()}" th:href="@{'/phits/notice-view/'+${item.getBoardNo()}}" style="display: flex;">
+                <a th:each="item : ${list.getList()}" th:onclick="moveNoticeView([[${item.getBoardNo()}]], true)" style="display: flex;">
                     <div th:text="${item.getBSubject()}"></div>
                     <div class="item-right">
                         <div th:text="${item.getRegDate()}"></div>
@@ -59,6 +59,7 @@
         }
         let searchText = '';
         let searchType = '';
-        window.location.href = '/notice/list?page=' + page + '&searchText='+searchText+'&searchType=' + searchType;
+        window.location.href = '/phits/notice-list?page=' + page + '&searchText='+searchText+'&searchType=' + searchType;
     }
+
 </script>

+ 162 - 12
src/main/resources/templates/admin/notice-view.html

@@ -19,23 +19,27 @@
             <div class="button-box">
                 <div class="bl-button" onclick="movePath('/phits/notice-list')">목록</div>
                 <div style="display: flex;">
-                    <div class="bl-button" onclick="edit()">편집</div>
-                    <div class="wt-button" onclick="delEvent()">삭제</div>
+                    <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" th:value="${notice.getBSubject()}" readonly>
-                    <textarea class="b_content" rows="16" th:text="${notice.getBContent()}" readonly></textarea>
-                    <div class="attach-box">
+                    <input class="title" name="b_subject" th:value="${notice.getBSubject()}" readonly>
+                    <textarea class="b_content" rows="16" name="b_content" th:text="${notice.getBContent()}" readonly></textarea>
+                    <div class="attach-box admin">
                         <div class="attach">
                             <div th:if="${notice.getAttachFile() == '||' or #strings.isEmpty(notice.getAttachFile())}">첨부파일 없음</div>
                             <div class="attach-file" th:if="${notice.getAttachFile() != '||' and not #strings.isEmpty(notice.getAttachFile())}"
                                  th:each="item, i : ${#strings.arraySplit(notice.getAttachFile(), '|')}"
                                  th:text="${item}" th:title="${item + ' 다운로드'}"
-                                 th:onclick="attachFileDownload([[${i.index}]], [[${notice}]], [[${item}]])"
+                                 th:onclick="attachFileDownload([[${i.index}]], [[${notice.getBoardNo()}]], [[${notice.getAttachFileId()}]],[[${item}]])"
                             ></div>
                         </div>
+                        <input type="file" name="attachFile" id="attach-file">
+                        <div class="bl-button off attach-btn" onclick="attachFile()">파일첨부</div>
                     </div>
                 </div>
             </div>
@@ -46,19 +50,29 @@
 </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 notice      = [[${notice}]];
+
     let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
 
     if (isMobile) {
-        $('.b_content').attr('rows', 10);
+        $content.attr('rows', 10);
     }
 
-    function attachFileDownload(index, notice, fileName) {
-        let attachFileId = notice.attach_file_id;
+    function attachFileDownload(index, id, attachFileId, fileName) {
         if (attachFileId && attachFileId.split('|').length > 0) {
             attachFileId = attachFileId.split('|')[index];
         }
         const param = {
-            boardNo : notice.board_no,
+            boardNo : id,
             fileId  : attachFileId,
         }
         getDataAsync('/api/notice/attach', 'POST', param, null, (jsonData)=>{
@@ -68,9 +82,11 @@
                 if (!ext) {
                     return alert("파일 확장자명이 잘못 되었습니다.");
                 }
-                const file = new Blob([attachFile], {type: "application/" + ext});
+                const file = b64ToBlob(attachFile);
+                // const file = new Blob([attachFile], {type: "image/" + ext});
                 const link = document.createElement('a');
-                link.href = window.URL.createObjectURL(file);
+                // link.href = window.URL.createObjectURL(file);
+                link.href = file;
                 link.download = fileName;
                 link.click();
             }
@@ -81,11 +97,145 @@
         }, 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');
+
+        $title.attr('readonly', false);
+        $content.attr('readonly', false);
 
+        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>")
+        console.log($attachFile[0].files[0]);
     }
 
     function delEvent() {
+        const boardNo    = [[${notice.getBoardNo()}]];
+        const bSubject   = [[${notice.getBSubject()}]];
+        if (confirm( "번호 : " + boardNo + "\n제목 : " + bSubject + '\n게시물을 삭제하시겠습니까?')) {
+            getDataAsync('/api/notice/deleteNotice', 'DELETE', {boardNo : boardNo}, null, (jsonData)=>{
+                if (jsonData) {
+                    alert(jsonData.message);
+                    if (jsonData.success === "S") {
+                        window.location.href = '/phits/notice-list';
+                    }
+                }
+            }, (error)=>{
+                console.log(error);
+            });
+        }
+    }
 
+    function save() {
+        const file     = $attachFile[0].files[0];
+        const title    = $title.val();
+        const content  = $content.val();
+        const attachNm = notice.attach_file.split("|")[0];
+
+        if (notice.b_subject === title && notice.b_content === content) {
+            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("boardNo", notice.board_no);
+        formData.append("bSubject", title);
+        formData.append("bContent", content);
+        formData.append("attachFile", file);
+        $.ajax({
+            url: '/api/notice/modifyNotice',
+            processData : false,
+            contentType: false,
+            data: formData,
+            type: 'POST',
+            success: function(jsonData) {
+                if (jsonData) {
+                    alert(jsonData.message);
+                    if (jsonData.success === "S") {
+                        window.location.href = "/phits/notice-view/" + notice.board_no;
+                    }
+                }
+            },
+            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);
+
+        const attachFileNames = notice.attach_file;
+
+        if (attachFileNames) {
+            const attachFileName = attachFileNames.split("|")[0];
+            let attachText = "첨부파일 없음";
+            if (attachFileName) {
+                attachText = `<div class="attach-file" title="${attachFileName} 다운로드"
+                                onclick="attachFileDownload(0, ${notice.board_no}, '${notice.attach_file_id}','${attachFileName}')">
+                                ${attachFileName}
+                             </div>`;
+            }
+            $attach.html(attachText);
+        }
     }
 </script>

+ 115 - 0
src/main/resources/templates/admin/notice-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/notice-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="내용"></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/notice/writeNotice',
+            processData : false,
+            contentType: false,
+            data: formData,
+            type: 'POST',
+            success: function(jsonData) {
+                if (jsonData) {
+                    alert(jsonData.message);
+                    if (jsonData.success === "S") {
+                        window.location.href = "/phits/notice-list";
+                    }
+                }
+            },
+            error: function(error) {
+                alert(error.responseJSON.message);
+            }
+        });
+    }
+</script>

+ 2 - 2
src/main/resources/templates/main/main.html

@@ -41,8 +41,8 @@
                         <a href="/notice/list">+</a>
                     </div>
                     <div class="content">
-                        <div th:each="item, i:${notice}">
-                            <div th:onclick="movePath([['/notice/view/'+ ${item.getBoardNo()}]])" th:title="${item.getBSubject()}" th:text="${item.getBSubject()}"></div>
+                        <div th:onclick="moveNoticeView([[${item.getBoardNo()}]], false)" th:each="item, i:${notice}">
+                            <div th:title="${item.getBSubject()}" th:text="${item.getBSubject()}"></div>
                             <span th:text="${item.getRegDate()}"></span>
                         </div>
                     </div>

+ 1 - 1
src/main/resources/templates/notice/list.html

@@ -16,7 +16,7 @@
     <div class="container">
         <h2 class="header">공지사항</h2>
         <div class="content list">
-            <a th:each="item : ${list.getList()}" th:href="@{'/notice/view/'+${item.getBoardNo()}}" style="display: flex;">
+            <a th:each="item : ${list.getList()}" th:onclick="moveNoticeView([[${item.getBoardNo()}]], false)" style="display: flex;">
                 <div th:text="${item.getBSubject()}"></div>
                 <div class="item-right">
                     <div th:text="${item.getRegDate()}"></div>