KSI일기장
0508spring 게시판(4) 글 작성, 수정 본문
board.js
// 상세조회, 게시글 작성 - 목록으로 버튼
// 즉시 실행 함수
(function(){
const goToListBtn = document.getElementById("goToListBtn");
if(goToListBtn != null){ // 목록으로 버튼이 화면에 있을 때만 이벤트 추가
goToListBtn.addEventListener("click", function(){
// location 객체(BOM)
const pathname = location.pathname; // 주소상에서 요청 경로 반환
// /community/board/detail
// 문자열.substring(시작,끝) : 시작 이상 끝 미만 인덱스 까지 문자열 자르기
// 문자열.indexOf("검색 문자열", 시작 인덱스)
// : 문자열에서 "검색 문자열"의 위치(인덱스)를 찾아서 반환
// 단, 시작 인덱스 이후 부터 검색
// 이동할 주소 저장
let url = pathname.substring(0, pathname.indexOf("/", 1)); //
// /community
url += "/board/list/"+boardCode+"?"; // /board/list/1?cp=1
// URL 내장 객체 : 주소 관련 정보를 나타내는 객체
// location.href : 현재 페이지 주소 + 쿼리스트링
// URL.searchParams : 쿼리 스트링만 별도 객체로 반환
// http://localhost:8080/community/board/detail?no=249&cp=6&type=1&key=c&query=9
const params = new URL(location.href).searchParams;
let cp;
if(params.get("cp") != null){ // 쿼리스트링에 cp가 있을 경우
cp = "cp=" + params.get("cp");
}else{
cp = "cp=1";
}
// 조립
// /commy/board/list/1?cp=1
url += cp;
// 검색 key, query가 존재하는 경우 url에 추가
if(params.get("key") != null){
const key = "&key=" + params.get("key");
const query = "&query=" + params.get("query");
url += key + query; // url 뒤에 붙이기
}
// location.href = "주소"; -> 해당 주소로 이동
location.href = url;
});
}
})();
// 즉시 실행 함수
(function(){
const thumbnail = document.getElementsByClassName("list-thumbnail");
if(thumbnail.length > 0){ // 목록에 썸네일 이미지가 있을 경우에만 이벤트 추가
const modal = document.querySelector('.modal');
const modalImage = document.getElementById("modal-image");
const modalClose = document.getElementById("modal-close");
for(let th of thumbnail){
th.addEventListener("click", function(){
modalImage.setAttribute("src", th.getAttribute("src") );
/* on/off 스위치 */
// classList.toggle("클래스명") : 클래스가 없으면 추가(add)
// 클래스가 있으면 제거(remove)
modal.classList.toggle('show'); // add
});
}
// X버튼
modalClose.addEventListener("click", function(){
modal.classList.toggle('hide'); // hide 클래스 추가
setTimeout(function(){ // 0.45초 후 동작
modal.classList.toggle('hide'); // hide 클래스 제거
modal.classList.toggle('show'); // remove
}, 450);
});
}
})();
// 즉시 실행 함수 : 성능up, 변수명 중복 X
(function(){
const deleteBtn = document.getElementById("deleteBtn"); // 존재하지 않으면 null
if(deleteBtn != null){ // 버튼이 화면에 존재할 때
deleteBtn.addEventListener("click", function(){
// 현재 : /board/detail/{boardCode}/{boardNo}
// 목표 : /board/delete/{boardCode}/{boardNo}
let url = contextPath + "/board/delete/" + boardCode + "/" + boardNo;
// 삭제 성공 -> 해당 게시판 목록 조회 1페이지로 리다이렉트
// 삭제 실패 -> 요청 이전 페이지(referer)로 리다이렉트
// UPDATE BOARD SET
// BOARD_ST = 'Y'
// WHERE BOARD_NO = #{boardNo}
if( confirm("정말로 삭제 하시겠습니까?") ){
location.href = url; // get방식으로 url에 요청
}
});
}
})();
// 검색창에 이전 검색기록 반영하기
(function(){
const select = document.getElementById("search-key");
// const option = select.children;
const option = document.querySelectorAll("#search-key > option");
const input = document.getElementById("search-query");
if(select != null){ // 검색창이 화면이 존재할 때에만 코드 적용
// 현재 주소에서 쿼리스트링(파라미터) 얻어오기
const params = new URL(location.href).searchParams;
// 얻어온 파라미터 중 key, query만 변수에 저장
const key = params.get("key");
const query = params.get("query");
// input에 query 값 대입
input.value = query;
// option을 반복 접근해서 value와 key가 같으면 selected 속성 추가
for(let op of option){
if(op.value == key ){
op.selected = true;
}
}
}
})();
// 검색 유효성 검사(검색어를 입력 했는지 확인)
function searchValidate(){
const query = document.getElementById("search-query");
if(query.value.trim().length == 0){ // 미작성
query.value = ""; // 빈칸
query.focus();
return false;
}
return true;
}
boardDetail.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="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시판</title>
<link rel="stylesheet" href="${contextPath}/resources/css/main-style.css">
<link rel="stylesheet" href="${contextPath}/resources/css/boardDetail-style.css">
<link rel="stylesheet" href="${contextPath}/resources/css/reply-style.css">
<script src="https://kit.fontawesome.com/a2e8ca0ae3.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<section class="board-detail">
<!-- 제목 -->
<h1 class="board-title">${detail.boardTitle} <span> - ${detail.boardName}</span> </h1>
<!-- 프로필 + 닉네임 + 작성일 + 조회수 -->
<div class="board-header">
<div class="board-writer">
<c:if test="${empty detail.profileImage}">
<!-- 프로필 이미지가 없는 경우 -->
<img src="${contextPath}/resources/images/user.png">
</c:if>
<c:if test="${ !empty detail.profileImage}">
<!-- 프로필 이미지가 있는 경우 -->
<img src="${contextPath}${detail.profileImage}">
</c:if>
<span>${detail.memberNickname}</span>
</div>
<div class="board-info">
<p> <span>작성일</span> ${detail.createDate} </p>
<c:if test="${ !empty detail.updateDate}">
<!-- updateDate가 존재하는 경우 -->
<p> <span>마지막 수정일</span> ${detail.updateDate} </p>
</c:if>
<p> <span>조회수</span> ${detail.readCount} </p>
</div>
</div>
<!-- 이미지가 있을 경우 -->
<c:if test="${!empty detail.imageList}">
<!-- 썸네일이 있을 경우 변수 생성 -->
<c:if test="${detail.imageList[0].imageLevel == 0}">
<c:set var="thumbnail" value="${detail.imageList[0]}" />
<!-- page scope (페이지 어디서든 사용 가능) -->
</c:if>
<!-- 썸네일 영역(썸네일이 있을 경우) -->
<c:if test="${!empty thumbnail}">
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<img src="${contextPath}${thumbnail.imageReName}">
<a href="${contextPath}${thumbnail.imageReName}" download="${thumbnail.imageOriginal}">다운로드</a>
</div>
</div>
</c:if>
<c:if test="${empty thumbnail}"> <!-- 썸네일 X -->
<c:set var="start" value="0"/>
</c:if>
<c:if test="${!empty thumbnail}"> <!-- 썸네일 O -->
<c:set var="start" value="1"/>
</c:if>
<!-- 업로드 이미지가 있는 경우 -->
<c:if test="${fn:length(detail.imageList) > start}">
<!-- 업로드 이미지 영역 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<c:forEach var="i" begin="${start}" end="${fn:length(detail.imageList) -1 }">
<div class="boardImg">
<img src="${contextPath}${detail.imageList[i].imageReName}">
<a href="${contextPath}${detail.imageList[i].imageReName}" download="${detail.imageList[i].imageOriginal}">다운로드</a>
</div>
</c:forEach>
</div>
</c:if>
</c:if>
<!-- 내용 -->
<div class="board-content">
${detail.boardContent}
</div>
<!-- 버튼 영역-->
<div class="board-btn-area">
<c:if test="${loginMember.memberNo == detail.memberNo}">
<!-- detail?type=1&cp=3&no=100 -->
<!-- detail?no=1522&type=2 -->
<%-- cp가 없을 경우에 대한 처리 --%>
<c:if test="${empty param.cp}">
<!-- 파라미터에 cp가 없을 경우 1 -->
<c:set var="cp" value="1" />
</c:if>
<c:if test="${!empty param.cp}">
<!-- 파라미터에 cp가 있을 경우 param.cp -->
<c:set var="cp" value="${param.cp}" />
</c:if>
<button id="updateBtn" onclick="location.href='../../write/${boardCode}?mode=update&cp=${cp}&no=${detail.boardNo}'">수정</button>
<button id="deleteBtn">삭제</button>
</c:if>
<!-- onclick="history.back();" 뒤로가기
history.go(숫자) : 양수(앞으로가기), 음수(뒤로가기)
-->
<button id="goToListBtn">목록으로</button>
</div>
</section>
<!-- 댓글 -->
<jsp:include page="/WEB-INF/views/board/reply.jsp"/>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<!-- jQuery 추가 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="${contextPath}/resources/js/board/board.js"></script>
<script>
// 댓글 관련 JS 코드에 필요한 값을 전역 변수로 선언
// jsp 파일 : html, css, js, el, jstl 사용 가능
// js 파일 : js
// 코드 해석 순서 : EL == JSTL > HTML > JS
// ** JS 코드에서 EL/JSTL을 작성하게 된다면 반드시 ""를 양쪽에 추가 **
// 최상위 주소
const contextPath = "${contextPath}";
// 게시글 번호
const boardNo = "${detail.boardNo}"; // "500"
// 로그인한 회원 번호
const loginMemberNo = "${loginMember.memberNo}";
// -> 로그인 O : "10";
// -> 로그인 X : ""; (빈문자열)
// 게시판 번호
const boardCode = "${boardCode}";
</script>
<script src="${contextPath}/resources/js/board/reply.js"></script>
</body>
</html>
boardList.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!-- map에 저장된 값을 각각 변수에 저장 -->
<c:forEach var = "boardType" items="${boardTypeList}">
<c:if test="${boardCode == boardType.boardCode}">
<c:set var="boardName" value="${boardType.boardCode}" />
</c:if>
</c:forEach>
<c:set var="pagination" value="${map.pagination}" />
<c:set var="boardList" value="${map.boardList}" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${boardName}</title>
<link rel="stylesheet" href="${contextPath}/resources/css/main-style.css">
<link rel="stylesheet" href="${contextPath}/resources/css/boardList-style.css">
<script src="https://kit.fontawesome.com/a2e8ca0ae3.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<%-- 검색을 진행한 경우 key, query를 쿼리스트링 형태로 저장한 변수 생성 --%>
<c:if test="${!empty param.key}">
<c:set var="sURL" value="&key=${param.key}&query=${param.query}" />
</c:if>
<section class="board-list">
<h1 class="board-name">${boardName}</h1>
<c:if test="${!empty param.key}">
<h3 style="margin-left:30px;"> "${param.query}" 검색 결과 </h3>
</c:if>
<div class="list-wrapper">
<table class="list-table">
<thead>
<tr>
<th>글번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
</thead>
<tbody>
<c:choose>
<c:when test="${empty boardList}">
<!-- 게시글 목록 조회 결과가 비어있다면 -->
<tr>
<th colspan="5">게시글이 존재하지 않습니다.</th>
</tr>
</c:when>
<c:otherwise>
<!-- 게시글 목록 조회 결과가 비어있지 않다면 -->
<!-- 향상된 for문처럼 사용 -->
<c:forEach var="board" items="${boardList}">
<tr>
<td>${board.boardNo}</td>
<td>
<c:if test="${!empty board.thumbnail}">
<img class="list-thumbnail" src="${contextPath}${board.thumbnail}">
</c:if>
<a href="../detail/${boardCode}/${board.boardNo}?cp=${pagination.currentPage}${sURL}">${board.boardTitle}</a>
<%-- "detail?no=${board.boardNo}&cp=${pagination.currentPage}&type=${param.type}${sURL}" --%>
<%--
현제페이지(게시판 첫페이지) 주소 : /board/list/1?cp=1
상세 조회 주소 : /board/detail/1/300?cp=
--%>
<%-- ../detail/${boardCode}/${board.boardNo} == board/detail/{boardCode}/{boardNo} --%>
</td>
<td>${board.memberNickname}</td>
<td>${board.createDate}</td>
<td>${board.readCount}</td>
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</tbody>
</table>
</div>
<div class="btn-area">
<c:if test="${!empty loginMember}">
<!-- /community/board/write -->
<%-- <button id="insertBtn" onclick="location.href='write?mode=insert&type=${param.type}&cp=${param.cp}'">글쓰기</button> --%>
<button id="insertBtn" onclick="location.href='../write/${boardCode}?mode=insert&cp=${pagination.currentPage}'">글쓰기</button>
</c:if>
</div>
<div class="pagination-area">
<!-- 페이지네이션 a태그에 사용될 공통 주소를 저장한 변수 선언 -->
<c:set var="url" value="${boardCode}?cp="/>
<ul class="pagination">
<!-- 첫 페이지로 이동 -->
<li><a href="${url}1${sURL}"><<</a></li>
<!-- 이전 목록 마지막 번호로 이동 -->
<li><a href="${url}${pagination.prevPage}${sURL}"><</a></li>
<!-- 범위가 정해진 일반 for문 사용 -->
<c:forEach var="i" begin="${pagination.startPage}" end="${pagination.endPage}" step="1">
<c:choose>
<c:when test="${i == pagination.currentPage}">
<li><a class="current">${i}</a></li>
</c:when>
<c:otherwise>
<li><a href="${url}${i}${sURL}">${i}</a></li>
</c:otherwise>
</c:choose>
</c:forEach>
<!-- 다음 목록 시작 번호로 이동 -->
<li><a href="${url}${pagination.nextPage}${sURL}">></a></li>
<!-- 끝 페이지로 이동 -->
<li><a href="${url}${pagination.maxPage}${sURL}">>></a></li>
</ul>
</div>
<!-- /board/list?type=1&cp=3 -->
<!-- /board/list?type=1&cp=10 &key=t&query=안녕 -->
<form action="list" method="get" id="boardSearch" onsubmit="return searchValidate()">
<input type="hidden" name="type" value="${param.type}">
<select name="key" id="search-key">
<option value="t">제목</option>
<option value="c">내용</option>
<option value="tc">제목+내용</option>
<option value="w">작성자</option>
</select>
<input type="text" name="query" id="search-query" placeholder="검색어를 입력해주세요.">
<button>검색</button>
</form>
</section>
</main>
<div class="modal">
<span id="modal-close">×</span>
<img id="modal-image" src="${contextPath}/resources/images/user.png">
</div>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<script src="${contextPath}/resources/js/board/board.js"></script>
</body>
</html>
BoardController
//게시글 작성,수정 화면 전환
//<button id="insertBtn" onclick="location.href='../write/${boardCode}?mode=insert&cp=${pagination.currentPage}'">글쓰기</button>
//<button id="updateBtn" onclick="location.href='../../write/${boardCode}?mode=update&cp=${cp}&no=${detail.boardNo}'">수정</button>
//작성화면 전환
@GetMapping("/write/{boardCode}")
public String boardWriterForm(@PathVariable("boardCode") int boardCode,
String mode,// ==@RequestParam
Model model, //Model을 이용해 view단에 데이터를 넘겨줄 수 있다
@RequestParam(value="no", required=false, defaultValue="0") int boardNo) {
//-> insert의 경우 파라미터에 no(게시물번호)가 없을 수 있어서
if(mode.equals("update")) { //수정화면 전환
//mode가 update일 경우
//BoardWriteForm.jsp에서
// <c:if test="${param.mode == 'update'}">
//<button id="updateBtn" onclick="location.href='../../write/${boardCode}?mode=update&cp=${cp}&no=${detail.boardNo}'">수정</button>
//mode=update
//게시글 상세조회 서비스호출(boardNo)
BoardDetail detail = service.selectBoardDetail(boardNo);
//->개행문자가 <br>태그로 되있는 상태
//->textarea출력 예정이기 때문에 \n으로 변경 필요
//->Util 클래스에 있음 <br> -> \n
//Util.newLineClear(detail.getBoardContent());
detail.setBoardContent( Util.newLineClear(detail.getBoardContent()) );
model.addAttribute("detail", detail);
//Model을 이용해 view단에 데이터를 넘겨줄 수 있다
}
return "board/boardWriteForm";
// WEB-INF/views/board/boardWriteFrom
}
//개행문자가 <br>로 되있는 상태 -> textarea 출력하려면 \n 변경해야한다
//-> Util.newLineClear() 메서드 사용해서
//게시글 작성 (삽입/수정)
//<button id="insertBtn" onclick="location.href='write?mode=insert&type=${param.type}&cp=${param.cp}'">글쓰기</button>
//<button id="updateBtn" onclick="location.href='write?mode=update&type=${param.type}&cp=${cp}&no=${detail.boardNo}'">수정</button>
// <form action="${boardCode}" enctype="multipart/form-data" method="POST"
// "/board/write/{boardCode}"
@PostMapping("/write/{boardCode}")
public String boardWrite(BoardDetail detail, //boardTitle, boardContent, boardNo
@RequestParam(value="images", required=false) List<MultipartFile> imageList, //업로드 파일(이미지) 리스트
@PathVariable("boardCode") int boardCode,
String mode,
@ModelAttribute("loginMember") Member loginMember,
RedirectAttributes ra,
HttpServletRequest req, //이미지 저장 경로 얻어오기 위해 쓰일 것
@RequestParam(value="cp", required=false, defaultValue="1") int cp,
@RequestParam(value="deleteList", required=false) String deleteList
//boardWriteForm.jsp에서 name="deleteList
) throws Exception{
//1.로그인한 회원번호 얻어와 detail에 세팅
detail.setMemberNo(loginMember.getMemberNo());
//2.이미지 저장 경로 얻어오기(webPath, folderPath)
String webPath = "/resources/images/board/";
String folderPath = req.getSession().getServletContext().getRealPath(webPath); //실제 이미지파일 경로
//3.삽입인지 수정인지
if(mode.equals("insert")) { //BoardWriteForm.jsp에서
// <c:if test="${param.mode == 'insert'}"> ,
//mode == intsert일 경우 삽입
//<button id="insertBtn" onclick="location.href='../write/${boardCode}?mode=insert&cp=${pagination.currentPage}'">글쓰기</button>
//에서 mode=insert
//1)게시글 부분 삽입(이미지 빼고 제목,내용,회원번호,게시판코드)
//->삽입끝난 후 게시글번호(boardNo) 반환 (이유: 게시글삽입이 끝나면 게시글 상제조회로 리다이렉트할거기 때문에)
//2)게시글에 포함된 이미지 정보 삽입 (이미지0~5개 (게시글번호 필요) )
//->실제 파일로 변환해서 서버에 저장(transferTo())
//3)위 두번의 insert중 한번이라도 실패하면 전체 rollback(트랜잭션처리)
int boardNo = service.insertBoard(detail, imageList, webPath, folderPath);
String path = null;
String message = null;
if(boardNo>0) {
//
//현재주소 /board/write/1
// ->게시글 등록 후 게시글 상세조회 페이지 /board/detail/1/1500
path = "../detail/" + boardCode + "/" + boardNo;
message = "게시글이 등록되었습니다";
}else {
path = req.getHeader("referer"); //referer ( 이전페이지 url)
//실패시 이전페이지로 돌아간다
message = "게시글 삽입 실패";
}
ra.addFlashAttribute("message",message);
return "redirect:" + path;
}else { // //BoardWriteForm.jsp에서
// <c:if test="${param.mode == 'update'}">
//mode가 update인 경우 수정
//게시글 수정 서비스 호출
//게시글 번호를 알고있기 때문에 수정결과만 반환 받으면 된다
int result = service.updateBoard(detail, imageList, webPath, folderPath, deleteList);
String path = null;
String message = null;
if(result>0) {
//현재주소 /board/write/1
// ->게시글 등록 후 게시글 상세조회 페이지 /board/detail/1/1500
// /board/detail/{boardCode}/{boardNo}?cp=10
path = "../detail/" + boardCode + "/" + detail.getBoardNo() + "?cp=" + cp;
message = "게시글이 등록되었습니다";
}else {
path = req.getHeader("referer"); //referer ( 이전페이지 url)
//실패시 이전페이지로 돌아간다
message = "게시글 수정 실패";
}
ra.addFlashAttribute("message",message);
return "redirect:" + path;
}
}
BoardService
/**게시글 수정 서비스
* @param detail
* @param imageList
* @param webPath
* @param folderPath
* @param deleteList
* @return result
* @throws IOException
* @throws Exception
*/
int updateBoard(BoardDetail detail, List<MultipartFile> imageList, String webPath, String folderPath,
String deleteList) throws Exception, IOException;
BoardServiceImpl
//게시글삽입 + 이미지 삽입 서비스
//spring에서 트랜잭션 처리하는 방법
//AOP(관점지향프로그래밍)를 이용해 (DAO에서 Service보낼때) or (Service코드 수행 시점)에
//예외가 발생하면 rollback을 수행
//방법1. <tx:advice> XML을 이용한 방법 -> 패턴을 지정해서 일치하는 메서드 호출 시 자동으로 트랜잭션 제어
//방법2. @Transactional 선언적 트랜잭션 처리 방법 -> RuntimeException 예외처리를 기본값으로 갖는다
//rollbackFor :rollback을 수행하기 위한 예외의 종류를 작성
//RuntimeException (Unchecked Exception)
//checked Exception :예외처리가 필수( transferTo() ) -> SQL관련 예외, 파일업로드 관련 예외
//Unchecked Exception :예외처리가 선택적 ( ex. int a = 10/0; )
@Transactional(rollbackFor = {Exception.class})
@Override
public int insertBoard(BoardDetail detail,
List<MultipartFile> imageList,
String webPath, String folderPath) throws IOException {
//1.게시글 삽입
//1)XSS방지처리 + 개행문자 처리 (Util에 있음)
detail.setBoardTitle(Util.XSSHandling(detail.getBoardTitle()));
detail.setBoardContent(Util.XSSHandling(detail.getBoardContent()));
//->XSS방지 처리
detail.setBoardContent(Util.newLineHandling(detail.getBoardContent()));
//-> 개행문자처리 <br> -> \n 으로 변환 (Util에 있음)
//2)게시글삽입 DAO호출 후 게시글 번호 반환받기
//1.서비스 결과 반환 후 컨트롤러에서 상세조회로 리다이렉트 하기 위해
//2.동일한 시간에 삽입이 2회이상(거의동시에) 진행되는 경우 시퀀스 번호가 의도와 달리 여러번 증가해
// 이후에 작성된 이미지 삽입 코드에 영향 미치는걸 방지하기 위해
int boardNo = dao.insertBoard(detail);
List<BoardImage> boardImageList = new ArrayList<>();
List<String> reNameList = new ArrayList<>();
if(boardNo>0) {
//이미지 삽입
//imageList :실제파일이 담겨있는 리스트
//boardImageList :DB에 삽입할 이미지정보만 담겨있는 리스트
//reNameList :변경된 파일명 담겨있는 리스트
//imageList에 담겨있는 파일정보 중 실제업로드된 파일만 분류하는 작업
//리스트에서 실제파일이 들어가서 업로드될 파일만 분류
for(int i = 0; i<imageList.size(); i++) { //i번째 요소에 업로드된 이미지가 있을경우
//변경된 파일명 저장
//파일명 ex. 20220222_10
String reName = Util.fileRename(imageList.get(i).getOriginalFilename());
reNameList.add(reName);
//BoardList객체를 생성해 값 세팅 후 boardImageList에 추가
BoardImage img = new BoardImage();
img.setBoardNo(boardNo); //게시글 번호
img.setImageLevel(i); //이미지 순서(파일레벨)
img.setImageOriginal(imageList.get(i).getOriginalFilename()); //원본파일명
img.setImageReName(webPath + reName); //웹 접근경로 + 변경된 파일명
boardImageList.add(img);
}//end of for
}
//분류 작업 종료 후 boardImageList가 비어있지 않은 경우 ( == 파일 업로드가 된 경우)
if(!boardImageList.isEmpty()) {
int result = dao.insertBoardImageList(boardImageList);
//result == 삽입 성공한 행 개수
if(result == boardImageList.size() ) { //삽입된 행의 개수와 업로드 이미지 수가 같을 경우
//서버에 이미지 저장
for(int i = 0; i<boardImageList.size(); i++) {
int index = boardImageList.get(i).getImageLevel();
imageList.get(index).transferTo( new File(folderPath + reNameList.get(i) ) );
}
}else { //이미지 삽입 실패시
//강제로 예외 발생시켜 rollback을 수행하게 한다
//-> 사용자 정의 예외
throw new InsertFailException();
}
}
return boardNo;
}
//게시글 수정
@Transactional(rollbackFor = {Exception.class} ) //모든종류예외(Exception)발생시 rollback한다는 의미
@Override
public int updateBoard(BoardDetail detail, List<MultipartFile> imageList, String webPath, String folderPath,
String deleteList) throws Exception, IOException {
//1)XSS방지처리 + 개행문자 처리 (Util에 있음)
detail.setBoardTitle(Util.XSSHandling(detail.getBoardTitle()));
detail.setBoardContent(Util.XSSHandling(detail.getBoardContent()));
//->XSS방지 처리
detail.setBoardContent(Util.newLineHandling(detail.getBoardContent()));
//-> 개행문자처리 <br> -> \n 으로 변환 (Util에 있음)
//2)게시글만(제목, 내용, 마지막수정일(sysdate) 수정하는 DAO호출
int result = dao.updateBoard(detail);
if(result>0) {
//3)업로드된 이미지만 분류
List<BoardImage> boardImageList = new ArrayList<>();
List<String> reNameList = new ArrayList<>();
//이미지 삽입
//imageList :실제파일이 담겨있는 리스트
//boardImageList :DB에 삽입할 이미지정보만 담겨있는 리스트
//reNameList :변경된 파일명 담겨있는 리스트
//imageList에 담겨있는 파일정보 중 실제업로드된 파일만 분류하는 작업
//리스트에서 실제파일이 들어가서 업로드될 파일만 분류
for(int i = 0; i<imageList.size(); i++) { //i번째 요소에 업로드된 이미지가 있을경우
if(imageList.get(i).getSize() > 0) {
//변경된 파일명 저장
//파일명 ex. 20220222_10
String reName = Util.fileRename(imageList.get(i).getOriginalFilename());
reNameList.add(reName);
//BoardList객체를 생성해 값 세팅 후 boardImageList에 추가
BoardImage img = new BoardImage();
img.setBoardNo(detail.getBoardNo()); //게시글 번호
img.setImageLevel(i); //이미지 순서(파일레벨)
img.setImageOriginal(imageList.get(i).getOriginalFilename()); //원본파일명
img.setImageReName(webPath + reName); //웹 접근경로 + 변경된 파일명
boardImageList.add(img);
}//end of if
}//end of for
//deleteList를 이용해서 삭제된, 이미 delete
if(!deleteList.equals("")) {
Map<String, Object> map = new HashMap<>();
map.put("boardNo", detail.getBoardNo());
map.put("deleteList", deleteList);
result = dao.deleteBoardImage(map);
}
if(result>0) {
//5)boardImageList를 순차 접근하면서 하나씩 update
for(BoardImage img : boardImageList) {
result = dao.updateBoardImage(img); //변경할명, 원본명, 게시글번호, 레벨
//결과1 -> 수정 완료(기존이미지 존재)
//->update
//결과0 -> 수정 실패(기존이미지 존재X)
//->insert
//-> image가 기존에 4개 자리중 2개있는 경우 1개추가하면 insert
//-> image가 기존에 4개 자리중 4개있는 경우 1개바꾸면 update
//6)update를 실패하면 insert
if(result==0) {
result = dao.insertBoardImage(img);
//->값을 하나씩 대입해서 삽입하는 경우 결과가 0이 나올 수는 없다
//단, 예외는 발생할 수 있다(제약조건위배 예외, sql문법 예외 등)
}
}//end of for
//업로드된
if(!boardImageList.isEmpty() && result != 0) {
//서버에 이미지 저장
for(int i = 0; i<boardImageList.size(); i++) {
int index = boardImageList.get(i).getImageLevel();
imageList.get(index).transferTo( new File(folderPath + reNameList.get(i) ) );
}
}
}//end of if
}
return result;
}
BoardDAO
/**게시글 삽입 DAO
* @param detail
* @return
*/
public int insertBoard(BoardDetail detail) {
int result = sqlSession.insert("boardMapper.insertBoard", detail); //0 or 1
if(result>0) {
result = detail.getBoardNo();
}
return result;
}
/**게시글 이미지 삽입(리스트) DAO
* @param boardImageList
* @return result
*/
public int insertBoardImageList(List<BoardImage> boardImageList) {
return sqlSession.insert("boardMapper.insertBoardImageList", boardImageList);
}
/**게시글 수정 DAO
* @param detail
* @return
*/
public int updateBoard(BoardDetail detail) {
return sqlSession.update("boardMapper.updateBoard", detail);
}
/**게시글 이미지 삭제
* @param map
* @return
*/
public int deleteBoardImage(Map<String, Object> map) {
return sqlSession.delete("boardMapper.deleteBoardImage", map);
}
/**게시글 이미지 1개 수정
* @param img
* @return
*/
public int updateBoardImage(BoardImage img) {
return sqlSession.update("boardMapper.updateBoardImage", img);
}
/**게시글 이미지 1개 삽입
* @param img
* @return
*/
public int insertBoardImage(BoardImage img) {
return sqlSession.insert("boardMapper.insertBoardImage", img);
}
board-mapper.xml
<!-- 게시글 삽입 -->
<!--
useGeneratedKeys : 기본값 false,
DB내부적으로 생성한 키(ex.시퀀스)를 사용하겠다는 의미
selectKey :동적 SQL중 하나로,
INSERT or UPDATE에 사용된 키(ex.시퀀스)를 원하는 변수/필드에 담아 반환하는 태그
keyProperty :key를 담을 변수/필드를 지정하는 속성
order : INSERT/UPDATE에 작성된 메인 SQL이 수행되기전(BEFORE) 또는 후(AFTER)에
<selcetKey> 내부 SQL이 수행되도록 순서를 지정하는 속성
-->
<insert id="insertBoard" parameterType="detail" useGeneratedKeys="true">
<selectKey keyProperty="boardNo" resultType="_int" order="BEFORE">
SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO BOARD VALUES(
#{boardNo}, #{boardTitle}, #{boardContent},
DEFAULT, DEFAULT, DEFAULT, DEFAULT, #{memberNo}, #{boardCode} )
</insert>
<!-- 동적SQL 중 <foreach>
:특정 SQL구문을 반복 사용할 때 사용
:반복되는 사이에 구분자(separator) 추가 가능
collection: 반복할 객체의 타입 작성(list, set, map..)
item :collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
index :현재 반복 접근중인 인덱스(0,1,2,3,...)
open :반복 전에 출력할 sql
close :반복 종료 후에 출력할 sql
separator :반복 사이에 들어갈 구분자 -->
<insert id="insertBoardImageList" parameterType="list">
INSERT INTO BOARD_IMG
SELECT SEQ_IMG_NO.NEXTVAL IMG_NO, A.* FROM(
<foreach collection="list" item="img" separator="UNION ALL" >
SELECT #{img.imageReName} IMG_RENAME,
#{img.imageOriginal} IMG_ORIGINAL,
#{img.imageLevel} IMG_LEVEL,
#{img.boardNo} BOARD_NO
FROM DUAL
</foreach>
) A
</insert>
<!--
위 foreach 반복문을 풀면
->
INSERT INTO BOARD_IMG
SELECT SEQ_IMG_NO.NEXTVAL IMG_NO, A.* FROM(
SELECT #{img.imageReName} IMG_RENAME,
#{img.imageOriginal} IMG_ORIGINAL,
#{img.imageLevel} IMG_LEVEL,
#{img.boardNo} BOARD_NO
FROM DUAL
UNION ALL
SELECT #{img.imageReName} IMG_RENAME,
#{img.imageOriginal} IMG_ORIGINAL,
#{img.imageLevel} IMG_LEVEL,
#{img.boardNo} BOARD_NO
FROM DUAL
UNION ALL
) A
......
-->
<!-- 게시글 수정 -->
<update id="updateBoard">
UPDATE BOARD SET
BOARD_TITLE = #{boardTitle},
BOARD_CONTENT = #{boardContent},
UPDATE_DT = SYSDATE
WHERE BOARD_NO = #{boardNo}
</update>
<!-- 게시글 이미지 삭제 -->
<!-- ***주의*** IN구문 작성시 ''가 없도록 $ 사용 -->
<delete id="deleteBoardImage">
DELETE FROM BOARD_IMG
WHERE BOARD_NO = ${boardNo}
AND IMG_LEVER IN ( ${deleteList} )
</delete>
<!-- 게시글 이미지 1개 수정 -->
<update id="updateBoardImage">
UPDATE BOARD_IMG SET
IMG_RENAME = #{imageReName},
IMG_ORIGINAL = #{imageOriginal}
WHERE BOARD_NO = #{boardNo}
AND IMG_LEVEL = #{imageLevel}
</update>
<!-- 게시글 이미지 1개 삽입 -->
<insert id="insertBoardImage">
INSERT INTO BOARD_IMG VALUES(
SEQ_IMG_NO.NEXTVAL,
#{imageReName},
#{imageOriginal},
#{imageLevel},
#{boardNo})
</insert>
'Spring' 카테고리의 다른 글
0508spring 게시판(5) 글 삭제 (0) | 2023.05.09 |
---|---|
0509spring 게시물 댓글(1) 댓글조회 (댓글 DB테이블 생성, jsp,js포함) (0) | 2023.05.08 |
Mybatis 동적 SQL( if, choose(when,oterwise), trim(where,set) ) (1) | 2023.05.03 |
0428spring 마이페이지(3) 회원탈퇴 (1) | 2023.05.03 |
0428spring 마이페이지(2) 비밀번호변경(암호화) (0) | 2023.05.03 |