게시글 작성 구현 전 알아야 할 것!
- 목록 조회 : /board/1?cp=1 (cp : current page(현재페이지))
- 상세 조회 : /board/1/1500?cp=1
- 컨트롤러 따로 생성
- 삽입 : /board2/1/inssert?code=1(code ==BOARD_CODE , 게시판 종류)
- 수정 : /board2/1/update?code=1&no=1500 (no == BOARD_NO , 게시글 번호)
- 삭제 : /board2/1/delete?code=1&no=1500
package edu.kh.project.board.controller;
import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.fasterxml.jackson.annotation.JacksonInject.Value;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.service.BoardService2;
import edu.kh.project.member.model.dto.Member;
@Controller
@RequestMapping("/board2")
@SessionAttributes({"loginMember"})
public class BoardController2 {
@Autowired
private BoardService2 service;
// 게시글 작성 화면 전환
@GetMapping("/{boardCode:[0-9]+}/insert")
public String boardInsert(@PathVariable("boardCode") int boardCode) {
//@PathVariable :주소 값 가져오기 + requset scope 에 값올리기
return "board/boardWrite";
}
// 게시글 작성
@PostMapping("/{boardCode:[0-9]+}/insert")
public String boardInsert(
@PathVariable("boardCode") int boardCode
, Board board /*커멘드 객체(필드에 파라미터 담겨져 있음)*/
, @RequestParam(value="images", required = false) List<MultipartFile> images
, @SessionAttribute("loginMember") Member loginMember
, RedirectAttributes ra
, HttpSession session) throws IllegalStateException, IOException{
// 파라미터 : 제목, 내용, 파일(0~5개)
// 파일저장 경로 : httpSession
// 세션 : 로그인한 회원 번호
//리다이렉트 시 데이터 전달 : RedirectAuttributrs
// 작성 성공 시 이동할 게시판 코드 : @PathVariable("boardCode")
/* List<MultipartFile>
* -업로드된 이미지가 없어도 List에 요소 MultipartFile 객체가 추가됨
*
* -단, 업로드된 이미지가 없는 MultipartFile 객체는
* 파일크기(size)가 0 또는 파일명(getOriginalFileName())이 ""
* */
// 1. 로그인한 회원 번호를 얻어와 board에 세팅
board.setMemberNo(loginMember.getMemberNo());
// 2. boardCode도 board에 세팅
board.setBoardCode(boardCode);
// 3. 업로드된 이밎 서버에 실제로 저장되는 경로
// + 웹에서 요청 시 이미지를 볼 수 있는 경로
String webPath ="/resources/images/board/";
String filePath = session.getServletContext().getRealPath(webPath);
// 게시글 삽입을 하는 서비스 호출 후 삽입된 게시글의 번호 반환 받기
int boardNo = service.boardInsert(board, images, webPath, filePath);
// 게시글 삽입 셩공 시
// -> 방금 삽입한 게시글의 상세 조회 페이지로 리다이렉트
// -> /board/{boardCode}/{boardNo}
String message = null;
String path ="redirect:";
if(boardNo > 0) { //성공 시
message="게시글이 등록 되었습니다.";
path +="/board/"+boardCode +"/"+boardNo;
}else {
message="게시글 등록 실패ㅠㅠ";
path += "insert";
}
ra.addFlashAttribute("message", message);
return path;
}
}
/** 게시글 삽입
* @param board
* @param images
* @param webPath
* @param filePath
* @return boardNo
*/
int boardInsert(Board board, List<MultipartFile> images, String webPath, String filePath)throws IllegalStateException, IOException;
package edu.kh.project.board.model.service;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import edu.kh.project.board.common.utility.Util;
import edu.kh.project.board.model.dao.BoardDAO2;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
import edu.kh.project.board.model.exception.FileUploadException;
@Service
public class BoardServiceImpl2 implements BoardService2{
@Autowired
private BoardDAO2 dao;
// 게시글 삽입
@Override
@Transactional(rollbackFor = Exception.class)
public int boardInsert(Board board, List<MultipartFile> images, String webPath, String filePath) throws IllegalStateException, IOException {
// 0. XSS 방지 처리
board.setBoardContent(Util.XSSHandling(board.getBoardContent()));
board.setBoardTitle(Util.XSSHandling(board.getBoardTitle()));
// 1. BOARD 테이블에 INSERT하기(제목, 내용, 작성자, 게시판 코드)
// -> boardNo (시퀀스로 생성한 번호) 반환 받기
int boardNo = dao.boardInsert(board);
// 2. 게시글 삽입 성공시
// 업로드 된 이미지가 있다면 BOARD_IMG 테이블에 삽입하는 DAO 호출
if(boardNo > 0) { // 게시글 성공 시
// List<MultipartFile> images
// -> 업로드된 파일이 담긴 객체 MultipartFile이 5개 존재
// -> 단, 업로드된 파일이 없어도 MultipartFile 개체는 존재
// 실제로 업로드된 파일을 기록할 List
List<BoardImage> uploadList = new ArrayList<BoardImage>();
// images에 담겨 잇는 파일 중에서 실제로 업로드된 파일 만 분류
for(int i= 0; i <images.size(); i++) {
//i 번째 요소에 업로드한 파일이 있다면
if(images.get(i).getSize() > 0) {
BoardImage img = new BoardImage();
//img에 파일 정보를 담아서 uploadList에 추가
img.setImagePath(webPath); //웹 접근 경로
img.setBoardNo(boardNo); //게시글 번호
img.setImageOrder(i); //이미지 순서
//파일 원본명
String fileName = images.get(i).getOriginalFilename();
img.setImageOriginal(fileName); //원본명
img.setImageReName(Util.fileRename(fileName)); // 변경명
uploadList.add(img);
}
} // 분류하는 for문 종료
// 분류 작업 후 uploadList가 비어 있지 않은 경우
// == 업로드한 파일이 있다
if(!uploadList.isEmpty()) {
// BOARD_IMG테이블에 INSERT 하는 DAO호출
int result = dao.insertImageList(uploadList);
// result == 삽입된 행의 개수 == uploadList.size()
// 삽입된 행의 개수와 uploadList의 개수가 같다면
// == 전체 insert 성공
if(result == uploadList.size()) {
// 서버에 파일 저장(transferTo())
// images : 실제 파일이 담긴 객체 리스트
// (업로드 안된 인덱스 빈칸)
// uploadList : 업로 된 파일의 정보 리스트
// (원본명, 변경명, 순서, 경로, 게시글 번호)
// 순서 == images 업로드된 인덱스 번호
for(int i =0; i <uploadList.size(); i++) {
int index = uploadList.get(i).getImageOrder();
// 파일로 변환
String rename = uploadList.get(i).getImageReName();
images.get(index).transferTo(new File(filePath+ rename));
}
}else { //일부 또는 전체 insert 실패
// **웹서비스 실행 중 1개라도 실패하면 전체 실패**
// -> rollback필요
// @Transactional(rollbackFor = Exception.class)
// -> 예외가 발생 해야지만 롤백
// [결론]
// 예외 강제 발생 시켜서 rollback 해야한다
// -> 사용자 정의 예외 생성
throw new FileUploadException(); //예외 강제 발생
}
}
}
return boardNo;
}
}
package edu.kh.project.board.model.dao;
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
@Repository
public class BoardDAO2 {
@Autowired
private SqlSessionTemplate sqlsession;
/** 게시글 삽입
* @param board
* @return
*/
public int boardInsert(Board board) {
int result = sqlsession.insert("boardMapper.boardInsert", board);
// ->sql 수행 후 매개변수 board 객체에는 boardNo 존재O
// 삽입 성공시
if(result > 0) result = board.getBoardNo();
return result; // 삽입 성공 시 boardNo, 실패 시 0 반환
}
/** 이미지 리스트(여러개)삽입
* @param uploadList
* @return result
*/
public int insertImageList(List<BoardImage> uploadList) {
return sqlsession.insert("boardMapper.insertImageList", uploadList);
}
}
게시글 삽입(INSERT) 시 미리 boardNo 결과를 Select 구문으로 얻어와서 INSERT구문에 넣어 삽입 결과 1 or 0 얻어오기!
useGeneratedKeys 속성 : DB 내부적으로 생성한 key(시퀀스)를 전달된 파라미터의 필드로 대입 가능 여부를 지정(trur/false)
동적 SQL
프로그램 수행 중 SQL를 변경하는 기능 (마이바티스의 가장 강력한 기능)
selectKey태그
INSERT/UPDATE 시 사용할 키(시퀀스)를 조회해서 파라미터의 지정된 필드에 대입
oder속성
메인 SQL이 수행되기 전/후 selectKey가 수행되도록 지정한다
전: BEFORE / 후: AFTER
foreach
이미지 리스트 (여러개 삽입)<!-- 게시글 삽입 --> <!-- useGeneratedKeys 속성 : DB 내부적으로 생성한 key(시퀀스)를 전달된 파라미터의 필드로 대입 가능 여부를 지정
**동적 SQL**
- 프로그램 수행 중 SQL를 변경하는 기능 (마이바티스의 가장 강력한 기능)
<selectKey> 태그 : INSERT/UPDATE 시 사용할 키(시퀀스)를
조회해서 파라미터의 지정된 필드에 대입
oder속성 : 메인 SQL이 수행되기 전/후 selectKey가 수행되도록 지정한다
전: BEFORE
후: AFTER
keyProperty 속성 : selectKey 조회 결과를 저장할 파라미터의 필드
-->
<insert id="boardInsert" parameterType="Board" useGeneratedKeys="true">
<selectKey order="BEFORE" resultType="_int" keyProperty="boardNo">
SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO BOARD
VALUES( #{boardNo},
#{boardTitle},
#{boardContent},
DEFAULT, DEFAULT, DEFAULT, DEFAULT,
#{memberNo},
#{boardCode} )
</insert>
<!-- 동적 SQL 중 <foreach>
- 특정 SLQ 구문을 반복할 때 사용
- 반복되는 사이에 구분자(separator)를 추가할 수 있음.
collection : 반복할 객체의 타입 작성(list, set, map...)
item : collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
index : 현재 반복 접근중인 인덱스 (0,1,2,3,4 ..)
open : 반복 전에 출력할 sql
close : 반복 종료 후에 출력한 sql
separator : 반복 사이사이 구분자
-->
<!-- 이미지 리스트(여러개 삽입) -->
<insert id="insertImageList" parameterType="list">
INSERT INTO BOARD_IMG
SELECT SEQ_IMG_NO.NEXTVAL, A.*
FROM(
<foreach collection="list" item="img" separator=" UNION ALL ">
SELECT #{img.imagePath} IMG_PATH,
#{img.imageReName} IMG_RENAME,
#{img.imageOriginal} IMG_ORIGINAL,
#{img.imageOrder} IMG_ORDER,
#{img.boardNo} BOARD_NO
FROM DUAL;
</foreach>
) A
</insert>
## VS CODE
### boardList.jsp
```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:set var="pagination" value="${map.pagination}"/>
<c:set var="boardList" value="${map.boardList}"/>
<%-- <c:set var="boardName" value="${boardTypeList[boardCode-1].BOARD_NAME}"/> --%>
<c:forEach items="${boardTypeList}" var="boardType">
<c:if test="${boardType.BOARD_CODE == boardCode}" >
<c:set var="boardName" value="${boardType.BOARD_NAME}"/>
</c:if>
</c:forEach>
<!DOCTYPE html>
<html lang="ko">
<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="/resources/css/board/boardList-style.css">
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<section class="board-list">
<h1 class="board-name">${boardName}</h1>
<div class="list-wrapper">
<table class="list-table">
<thead>
<tr>
<th>글번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
<th>좋아요</th>
</tr>
</thead>
<tbody>
<c:choose>
<c:when test="${empty boardList}">
<%-- 조회된 게시글 목록 비어있거나 null인 경우 --%>
<!-- 게시글 목록 조회 결과가 비어있다면 -->
<tr>
<th colspan="6">게시글이 존재하지 않습니다.</th>
</tr>
</c:when>
<c:otherwise>
<c:forEach items="${boardList}" var="board">
<!-- 게시글 목록 조회 결과가 있다면-->
<tr>
<td>${board.boardNo}</td>
<td>
<%-- 썸네일이 있을 경우 --%>
<c:if test="${!empty board.thumbnail}" >
<img class="list-thumbnail" src="${board.thumbnail}">
</c:if>
<%-- ${boardCode} : @PathVariable로 request scope에 추가된 값 --%>
<a href="/board/${boardCode}/${board.boardNo}?cp=${pagination.currentPage}">${board.boardTitle}</a>
[${board.commentCount}]
</td>
<td>${board.memberNickname}</td>
<td>${board.boardCreateDate}</td>
<td>${board.readCount}</td>
<td>${board.likeCount}</td>
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</tbody>
</table>
</div>
<div class="btn-area">
<!-- 로그인 상태일 경우 글쓰기 버튼 노출 -->
<c:if test="${!empty loginMember}" >
<button id="insertBtn">글쓰기</button>
</c:if>
</div>
<div class="pagination-area">
<ul class="pagination">
<!-- 첫 페이지로 이동 -->
<li><a href="/board/${boardCode}?cp=1"><<</a></li>
<!-- 이전 목록 마지막 번호로 이동 -->
<li><a href="/board/${boardCode}?cp=${pagination.prevPage}"><</a></li>
<!-- 특정 페이지로 이동 -->
<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="/board/${boardCode}?cp=${i}">${i}</a></li>
</c:otherwise>
</c:choose>
</c:forEach>
<!-- 다음 목록 시작 번호로 이동 -->
<li><a href="/board/${boardCode}?cp=${pagination.nextPage}">></a></li>
<!-- 끝 페이지로 이동 -->
<li><a href="/board/${boardCode}?cp=${pagination.maxPage}">>></a></li>
</ul>
</div>
<!-- 검색창 -->
<form action="#" method="get" id="boardSearch">
<select name="key" id="searchKey">
<option value="t">제목</option>
<option value="c">내용</option>
<option value="tc">제목+내용</tion>
<option value="w">작성자</option>
</select>
<input type="text" name="query" id="searchQuery" placeholder="검색어를 입력해주세요.">
<button>검색</button>
</form>
</section>
</main>
<!-- 썸네일 클릭 시 모달창 출력 -->
<div class="modal">
<span id="modalClose">×</span>
<img id="modalImage" src="/resources/images/user.png">
</div>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<script src="/resources/js/board/boardList.js"></script>
</body>
</html>
const insertBtn = document.getElementById("insertBtn");
// 글쓰기 버튼 을 클릭했을 때
if(insertBtn != null){
insertBtn.addEventListener("click", ()=>{
// JS BOM 객체 location
// location.href="주소"
// 해당 주소 요청(GET방식)
// location.href="/board2/"+location.pathname.split("/")[2]
location.href=`/board2/${location.pathname.split("/")[2]}/insert`
//board2/1/insert
})
}
<%@ 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" %>
<c:forEach items="${boardTypeList}" var="boardType">
<c:if test="${boardType.BOARD_CODE == boardCode}" >
<c:set var="boardName" value="${boardType.BOARD_NAME}"/>
</c:if>
</c:forEach>
<!DOCTYPE html>
<html lang="ko">
<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="/resources/css/board/boardWrite-style.css">
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<%-- @PathVariable에서 request scope로 가져옴 --%>
<form action="/board2/${boardCode}/insert" method="POST"
class="board-write" id="boardWriteFrm" enctype="multipart/form-data">
<%--enctype="multipart/form-data" : 제출 이코딩 X
-> 파일제출 가능
-> Multipath Resolver 가 문자열, 파일 구분
--> 문자열, String, int, DTO, Map (HttpMessageConverter)
--> 파일 -> MultiPathFile 객체 -> transferTo() (파일을 서버에 저장) --%>
<h1 class="board-name">${boardName}</h1>
<!-- 제목 -->
<h1 class="board-title">
<input type="text" name="boardTitle" placeholder="제목" value="">
</h1>
<!-- 썸네일 영역 -->
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<label for="img0">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img0" accept="image/*">
<span class="delete-image">×</span>
</div>
</div>
<!-- 업로드 이미지 영역 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<div class="boardImg">
<label for="img1">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img1" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img2">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img2" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img3">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img3" accept="image/*">
<span class="delete-image">×</span>
</div>
<div class="boardImg">
<label for="img4">
<img class="preview" src="">
</label>
<input type="file" name="images" class="inputImage" id="img4" accept="image/*">
<span class="delete-image">×</span>
</div>
</div>
<!-- 내용 -->
<div class="board-content">
<textarea name="boardContent"></textarea>
</div>
<!-- 버튼 영역 -->
<div class="board-btn-area">
<button type="submit" id="writebtn">등록</button>
</div>
</form>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<script src="/resources/js/board/boardWrite.js"></script>
</body>
</html>
//미리보기 관련 요소 모두 얻어오기
//img 5개
const preview = document.getElementsByClassName("preview");
//file 5개
const inputImage = document.getElementsByClassName("inputImage");
// X버튼 5개
const deleteImage = document.getElementsByClassName("delete-image");
// 위에 얻어온 요소들의 개수가 같음 == 인덱스가 일치함
for( let i=0; i < inputImage.length; i++){
// 파일이 선택되거나, 선택 후 취소 되었을 때
inputImage[i].addEventListener("change", e=>{
const file = e.target.files[0]; //선택된 파일의 데이터
if(file != undefined){ //파일이 선택되었을 때
const reader = new FileReader(); //파일을 읽는 객체
reader.readAsDataURL(file);
//지정된 파일을 읽은 후 result변수에 URL형식으로 저장
reader.onload = e =>{ //파일을 다 읽은 후 수행
preview[i].setAttribute("src", e.target.result);
}
}else{ // 파일 선택 후 취소 눌렀을 때
// -> 선택된 파일이 없다 -> 미리보기 없다
preview[i].removeAttribute("src");
}
});
// 미리보기 삭제 버튼(x버튼)
deleteImage[i].addEventListener("click", ()=>{
// 미리보기 이미지가 있을 경우
if(preview[i].getAttribute("src") != ""){
// 미리보기 삭제
preview[i].removeAttribute("src");
// input type="file" 태그의 value를 삭제
// input type="file" 의 value ""(빈칸)만 대입가능
inputImage[i].value="";
}
})
}
// 게시글 등록 시 제목, 내용 작성 여부 검사
const boardWriteFrm = document.getElementById("boardWriteFrm")
const boardTilte = document.querySelector("[name='boardTitle']")
const boardContent = document.querySelector("[name='boardContent']")
boardWriteFrm.addEventListener("submit", e=>{
if( boardTilte.value.trim().length == 0 ){
alert("제목을 입력해주세요")
boardTilte.value="";
boardTilte.focus();
e.preventDefault();
return;
}
if( boardContent.value.trim().length == 0){
alert("내용을 입력해주세요");
boardContent.value="";
boardContent.focus();
e.preventDefault();
return;
}
});