2020년 02월 10일
업데이트:
게시글 수정
게시글 수정 버튼
// boardView.jsp
<c:url var="updateUrl" value="${board.boardNo}/update"/>
<!-- 로그인된 회원이 글 작성자인 경우 -->
<c:if test="${(loginMember != null) && (board.memberId == loginMember.memberId)}">
<a href="${updateUrl}" class="btn btn-success ml-1 mr-1">수정</a>
<button id="deleteBtn" class="btn btn-success">삭제</button>
</c:if>
게시글 수정 화면 전환
// BoardController.java
// @PathVarilable , QueryString 각각 언제 써야 되는가?
// @PathVarilable : 식별 용도로 사용한다.
// QueryString(주소 마지막에 k=v 형태로 파라미터를 전달하는 문자열) : 필터링, 정렬 등에 사용한다.
// 게시글 수정 화면 전환용 Controller
@RequestMapping("{type}/{boardNo}/update")
public String update(@PathVariable("type") int type, @PathVariable("boardNo") int boardNo, Model model) {
// @PathVariable("boardNo") int boardNo -> @PathVariable int boardNo
// ("boardNo")를 삭제하면 경로상에서 지정된 이름이 변수명과 같은 것을 매핑해줌.
// 1) 게시글 상세 조회
Board board = service.selectBoard(boardNo, type);
// 2) 해당 게시글에 포함된 이미지 목록 조회
if(board!=null) {
List<Attachment> attachmentList = service.selectAttachmentList(boardNo);
model.addAttribute("attachmentList", attachmentList);
// null 값이 전달되어도 EL이 빈 문자열로 처리해 줌
}
model.addAttribute("board", board);
return "board/boardUpdate";
}
//boardUpdate.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시글 수정</title>
<style>
.insert-label {
display: inline-block;
width: 80px;
line-height: 40px
}
#content-main{ margin: 100px auto;}
.deleteImg{
position: absolute;
display : inline-block;
margin-left: -15px;
border: none;
background-color: rgba(1,1,1,0);
width: 20px;
height: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<jsp:include page="../common/header.jsp"/>
<div class="container pb-5 mb-5" id="content-main">
<h3>게시글 수정</h3>
<hr>
<form action="updateAction" method="post" enctype="multipart/form-data" name="updateForm" role="form" onsubmit="return validate();">
<div class="mb-2">
<label class="input-group-addon mr-3 insert-label">카테고리</label>
<select class="custom-select" id="category" name="categoryName" style="width: 150px;">
<option value="10">운동</option>
<option value="20">영화</option>
<option value="30">음악</option>
<option value="40">요리</option>
<option value="50">게임</option>
<option value="60">기타</option>
</select>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">제목</label>
<input type="text" class="form-control" id="title" name="boardTitle" size="70"
value="${board.boardTitle }">
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성자</label>
<h5 class="my-0" id="writer">${loginMember.memberId}</h5>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성일</label>
<h5 class="my-0" id="today">
<jsp:useBean id="now" class="java.util.Date" />
<fmt:formatDate value="${now}" pattern="yyyy-MM-dd"/>
</h5>
</div>
<hr>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">썸네일</label>
<div class="mx-2 boardImg" id="titleImgArea">
<img id="titleImg" width="200" height="200">
<span class="deleteImg">x</span>
</div>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">업로드<br>이미지</label>
<div class="mx-2 boardImg" id="contentImgArea1">
<img id="contentImg1" width="150" height="150">
<span class="deleteImg">x</span>
</div>
<div class="mx-2 boardImg" id="contentImgArea2">
<img id="contentImg2" width="150" height="150">
<span class="deleteImg">x</span>
</div>
<div class="mx-2 boardImg" id="contentImgArea3">
<img id="contentImg3" width="150" height="150">
<span class="deleteImg">x</span>
</div>
</div>
<!-- 파일 업로드 하는 부분 -->
<div id="fileArea">
<input type="file" id="img0" name="images" onchange="LoadImg(this,0)">
<input type="file" id="img1" name="images" onchange="LoadImg(this,1)">
<input type="file" id="img2" name="images" onchange="LoadImg(this,2)">
<input type="file" id="img3" name="images" onchange="LoadImg(this,3)">
</div>
<div class="form-group">
<div>
<label for="content">내용</label>
</div>
<textarea class="form-control" id="content" name="boardContent"
rows="10" style="resize: none;">${board.boardContent }</textarea>
</div>
<hr class="mb-4">
<div class="text-center">
<button type="submit" class="btn btn-primary">수정</button>
<a class="btn btn-primary float-right" href="${header.referer}">목록으로</a>
</div>
</form>
</div>
</div>
<jsp:include page="../common/footer.jsp"/>
<script>
// 게시글에 업로드 된 이미지 삭제
var deleteImages = [];
// 배열을 생성하여 이미지 삭제버튼 수 만큼 배열에 false 요소를 추가
// -> 배열에 4개의 false가 추가됨 == 인덱스 0~3 == fileLevel 0~3과 같음
// 이미지 삭제 버튼이 클릭 될 경우 해당 fileLevel과 같은 index 값을 true로 변경해서
// 해당 이미지가 삭제되었음을 전달하기 위한 용도로 사용할 예정.
// deleteImages 배열에 false 4개 추가
for(var i=0; i<$(".deleteImg").length ; i++){
deleteImages.push(false);
}
// 카테고리 선택
$.each($("#category > option"), function(index, item){
if($(item).text() == "${board.categoryName}"){
$(item).prop("selected", "true");
}
});
// 이미지 배치
<c:forEach var="at" items="${attachmentList}">
$(".boardImg").eq(${at.fileLevel}).children("img").attr("src", "${contextPath}${at.filePath}/${at.fileName}");
</c:forEach>
// 이미지 영역을 클릭할 때 파일 첨부 창이 뜨도록 설정하는 함수
$(function(){
$("#fileArea").hide(); // #fileArea 요소를 숨김.
$(".boardImg").on("click", function(){ // 이미지 영역이 클릭 되었을 때
var index = $(".boardImg").index(this);// 클릭한 이미지 영역 인덱스 얻어오기
$("#img" + index).click(); // 클릭된 영역 인덱스에 맞는 input file 태그 클릭
});
});
// 각각의 영역에 파일을 첨부 했을 경우 미리 보기가 가능하도록 하는 함수
function LoadImg(value, num) {
if(value.files && value.files[0]){ // 해당 요소에 업로드된 파일이 있을 경우
var reader = new FileReader();
reader.readAsDataURL(value.files[0]);
// reader.onload == 파일 업로드에 성공했을 때
// 미리보기
reader.onload = function(e){
$(".boardImg").eq(num).children("img").attr("src", e.target.result);
// 특정 fileLevel에 이미지가 업로드된 경우
// == deleteImages 배열에서 해당 fileLevel과 일치하는 인덱스의 값을 false로 바꿔 삭제되지 않음을 알려줌.
deleteImages[num] = false;
}
}
}
// 유효성 검사
function validate() {
if ($("#title").val().trim().length == 0) {
alert("제목을 입력해 주세요.");
$("#title").focus();
return false;
}
if ($("#content").val().trim().length == 0) {
alert("내용을 입력해 주세요.");
$("#content").focus();
return false;
}
// 유효성 검사에서 문제가 없을 경우 서버에 제출 전
// deleteImages배열의 내용을 hidden 타입으로 하여 form태그 마지막에 추가하여 파라미터로 전달
for(var i=0 ; i<deleteImages.length ; i++){
$deleteImages = $("<input>", {type : "hidden", name : "deleteImages", value : deleteImages[i]});
$("form[name=updateForm]").append($deleteImages);
}
}
// 이미지 삭제 버튼 동작
$(".deleteImg").on("click",function(event){
// event : 현재 발생한 이벤트에 대한 정보가 담긴 객체
event.stopPropagation(); // 이벤트가 연달아 실행되는 것을 방지
// 기존 img태그를 삭제하고 새로운 img태그를 만들어서 제자리에 추가
// 기존 img태그
var $beforeImg = $(this).prev(); // 이벤트가 발생한 요소의 이전 요소 선택
// 기존 정보를 토대로 새로운 img 태그 생성
var $newImg =$("<img>", {id:$beforeImg.attr("id"),
width : $beforeImg.css("width"),
height : $beforeImg.css("height")});
$(this).prev().remove(); // 기존 img태그 삭제
$(this).before($newImg); // 새로운 img태그 추가
// 특정 fileLevel의 요소가 삭제 되었음을 알리기 위해 deleteImages에 기록.
// == deleteImages에 클릭된 삭제버튼 인덱스와 일치하는 true로 변경
// $(".deleteImg").index(this):
// deleteImg 클래스 중 현재 클릭된 버튼의 인덱스를 반환
deleteImages[$(".deleteImg").index(this)] = true;
console.log(deleteImages);
// 삭제버튼이 클릭된 경우
// 해당 이미지와 연결된 input type="file" 태그의 값을 없앤다.
$("#img" + ($(".deleteImg").index(this) )).val("");
});
</script>
</body>
</html>
게시글 수정
// boardController.java
// 게시글 수정 Controller
@RequestMapping("{type}/{boardNo}/updateAction")
public String updateAction(@PathVariable("boardNo") int boardNo,
@ModelAttribute Board updateBoard,
Model model, RedirectAttributes ra, HttpServletRequest request,
@RequestParam("deleteImages") boolean[] deleteImages,
@RequestParam(value="images", required=false) List<MultipartFile> images) {
// updateBoard = boardTitle, boardContent, categoryName이 넘어옴
/*
* System.out.println(Arrays.toString(deleteImages)); for(MultipartFile m :
* images) { System.out.println(m.getOriginalFilename()); }
*/
// boardNo를 updateBoard에 세팅
updateBoard.setBoardNo(boardNo);
// 파일 저장 경로 얻어오기
String savePath = request.getSession().getServletContext().getRealPath("resources/uploadImages");
// 파일 수정 Service 호출
int result = service.updateBoard(updateBoard,images,savePath,deleteImages);
// updateBoard : .수정된 게시글의 정보
// images: 새롭게 업데이트 되거나, 추가된 이미지 정보 (파일 정보 전체)
// savePath: 파일들을 저장할 저장 경로
// deleteImages: 삭제된 파일 레벨을 기록하고 있는 배열
String url = null;
if(result > 0) {
swalIcon = "success";
swalTitle = "게시글 수정 성공";
url = "redirect:../"+boardNo;
}else {
swalIcon = "error";
swalTitle = "게시글 수정 실패";
url = "redirect:" + request.getHeader("referer");
}
ra.addFlashAttribute("swalIcon", swalIcon);
ra.addFlashAttribute("swalTitle", swalTitle);
return url;
}
//BoardService.java
/** 게시글 수정 Service
* @param updateBoard
* @param images
* @param savePath
* @param deleteImages
* @return result
*/
public abstract int updateBoard(Board updateBoard, List<MultipartFile> images, String savePath,
boolean[] deleteImages);
//BoardServiceImpl.java
// 게시글 수정 Service 구현
@Transactional(rollbackFor=Exception.class)
@Override
public int updateBoard(Board updateBoard, List<MultipartFile> images, String savePath, boolean[] deleteImages) {
// 1) 게시글(text) 수정
// 제목, 내용 크로스사이트 스크립팅 방지 처리
updateBoard.setBoardTitle(replaceParameter(updateBoard.getBoardTitle()));
updateBoard.setBoardContent(replaceParameter(updateBoard.getBoardContent()));
// 게시글 수정 DAO 호출
int result = dao.updateBoard(updateBoard);
if(result>0) {
// 2) 이미지 수정
// 수정 전 업로드된 파일 정보를 얻어옴.
// -> 새롭게 삽입 또는 수정되는 파일과 비교하기 위함.
List<Attachment> oldFiles = dao.selectAttachmentList(updateBoard.getBoardNo());
// 새로 업로드된 파일 정보를 담을 리스트
List<Attachment> uploadImages = new ArrayList<Attachment>();
// 삭제 되어야할 파일 정보를 담을 리스트
List<Attachment> removeFileList = new ArrayList<Attachment>();
// DB에 저장할 웹 상 이미지 접근 경로
String filePath = "/resources/uploadImages";
// 새롭게 업로드된 파일 정보를 가지고 있는 images에 반복 접근
for(int i=0; i<images.size(); i++) {
if(!images.get(i).getOriginalFilename().equals("")) { // 업로드된 이미지가 있을 경우
// 파일명 변경
String fileName = rename(images.get(i).getOriginalFilename());
// Attachment 객체 생성
Attachment at = new Attachment(filePath, fileName, i, updateBoard.getBoardNo());
uploadImages.add(at); // 업로드 이미지 리스트에 추가
// true : update 진행
// false : insert 진행
boolean flag = false;
// 새로운 파일 정보와 이전 파일 정보를 비교하는 반복문
for(Attachment old : oldFiles) {
if(old.getFileLevel()==i) {
// 현재 접근한 이전 파일의 레벨이 새롭게 업로드한 파일의 레벨과 같은 경우
// -> 같은 레벨에 새로운 이미지 업로드된 경우 --> update
flag = true;
// DB에서 파일 번호가 일치하는 행의 내용을 수정하기 위해 파일번호를 얻어옴.
at.setFileNo(old.getFileNo());
removeFileList.add(old); // 삭제할 파일 목록에 이전 파일 정보 추가
}
}
// flag 값에 따른 insert / update 제어
if(flag) { // true : update 진행
result = dao.updateAttachment(at);
}else { // false : insert 진행
result = dao.insertAttachment(at);
}
// insert 또는 update 실패 시 Rollback 수행
// -> 예외를 발생시켜서 @Transactional을 이용해 수행
if(result<=0) {
throw new UpdateAttachmentFailException("파일 정보 수정 실패");
}
}else { // 업로드된 이미지가 없을 경우
// deleteImages 배열 : 화면에서 X버튼을 클릭해서 삭제한 배열 인덱스를 표시하는 역할.
// -> 배열 요소 중 true가 되어있는 부분은 해당 인덱스(==파일레벨)에 있던 이미지가 삭제되었다는 의미
// --> DB에서 해당 파일 정보를 삭제
if(deleteImages[i]) {
// x버튼으로 삭제가 되었다고 deleteImages에 true로 저장 되어 있지만
// 혹시라도 이미지가 없는데 x버튼을 누른걸 수도 있으니 진짜로 이전 이미지가 있었는지 검사하기.
for(Attachment old: oldFiles) {
if(old.getFileLevel()==i) {
result = dao.deleteAttachment(old.getFileNo());
if(result>0) { //삭제 성공 시
// removeFileList : 서버에서 삭제할 파일 정보를 모아둔 리스트
removeFileList.add(old); // 서버 파일 삭제 리스트에 추가
}else { // 삭제 실패 시
throw new UpdateAttachmentFailException("파일 정보 삭제 실패");
}
}
}
}
}
} // images 반복 접근 for문 종료
// uploadImages == 업로드된 파일 정보 --> 서버에 파일 저장
// removeFileList == 제거해야될 파일 정보 --> 서버에서 파일 삭제
// 수정되거나 새롭게 삽입된 이미지를 서버에 저장하기 위해 transferTo() 수행
if(result > 0) {
for(int i=0 ; i<uploadImages.size(); i++) {
try {
images.get(uploadImages.get(i).getFileLevel())
.transferTo(new File(savePath + "/" + uploadImages.get(i).getFileName()) );
}catch (Exception e) {
e.printStackTrace();
throw new UpdateAttachmentFailException("파일 정보 수정 실패");
}
}
}
// ------------------------------------------
// 이전 파일 서버에서 삭제하는 코드
for(Attachment removeFile : removeFileList) {
File tmp = new File(savePath + "/" + removeFile.getFileName());
tmp.delete();
}
// ------------------------------------------
}
return result;
}
//BoardDAO.java
/** 게시글 수정 DAO
* @param updateBoard
* @return result
*/
public int updateBoard(Board updateBoard) {
return sqlSession.update("boardMapper.updateBoard", updateBoard);
}
/** 파일 정보 수정 DAO
* @param at
* @return result
*/
public int updateAttachment(Attachment at) {
return sqlSession.update("boardMapper.updateAttachment", at);
}
/** 파일 정보 삽입 DAO
* @param at
* @return result
*/
public int insertAttachment(Attachment at) {
return sqlSession.insert("boardMapper.insertAttachment", at);
}
/** 파일 정보 삭제 DAO
* @param fileNo
* @return result
*/
public int deleteAttachment(int fileNo) {
return sqlSession.delete("boardMapper.deleteAttachment", fileNo);
}
/* board-mapper.xml */
<!-- 게시글 수정 -->
<update id="updateBoard" parameterType="Board">
UPDATE BOARD SET
BOARD_TITLE = #{boardTitle} ,
BOARD_CONTENT = #{boardContent},
CATEGORY_CD = #{categoryName},
BOARD_MODIFY_DT = SYSDATE
WHERE BOARD_NO = #{boardNo}
</update>
<!-- 파일 정보 수정 -->
<update id="updateAttachment" parameterType="Attachment">
UPDATE ATTACHMENT SET
FILE_PATH = #{filePath},
FILE_NAME = #{fileName}
WHERE FILE_NO = #{fileNo}
</update>
<!-- 파일 정보 삽입 -->
<insert id="insertAttachment" parameterType="Attachment">
INSERT INTO ATTACHMENT
VALUES(SEQ_FNO.NEXTVAL, #{filePath}, #{fileName}, #{fileLevel}, #{parentBoardNo})
</insert>
<!-- 파일 정보 삭제 -->
<delete id="deleteAttachment" parameterType="_int">
DELETE FROM ATTACHMENT
WHERE FILE_NO = #{fileNo}
</delete>
공유하기
Twitter Google+ LinkedIn
댓글남기기