본문 바로가기

Server Programming/Spring Boot Backend Programming

7장-5. 이미지 추가를 위한 컨트롤러와 화면 처리 (+ 파일명에 언더바가 들어간 경우 에러 발생)

반응형

요구사항

 

기능 설명
게시물 등록 모달창으로 파일 등록, 추가된 파일은 섬네일로 표시
파일의 삭제시 업로드된 파일도 함께 삭제
게시물 목록 게시물과 함께 파일을 목록상에 출력
게시물 조회 해당 게시물에 속한 모든 파일 함께 출력
게시물 수정 게시물 조회 기능
파일 삭제는 일단 화면에서만 안 보이도록 처리, 실제 파일 삭제는 수정 작업 처리시에 파일도 함께 처리
게시물 삭제 해당 게시물 삭제 + 업로드된 모든 파일 삭제

이미지 추가를 위한 등록 처리

 

요구사항

  • 모달창을 이용해 파일 업로드 처리
  • 업로드한 파일 섬네일로 표시
  • 업로드한 파일 삭제 처리

 

<form>태그의 submit함수가 아니라 자바스크립트를 활용해 Ajax를 적용

  1. <form>태그 자체에 id 속성을 부여해 자바스크립트 처리시 사용한다.
  2. 파일 업로드를 위한 모달창을 위해 class 속성값 추가 (uploadFileBtn, uploadHidden)
  3. 버튼에 class 속성값으로 submitBtn 지정

 

1. 파일 업로드 위한 버튼을 추가

 

변경 전, register.html

 <!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
<div class="card-body">
  <form action="/board/register" method="post">
    <!--제목 -->
    <div class="input-group mb-3">
    <div class="input-group-text">Title</div>
    <input type="text" name="title" class="form-control" placeholder="Title">
    </div>
    <!--내용 -->
    <div class="input-group mb-3">
    <div class="input-group-text">Content</div>
    <textarea class="form-control col-sm-5" rows="5" name="content"></textarea>
    </div>
    <!--작성자-->
    <div class="input-group mb-3">
    <div class="input-group-text">Writer</div>
    <input type="text" name="writer" class="form-control" placeholder="Writer">
    </div>
    <!--등록 / 초기화 버튼-->
    <div class="float-end">
      <button type="submit" class="btn btn-primary">Submit</button>
      <button type="reset" class="btn btn-primary">Reset</button>
    </div>
  </form>

</div>

 

변경 후, register.html

</div>
  <!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
<div class="card-body">
  <form action="/board/register" method="post">
    <!--제목 -->
    <div class="input-group mb-3">
    <div class="input-group-text">Title</div>
    <input type="text" name="title" class="form-control" placeholder="Title">
    </div>
    <!--내용 -->
    <div class="input-group mb-3">
    <div class="input-group-text">Content</div>
    <textarea class="form-control col-sm-5" rows="5" name="content"></textarea>
    </div>
    <!--작성자-->
    <div class="input-group mb-3">
    <div class="input-group-text">Writer</div>
    <input type="text" name="writer" class="form-control" placeholder="Writer">
    </div>
    
    <!-- 파일 추가를 위한 업로드 버튼-->
    <div class="input-group mb-3">
      <span class="input-group-text"> Images</span>
      <div class="float-end uploadHidden">
        <button type="button" class="btn btn-primary uploadFileBtn">
          ADD Files
        </button>
      </div>
    </div>
    
    <!--등록 / 초기화 버튼-->
    <div class="my-4">
      <div class="float-end">
        <button type="submit" class="btn btn-primary submitBtn">Submit</button>
        <button type="reset" class="btn btn-secondary">Reset</button>
      </div>
    </div>
  </form>

</div>

 

2. Axios를 이용해 업로드 처리하기 위해 upload.js 작성

-upload.js의 역할

  • 파일을 서버에 업로드
  • 서버에 특정 파일을 삭제
async function uploadToServer (formObj){
    console.log("upload to server...")
    console.log(formObj)

    const response = await axios({
       method :'post',
       url : '/upload',
        data: formObj,
        headers:{
          'Content-Type' : 'multipart/form-data'
        }
    });
    return response.data
}

async function removeFilelToServer(uuid, fileName){
    const response = await axios.delete(`/remove/${uuid}_${fileName}`)
    
    return response.data
}

 

3. register.html에 Axios 라이브러리와 upload.js를 사용하도록 스크립트 추가

-업로드 결과를 보여주기 위해 <div>에 모달창을 함께 구성

 

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/js/upload.js"></script>

 

<!-- 파일 업로드하는 모달창-->
<div class="modal uploadModal" tabindex="-1">
  <div class="modal-dialog">
    <div class="modal-content">
      <!-- 모달창 헤더-->
      <div class="modal-header">
        <h5 class="modal-title">Upload File</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <!-- 모달창 본문-->
      <div class="modal-body">
        <div class="input-group mb-3">
          <input type="file" name="files" class="form-control" multiple>
        </div>
      </div>
       <!-- 모달창 푸터-->
        <div class="modal-footer">
          <button type="button" class="btn btn-primary uploadBtn">Upload</button>
          <button type="button" class="btn btn-outline-dark closeUploadBtn" >Close</button>
        </div>
      
    </div>
  </div>
</div>

 

4. 에러 메시지 처리와 모달창 띄우기

//에러메시지 처리
const errors=[[${errors}]]
console.log(errors)

let errorMsg=''

if(errors){
  for(let i=0;i<errors.length;i++){
    errorMsg+=`${errors[i].field}은(는) ${errors[i].code} \n`
  }
  alert(errorMsg)
}

//업로드 모달 창 띄우기
const uploadModal = new bootstrap.Modal(document.querySelector(".uploadModal"))

document.querySelector(".uploadFileBtn").addEventListener("click", function (e){
  e.stopPropagation()
  e.preventDefault()
  
  uploadModal.show()
},false)

 

5. 모달창의 파일 업로드 이벤트 처리

-FormData 객체를 이용해 <form>태그 정보를 가져와서 파일 정보 추가

-js파일의 uploadToServer() 함수 호출해 파라미터로 파일 정보를 전달

-Axios를 통해 파일 한꺼번에 업로드 후, 업로드 결과를 JSON으로 출력

//모달창의 파일 업로드 버튼 이벤트 처리
document.querySelector(".uploadFileBtn").addEventListener("click", function (e){
  //<form>태그의 정보를 가져오는 함수
  const formObj = new FormData();

  //업로드한 파일들을 가져오기
  const fileInput = document.querySelector("input[name='files']")

  console.log(fileInput.files)

  const files = fileInput.files


  for(let i=0;i<files.length; i++){
    formObj.append("files", files[i]);
  }

  //업로드 성공시 -> 업로드한 파일들 출력
  uploadToServer(formObj).then(result =>{
    console.log(result)
    uploadModal.hide()
  }).catch(e=>{
    uploadModal.hide()
  })

},false)

 

6. 업로드 결과를 카드 컴포넌트에 섬네일로 출력하도록 변경

-showUploadFile 함수 호출해 섬네일 출력

 

//카드 컴포넌트를 이용해 섬네일 출력하는 이벤트 처리
////업로드 성공시 -> 업로드한 파일들 출력
// : Axios 호출 후 결과는 showUploadFile() 함수 호출하도록

uploadToServer(formObj).then(result =>{
  console.log(result)
  for(const uploadResult of result){
    showUploadFile(uploadResult)
  }
  uploadModal.hide()
}).catch(e=>{
  uploadModal.hide()
})

},false)

 

7. <form>태그 정보를 전달받아 섬네일을 출력하는 showUploadFile 함수 작성

  //showUploadFile() 함수 작성
  const uploadResult = document.querySelector(".uploadResult")
  function showUploadFile({uuid, fileName, link}){
    const str=`<div class="card col-4">
  <div class="card-header d-flex justify-content-center">
    ${fileName}
    <button class="btn-sm btn-danger" onclick="javascript:removeFile('${uuid}', '${fileName}', this)"></button>
  </div> 
  <div class="card-body">
    <img src="/view/${link}" data-src="${uuid+"_"+fileName}"
  </div>
</div>`
    
    uploadResult.innerHTML+=str
  }

 

 

8. 첨부한 이미지 삭제하는 함수 작성

-서버에서도 삭제, 화면에서도 삭제

//첨부한 이미지 삭제하는 함수 작성
function removeFile(uuid, fileName, obj){
  console.log(uuid)
  console.log(fileName)
  console.log(obj)

  const targetDiv=obj.closest(".card")

  removeFileToServer(uuid, fileName).then(data=>{
    targetDiv.remove()
  })
}

 

이미지 추가한 게시물 등록 과정

- <form>태그의 submit() 동작 시에 업로드된 파일 정보를 <form>태그에 추가해서 같이 submit() 하도록 변경한다.

- <hidden> 태그를 이용해 남아있는 파일 정보를 읽는다.

-submit() 호출 시, BoardController에서 BoardDTO로 데이터를 수집/처리

 

1. register.html에서 버튼 이벤트 처리

  //이미지를 추가한 게시물을 submit버튼 이벤트 처리
  document.querySelector(".submitBtn").addEventListener("click", function (e){
    e.preventDefault()
    e.stopPropagation()

    const target = document.querySelector(".uploadHidden")

    const uploadFiles =uploadResult.querySelectorAll("img")
    
    let str=''
    
    for(let i=0; i<uploadFiles.length; i++){
      const uploadFile = uploadFiles[i]
      const imgLink = uploadFile.getAttribute("data-src")
      
      str +=`<input type='hidden' name='fileNames' value="${imgLink}">`
    }
    target.innerHTML=str;
    
    //document.querySelector("form").submit();

  },false)

 

2. BoardController에서 BoardService의 등록 메서드를 호출하면서 전달받은 BoardDTO를 파라미터로 전달

-BoardService의 이미 작성한 registerPost() 메서드 확인

@PostMapping("/register")
public String registerPost(@Valid BoardDTO boardDTO, BindingResult bindingResult, RedirectAttributes redirectAttributes){
    log.info("board POST register...");

    if(bindingResult.hasErrors()){
        log.info("hasErrors");
        redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());

        return "redirect:/board/register";
    }
    log.info(boardDTO);

    Long bno=boardService.register(boardDTO);

    redirectAttributes.addFlashAttribute("result", bno);

    return "redirect:/board/list";
}

게시물 목록과 첨부파일 처리

화면 목록에서 여러 개의 파일을 함께 화면에 출력

 

 

1. 등록 화면을 통해서 만들어진 첨부파일 데이터만 남기기

이미지 테이블의 테스트용 데이터를 지운다.

 

(1) BoardController에 BoardService의 listWithReplyCount()메서드를 listWithAll() 메서드 호출하도록 변경

@GetMapping("/list")
public void list(PageRequestDTO pageRequestDTO, Model model){

    //BoardDTO + ListReplyCount
    //PageResponseDTO<BoardListReplyDTO> responseDTO = boardService.listWithReplyCount(pageRequestDTO);
    //BoardDTO + ListReplyCount + BoardImage
    PageResponseDTO<BoardListAllDTO> responseDTO = boardService.listWithAll(pageRequestDTO);

    log.info(responseDTO);

    model.addAttribute("responseDTO", responseDTO);
}

 

(2) list.html에 BoardListAllDTO 객체의 boardImages에 목록 출력할 때 첨부파일을 보여주도록 추가

 

변경 전, list.html

<table class="table">
    <thead>
    <tr>
        <th scope="col">Bno</th>
        <th scope="col">Title</th>
        <th scope="col">Writer</th>
        <th scope="col">RegDate</th>
    </tr>
    </thead>
    <!--th:with을 이용해 재사용해서 게시물의 링크처리 -->

    <tbody th:with="link =${pageRequestDTO.getLink()}">
    <tr th:each="dto:${responseDTO.dtoList}">
        <th scope="row">
            [[${dto.bno}]]
        </th>
        <td>
            <a th:href="|@{/board/read(bno =${dto.bno})}&${link}|"> [[${dto.title}]] </a>

            <!-- 게시물당 댓글 정보 추가-->
            <span class="badge progress-bar-success" style="background-color: #0a53be">
                [[${dto.replyCount}]]
            </span>

        </td>
        <td>
            [[${dto.writer}]]
        </td>
        <td>
            <!--#temporals 유틸리티 객체를 이용해 날짜 포맷팅 -->
            [[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]
        </td>

    </tr>
    </tbody>
</table>

 

변경 후, list.html

: 목록에 boardImage를 추가

<table class="table">
    <thead>
    <tr>
        <th scope="col">Bno</th>
        <th scope="col">Title</th>
        <th scope="col">Writer</th>
        <th scope="col">RegDate</th>
    </tr>
    </thead>
    <!--th:with을 이용해 재사용해서 게시물의 링크처리 -->

    <tbody th:with="link =${pageRequestDTO.getLink()}">
    <tr th:each="dto:${responseDTO.dtoList}">
        <th scope="row">
            [[${dto.bno}]]
        </th>
        <td>
            <a th:href="|@{/board/read(bno =${dto.bno})}&${link}|"> [[${dto.title}]] </a>

            <!-- 게시물당 댓글 정보 추가-->
            <span class="badge progress-bar-success" style="background-color: #0a53be">
                [[${dto.replyCount}]]
            </span>

            <!-- 게시물당 이미지 추가-->
            <div th:if="${dto.boardImages} != null && dto.boardImages.size()>0">
                <img style="width:100px" th:each="boardImage : ${dto.boardImages}" th:src="|/view/s_${boardImages.uuid}_${boardImage.fileName}|">
                
            </div>

        </td>
        <td>
            [[${dto.writer}]]
        </td>
        <td>
            <!--#temporals 유틸리티 객체를 이용해 날짜 포맷팅 -->
            [[${#temporals.format(dto.regDate, 'yyyy-MM-dd')}]]
        </td>

    </tr>
    </tbody>
</table>

 

파일명에 언더바가 들어간 경우 에러 발생

이미지를 포함한 게시물 조회

BoardDTO 타입으로 전달된 게시물과 첨부파일을 함께 출력

uuid와 fileName이 결합된 fileNames 리스트를 이용해 원본 이미지를 보여준다.

 

(1) read.html에서 전달받은 BoardDTO의 fileNames 리스트 출력

 

  <!-- 글 조회 하단의 목록과 수정 버튼 다음 부분-->
  <div class="col">
    <!-- 이미지가 존재하면 출력-->
    <div class="card" th:if="${dto.fileNames!=null && dto.fileNames.size()>0}">
      <!-- 반복문으로 -->
      <img class="card-img-top" th:each="fileName : ${dto.fileNames}" th:src="|/view/${fileName}|">
  </div>
</div>
<!--1행 : 글 내용 화면-->
<div class="row mt-3">
  <div class="col">
    <div class="card">
      <!--굵음-->
      <div class="card-header">
        Board Read-header
      </div>
      <!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
      <div class="card-body">
        <!-- 글번호-->
        <div class="input-group mb-3">
          <div class="input-group-text">Bno</div>
          <input type="text" class="form-control" th:value="${dto.Bno}" readonly>
        </div>
        <!--제목 -->
        <div class="input-group mb-3">
          <div class="input-group-text">Title</div>
          <input type="text" class="form-control" th:value="${dto.title}" readonly>
        </div>
        <!--내용 -->
        <div class="input-group mb-3">
          <div class="input-group-text">Content</div>
          <textarea class="form-control col-sm-5" rows="5" readonly>[[${dto.content}]]</textarea>
        </div>
        <!--작성자-->
        <div class="input-group mb-3">
          <div class="input-group-text">Writer</div>
          <input type="text" class="form-control" th:value="${dto.writer}" readonly>
        </div>
        <!--등록시간-->
        <div class="input-group mb-3">
          <div class="input-group-text">regDate</div>
          <input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
        </div>
        <!--수정시간-->
        <div class="input-group mb-3">
          <div class="input-group-text">modDate</div>
          <input type="text" class="form-control" th:value="${#temporals.format(dto.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
        </div>
        <!--등록 / 초기화 버튼-->
         <div class="my-4">
               <div class="float-end" th:with="link = ${pageRequestDTO.getLink()}">
                   <a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
                       <button type="button" class="btn btn-primary">List</button>
                   </a>
              <a th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
                       <button type="button" class="btn btn-secondary">Modify</button>
                   </a>
               </div>
           </div>




      </div> <!-- end card body-->
      <!-- 글 조회 하단의 목록과 수정 버튼 다음 부분-->
      <div class="col">
        <!-- 이미지가 존재하면 출력-->
        <div class="card" th:if="${dto.fileNames!=null && dto.fileNames.size()>0}">
          <!-- 반복문으로 -->
          <img class="card-img-top" th:each="fileName : ${dto.fileNames}" th:src="|/view/${fileName}|">
      </div>
    </div>

</div> <!-- end 1row-->

 


첨부파일을 포함한 게시물 수정과 삭제

조회와 다르게 수정/삭제 페이지에서는 이미지의 원본이 아닌 섬네일을 출력

또한, GET방식의 '/board/read' 페이지를 재사용한다.

 

업로드된 첨부파일 출력과 업로드된 첨부파일을 수정하기 위한 모달창

(1) modify.html에서 첨부파일 출력 화면과 첨부파일 수정시 사용하는 모달창 추가

-register.html에서 가져오되, BoardDTO가 가진 첨부파일 출력하도록 변경

-removeFile 함수를 이용해 삭제처리도 구현

<!--register.html에서 만든 부분 재사용 -->
<!--카드 컴포넌트를 이용한 파일 섬네일 부분-->
<div class="row mt-3">
  <div class="col">
    <div class="container-fluid d-flex uploadResult" style="flex-wrap : wrap">
      <!--register.html과는 달리 BoardDTO가 가진 첨부파일 출력하도록 변경 -->
      <th:block th:each="fileName:${dto.fileNames}">
        <div class="card col-4" th:with="arr =${fileName.split('_')}">
          <div class="card-header d-flex justify-content-center">
            [[${arr[1]}]]
            <button class="btn-sm btn-danger" th:onclick="removeFile([[${arr[0]}]],[[${arr[1]}]], this)">
            </button>
          </div>
          <div class="card-body">
            <img th:src="|/view/s_${fileName}|" th:data-src ="${fileName}">
            
          </div>
        </div>
      </th:block>

    </div>
  </div>
</div>

 

이미지를 삭제하는 함수

(2) removeFile() 함수 작성

-화면에서만 보이지 않도록 처리 후, Modify 버튼 클릭시 서버에서 파일 삭제

-서버에서 삭제하기 전엔 removeFileList라는 배열에 파일 정보를 보관

//첨부한 이미지를 삭제하는 removeFile 함수
const removeFileList=[]

function removeFile(uuid, fileName, obj){
  if(!confirm("파일 삭제?")){
    return
  }

  console.log(uuid)
  console.log(fileName)

  console.log(obj)

  removeFileList.push({uuid,fileName})

  //해당 선택자와 일치하는 요소를 위쪽으로 찾는다.
  const targetDiv = obj.closest(".card")
  targetDiv.remove()
}

 

새로운 첨부파일 추가

-register.html에서 작성한 첨부파일 추가 버튼과 업로드를 위한 스크립트 임포트

-writer아래에 첨부파일 추가 버튼 생성

 

(1) 첨부파일 추가 버튼 생성

변경 전, modify.html

<div layout:fragment="content">
  <div class="row mt-3">
    <div class="col">
      <div class="card">
        <!--굵음-->
        <div class="card-header">
          Board Register-header
        </div>
        <!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
        <div class="card-body">
          <form action="/board/modify" method="post" id="f1">
            <!-- 글번호-->
            <div class="input-group mb-3">
              <div class="input-group-text">Bno</div>
              <input type="text" class="form-control" th:value="${dto.Bno}" name="bno" readonly>
            </div>
            <!--제목 -->
            <div class="input-group mb-3">
              <div class="input-group-text">Title</div>
              <input type="text" class="form-control" th:value="${dto.title}"name="title">
            </div>
            <!--내용 -->
            <div class="input-group mb-3">
              <div class="input-group-text">Content</div>
              <textarea class="form-control col-sm-5" rows="5" name="content">[[${dto.content}]]</textarea>
            </div>
            <!--작성자-->
            <div class="input-group mb-3">
              <div class="input-group-text">Writer</div>
              <input type="text" class="form-control" th:value="${dto.writer}" name="writer" readonly>
            </div>
            
            
            
            <!--등록시간-->
            <div class="input-group mb-3">
              <div class="input-group-text">regDate</div>
              <input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
            </div>
            <!--수정시간-->
            <div class="input-group mb-3">
              <div class="input-group-text">modDate</div>
              <input type="text" class="form-control" th:value="${#temporals.format(dto.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
            </div>
            <!--목록/수정/삭제 버튼-->
            <div class="my-4">
              <div class="float-end">
                <button type="button" class="btn btn-primary listBtn">List</button>
                <button type="button" class="btn btn-secondary modBtn">Modify</button>
                <button type="button" class="btn btn-danger removeBtn">Remove</button>
              </div>
            </div>
          </form>

        </div> <!--end card body -->
      </div><!--end card-->
    </div><!-- end col-->
  </div><!-- end row-->

  <!--register.html에서 만든 부분 재사용 -->
  <!--카드 컴포넌트를 이용한 파일 섬네일 부분-->
  <div class="row mt-3">
    <div class="col">
      <div class="container-fluid d-flex uploadResult" style="flex-wrap : wrap">
        <!--register.html과는 달리 BoardDTO가 가진 첨부파일 출력하도록 변경 -->
        <th:block th:each="fileName:${dto.fileNames}">
          <div class="card col-4" th:with="arr =${fileName.split('_')}">
            <div class="card-header d-flex justify-content-center">
              [[${arr[1]}]]
              <button class="btn-sm btn-danger" th:onclick="removeFile([[${arr[0]}]],[[${arr[1]}]], this)">X
              </button>
            </div>
            <div class="card-body">
              <img th:src="|/view/s_${fileName}|" th:data-src ="${fileName}">

            </div>
          </div>
        </th:block>

      </div>
    </div>
  </div>
    <!-- 파일 업로드하는 모달창-->
  <div class="modal uploadModal" tabindex="-1">
    <div class="modal-dialog">
      <div class="modal-content">
        <!-- 모달창 헤더-->
        <div class="modal-header">
          <h5 class="modal-title">Upload File</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <!-- 모달창 본문-->
        <div class="modal-body">
          <div class="input-group mb-3">
            <input type="file" name="files" class="form-control" multiple>
          </div>
        </div>
        <!-- 모달창 푸터-->
        <div class="modal-footer">
          <button type="button" class="btn btn-primary uploadBtn">Upload</button>
          <button type="button" class="btn btn-outline-dark closeUploadBtn" >Close</button>
        </div>

      </div>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="/js/upload.js"></script>
</div>

 

변경 후, modify.html

<div layout:fragment="content">
  <div class="row mt-3">
    <div class="col">
      <div class="card">
        <!--굵음-->
        <div class="card-header">
          Board Register-header
        </div>
        <!-- 카드 본문에 <form>태그로 게시물에 입력할 항목 추가-->
        <div class="card-body">
          <form action="/board/modify" method="post" id="f1">
            <!-- 글번호-->
            <div class="input-group mb-3">
              <div class="input-group-text">Bno</div>
              <input type="text" class="form-control" th:value="${dto.Bno}" name="bno" readonly>
            </div>
            <!--제목 -->
            <div class="input-group mb-3">
              <div class="input-group-text">Title</div>
              <input type="text" class="form-control" th:value="${dto.title}"name="title">
            </div>
            <!--내용 -->
            <div class="input-group mb-3">
              <div class="input-group-text">Content</div>
              <textarea class="form-control col-sm-5" rows="5" name="content">[[${dto.content}]]</textarea>
            </div>
            <!--작성자-->
            <div class="input-group mb-3">
              <div class="input-group-text">Writer</div>
              <input type="text" class="form-control" th:value="${dto.writer}" name="writer" readonly>
            </div>

            <!-- 첨부파일 추가하는 버튼 생성-->
            <div class="input-group mb-3">
              <span class="input-group-text">Images</span>
              <div class="float-end uploadHidden">
                <button type="button" class="btn btn-primary uploadFileBtn">ADD Files</button>
              </div>
            </div>


            <!--등록시간-->
            <div class="input-group mb-3">
              <div class="input-group-text">regDate</div>
              <input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
            </div>
            <!--수정시간-->
            <div class="input-group mb-3">
              <div class="input-group-text">modDate</div>
              <input type="text" class="form-control" th:value="${#temporals.format(dto.modDate, 'yyyy-MM-dd HH:mm:ss')}" readonly>
            </div>
            <!--목록/수정/삭제 버튼-->
            <div class="my-4">
              <div class="float-end">
                <button type="button" class="btn btn-primary listBtn">List</button>
                <button type="button" class="btn btn-secondary modBtn">Modify</button>
                <button type="button" class="btn btn-danger removeBtn">Remove</button>
              </div>
            </div>
          </form>

        </div> <!--end card body -->
      </div><!--end card-->
    </div><!-- end col-->
  </div><!-- end row-->

  <!--register.html에서 만든 부분 재사용 -->
  <!--카드 컴포넌트를 이용한 파일 섬네일 부분-->
  <div class="row mt-3">
    <div class="col">
      <div class="container-fluid d-flex uploadResult" style="flex-wrap : wrap">
        <!--register.html과는 달리 BoardDTO가 가진 첨부파일 출력하도록 변경 -->
        <th:block th:each="fileName:${dto.fileNames}">
          <div class="card col-4" th:with="arr =${fileName.split('_')}">
            <div class="card-header d-flex justify-content-center">
              [[${arr[1]}]]
              <button class="btn-sm btn-danger" th:onclick="removeFile([[${arr[0]}]],[[${arr[1]}]], this)">X
              </button>
            </div>
            <div class="card-body">
              <img th:src="|/view/s_${fileName}|" th:data-src ="${fileName}">

            </div>
          </div>
        </th:block>

      </div>
    </div>
  </div>

  <!-- 파일 업로드하는 모달창-->
  <div class="modal uploadModal" tabindex="-1">
    <div class="modal-dialog">
      <div class="modal-content">
        <!-- 모달창 헤더-->
        <div class="modal-header">
          <h5 class="modal-title">Upload File</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <!-- 모달창 본문-->
        <div class="modal-body">
          <div class="input-group mb-3">
            <input type="file" name="files" class="form-control" multiple>
          </div>
        </div>
        <!-- 모달창 푸터-->
        <div class="modal-footer">
          <button type="button" class="btn btn-primary uploadBtn">Upload</button>
          <button type="button" class="btn btn-outline-dark closeUploadBtn" >Close</button>
        </div>

      </div>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script src="/js/upload.js"></script>
</div>

 

(2) 첨부파일을 추가하기 위한 버튼 이벤트 처리와 첨부된 파일을 출력하는 자바스크립트

-register.html에서 작성한 스크립트 재사용

//첨부파일 추가 버튼 이벤트 처리
//1. 업로드 모달 창 띄우기
const uploadModal = new bootstrap.Modal(document.querySelector(".uploadModal"))

document.querySelector(".uploadFileBtn").addEventListener("click", function (e){
  e.stopPropagation()
  e.preventDefault()

  uploadModal.show()
},false)

//2. 모달창의 파일 업로드 버튼 이벤트 처리
document.querySelector(".uploadBtn").addEventListener("click", function (e){
  //<form>태그의 정보를 가져오는 함수
  const formObj = new FormData();

  //업로드한 파일들을 가져오기
  const fileInput = document.querySelector("input[name='files']")

  console.log(fileInput.files)

  const files = fileInput.files

  for(let i=0;i<files.length; i++){
    formObj.append("files", files[i]);
  }

  //3. 카드 컴포넌트를 이용해 섬네일 출력하는 이벤트 처리
  ////업로드 성공시 -> 업로드한 파일들 출력
  // : Axios 호출 후 결과는 showUploadFile() 함수 호출하도록

  uploadToServer(formObj).then(result =>{
    console.log(result)
    for(const uploadResult of result){
      showUploadFile(uploadResult)
    }
    uploadModal.hide()
  }).catch(e=>{
    uploadModal.hide()
  })

},false)

//showUploadFile() 함수 작성
const uploadResult = document.querySelector(".uploadResult")

function showUploadFile({uuid, fileName, link}){

  const str =`<div class="card col-4">
          <div class="card-header d-flex justify-content-center">
              ${fileName}
              <button class="btn-sm btn-danger" onclick="javascript:removeFile('${uuid}', '${fileName}', this)" >X</button>
          </div>
          <div class="card-body">
               <img src="/view/${link}" data-src="${uuid+"_"+fileName}" >
          </div>
      </div><!-- card -->`

  uploadResult.innerHTML += str
}

 

이미지 수정/삭제 처리를 포함한 게시물 수정

-등록 처리와 유사하지만, 삭제하려고 했던 파일을 삭제하도록 호출 필요

- <form> 태그의 submit() 이벤트 처리 전에 현재 첨부파일들을 추가하는 appendFileData()함수 작성

- 결정한 파일을 Ajax로 호출하는 callRemoveFiles() 함수 작성

 

1. 이전에 작성한 modBtn 버튼 이벤트 처리 수정

 

변경 전, modBtn 버튼 이벤

document.querySelector(".modBtn").addEventListener("click", function(e){
  e.preventDefault()
  e.stopPropagation()

  formObj.action = `/board/modify?${link}`
  formObj.method ='post'
  formObj.submit()


}, false)

 

변경 후, modBtn 버튼 이벤트

//1. modBtn 버튼 이벤트 처리
document.querySelector(".modBtn").addEventListener("click", function(e){
  e.preventDefault()
  e.stopPropagation()

  formObj.action = `/board/modify?${link}`

  //이미지 수정작업 수행하는 함수 호출
  //추가: 첨부파일을 hidden으로 추가하는 함수 호출
  appendFileData()
  //삭제 : 삭제대상 첨부파일 삭제하는 함수 호출
  callRemoveFiles()

  formObj.method ='post'
  formObj.submit()


}, false)

 

2. 추가한 첨부파일들을 출력하기 위해 <input type='hidden'>으로 추가

function appendFileData(){
  const target = document.querySelector(".uploadHidden")
  const uploadFiles = uploadResult.querySelectorAll("img")

  let str=''

  for(let i=0;i<uploadFiles.length; i++){
    const uploadFile = uploadFiles[i]
    const imgLink = uploadFile.getAttribute("data-src")

    str+=`<input type='hidden' name='fileNmaes' value="${imgLink}">`
  }
  target.innerHTML =str;

}

 

3. 첨부파일 삭제 함수 작성

function callRemoveFiles(){
  removeFileList.forEach(({uuid, fileName})=>{
    removeFileToServer({uuid, fileName})
  })
}

 

이미지 수정/삭제 처리를 포함한 게시물 삭제

-게시물 삭제는 FK 제약조건에 의해 게시물에 달린 댓글이 없을 때 가능하다.

-게시글과 첨부파일 데이터를 삭제하는 것 + 해당 게시물이 가진 첨부파일도 같이 삭제해야 한다.

-> 컨트롤러에서 게시물 번호만 전송하는 것이 아니라,

화면의 첨부파일 + 게시물 수정 처리시 hidden으로 추가한 파일목록 (현재 추가한 첨부파일들) 전송

-> 게시물 삭제 된 후, 첨부파일들을 삭제하도록

 

(1) 이전에 작성한 removeBtn 버튼 이벤트에 함수 호출 추가

-화면에 보이는 파일들을 form 태그에 <input type='hidden'>으로 추가하는 함수 호출

-화면에 안보이는 파일들을 form태그에 추가하는 함수 호출

 

변경 전, removeBtn 버튼 이벤트

document.querySelector(".removeBtn").addEventListener("click", function(e){
  e.preventDefault()
  e.stopPropagation()

  formObj.action = `/board/remove`
  formObj.method ='post'
  formObj.submit()
}, false)

 

변경 후, removeBtn 버튼 이벤트

//게시물의 첨부파일 이벤트 삭제 처리
document.querySelector(".removeBtn").addEventListener("click", function(e){
  e.preventDefault()
  e.stopPropagation()
  
  //화면에 보이는 파일들을 form 태그에 추가하는 함수 호출
  appendFileData()
  //화면에 안 보이도록 처리된 파일들을 form 태그에 추가하는 함수 호출
  appendNotShownData()
  

  formObj.action = `/board/remove`
  formObj.method ='post'
  formObj.submit()
}, false)

 

 

(2) appendNotShownData() 작성

-화면에 안보이도록 처리된 첨부파일들 removeFileList를 <form>태그에 추가

//화면에 안 보이도록 처리된 파일들을 form 태그에 추가하는 함수 작성
function appendNotShownData(){
  if(removeFileList.length==0){
    return
  }

  const target=document.querySelector(".uploadHidden")
  let str=''

  for(let i=0;i<removeFileList.length;i++){
    const {uuid, fileName} = removeFileList[i];

    str+=`<input type='hidden' name='fileNames' value="${uuid}_${fileName}">`

    target.innerHTML+=str;
  }
}

 

(3) BoardController 삭제 처리

-BoardController에서는 bno를 이용하는 것이 아닌, BoardDTO를 이용해 삭제 후 첨부파일을 삭제하도록 구성

-실제 파일 삭제도 이루어질 수 있도록 첨부파일 경로를 주입한다.

 

@Value("${org.zerock.upload.path}") //springframework.bean.factory.annotation.value
private String uploadPath;

 

변경 전, BoardController

@PostMapping("/remove")
public String remove(Long bno, RedirectAttributes redirectAttributes){

    log.info("remove post..."+bno);

    boardService.remove(bno);

    redirectAttributes.addFlashAttribute("result", "removed");

    return "redirect:/board/list";
}

 

(4) 게시물 삭제 후, DB에서 삭제되었으면, 실제 첨부파일을 삭제하는 메서드 작성


//게시물 삭제 후, 첨부파일을 삭제하는 메서드 
public void removeFiles(List<String> files){
    for(String fileName :files){
        Resource resource=new FileSystemResource(uploadPath+ File.separator+fileName);
        
        String resourceName = resource.getFilename();
        
        try{
            String contentType = Files.probeContentType(resource.getFile().toPath());
            
            resource.getFile().delete();
            
            //섬네일이 존재하는 파일이라면 -> img라면
            if(contentType.startsWith("image")){
                File thumbnailFile = new File(uploadPath+File.separator+"s_"+fileName);
                
                thumbnailFile.delete();
            }
            
        }catch (Exception e){
            log.error(e.getMessage());
        }
    }
}
반응형