SpringBoot
: insert, update, delete
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="edu.kh.project.board.model.dao.BoardMapper2">
<!-- 게시글 삽입 -->
<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>
- 특정 sql 구문을 반복할 때 사용
- 반복되는 사이에 구분자를 추가할 수 있음.
-->
<!-- 이미지 리스트(여러개)삽입 -->
<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>
<!-- 게시글 수정 -->
<update id="boardUpdate">
UPDATE "BOARD" SET
BOARD_TITLE = #{boardTitle},
BOARD_CONTENT = #{boardContent},
B_UPDATE_DATE = SYSDATE
WHERE BOARD_CODE = #{boardCode}
AND BOARD_NO = #{boardNo}
</update>
<!-- 이미지 삭제 -->
<delete id="imageDelete">
DELETE FROM "BOARD_IMG"
WHERE BOARD_NO = #{boardNo}
AND IMG_ORDER IN ( ${deleteList} )
</delete>
<!-- 이미지 수정 -->
<update id="imageUpdate">
UPDATE "BOARD_IMG" SET
IMG_PATH = #{imagePath},
IMG_ORIGINAL = #{imageOriginal},
IMG_RENAME = #{imageReName}
WHERE BOARD_NO = #{boardNo}
AND IMG_ORDER = #{imageOrder}
</update>
<!-- 이미지 삽입 -->
<insert id="imageInsert">
INSERT INTO "BOARD_IMG"
VALUES(SEQ_IMG_NO.NEXTVAL, #{imagePath}, #{imageReName},
#{imageOriginal}, #{imageOrder}, #{boardNo}
)
</insert>
<!-- 게시글 삭제 -->
<update id="boardDelete">
UPDATE BOARD SET
BOARD_DEL_FL = 'Y'
WHERE BOARD_CODE = #{boardCode}
AND BOARD_NO = #{boardNo}
</update>
</mapper>
package edu.kh.project.board.controller;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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 edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.service.BoardService;
import edu.kh.project.board.model.service.BoardService2;
import edu.kh.project.member.model.dto.Member;
import jakarta.servlet.http.HttpSession;
@Controller
@RequestMapping("/board2")
@SessionAttributes({"loginMember"})
public class BoardController2 {
@Autowired
private BoardService2 service; // 삽입,수정,삭제
@Autowired
private BoardService boardService; // 목록, 상세 조회
// 게시글 작성 화면 전환
@GetMapping("/{boardCode:[0-9]+}/insert")
public String boardInsert(@PathVariable("boardCode") int boardCode) {
// @PathVariable : 주소 값 가져오기 + request 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
) throws IllegalStateException, IOException {
// 파라미터 : 제목, 내용, 파일(0~5개)
// 파일 저장 경로 : HttpSession
// 세션 : 로그인한 회원의 번호
// 리다이렉트 시 데이터 전달 : RedirectAttributes ra (message)
/* List<MultipartFile>
* - 업로드된 이미지가 없어도 List에 MultipartFile 요소는 존재함.
*
* - 단, 업로드된 이미지가 없는 MultipartFile 요소는
* 파일 크기(size)가 0 또는 파일명(getOriginalFileName()) "" 빈칸
*
* */
// 1. 로그인한 회원 번호를 얻어와 board에 세팅
board.setMemberNo(loginMember.getMemberNo());
// 2. boardCode도 board에 세팅
board.setBoardCode(boardCode);
// 게시글 삽입 서비스 호출 후 삽입된 게시글 번호 반환 받기
int boardNo = service.boardInsert(board, images);
// 게시글 삽입 성공 시
// -> 방금 삽입한 게시글의 상세 조회 페이지 리다이렉트
// -> /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;
}
// 게시글 수정 화면 전환
@GetMapping("/{boardCode}/{boardNo}/update") // /board2/2/2006/update?cp=1
public String boardUpdate(
@PathVariable("boardCode") int boardCode,
@PathVariable("boardNo") int boardNo,
Model model
// Model : 데이터 전달용 객체 (기본 scope: request)
) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("boardCode", boardCode);
map.put("boardNo", boardNo);
Board board = boardService.selectBoard(map);
model.addAttribute("board", board);
return "board/boardUpdate";
}
// 게시글 수정
@PostMapping("/{boardCode}/{boardNo}/update")
public String boardUpdate(
Board board, // 커맨드 객체(name == 필드 경우 필드에 파라미터 세팅)
@PathVariable("boardCode") int boardCode,
@PathVariable("boardNo") int boardNo,
@RequestParam(value="cp", required = false, defaultValue = "1") int cp, // 쿼리스트링 유지
@RequestParam(value="images", required = false) List<MultipartFile> images, // 업로드된 파일 리스트
@RequestParam(value="deleteList", required = false) String deleteList, // 삭제할 이미지 순서
HttpSession session, // 서버 파일 저장 경로 얻어올 용도
RedirectAttributes ra // 리다이렉트 시 값 전달용(message)
) throws IllegalStateException, IOException {
// 1) boardCode, boardNo를 커맨드 객체(board)에 세팅
board.setBoardCode(boardCode);
board.setBoardNo(boardNo);
// board ( boardCode, boardNo, boardTitle, boardContent )
// 2) 이미지 서버 저장경로, 웹 접근 경로
String webPath = "/resources/images/board/";
String filePath = session.getServletContext().getRealPath(webPath);
// 3) 게시글 수정 서비스 호출
int rowCount = service.boardUpdate(board, images, webPath, filePath, deleteList);
// 4) 결과에 따라 message, path 설정
String message = null;
String path = "redirect:";
if(rowCount > 0) {
message = "게시글이 수정되었습니다";
path += "/board/" + boardCode + "/" + boardNo + "?cp=" + cp;
} else {
message = "게시글 수정 실패";
path += "update";
}
ra.addFlashAttribute("message", message);
return path;
}
// 게시글 삭제 = DELETE 하지 않고 UPDATE 하기!
@GetMapping("/{boardCode}/{boardNo}/delete")
public String boardDelete(
@PathVariable("boardCode") int boardCode,
@PathVariable("boardNo") int boardNo,
RedirectAttributes ra
) {
// boardCode, boardNo 서비스로 넘겨야함
// map 으로 담아서 보내는걸 추천
Map<String, Object> map = new HashMap<String, Object>();
map.put("boardCode", boardCode);
map.put("boardNo", boardNo);
int result = service.boardDelete(map);
// 결과값이 > 0 라면
// "삭제되었습니다"
// /board/{boardCode} = 게시글 목록으로 돌아감
// else
// "삭제 실패"
// /board/{boardCode}/{boardNo}
String message = null;
String path = "redirect:";
if(result > 0) {
System.out.println("삭제되었습니다");
message = "삭제되었습니다";
path += "/board/" + boardCode;
} else {
System.out.println("삭제 실패");
message = "삭제 실패";
path += "/board/" + boardCode + boardNo;
}
ra.addFlashAttribute("message", message);
return path;
}
}
package edu.kh.project.board.model.service;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
import edu.kh.project.board.model.dto.Board;
public interface BoardService2 {
/** 게시글 삽입
* @param board
* @param images
* @param webPath
* @param filePath
* @return boardNo
*/
int boardInsert(Board board, List<MultipartFile> images)
throws IllegalStateException, IOException;
/** 게시글 수정 서비스
* @param board
* @param images
* @param webPath
* @param filePath
* @param deleteList
* @return rowCount
*/
int boardUpdate(Board board, List<MultipartFile> images,
String webPath, String filePath, String deleteList) throws IllegalStateException, IOException;
/** 게시글 삭제 서비스
* @param map
* @return result
*/
int boardDelete(Map<String, Object> map);
}
package edu.kh.project.board.model.service;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import edu.kh.project.board.model.exception.FileUploadException;
import edu.kh.project.board.model.exception.ImageDeleteException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import edu.kh.project.board.model.dao.BoardMapper2;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
import edu.kh.project.common.utility.Util;
@Service
@PropertySource("classpath:/config.properties")
public class BoardServiceImpl2 implements BoardService2{
@Autowired
private BoardMapper2 mapper;
@Value("${my.board.webpath}")
private String webPath;
@Value("${my.board.location}")
private String filePath;
// 게시글 삽입
@Transactional(rollbackFor = Exception.class)
@Override
public int boardInsert(Board board, List<MultipartFile> images)
throws IllegalStateException, IOException {
/* XSS 방지 처리 더이상 하지 않아도 됨.
*
* th:text ------> <h1>안녕하세요</h1> 태그 그대로 나와서 따로 처리하지 않아도 됨
*
* 태그 그대로 해석하고 싶을때? th:utext [(${text})]
*
* */
// 1. BOARD 테이블 INSERT 하기 (제목, 내용, 작성자, 게시판 코드)
// -> boardNo (시퀀스로 생성한 번호) 반환 받기
int result = mapper.boardInsert(board);
// 실패 시 서비스 종료
if(result == 0) return 0;
// mapper.xml에서 selectKey 태그로 인해 세팅된 값 얻어오기
int boardNo = board.getBoardNo();
// 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 호출
result = mapper.insertImageList(uploadList);
// result == 삽입된 행의 개수
// 전체 insert 성공 여부 확인
// 삽입된 행의 개수와 uploadList 의 개수가 같다면
// == 전체 insert 성공
if(result == uploadList.size()) {
// 서버에 파일을 저장 (transferTo)
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 실패
// rollback 필요
// @Transactional(rollbackFor = Exception.class)
// -> 예외가 발생 해야지만 롤백
// [결론]
// 예외를 강제 발생 시켜서 rollback 해야된다
// 사용자 정의 예외 만들어 발생시키자!
throw new FileUploadException();
}
}
}
return boardNo;
}
// 게시글 수정 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int boardUpdate(Board board, List<MultipartFile> images, String webPath, String filePath,
String deleteList) throws IllegalStateException, IOException {
// 1. 게시글 제목/내용만 수정
// 1) XSS 방지 처리
board.setBoardTitle( Util.XSSHandling( board.getBoardTitle() ));
board.setBoardContent( Util.XSSHandling( board.getBoardContent() ));
// 2) DAO 호출
int rowCount = mapper.boardUpdate(board);
// 2. 게시글 부분이 수정 성공 했을 때
if(rowCount > 0) {
if(!deleteList.equals("")) { // 삭제할 이미지가 있다면
// 3. deleteList에 작성된 이미지 모두 삭제
Map<String, Object> deleteMap = new HashMap<String, Object>();
deleteMap.put("boardNo", board.getBoardNo());
deleteMap.put("deleteList", deleteList);
rowCount = mapper.imageDelete(deleteMap);
if(rowCount == 0) { // 이미지 삭제 실패 시 전체 롤백
// -> 예외 강제로 발생
throw new ImageDeleteException();
}
}
// 4. 새로 업로드된 이미지 분류 작업
// images : 실제 파일이 담긴 List
// -> input type="file" 개수만큼 요소가 존재
// -> 제출된 파일이 없어도 MultipartFile 객체가 존재
List<BoardImage> uploadList = new ArrayList<>();
for(int i=0 ; i<images.size(); i++) {
if(images.get(i).getSize() > 0) { // 업로드된 파일이 있을 경우
// BoardImage 객체를 만들어 값 세팅 후
// uploadList에 추가
BoardImage img = new BoardImage();
// img에 파일 정보를 담아서 uploadList에 추가
img.setImagePath(webPath); // 웹 접근 경로
img.setBoardNo(board.getBoardNo()); // 게시글 번호
img.setImageOrder(i); // 이미지 순서
// 파일 원본명
String fileName = images.get(i).getOriginalFilename();
img.setImageOriginal(fileName); // 원본명
img.setImageReName( Util.fileRename(fileName) ); // 변경명
uploadList.add(img);
// 오라클은 '다중 UPDATE를 지원하지 않기 때문에'
// 하나씩 UPDATE 수행
rowCount = mapper.imageUpdate(img);
if(rowCount == 0) {
// 수정 실패 == DB에 이미지가 없었다
// -> 이미지를 삽입
rowCount = mapper.imageInsert(img);
}
}
}
// 5. uploadList에 있는 이미지들만 서버에 저장(transferTo())
if(!uploadList.isEmpty()) {
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) );
}
}
}
return rowCount;
}
// 게시글 삭제 서비스
@Override
public int boardDelete(Map<String, Object> map) {
return mapper.boardDelete(map);
}
}
package edu.kh.project.board.model.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
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;
@Mapper
public interface BoardMapper2 {
/** 게시글 삽입
* @param board
* @return boardNo
*/
public int boardInsert(Board board);
/** 이미지 리스트 삽입
* @param uploadList
* @return result
*/
public int insertImageList(List<BoardImage> uploadList);
/** 게시글 수정
* @param board
* @return rowCount
*/
public int boardUpdate(Board board);
/** 이미지 삭제
* @param deleteMap
* @return rowCount
*/
public int imageDelete(Map<String, Object> deleteMap);
/** 이미지 수정
* @param img
* @return
*/
public int imageUpdate(BoardImage img);
/** 이미지 삽입
* @param img
* @return rowCount
*/
public int imageInsert(BoardImage img);
/** 게시글 삭제
* @param map
* @return
*/
public int boardDelete(Map<String, Object> map);
}
== Console에서 에러나지 않으면 백엔드 코드 잘 고쳐진 것!
<!DOCTYPE html>
<html lang="ko" xmlns="http://thymeleaf.org">
<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 th:text="${boardName}">게시판 이름</title>
<link rel="stylesheet" th:href="@{/css/board/boardDetail-style.css}">
<link rel="stylesheet" th:href="@{/css/board/comment-style.css}">
<script th:src="@{https://kit.fontawesome.com/f7459b8054.js}" crossorigin="anonymous"></script>
</head>
<body>
<main>
<th:block th:replace="~{common/header}"></th:block>
<section class="board-detail" th:object="${board}">
<!-- 제목 -->
<h1 class="board-title" th:utext="|*{boardTitle} <span> - ${boardName}</span>|"></h1>
<!-- 프로필 + 닉네임 + 작성일 + 조회수 -->
<div class="board-header">
<div class="board-writer">
<!-- 프로필 이미지가 없을 경우 기본 이미지 출력 -->
<img th:unless="*{profileImage}" th:src="@{/images/user.png}">
<!-- 프로필 이미지가 있을 경우 등록한 이미지 출력 -->
<img th:if="*{profileImage}" th:src="*{profileImage}" />
<span th:text="*{memberNickname}">닉네임</span>
<!-- 좋아요 하트 -->
<span class="like-area">
<!-- th:classappend : 클래스 추가 -->
<!-- 삼항 연산자 ==== 조건 ? 참 : 거짓 -->
<i class="fa-heart"
th:classappend="${likeCheck} ? fa-solid : fa-regular"
id="boardLike"></i>
<span th:text="*{likeCount}">좋아요 개수</span>
</span>
</div>
<div class="board-info">
<p> <span>작성일</span> [[*{boardCreateDate}]] </p>
<!-- 수정한 게시글인 경우 -->
<p th:if="*{boardUpdateDate}">
<span>마지막 수정일</span> [[*{boardUpdateDate}]]
</p>
<p> <span>조회수</span> [[${board.readCount}]] </p>
</div>
</div>
<!-- 이미지가 있을 경우 -->
<th:block th:if="${#lists.size(board.imageList) > 0}">
<!-- 썸네일 영역(썸네일이 있을 경우) -->
<!--
- 이미지는 IMG_ORDER 오름차순 정렬된다
- IMG_ORDER의 값이 0인 이미지 썸네일이다
-> imageList에 썸네일이 있다면
조회되었을때 IMG_ORDER가 0인 이미지가
imageList[0]에 저장되어 있을 것이다.
-->
<th:block th:if="${thumbnail}"> <!-- 복잡한 로직 웬만해서는 타임리프 사용 X -->
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<img th:src="|${thumbnail.imagePath}${thumbnail.imageReName}|">
<a th:href="|${thumbnail.imagePath}${thumbnail.imageReName}|"
th:download="${thumbnail.imageOriginal}"
>다운로드</a>
</div>
</div>
</th:block>
<!-- 일반 이미지가 있는 경우 -->
<th:block th:if="${#lists.size(board.imageList) > start}">
<!-- 업로드 이미지 영역 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<th:block th:each="i : ${#numbers.sequence(start, #lists.size(board.imageList)-1, 1)}">
<div class="boardImg"
th:with="path=|${board.imageList[i].imagePath}${board.imageList[i].imageReName}|">
<img th:src="${path}">
<a th:href="${path}" th:download="${board.imageList[i].imageOriginal}">다운로드</a>
</div>
</th:block>
</div>
</th:block>
</th:block>
<!-- 내용 -->
<div class="board-content">[[${board.boardContent}]]</div>
<!-- 버튼 영역-->
<div class="board-btn-area">
<!-- 로그인한 회원과 게시글 작성자 번호가 같은 경우 -->
<!-- 객체?.필드명 : 안전 탐색 연산자(Safe Navigation Operator)
객체가 null인지 탐색해서 null 이 아닐 경우 필드 접근
(NullPointerException 방지)
-->
<!-- <th:block th:if="${session.loginMember != null and session.loginMember.memberNo == board.memberNo}" ></th:block> -->
<th:block th:if="${session.loginMember?.memberNo == board.memberNo}">
<button id="updateBtn">수정</button>
<button id="deleteBtn">삭제</button>
</th:block>
<button id="goToListBtn">목록으로</button>
</div>
</section>
<!-- 댓글 include-->
<th:block th:replace="~{board/comment}"></th:block>
</main>
<th:block th:replace="~{common/footer}"></th:block>
<script th:inline="javascript">
console.log(/*[[${board}]]*/ "board객체");
const boardNo = /*[[${board.boardNo}]]*/ "게시글번호";
const loginMemberNo = /*[[${session.loginMember?.memberNo}]]*/ "로그인 회원번호";
if(loginMemberNo == null) loginMemberNo = "";
console.log(boardNo);
console.log(loginMemberNo);
</script>
<script th:src="@{/js/board/boardDetail.js}"></script>
<script th:src="@{/js/board/comment.js}"></script>
</body>
</html>
package edu.kh.project.board.controller;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
import edu.kh.project.board.model.service.BoardService;
import edu.kh.project.member.model.dto.Member;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@SessionAttributes({"loginMember"})
@RequestMapping("/board")
@Controller
public class BoardController {
@Autowired
private BoardService service;
/* 목록 조회 : /board/1?cp=1 (cp: current page(현재페이지))
* 상세 조회 : /board/1/1500?cp=1
*
* ** 컨트롤러 따로 생성 **
* 삽입 : /board2/1/insert
* 수정 : /board2/1/1500/update
* 삭제 : /board2/1/1500/delete
* */
/*
* ******** @PathVariable 사용 시 문제점과 해결법 ********
*
* 문제점 : 요청 주소와 @PathVariable로 가져다 쓸 주소와 레벨이 같다면
* 구분하지 않고 모두 매핑되는 문제가 발생
*
* 해결방법 : @PathVariable 지정 시 정규 표현식 사용
* {키:정규표현식}
*
*
* @PathVariable : URL 경로에 있는 값을 매개변수로 이용할 수 있게하는 어노테이션
* + request scope에 세팅
*
* /board/1 /board?code=1 -> 용도의 차이점이 존재
*
* - 자원(resource) 구분/식별
* ex) github.com/cmhinst
* ex) github.com/testUser
* ex) /board/1 -> 1번(공지사항) 게시판
* ex) /board/2 -> 2번(자유 게시판) 게시판
*
* query string 을 사용하는 경우 -> 부가적인 옵션이라고 생각하기!
* - 검색, 정렬, 필터링
* ex) search.naver.com?query=날씨
* ex) search.naver.com?query=종로맛집
* ex) /board2/insert?code=1
* ex) /board2/insert?code=2
* -> 삽입이라는 공통된 동작 수행
* 단, code에 따라 어디에 삽입할지 지정(필터링)
*
* ex) /board/list?order=recent (최신순)
* ex) /board/list?order=most (인기순)
*
* */
// 게시글 목록 조회
@GetMapping("/{boardCode:[0-9]+}") // boardCode는 1자리 이상 숫자
// @PathVariable : 주소를 값 자체로 쓸 수 있는 것
public String selectBoardList( @PathVariable("boardCode") int boardCode,
@RequestParam(value="cp", required = false, defaultValue = "1") int cp,
Model model, // 데이터 전달용 객체
@RequestParam Map<String, Object> paramMap // 파라미터 전부 다 담겨있음(검색 시) // {"key":"t", "query":"test"}
) {
// boardCode 확인
//System.out.println("boardCode : " + boardCode);
if( paramMap.get("key") == null ) { // 검색어가 없을 때 (검색 X)
// 게시글 목록 조회 서비스 호출
Map<String, Object> map = service.selectBoardList(boardCode, cp);
// 조회 결과를 request scope에 세팅 후 forward
model.addAttribute("map", map);
} else { // 검색어가 있을 때 (검색 O)
paramMap.put("boardCode", boardCode); // paramMap = key, query, boardCode
Map<String, Object> map = service.selectBoardList(paramMap, cp); // 오버로딩 적용
model.addAttribute("map", map);
}
return "board/boardList";
}
// @PathVariable : 주소에 지정된 부분을 변수에 저장
// + request scope 세팅
// 게시글 상세 조회 // /board/1/1500
@GetMapping("/{boardCode}/{boardNo}")
public String boardDetail(
@PathVariable("boardCode") int boardCode,
@PathVariable("boardNo") int boardNo,
Model model, // 데이터 전달용 객체
RedirectAttributes ra, // 리다이렉트 시 데이터 전달 객체
@SessionAttribute(value="loginMember", required = false) Member loginMember,
// 세션에서 loginMember를 얻어오는데 없으면 null, 있으면 회원정보 저장
// 쿠키를 이용한 조회 수 증가에서 사용
HttpServletRequest req,
HttpServletResponse resp
) throws ParseException {
Map<String, Object> map = new HashMap<String, Object>();
map.put("boardCode", boardCode);
map.put("boardNo", boardNo);
// 게시글 상세 조회 서비스 호출
Board board = service.selectBoard(map);
String path = null;
if(board != null) { // 조회 결과가 있을 경우
// ----------------------------------------------------------
// 현재 로그인 상태인 경우
// 로그인한 회원이 해당 게시글에 좋아요를 눌렀는지 확인
// 로그인 상태인 경우
if(loginMember != null) {
// 회원번호를 얻어와야해요
// map(boardCode, boardNo, memberNo)
map.put("memberNo", loginMember.getMemberNo());
// 좋아요 여부 확인 서비스 호출
int result = service.boardLikeCheck(map);
// 결과값을 통해 분기처리
// 누른적이 있을 경우 처리
// "likeCheck"
if(result > 0) model.addAttribute("likeCheck", "on"); // 누른적이 있을 경우 "likeCheck" 키 값에 "on" 값을 넣겠다.
}
// ---------------------------------------------------------
// 쿠키를 이용한 조회 수 증가 처리
// 1) 비회원(로그인 안한 상태인 사람) 또는 로그인한 회원의 글이 아닌경우
if(loginMember == null ||
loginMember.getMemberNo() != board.getMemberNo() ) {
// 2) 쿠키 얻어오기
Cookie c = null;
// 요청에 담겨있는 모든 쿠키 얻어오기
Cookie[] cookies = req.getCookies(); // Cookies ['아이디 저장', '다른 쿠키', ..., 'readBoardNo']
if(cookies != null) { // 쿠키가 존재할 경우
// 쿠키 중 "readBoardNo" 라는 쿠키를 찾아서 c에 대입
for(Cookie cookie : cookies) {
if(cookie.getName().equals("readBoardNo")) {
c = cookie;
break;
}
}
}
// 3) 기존 쿠키가 없거나 ( c == null )
// 존재는 하나 현재 게시글 번호가
// 쿠키에 저장되지 않은 경우 ( 해당 게시글 본적 없음 )
// 결과 저장용 변수 선언
int result = 0;
if(c == null) {
// 쿠키가 존재 X -> 하나 새로 생성
c = new Cookie("readBoardNo", "|" + boardNo + "|");
// 조회수 증가 서비스 호출
result = service.updateReadCount(boardNo);
} else {
// 현재 게시글 번호가 쿠키에 있는지 확인
// Cookie.getValue() : 쿠키에 저장된 모든 값을 읽어옴 -> String으로 반환
// String.indexOf("문자열")
// : 찾는 문자열이 String 몇번 인덱스에 존재하는지 반환
// 단, 없으면 -1 반환
if(c.getValue().indexOf("|" + boardNo + "|") == -1) {
// 현재 게시글 번호가 쿠키에 없다면
// 기존 값에 게시글 번호 추가해서 다시 세팅
c.setValue( c.getValue() + "|" + boardNo + "|" );
// 조회수 증가 서비스 호출
result = service.updateReadCount(boardNo);
}
} // 3) 종료
// 쿠키의 수명 세팅
if(result > 0) {
board.setReadCount(board.getReadCount() + 1);
// 조회된 board 조회 수와 DB 조회 수 동기화
// 적용 경로 설정
c.setPath("/"); // "/" 이하 경로 요청 시 쿠키 서버로 전달
// 수명 지정
Calendar cal = Calendar.getInstance(); // 싱글톤 패턴(미리 만들어진 하나의 객체만을 이용)
cal.add(cal.DATE, 1);
// 날짜 표기법 변경 객체 (DB의 TO_CHAR()와 비슷)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// java.util.Date
Date a = new Date(); // 현재 시간
Date temp = new Date(cal.getTimeInMillis()); // 내일 (24시간 후)
// 2023-05-11 12:16:10
Date b = sdf.parse(sdf.format(temp)); // 내일 0시 0분 0초
// 내일 0시 0분 0초 - 현재 시간
long diff = (b.getTime() - a.getTime()) / 1000;
// -> 내일 0시 0분 0초까지 남은 시간을 초단위로 반환
c.setMaxAge((int)diff); // 수명 설정
resp.addCookie(c); // 응답 객체를 이용해서
// 클라이언트에게 전달
}
}
// ---------------------------------------------------------
path = "board/boardDetail"; // forward 할 jsp 경로
model.addAttribute("board", board);
// 게시글에 이미지가 있을 경우
//if(!board.getImageList().isEmpty())
if(board.getImageList().size() > 0) {
BoardImage thumbnail = null;
// 0번 인덱스 이미지의 순서가 0인 경우 == 썸네일
if(board.getImageList().get(0).getImageOrder() == 0) {
thumbnail = board.getImageList().get(0);
}
model.addAttribute("thumbnail", thumbnail); // 썸네일 없으면 null
// 썸네일 있으면 start = 1
// 없으면 start = 0
model.addAttribute("start", thumbnail != null ? 1 : 0);
}
} else { // 조회 결과가 없을 경우
path = "redirect:/board/" + boardCode;
// 게시판 첫페이지로 리다이렉트
ra.addFlashAttribute("message", "해당 게시글이 존재하지 않습니다");
}
return path;
}
// 좋아요 처리
@PostMapping("/like")
@ResponseBody // 반환되는 값이 비동기 요청한 곳으로 돌아가게 함
public int like(@RequestBody Map<String, Integer> paramMap) {
return service.like(paramMap);
}
}
<div id="commentArea">
<!-- 댓글 목록 -->
<div class="comment-list-area">
<ul id="commentList">
<!-- 부모/자식 댓글 -->
<!-- 삼항 연산자 === 거짓일 때 딱히 해야할게 없다면 생략해도 상관없음! -->
<li class="comment-row"
th:each="comment : ${board.commentList}"
th:classappend="${comment.parentNo} != 0 ? child-comment"
th:object="${comment}"
>
<p class="comment-writer">
<!-- 프로필 이미지 -->
<!-- 없을 경우 기본 이미지 -->
<img th:unless="*{profileImage}" th:src="@{/images/user.png}">
<!-- 있을 경우 프로필 이미지 -->
<img th:if="*{profileImage}" th:src="*{profileImage}">
<!-- 닉네임 -->
<span>[[*{memberNickname}]]</span>
<!-- 작성일 -->
<span class="comment-date" th:text="*{commentCreateDate}">작성일</span>
</p>
<!-- 댓글 내용 -->
<p class="comment-content" th:text="*{commentContent}">내용</p>
<!-- 버튼 영역 -->
<div class="comment-btn-area">
<button th:onclick="|showInsertComment(*{commentNo}, this)|">답글</button>
<!-- 로그인한 회원과 댓글 작성자가 같은 경우 -->
<th:block th:if="${session.loginMember?.memberNo == comment.memberNo}">
<button th:onclick="|showUpdateComment(*{commentNo}, this)|">수정</button>
<button th:onclick="|deleteComment(*{commentNo})|">삭제</button>
</th:block>
</div>
</li>
</ul>
</div>
<!-- 댓글 작성 부분 -->
<div class="comment-write-area">
<textarea id="commentContent"></textarea>
<button id="addComment">
댓글<br>
등록
</button>
</div>
</div>
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org">
<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 th:text="${boardName}">게시판 이름</title>
<link rel="stylesheet" th:href="@{/css/board/boardWrite-style.css}">
</head>
<body>
<main>
<th:block th:replace="~{common/header}"></th:block>
<form th:action="@{insert}" method="POST" class="board-write" id="boardWriteFrm" enctype="multipart/form-data">
<h1 class="board-name" th:text="${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>
<th:block th:replace="~{common/footer}"></th:block>
<script th:src="@{/js/board/boardWrite.js}"></script>
</body>
</html>