07_Framework
( 조회 )
package edu.kh.project.board.controller;
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.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.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.service.BoardService;
import edu.kh.project.member.model.dto.Member;
@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 // 데이터 전달용 객체
) {
// boardCode 확인
//System.out.println("boardCode : " + boardCode);
// 게시글 목록 조회 서비스 호출
Map<String, Object> map = service.selectBoardList(boardCode, cp);
// 조회 결과를 request scope에 세팅 후 forward
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, 있으면 회원정보 저장
) {
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" 값을 넣겠다.
}
path = "board/boardDetail"; // forward 할 jsp 경로
model.addAttribute("board", board);
} else { // 조회 결과가 없을 경우
path = "redirect:/board/" + boardCode;
// 게시판 첫페이지로 리다이렉트
ra.addFlashAttribute("message", "해당 게시글이 존재하지 않습니다");
}
return path;
}
}
package edu.kh.project.board.model.service;
import java.util.List;
import java.util.Map;
import edu.kh.project.board.model.dto.Board;
public interface BoardService {
List<Map<String, Object>> selectBoardTypeList();
/** 게시글 목록 조회
* @param boardCode
* @param cp
* @return map
*/
Map<String, Object> selectBoardList(int boardCode, int cp);
/** 게시글 상세 조회
* @param map
* @return board
*/
Board selectBoard(Map<String, Object> map);
/** 좋아요 여부 확인 서비스
* @param map
* @return result
*/
int boardLikeCheck(Map<String, Object> map);
}
package edu.kh.project.board.model.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import edu.kh.project.board.model.dao.BoardDAO;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.Pagination;
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardDAO dao;
// 게시판 종류 목록 조회
@Override
public List<Map<String, Object>> selectBoardTypeList() {
return dao.selectBoardTypeList();
}
// 게시글 목록 조회
@Override
public Map<String, Object> selectBoardList(int boardCode, int cp) {
// 1. 특정 게시판의 삭제되지 않은 게시글 수 조회
int listCount = dao.getListCount(boardCode);
// 2. 1번 조회 결과 + cp 를 이용해서 Pagination 객체 생성
// -> 내부 필드가 모두 계산되어 초기화됨
Pagination pagination = new Pagination(listCount, cp);
// 3. 특정 게시판에서
// 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
// ex) 100개
// 10개 씩 보여준다
// 1page -> 100 ~ 91
// 2page -> 90 ~ 81
// 어떤 게시판에서(boardCode)
// 몇페이지(pagination.currentPage)에 대한
// 게시글 몇개(pagination.limit)인지 조회
List<Board> boardList = dao.selectBoardList(pagination, boardCode);
// 4. pagination, boardList를 Map에 담아서 반환
Map<String, Object> map = new HashMap<String, Object>();
map.put("pagination", pagination);
map.put("boardList", boardList);
return map;
}
// 게시글 상세 조회
@Override
public Board selectBoard(Map<String, Object> map) {
return dao.selectBoard(map);
}
// 좋아요 여부 확인 서비스
@Override
public int boardLikeCheck(Map<String, Object> map) {
return dao.boardLikeCheck(map);
}
}
package edu.kh.project.board.model.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.RowBounds;
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.Pagination;
@Repository
public class BoardDAO {
@Autowired
private SqlSessionTemplate sqlSession;
/** 게시판 종류 목록 조회
* @return
*/
public List<Map<String, Object>> selectBoardTypeList() {
return sqlSession.selectList("boardMapper.selectBoardTypeList");
}
/** 특정 게시판의 삭제되지 않은 게시글 수 조회
* @param boardCode
* @return listCount
*/
public int getListCount(int boardCode) {
return sqlSession.selectOne("boardMapper.getListCount", boardCode);
}
/** 특정 게시판에서 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
* @param pagination
* @param boardCode
* @return
*/
public List<Board> selectBoardList(Pagination pagination, int boardCode) {
// RowBounds 객체
// - 마이바티스에서 페이징처리를 위해 제공하는 객체
// - offset 만큼 건너뛰고
// 그 다음 지정된 행 개수만큼(limit) 만큼 조회
// 1) offset 계산
int offset
= (pagination.getCurrentPage() - 1) * pagination.getLimit();
// 2) RowBounds 객체 생성
RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
// 3) selectList("namespace.id", 파라미터(boardCode), RowBounds) 호출
return sqlSession.selectList("boardMapper.selectBoardList", boardCode, rowBounds);
}
/** 게시글 상세 조회
* @param map
* @return board
*/
public Board selectBoard(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.selectBoard", map);
}
/** 좋아요 여부 확인 DAO
* @param map
* @return result
*/
public int boardLikeCheck(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.boardLikeCheck", map);
}
}
<?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="boardMapper">
<!-- Board DTO에 대한 resultMap -->
<resultMap type="Board" id="board_rm">
<id property="boardNo" column="BOARD_NO"/>
<result property="boardTitle" column="BOARD_TITLE"/>
<result property="boardContent" column="BOARD_CONTENT"/>
<result property="boardCreateDate" column="B_CREATE_DATE"/>
<result property="boardUpdateDate" column="B_UPDATE_DATE"/>
<result property="readCount" column="READ_COUNT"/>
<result property="commentCount" column="COMMENT_COUNT"/>
<result property="likeCount" column="LIKE_COUNT"/>
<result property="memberNickname" column="MEMBER_NICKNAME"/>
<result property="memberNo" column="MEMBER_NO"/>
<result property="profileImage" column="PROFILE_IMG"/>
<result property="thumbnail" column="THUMBNAIL"/>
<!-- collection 태그
select로 조회된 결과를 컬렉션(List)에 담아
지정된 필드에 세팅
property : List를 담을 DTO의 필드명
select : 실행할 select의 id
column : 조회 결과 중 지정된 컬럼의 값을 파라미터로 전달
javaType : List(컬렉션)의 타입을 지정
ofType : List(컬렉션)의 제네릭(타입 제한) 지정
-->
<collection property="imageList"
select="selectImageList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="BoardImage"
/>
<collection property="commentList"
select="selectCommentList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="Comment"
/>
</resultMap>
<!-- BoardImage resultMap -->
<resultMap type="BoardImage" id="boardImage_rm">
<id property="imageNo" column="IMG_NO"/>
<result property="imagePath" column="IMG_PATH"/>
<result property="imageReName" column="IMG_RENAME"/>
<result property="imageOriginal" column="IMG_ORIGINAL"/>
<result property="imageOrder" column="IMG_ORDER"/>
<result property="boardNo" column="BOARD_NO"/>
</resultMap>
<!-- Comment resultMap -->
<resultMap type="Comment" id="comment_rm">
<id property="commentNo" column="COMMENT_NO"/>
<result property="commentContent" column="COMMENT_CONTENT"/>
<result property="commentCreateDate" column="C_CREATE_DATE"/>
<result property="boardNo" column="BOARD_NO"/>
<result property="memberNo" column="MEMBER_NO"/>
<result property="commentDeleteFlag" column="COMMENT_DEL_FL"/>
<result property="parentNo" column="PARENT_NO"/>
<result property="memberNickname" column="MEMBER_NICKNAME"/>
<result property="profileImage" column="PROFILE_IMG"/>
</resultMap>
<!--
resultType이 "map"인 경우
K : 컬럼명(BOARD_CODE, BOARD_NAME)
V : 컬럼 값( 1 , 공지 사항 )
[{BOARD_NAME= 공지사항, BOARD_CODE=1}, {BOARD_NAME= 자유게시판, BOARD_CODE=2}, ...]
-->
<!-- 게시판 종류 목록 조회 -->
<select id="selectBoardTypeList" resultType="map">
SELECT * FROM "BOARD_TYPE" ORDER BY 1
</select>
<!-- 특정 게시판의 삭제되지 않은 게시글 수 조회 -->
<select id="getListCount" resultType="_int">
SELECT COUNT(*) FROM BOARD
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
</select>
<!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급 -->
<select id="selectBoardList" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT,
<![CDATA[
CASE
WHEN SYSDATE - B_CREATE_DATE < 1/24/60
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || '초 전'
WHEN SYSDATE - B_CREATE_DATE < 1/24
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || '분 전'
WHEN SYSDATE - B_CREATE_DATE < 1
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || '시간 전'
ELSE TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD')
END B_CREATE_DATE,
]]>
(SELECT COUNT(*) FROM "COMMENT" C
WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT,
(SELECT COUNT(*) FROM BOARD_LIKE L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,
(SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
WHERE I.BOARD_NO = B.BOARD_NO
AND IMG_ORDER = 0) THUMBNAIL
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
ORDER BY BOARD_NO DESC
</select>
<select id="selectBoard" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, BOARD_CONTENT, BOARD_CODE,
READ_COUNT, MEMBER_NICKNAME, MEMBER_NO, PROFILE_IMG,
TO_CHAR(B_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_CREATE_DATE,
TO_CHAR(B_UPDATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_UPDATE_DATE,
(SELECT COUNT(*)
FROM "BOARD_LIKE" L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
AND BOARD_NO = #{boardNo}
</select>
<!-- 특정 게시글 이미지 조회 -->
<select id="selectImageList" resultMap="boardImage_rm">
SELECT * FROM BOARD_IMG
WHERE BOARD_NO = #{boardNo}
ORDER BY IMG_ORDER
</select>
<!-- 특정 게시글 댓글 조회(바뀔 예정) -->
<select id="selectCommentList" resultMap="comment_rm">
SELECT COMMENT_NO, COMMENT_CONTENT,
TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = #{boardNo}
ORDER BY COMMENT_NO
</select>
<!-- 좋아요 여부 확인 -->
<select id="boardLikeCheck" resultType="_int">
SELECT COUNT(*) FROM BOARD_LIKE
WHERE BOARD_NO = #{boardNo}
AND MEMBER_NO = #{memberNo}
</select>
</mapper>
<%@ 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/boardDetail-style.css">
<link rel="stylesheet" href="/resources/css/board/comment-style.css">
<script src="https://kit.fontawesome.com/f7459b8054.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<section class="board-detail">
<!-- 제목 -->
<h1 class="board-title">${board.boardTitle} <span> - ${boardName}</span> </h1>
<!-- 프로필 + 닉네임 + 작성일 + 조회수 -->
<div class="board-header">
<div class="board-writer">
<!-- 프로필 이미지 -->
<c:choose>
<%-- 프로필 이미지가 없을 경우 기본 이미지 출력 --%>
<c:when test="${empty board.profileImage}">
<img src="/resources/images/user.png">
</c:when>
<%-- 프로필 이미지가 있을 경우 등록한 이미지 출력 --%>
<c:otherwise>
<img src="${board.profileImage}" />
</c:otherwise>
</c:choose>
<span>${board.memberNickname}</span>
<!-- 좋아요 하트 -->
<span class="like-area">
<%-- 좋아요 누른적이 없다, 로그인 하지 않았다 --%>
<c:if test="${empty likeCheck}">
<i class="fa-regular fa-heart" id="boardLike"></i>
</c:if>
<%-- 좋아요 누른적이 있다 --%>
<c:if test="${not empty likeCheck}">
<i class="fa-solid fa-heart" id="boardLike"></i>
</c:if>
<span>${board.likeCount}</span>
</span>
</div>
<div class="board-info">
<p> <span>작성일</span>${board.boardCreateDate}</p>
<!-- 수정한 게시글인 경우 -->
<c:if test="${not empty board.boardUpdateDate}">
<p> <span>마지막 수정일</span>${board.boardUpdateDate}</p>
</c:if>
<p> <span>조회수</span>${board.readCount}</p>
</div>
</div>
<!-- 이미지가 있을 경우 -->
<c:if test="${not empty board.imageList}">
<!-- 썸네일 영역(썸네일이 있을 경우) -->
<%--
- 이미지는 IMG_ORDER 오름차순 정렬된다
- IMG_ORDER의 값이 0인 이미지 썸네일이다
-> imageList에 썸네일이 있다면
조회되었을때 IMG_ORDER가 0인 이미지가
imageList[0]에 저장되어 있을 것이다.
--%>
<c:if test="${board.imageList[0].imageOrder == 0}">
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<img src="${board.imageList[0].imagePath}${board.imageList[0].imageReName}">
<a href="${board.imageList[0].imagePath}${board.imageList[0].imageReName}"
download="${board.imageList[0].imageOriginal}"
>다운로드</a>
</div>
</div>
</c:if>
</c:if>
<%-- 썸네일이 있을 경우 --%>
<c:if test="${board.imageList[0].imageOrder == 0}">
<c:set var="start" value="1" />
</c:if>
<%-- 썸네일이 없을 경우 --%>
<c:if test="${board.imageList[0].imageOrder != 0}">
<c:set var="start" value="0" />
</c:if>
<%-- fn:length(board.imageList) : imageList의 길이 반환 --%>
<!-- 일반 이미지가 있는 경우 -->
<c:if test="${fn:length(board.imageList) > start}">
<!-- 업로드 이미지 영역 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<c:forEach var="i" begin="${start}" end="${fn:length(board.imageList) - 1}">
<div class="boardImg">
<c:set var="path"
value="${board.imageList[i].imagePath}${board.imageList[i].imageReName}" />
<img src="${path}">
<a href="${path}" download="${board.imageList[i].imageOriginal}">다운로드</a>
</div>
</c:forEach>
</div>
</c:if>
<!-- 내용 (cf) white-space: pre-wrap; 설정이 되어 있어서 div 안 공백이 있어선 안됨. -->
<div class="board-content">${board.boardContent}</div>
<!-- 버튼 영역-->
<div class="board-btn-area">
<!-- 로그인한 회원과 게시글 작성자 번호가 같은 경우 -->
<c:if test="${loginMember.memberNo == board.memberNo}">
<button id="updateBtn">수정</button>
<button id="deleteBtn">삭제</button>
</c:if>
<button id="goToListBtn">목록으로</button>
</div>
</section>
<!-- 댓글 include-->
<jsp:include page="comment.jsp"/>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
</body>
</html>
o3odw98@gmail.com -> 타유저로 로그인하여 user01 게시글 조회 시,
꽉 찬 하트가 들어가 있는 것을 볼 수 있음.
( 쿠키 사용 )
= 쿠키를 이용해서 오늘 내가 본 게시글 번호 저장
-> 당일 23:59:59 초 까지 유지
-> 하루 한 번 조회 가능하도록 (조회수 증가)
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 javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.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.servlet.mvc.support.RedirectAttributes;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.service.BoardService;
import edu.kh.project.member.model.dto.Member;
@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 // 데이터 전달용 객체
) {
// boardCode 확인
//System.out.println("boardCode : " + boardCode);
// 게시글 목록 조회 서비스 호출
Map<String, Object> map = service.selectBoardList(boardCode, cp);
// 조회 결과를 request scope에 세팅 후 forward
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);
} else { // 조회 결과가 없을 경우
path = "redirect:/board/" + boardCode;
// 게시판 첫페이지로 리다이렉트
ra.addFlashAttribute("message", "해당 게시글이 존재하지 않습니다");
}
return path;
}
}
package edu.kh.project.board.model.service;
import java.util.List;
import java.util.Map;
import edu.kh.project.board.model.dto.Board;
public interface BoardService {
List<Map<String, Object>> selectBoardTypeList();
/** 게시글 목록 조회
* @param boardCode
* @param cp
* @return map
*/
Map<String, Object> selectBoardList(int boardCode, int cp);
/** 게시글 상세 조회
* @param map
* @return board
*/
Board selectBoard(Map<String, Object> map);
/** 좋아요 여부 확인 서비스
* @param map
* @return result
*/
int boardLikeCheck(Map<String, Object> map);
/** 조회수 증가 서비스
* @param boardNo
* @return result
*/
int updateReadCount(int boardNo);
}
package edu.kh.project.board.model.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import edu.kh.project.board.model.dao.BoardDAO;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.Pagination;
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardDAO dao;
// 게시판 종류 목록 조회
@Override
public List<Map<String, Object>> selectBoardTypeList() {
return dao.selectBoardTypeList();
}
// 게시글 목록 조회
@Override
public Map<String, Object> selectBoardList(int boardCode, int cp) {
// 1. 특정 게시판의 삭제되지 않은 게시글 수 조회
int listCount = dao.getListCount(boardCode);
// 2. 1번 조회 결과 + cp 를 이용해서 Pagination 객체 생성
// -> 내부 필드가 모두 계산되어 초기화됨
Pagination pagination = new Pagination(listCount, cp);
// 3. 특정 게시판에서
// 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
// ex) 100개
// 10개 씩 보여준다
// 1page -> 100 ~ 91
// 2page -> 90 ~ 81
// 어떤 게시판에서(boardCode)
// 몇페이지(pagination.currentPage)에 대한
// 게시글 몇개(pagination.limit)인지 조회
List<Board> boardList = dao.selectBoardList(pagination, boardCode);
// 4. pagination, boardList를 Map에 담아서 반환
Map<String, Object> map = new HashMap<String, Object>();
map.put("pagination", pagination);
map.put("boardList", boardList);
return map;
}
// 게시글 상세 조회
@Override
public Board selectBoard(Map<String, Object> map) {
return dao.selectBoard(map);
}
// 좋아요 여부 확인 서비스
@Override
public int boardLikeCheck(Map<String, Object> map) {
return dao.boardLikeCheck(map);
}
// 조회수 증가 서비스
@Override
public int updateReadCount(int boardNo) {
return dao.updateReadCount(boardNo);
}
}
package edu.kh.project.board.model.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.RowBounds;
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.Pagination;
@Repository
public class BoardDAO {
@Autowired
private SqlSessionTemplate sqlSession;
/** 게시판 종류 목록 조회
* @return
*/
public List<Map<String, Object>> selectBoardTypeList() {
return sqlSession.selectList("boardMapper.selectBoardTypeList");
}
/** 특정 게시판의 삭제되지 않은 게시글 수 조회
* @param boardCode
* @return listCount
*/
public int getListCount(int boardCode) {
return sqlSession.selectOne("boardMapper.getListCount", boardCode);
}
/** 특정 게시판에서 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
* @param pagination
* @param boardCode
* @return
*/
public List<Board> selectBoardList(Pagination pagination, int boardCode) {
// RowBounds 객체
// - 마이바티스에서 페이징처리를 위해 제공하는 객체
// - offset 만큼 건너뛰고
// 그 다음 지정된 행 개수만큼(limit) 만큼 조회
// 1) offset 계산
int offset
= (pagination.getCurrentPage() - 1) * pagination.getLimit();
// 2) RowBounds 객체 생성
RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
// 3) selectList("namespace.id", 파라미터(boardCode), RowBounds) 호출
return sqlSession.selectList("boardMapper.selectBoardList", boardCode, rowBounds);
}
/** 게시글 상세 조회
* @param map
* @return board
*/
public Board selectBoard(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.selectBoard", map);
}
/** 좋아요 여부 확인 DAO
* @param map
* @return result
*/
public int boardLikeCheck(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.boardLikeCheck", map);
}
/** 조회수 증가 DAO
* @param boardNo
* @return result
*/
public int updateReadCount(int boardNo) {
return sqlSession.update("boardMapper.updateReadCount", boardNo);
}
}
<?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="boardMapper">
<!-- Board DTO에 대한 resultMap -->
<resultMap type="Board" id="board_rm">
<id property="boardNo" column="BOARD_NO"/>
<result property="boardTitle" column="BOARD_TITLE"/>
<result property="boardContent" column="BOARD_CONTENT"/>
<result property="boardCreateDate" column="B_CREATE_DATE"/>
<result property="boardUpdateDate" column="B_UPDATE_DATE"/>
<result property="readCount" column="READ_COUNT"/>
<result property="commentCount" column="COMMENT_COUNT"/>
<result property="likeCount" column="LIKE_COUNT"/>
<result property="memberNickname" column="MEMBER_NICKNAME"/>
<result property="memberNo" column="MEMBER_NO"/>
<result property="profileImage" column="PROFILE_IMG"/>
<result property="thumbnail" column="THUMBNAIL"/>
<!-- collection 태그
select로 조회된 결과를 컬렉션(List)에 담아
지정된 필드에 세팅
property : List를 담을 DTO의 필드명
select : 실행할 select의 id
column : 조회 결과 중 지정된 컬럼의 값을 파라미터로 전달
javaType : List(컬렉션)의 타입을 지정
ofType : List(컬렉션)의 제네릭(타입 제한) 지정
-->
<collection property="imageList"
select="selectImageList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="BoardImage"
/>
<collection property="commentList"
select="selectCommentList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="Comment"
/>
</resultMap>
<!-- BoardImage resultMap -->
<resultMap type="BoardImage" id="boardImage_rm">
<id property="imageNo" column="IMG_NO"/>
<result property="imagePath" column="IMG_PATH"/>
<result property="imageReName" column="IMG_RENAME"/>
<result property="imageOriginal" column="IMG_ORIGINAL"/>
<result property="imageOrder" column="IMG_ORDER"/>
<result property="boardNo" column="BOARD_NO"/>
</resultMap>
<!-- Comment resultMap -->
<resultMap type="Comment" id="comment_rm">
<id property="commentNo" column="COMMENT_NO"/>
<result property="commentContent" column="COMMENT_CONTENT"/>
<result property="commentCreateDate" column="C_CREATE_DATE"/>
<result property="boardNo" column="BOARD_NO"/>
<result property="memberNo" column="MEMBER_NO"/>
<result property="commentDeleteFlag" column="COMMENT_DEL_FL"/>
<result property="parentNo" column="PARENT_NO"/>
<result property="memberNickname" column="MEMBER_NICKNAME"/>
<result property="profileImage" column="PROFILE_IMG"/>
</resultMap>
<!--
resultType이 "map"인 경우
K : 컬럼명(BOARD_CODE, BOARD_NAME)
V : 컬럼 값( 1 , 공지 사항 )
[{BOARD_NAME= 공지사항, BOARD_CODE=1}, {BOARD_NAME= 자유게시판, BOARD_CODE=2}, ...]
-->
<!-- 게시판 종류 목록 조회 -->
<select id="selectBoardTypeList" resultType="map">
SELECT * FROM "BOARD_TYPE" ORDER BY 1
</select>
<!-- 특정 게시판의 삭제되지 않은 게시글 수 조회 -->
<select id="getListCount" resultType="_int">
SELECT COUNT(*) FROM BOARD
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
</select>
<!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급 -->
<select id="selectBoardList" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT,
<![CDATA[
CASE
WHEN SYSDATE - B_CREATE_DATE < 1/24/60
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || '초 전'
WHEN SYSDATE - B_CREATE_DATE < 1/24
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || '분 전'
WHEN SYSDATE - B_CREATE_DATE < 1
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || '시간 전'
ELSE TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD')
END B_CREATE_DATE,
]]>
(SELECT COUNT(*) FROM "COMMENT" C
WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT,
(SELECT COUNT(*) FROM BOARD_LIKE L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,
(SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
WHERE I.BOARD_NO = B.BOARD_NO
AND IMG_ORDER = 0) THUMBNAIL
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
ORDER BY BOARD_NO DESC
</select>
<select id="selectBoard" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, BOARD_CONTENT, BOARD_CODE,
READ_COUNT, MEMBER_NICKNAME, MEMBER_NO, PROFILE_IMG,
TO_CHAR(B_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_CREATE_DATE,
TO_CHAR(B_UPDATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_UPDATE_DATE,
(SELECT COUNT(*)
FROM "BOARD_LIKE" L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
AND BOARD_NO = #{boardNo}
</select>
<!-- 특정 게시글 이미지 조회 -->
<select id="selectImageList" resultMap="boardImage_rm">
SELECT * FROM BOARD_IMG
WHERE BOARD_NO = #{boardNo}
ORDER BY IMG_ORDER
</select>
<!-- 특정 게시글 댓글 조회(바뀔 예정) -->
<select id="selectCommentList" resultMap="comment_rm">
SELECT COMMENT_NO, COMMENT_CONTENT,
TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = #{boardNo}
ORDER BY COMMENT_NO
</select>
<!-- 좋아요 여부 확인 -->
<select id="boardLikeCheck" resultType="_int">
SELECT COUNT(*) FROM BOARD_LIKE
WHERE BOARD_NO = #{boardNo}
AND MEMBER_NO = #{memberNo}
</select>
<!-- 조회 수 증가 -->
<update id="updateReadCount">
UPDATE "BOARD" SET
READ_COUNT = READ_COUNT + 1
WHERE BOARD_NO = #{boardNo}
</update>
</mapper>
내가 본 게시글 조회수 증가
콘솔 창
DB에도 업데이트 되어 있음
하트 눌러져있다면 다시 눌렀을 때 빈하트, 빈하트일 때 누르면 꽉찬하트가 되도록 구현해보기
cf ) 좋아요 눌렀을 때 화면이 바뀌지 않으므로 'Ajax 활용'
좋아요 삽입 : insert
좋아요 해제 : delete
좋아요 한 테이블 : boardLike
<%@ 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/boardDetail-style.css">
<link rel="stylesheet" href="/resources/css/board/comment-style.css">
<script src="https://kit.fontawesome.com/f7459b8054.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<section class="board-detail">
<!-- 제목 -->
<h1 class="board-title">${board.boardTitle} <span> - ${boardName}</span> </h1>
<!-- 프로필 + 닉네임 + 작성일 + 조회수 -->
<div class="board-header">
<div class="board-writer">
<!-- 프로필 이미지 -->
<c:choose>
<%-- 프로필 이미지가 없을 경우 기본 이미지 출력 --%>
<c:when test="${empty board.profileImage}">
<img src="/resources/images/user.png">
</c:when>
<%-- 프로필 이미지가 있을 경우 등록한 이미지 출력 --%>
<c:otherwise>
<img src="${board.profileImage}" />
</c:otherwise>
</c:choose>
<span>${board.memberNickname}</span>
<!-- 좋아요 하트 -->
<span class="like-area">
<%-- 좋아요 누른적이 없다, 로그인 하지 않았다 --%>
<c:if test="${empty likeCheck}">
<i class="fa-regular fa-heart" id="boardLike"></i>
</c:if>
<%-- 좋아요 누른적이 있다 --%>
<c:if test="${not empty likeCheck}">
<i class="fa-solid fa-heart" id="boardLike"></i>
</c:if>
<span>${board.likeCount}</span>
</span>
</div>
<div class="board-info">
<p> <span>작성일</span>${board.boardCreateDate}</p>
<!-- 수정한 게시글인 경우 -->
<c:if test="${not empty board.boardUpdateDate}">
<p> <span>마지막 수정일</span>${board.boardUpdateDate}</p>
</c:if>
<p> <span>조회수</span>${board.readCount}</p>
</div>
</div>
<!-- 이미지가 있을 경우 -->
<c:if test="${not empty board.imageList}">
<!-- 썸네일 영역(썸네일이 있을 경우) -->
<%--
- 이미지는 IMG_ORDER 오름차순 정렬된다
- IMG_ORDER의 값이 0인 이미지 썸네일이다
-> imageList에 썸네일이 있다면
조회되었을때 IMG_ORDER가 0인 이미지가
imageList[0]에 저장되어 있을 것이다.
--%>
<c:if test="${board.imageList[0].imageOrder == 0}">
<h5>썸네일</h5>
<div class="img-box">
<div class="boardImg thumbnail">
<img src="${board.imageList[0].imagePath}${board.imageList[0].imageReName}">
<a href="${board.imageList[0].imagePath}${board.imageList[0].imageReName}"
download="${board.imageList[0].imageOriginal}"
>다운로드</a>
</div>
</div>
</c:if>
</c:if>
<%-- 썸네일이 있을 경우 --%>
<c:if test="${board.imageList[0].imageOrder == 0}">
<c:set var="start" value="1" />
</c:if>
<%-- 썸네일이 없을 경우 --%>
<c:if test="${board.imageList[0].imageOrder != 0}">
<c:set var="start" value="0" />
</c:if>
<%-- fn:length(board.imageList) : imageList의 길이 반환 --%>
<!-- 일반 이미지가 있는 경우 -->
<c:if test="${fn:length(board.imageList) > start}">
<!-- 업로드 이미지 영역 -->
<h5>업로드 이미지</h5>
<div class="img-box">
<c:forEach var="i" begin="${start}" end="${fn:length(board.imageList) - 1}">
<div class="boardImg">
<c:set var="path"
value="${board.imageList[i].imagePath}${board.imageList[i].imageReName}" />
<img src="${path}">
<a href="${path}" download="${board.imageList[i].imageOriginal}">다운로드</a>
</div>
</c:forEach>
</div>
</c:if>
<!-- 내용 (cf) white-space: pre-wrap; 설정이 되어 있어서 div 안 공백이 있어선 안됨. -->
<div class="board-content">${board.boardContent}</div>
<!-- 버튼 영역-->
<div class="board-btn-area">
<!-- 로그인한 회원과 게시글 작성자 번호가 같은 경우 -->
<c:if test="${loginMember.memberNo == board.memberNo}">
<button id="updateBtn">수정</button>
<button id="deleteBtn">삭제</button>
</c:if>
<button id="goToListBtn">목록으로</button>
</div>
</section>
<!-- 댓글 include-->
<jsp:include page="comment.jsp"/>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<script>
// JSP에서 작성 가능한 언어/라이브러리
// -> html, css, js, java, EL, JSTL
// JSP 해석 우선 순위 : Java/EL/JSTL > HTML,CSS,JS
const boardNo = "${board.boardNo}"
const loginMemberNo = "${loginMember.memberNo}"
console.log(boardNo);
console.log(loginMemberNo);
</script>
<script src="/resources/js/board/boardDetail.js"></script>
</body>
</html>
// 좋아요 버튼이 클릭 되었을 때
const boardLike = document.getElementById("boardLike");
boardLike.addEventListener("click", e => {
// 로그인 여부 검사
if(loginMemberNo == ""){
alert("로그인 후 이용해주세요")
return;
}
let check; // 기존에 좋아요 X(빈하트) : 0
// 좋아요 O(꽉찬하트) : 1
// contains("클래스명") : 클래스가 있으면 true, 없으면 false
if(e.target.classList.contains("fa-regular")){ // 좋아요 X(빈하트)
check = 0;
}else{ // 좋아요 O(꽉찬하트)
check = 1;
}
// ajax로 서버로 제출할 파라미터를 모아둔 JS 객체
const data = {"boardNo" : boardNo ,
"memberNo" : loginMemberNo,
"check" : check };
// ajax 코드 작성
fetch("/board/like", {
method : "POST",
headers : {"Content-Type" : "application/json"},
body : JSON.stringify(data)
}) // 자료형 int 1개여서 text()로 파싱
.then(response => response.text()) // 응답 객체를 필요한 형태로 파싱하여 리턴
.then(count => {
console.log("count : " + count);
if(count == -1){ // INSERT, DELETE 실패 시
console.log("좋아요 처리 실패");
return;
}
// toggle() : 클래스가 있으면 없애고, 없으면 추가하고
e.target.classList.toggle("fa-regular");
e.target.classList.toggle("fa-solid");
// 현재 게시글의 좋아요 수를 화면에 출력
e.target.nextElementSibling.innerText = count;
}) // 파싱된 데이터를 받아서 처리하는 코드 작성
.catch(err => {
console.log("예외 발생");
console.log(err);
}) // 예외 발생 시 처리하는 부분
});
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 javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.service.BoardService;
import edu.kh.project.member.model.dto.Member;
@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 // 데이터 전달용 객체
) {
// boardCode 확인
//System.out.println("boardCode : " + boardCode);
// 게시글 목록 조회 서비스 호출
Map<String, Object> map = service.selectBoardList(boardCode, cp);
// 조회 결과를 request scope에 세팅 후 forward
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);
} 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);
}
}
package edu.kh.project.board.model.service;
import java.util.List;
import java.util.Map;
import edu.kh.project.board.model.dto.Board;
public interface BoardService {
List<Map<String, Object>> selectBoardTypeList();
/** 게시글 목록 조회
* @param boardCode
* @param cp
* @return map
*/
Map<String, Object> selectBoardList(int boardCode, int cp);
/** 게시글 상세 조회
* @param map
* @return board
*/
Board selectBoard(Map<String, Object> map);
/** 좋아요 여부 확인 서비스
* @param map
* @return result
*/
int boardLikeCheck(Map<String, Object> map);
/** 조회수 증가 서비스
* @param boardNo
* @return result
*/
int updateReadCount(int boardNo);
int like(Map<String, Integer> paramMap);
}
package edu.kh.project.board.model.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import edu.kh.project.board.model.dao.BoardDAO;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.Pagination;
@Service
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardDAO dao;
// 게시판 종류 목록 조회
@Override
public List<Map<String, Object>> selectBoardTypeList() {
return dao.selectBoardTypeList();
}
// 게시글 목록 조회
@Override
public Map<String, Object> selectBoardList(int boardCode, int cp) {
// 1. 특정 게시판의 삭제되지 않은 게시글 수 조회
int listCount = dao.getListCount(boardCode);
// 2. 1번 조회 결과 + cp 를 이용해서 Pagination 객체 생성
// -> 내부 필드가 모두 계산되어 초기화됨
Pagination pagination = new Pagination(listCount, cp);
// 3. 특정 게시판에서
// 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
// ex) 100개
// 10개 씩 보여준다
// 1page -> 100 ~ 91
// 2page -> 90 ~ 81
// 어떤 게시판에서(boardCode)
// 몇페이지(pagination.currentPage)에 대한
// 게시글 몇개(pagination.limit)인지 조회
List<Board> boardList = dao.selectBoardList(pagination, boardCode);
// 4. pagination, boardList를 Map에 담아서 반환
Map<String, Object> map = new HashMap<String, Object>();
map.put("pagination", pagination);
map.put("boardList", boardList);
return map;
}
// 게시글 상세 조회
@Override
public Board selectBoard(Map<String, Object> map) {
return dao.selectBoard(map);
}
// 좋아요 여부 확인 서비스
@Override
public int boardLikeCheck(Map<String, Object> map) {
return dao.boardLikeCheck(map);
}
// 조회수 증가 서비스
@Override
public int updateReadCount(int boardNo) {
return dao.updateReadCount(boardNo);
}
@Override // paramMap => boardNo, loginMemberNo, check
public int like(Map<String, Integer> paramMap) {
// check == 0 / 1
// check 값이 무엇이냐에 따라서 BOARD_LIKE 테이블 INSERT / DELETE
// 결과 저장용 변수선언
int result = 0;
if(paramMap.get("check") == 0) { // 좋아요 상태 X
// BOARD_LIKE 테이블 INSERT ( dao.insertBoardLike() )
result = dao.insertBoardLike(paramMap);
} else { // 좋아요 상태 O
// BOARD_LIKE 테이블 DELETE ( dao.deleteBoardLike() )
result = dao.deleteBoardLike(paramMap);
}
if(result == 0) return -1;
// 현재 게시글의 좋아요 개수 조회
int count = dao.countBoardLike(paramMap.get("boardNo"));
return count;
}
}
package edu.kh.project.board.model.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.RowBounds;
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.Pagination;
@Repository
public class BoardDAO {
@Autowired
private SqlSessionTemplate sqlSession;
/** 게시판 종류 목록 조회
* @return
*/
public List<Map<String, Object>> selectBoardTypeList() {
return sqlSession.selectList("boardMapper.selectBoardTypeList");
}
/** 특정 게시판의 삭제되지 않은 게시글 수 조회
* @param boardCode
* @return listCount
*/
public int getListCount(int boardCode) {
return sqlSession.selectOne("boardMapper.getListCount", boardCode);
}
/** 특정 게시판에서 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
* @param pagination
* @param boardCode
* @return
*/
public List<Board> selectBoardList(Pagination pagination, int boardCode) {
// RowBounds 객체
// - 마이바티스에서 페이징처리를 위해 제공하는 객체
// - offset 만큼 건너뛰고
// 그 다음 지정된 행 개수만큼(limit) 만큼 조회
// 1) offset 계산
int offset
= (pagination.getCurrentPage() - 1) * pagination.getLimit();
// 2) RowBounds 객체 생성
RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
// 3) selectList("namespace.id", 파라미터(boardCode), RowBounds) 호출
return sqlSession.selectList("boardMapper.selectBoardList", boardCode, rowBounds);
}
/** 게시글 상세 조회
* @param map
* @return board
*/
public Board selectBoard(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.selectBoard", map);
}
/** 좋아요 여부 확인 DAO
* @param map
* @return result
*/
public int boardLikeCheck(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.boardLikeCheck", map);
}
/** 조회수 증가 DAO
* @param boardNo
* @return result
*/
public int updateReadCount(int boardNo) {
return sqlSession.update("boardMapper.updateReadCount", boardNo);
}
/** 좋아요 테이블 삽입
* @param paramMap
* @return
*/
public int insertBoardLike(Map<String, Integer> paramMap) {
return sqlSession.insert("boardMapper.insertBoardLike", paramMap);
}
/** 좋아요 삭제
* @param paramMap
* @return
*/
public int deleteBoardLike(Map<String, Integer> paramMap) {
return sqlSession.delete("boardMapper.deleteBoardLike", paramMap);
}
/** 좋아요 개수 조회
* @param integer
* @return
*/
public int countBoardLike(Integer boardNo) {
return sqlSession.selectOne("boardMapper.countBoardLike", boardNo);
}
}
<?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="boardMapper">
<!-- Board DTO에 대한 resultMap -->
<resultMap type="Board" id="board_rm">
<id property="boardNo" column="BOARD_NO"/>
<result property="boardTitle" column="BOARD_TITLE"/>
<result property="boardContent" column="BOARD_CONTENT"/>
<result property="boardCreateDate" column="B_CREATE_DATE"/>
<result property="boardUpdateDate" column="B_UPDATE_DATE"/>
<result property="readCount" column="READ_COUNT"/>
<result property="commentCount" column="COMMENT_COUNT"/>
<result property="likeCount" column="LIKE_COUNT"/>
<result property="memberNickname" column="MEMBER_NICKNAME"/>
<result property="memberNo" column="MEMBER_NO"/>
<result property="profileImage" column="PROFILE_IMG"/>
<result property="thumbnail" column="THUMBNAIL"/>
<!-- collection 태그
select로 조회된 결과를 컬렉션(List)에 담아
지정된 필드에 세팅
property : List를 담을 DTO의 필드명
select : 실행할 select의 id
column : 조회 결과 중 지정된 컬럼의 값을 파라미터로 전달
javaType : List(컬렉션)의 타입을 지정
ofType : List(컬렉션)의 제네릭(타입 제한) 지정
-->
<collection property="imageList"
select="selectImageList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="BoardImage"
/>
<collection property="commentList"
select="selectCommentList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="Comment"
/>
</resultMap>
<!-- BoardImage resultMap -->
<resultMap type="BoardImage" id="boardImage_rm">
<id property="imageNo" column="IMG_NO"/>
<result property="imagePath" column="IMG_PATH"/>
<result property="imageReName" column="IMG_RENAME"/>
<result property="imageOriginal" column="IMG_ORIGINAL"/>
<result property="imageOrder" column="IMG_ORDER"/>
<result property="boardNo" column="BOARD_NO"/>
</resultMap>
<!-- Comment resultMap -->
<resultMap type="Comment" id="comment_rm">
<id property="commentNo" column="COMMENT_NO"/>
<result property="commentContent" column="COMMENT_CONTENT"/>
<result property="commentCreateDate" column="C_CREATE_DATE"/>
<result property="boardNo" column="BOARD_NO"/>
<result property="memberNo" column="MEMBER_NO"/>
<result property="commentDeleteFlag" column="COMMENT_DEL_FL"/>
<result property="parentNo" column="PARENT_NO"/>
<result property="memberNickname" column="MEMBER_NICKNAME"/>
<result property="profileImage" column="PROFILE_IMG"/>
</resultMap>
<!--
resultType이 "map"인 경우
K : 컬럼명(BOARD_CODE, BOARD_NAME)
V : 컬럼 값( 1 , 공지 사항 )
[{BOARD_NAME= 공지사항, BOARD_CODE=1}, {BOARD_NAME= 자유게시판, BOARD_CODE=2}, ...]
-->
<!-- 게시판 종류 목록 조회 -->
<select id="selectBoardTypeList" resultType="map">
SELECT * FROM "BOARD_TYPE" ORDER BY 1
</select>
<!-- 특정 게시판의 삭제되지 않은 게시글 수 조회 -->
<select id="getListCount" resultType="_int">
SELECT COUNT(*) FROM BOARD
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
</select>
<!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급 -->
<select id="selectBoardList" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT,
<![CDATA[
CASE
WHEN SYSDATE - B_CREATE_DATE < 1/24/60
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || '초 전'
WHEN SYSDATE - B_CREATE_DATE < 1/24
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || '분 전'
WHEN SYSDATE - B_CREATE_DATE < 1
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || '시간 전'
ELSE TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD')
END B_CREATE_DATE,
]]>
(SELECT COUNT(*) FROM "COMMENT" C
WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT,
(SELECT COUNT(*) FROM BOARD_LIKE L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,
(SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
WHERE I.BOARD_NO = B.BOARD_NO
AND IMG_ORDER = 0) THUMBNAIL
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
ORDER BY BOARD_NO DESC
</select>
<select id="selectBoard" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, BOARD_CONTENT, BOARD_CODE,
READ_COUNT, MEMBER_NICKNAME, MEMBER_NO, PROFILE_IMG,
TO_CHAR(B_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_CREATE_DATE,
TO_CHAR(B_UPDATE_DATE, 'YYYY"년" MM"월" DD"일" HH24:MI:SS') B_UPDATE_DATE,
(SELECT COUNT(*)
FROM "BOARD_LIKE" L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
AND BOARD_NO = #{boardNo}
</select>
<!-- 특정 게시글 이미지 조회 -->
<select id="selectImageList" resultMap="boardImage_rm">
SELECT * FROM BOARD_IMG
WHERE BOARD_NO = #{boardNo}
ORDER BY IMG_ORDER
</select>
<!-- 특정 게시글 댓글 조회(바뀔 예정) -->
<select id="selectCommentList" resultMap="comment_rm">
SELECT COMMENT_NO, COMMENT_CONTENT,
TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = #{boardNo}
ORDER BY COMMENT_NO
</select>
<!-- 좋아요 여부 확인 -->
<select id="boardLikeCheck" resultType="_int">
SELECT COUNT(*) FROM BOARD_LIKE
WHERE BOARD_NO = #{boardNo}
AND MEMBER_NO = #{memberNo}
</select>
<!-- 조회 수 증가 -->
<update id="updateReadCount">
UPDATE "BOARD" SET
READ_COUNT = READ_COUNT + 1
WHERE BOARD_NO = #{boardNo}
</update>
<!-- 좋아요 삽입 -->
<insert id="insertBoardLike">
INSERT INTO "BOARD_LIKE" VALUES (#{boardNo}, #{memberNo})
</insert>
<!-- 좋아요 삭제 -->
<delete id="deleteBoardLike">
DELETE FROM "BOARD_LIKE"
WHERE BOARD_NO = #{boardNo}
AND MEMBER_NO = #{memberNo}
</delete>
<!-- 좋아요 조회 -->
<select id="countBoardLike" resultType="_int">
SELECT COUNT(*) FROM "BOARD_LIKE" WHERE BOARD_NO = #{boardNo}
</select>
</mapper>
현재 상태에서 좋아요를 다시 누르면,
빈 하트 되고 카운트 -1
-> 다시 좋아요를 누르면
꽉찬 하트 되고 카운트 +1
@Transactional : 모든 예외에 대해 ROLLBACK 하는 것이 아님!
앞으로 해야 할 것
* 게시글 삽입 / 수정
* 자식 댓글 -> 계층형
* 채팅 -> 웹소켓
* 게시글 검색
* 시험 2개
* 스프링 스케줄러
* 로그
* AOP
* 세미 프로젝트 발표