✔️project(board(controller)) → BoardController.java
...
// @PathVariable : 주소에 지정된 부분을 변수에 저장
// + request scope 세팅
// 게시글 상세 조회
@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 추가
// map(boardCode, boardNo, memberNo)
map.put("memberNo", loginMember.getMemberNo());
// 좋아요 여부 확인 서비스 호출
int result = service.boardLikeCheck(map);
// 누른 적이 있을 경우
if(result > 0) model.addAttribute("likeCheck", "on");
}
// ------------------------------------------------
path = "board/boardDetail"; // forward 할 jsp 경로
model.addAttribute("board", board);
} else { // 조회 결과가 없을 경우
path = "redirect:/board/" + boardCode; // 게시판 첫 페이지로 리다이렉트
ra.addFlashAttribute("message", "해당 게시글이 존재하지 않습니다.");
}
return path;
}
}
✔️project(board(model(service))) → BoardServiceImpl.java
...
// 게시글 상세 조회
@Override
public Board selectBoard(Map<String, Object> map) {
return dao.selectBoard(map);
}
// 좋아요 여부 확인 서비스
@Override
public int boardLikeCheck(Map<String, Object> map) {
return dao.boardLikeCheck(map);
}
✔️project(board(model(service))) → BoardService.java
...
/** 게시글 상세 조회
* @param map
* @return board
*/
Board selectBoard(Map<String, Object> map);
/** 좋아요 여부 확인
* @param map
* @return result
*/
int boardLikeCheck(Map<String, Object> map);
✔️project(board(model(dao))) → BoardDAO.java
...
/** 게시글 상세 조회
* @param map
* @return board
*/
public Board selectBoard(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.selectBoard", map);
}
/** 좋아요 여부 확인
* @param map
* @return result
*/
public int boardLikeCheck(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.boardLikeCheck", map);
}
✔️resources(mappers) → board-mapper.xml
<?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)에 담아
지정된 필드에 세팅
-->
<collection property="imageList"
select="selectImageList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="BoardImage">
</collection>
<collection property="commentList"
select="selectCommentList"
column="BOARD_NO"
javaType="java.util.ArrayList"
ofType="Comment">
</collection>
</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>
...
<!-- 게시글 상세 조회 -->
<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>
insert, delete 따로 resultType을 써주지 않음
→ 따로 안 써줘도 int 라는 것을 알기에
✔️webapp(WEB-INF(views(board))) → boardDetail.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<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">
</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">
<!-- 좋아요를 누른 적이 없거나, 로그인 X -->
<c:if test="${empty likeCheck}">
<i class="fa-regular fa-heart" id="boardLike"></i>
</c:if>
<!-- 좋아요를 누른 적이 있을 때 -->
<c:if test="${!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="${!empty board.boardUpdateDate}">
<p> <span>마지막 수정일</span> ${board.boardUpdateDate} </p>
</c:if>
<p> <span>조회수</span> ${board.readCount} </p>
</div>
</div>
<!-- 이미지가 있을 경우 -->
<c:if test="${!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>
<!-- 내용 -->
<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"/>
<%-- 누가(로그인한 회원 번호) 어떤 게시글(현재 게시글 번호) 좋아요를 클릭/취소
로그인한 회원 번호 얻어오기 방법 3가지
1) ajax로 session에 있는 loginMember의 memberNo를 반환
2) HTML 요소에 로그인한 회원의 번호를 숨겨 놓고 JS로 얻어오기
3) jsp 파일 제일 위에 있는 script 태그에 JS + EL 이용해서
전역 변수로 선언해 두기
--%>
<script>
// JSP에서 작성 가능한 언어/라이브러리
// -> html, css, js, java, EL, JSTL
// JSP 해석 우선 순위 : Java/EL/JSTL > HTML, CSS, JS
// 게시글 번호 전역 변수로 선언
const boardNo = "${board.boardNo}";
// 로그인한 회원 번호를 전역 변수로 선언
// -> 작성한 EL 구문이 null일 경우 빈칸으로 출력되어
// 변수에 값이 대입되지 않는 문제가 발생할 수 있음!
// 해결 방법 : EL 구문을 '', "" 문자열로 감싸면 해결
// -> EL 값이 null이어도 ""(빈문자열)로 출력
const loginMemberNo = "${loginMember.memberNo}";
console.log(boardNo);
console.log(loginMemberNo);
const boardCode = "${boardCode}";
</script>
<%-- boardDetail.js 연결 --%>
<script src="/resources/js/board/boardDetail.js"></script>
<%-- comment.js 연결 --%>
<script src="/resources/js/board/comment.js"></script>
</body>
</html>
collection 태그
select로 조회된 결과를 컬렉션(List)에 담아 지정된 필드에 세팅
select 하나만 호출했는데 collection 영향으로 imageList, commentList가 같이 나온다
사이에 넣어주는 역할
collection을 쓰면 select board만 있어도
향상된 for문 var, beign, end
✔️webapp(WEB-INF(views(board))) → comment.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div id="commentArea">
<!-- 댓글 목록 -->
<div class="comment-list-area">
<ul id="commentList">
<c:forEach items="${board.commentList}" var="comment">
<!-- 부모/자식 댓글 -->
<li class="comment-row <c:if test='${comment.parentNo != 0}'>child-comment</c:if>">
<p class="comment-writer">
<!-- 프로필 이미지 -->
<c:if test="${empty comment.profileImage}">
<%-- 없을 경우 기본 이미지 --%>
<img src="/resources/images/user.png">
</c:if>
<c:if test="${!empty comment.profileImage}">
<%-- 있을 경우 기본 이미지 --%>
<img src="${comment.profileImage}">
</c:if>
<!-- 닉네임 -->
<span>${comment.memberNickname}</span>
<!-- 작성일 -->
<span class="comment-date">${comment.commentCreateDate}</span>
</p>
<!-- 댓글 내용 -->
<p class="comment-content">${comment.commentContent}</p>
<!-- 버튼 영역 -->
<div class="comment-btn-area">
<button onclick="showInsertComment(${comment.commentNo}, this)">답글</button>
<c:if test="${loginMember.memberNo == comment.memberNo}">
<!-- 로그인 회원과 댓글 작성자가 같은 경우 -->
<button onclick="showUpdateComment(${comment.commentNo}, this)">수정</button>
<button onclick="deleteComment(${comment.commentNo})">삭제</button>
</c:if>
</div>
</li>
</c:forEach>
</ul>
</div>
<!-- 댓글 작성 부분 -->
<div class="comment-write-area">
<textarea id="commentContent"></textarea>
<button id="addComment">
댓글<br>
등록
</button>
</div>
</div>
c:choose 사용할 때는 안에 html주석 조심해야한다
날짜 형식이 마음에 안 들면 mapper 들어가서 바꾼다
✔️webapp(WEB-INF(views(board))) → boardDetail.jsp
💡 세션 / 세션스토리지
세션은 톰캣 서버가 관리
세션스토리지는 브라우저가 관리
서로 이름만 비슷하지 서로 연결되어 있지 않음
그래서 ajax로 서버로 직접적으로 가져오는 방법이 있음
1) JSP 파일 제일 위에 있는 script 태그에 JS + EL 이용해서 전역 변수로 선언
2) HTML 요소에 로그인한 회원의 번호를 숨겨 놓고 JS로 얻어오기
3) ajax로 session에 있는 loginMember의 memberNo 반환
우리는 이 중에서 1번째 방법을 사용하겠다
제일 간단하니깐
✔️webapp(resources(js((board))) → boardDetail.js
// 좋아요 버튼이 클릭되었을 때
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 객체
// 로그인한 회원 번호, 게시글 번호, check
const data = { "boardNo" : boardNo,
"memberNo" : loginMemberNo,
"check" : check};
// ajax 코드 작성
fetch("/board/like", {
method : "POST",
headers : {"Content-Type" : "application/json"},
body : JSON.stringify(data)
})
.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);
}) // 예외 발생 시 처리하는 부분
})
// toggle() : 클래스가 있으면 제거, 없으면 추가 // 화살표 함수에서는 this를 사용하지 못함그럼 요소 선택을 어떻게?
매개변수 - 이벤트로 넣어준다
contains() 메소드 : 클래스가 있으면 true, 없으면 false
너무 보낼 애들이 많으니깐 → 자바스크립트 객체로 만들어서 한번에 보내주자
k:v 만들어서 보내줌
json : 자바스크립트 문자열 객체 표기법
“” 해서 문자열로 만들어준다
{키:정규표현식}으로 만들어준다
근데 게시글이 1개 이상일수도 있으니깐!
like는 0~9가 아니니깐 잡지 않고 pass 된다