📌좋아요 기능 구현
1) 내가 좋아요 누른 게시글은 목록 페이지에서 들어갔을 때 꽉찬 하트로 표현하기
2) let check변수 생성해서 0,1 담아서 ajxa 데이터로 boardNo, memberNo와 함께 전달해 보았음
📌쿠키를 이용한 조회수 증가 기능 구현 시 고려 사항
1) 비회원 또는 로그인한 회원의 글이 아닌 경우 2) 쿠키 얻어오기- 요청에 담겨 있는 쿠키 얻어오기 3) 기존 쿠키가 없거나 존재는 하나 현재 게시글 번호(오늘 해당 게시글 본적 없음)가 쿠키에 저장되지 않은 경우(어떠한 게시글도 본적이 없음) 4) 조회수 증가 성공시 쿠키가 적용 되는 경로, 수명(당일 23시 59분 59초) 지정
📌 ""로 감싸도 문자열로 반환되지 않는 이유
JS에서 작성 가능한 언어/ 라이브러리
-> 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}"
<%@ 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">
<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>
</section>
<!-- 댓글 include-->
<jsp:include page="comment.jsp"/>
</main>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<%-- 누가( 로그인한 회원 번호) 어떤 게시글(현재 게시글 번호) 좋아요 클릭/취소
로그인한 회원 번호 얻어오기
1) ajax로 session 에 있는 loginMember의 memberNO로 반환
2) HTML요소에 로그인한 회원의 번호를 숨겨 놓고 JS로 얻어오기
3) JSP 파일 제일 위에 있는 script태그에 JS + EL이용해서
전역변수로 선언해두기
--%>
<script>
// JS에서 작성 가능한 언어/ 라이브러리
// -> 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);
</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
}
//로그인 회원 번호 , 게시글 번호, check
//ajax 코드 작성
fetch("/board/like", {
method: "POST",
headers: {"Content-Type" : "application/json"},
body : JSON.stringify(data)
})
.then(response => response.text()) //응답 객체를 필요한 형태로 파싱하여 return
.then(count =>{
console.log("count:"+ count)
if(count == -1){ //inaert, 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);
})//예외 발생 시 처리 할 구문
})
📌 @PathVariable 문제점 / 해결방법
************************************************************ 문제점 : 요청주소와 @PathVariable로 가져다가 쓸 주소의 레벨이 같다면 구분하지 않고 모두 매핑되는 문제가 발생 -> 요청을 했는데 원하는 메소드가 실행 안됨 해결방법 : @PathVariable 지정시 정규식 표현 사용 {키 : 정규식} ************************************************************
@GetMapping("/{boardCode:[0-9]+}") // boardCode는 1자리 이상의 숫자
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;
//********************@PathVariable*************************
// 문제점 : 요청주소와 @PathVariable로 가져다 쓸 주소의 레벨이 같다면
// 구분하지 않고 모두 매핑되는 문제가 발생
// -> 요청을 했는데 원하는 메소드가 실ㄹ행 안됨
// 해결방법 : PathVariable 지정시 정규 표현식 사용
// {키:정규표현식}
//게시글 목록 조회
@GetMapping("/{boardCode:[0-9]+}") // boardCode는 1자리 이상 숫자
public String selectBoardList(@PathVariable("boardCode") int boardCode
, @RequestParam(value="cp", required = false, defaultValue = "1") int cp
, Model model) {
// boardCode 확인
//System.out.println("boardCode: "+ boardCode);
// 게시글을 목록 조회하는 service호출
Map<String, Object> map = service.selectBoardList(boardCode, cp);
//조회 결과를 request scope에 세팅 후 forward
model.addAttribute("map", map);
return "board/boardList";
}
//@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, 있으면 회원정보 저장
// 쿠키를 이용한 조회 수 증가에서 사용
, 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에 추가
// map(boardCode, boardNo, memberNo)
map.put("memberNo", loginMember.getMemberNo());
//좋아요 여부 확인 서비스 호출
int result = service.boardLikeCheck(map);
// 누른 적이 있는 경우
if(result > 0) model.addAttribute("likeCheck", "on");
}
//---------------------------------------------------
//쿠키를 이용한 조회수 증가 처리
// 1) 비회원 또는 로그인한 회원의 글이 아닌 경우
if(loginMember == null || //비회원
board.getMemberNo() != loginMember.getMemberNo()
// 로그인한 회원의 글이 아닌 경우
) {
//2) 쿠키 얻어오기
Cookie c = null;
//3) 요청에 담겨 있는 모든 쿠키 얻어오기
Cookie[] cookies = req.getCookies();
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.indextOf("문자열")
// : 찾는 문자열이 몇번째 인덱스에 존재하는지 반환
// 단, 없으면 -1반환
if(c.getValue().indexOf("|"+ boardNo + "|") == -1) {
//쿠키에 현재 게시글 번호가 없다면
//기존 값에 게시글 번호를 추가해서 다시 세팅
c.setValue(c.getValue()+"|"+ boardNo + "|");
// 조회수 증가 서비스 호출
result = service.updateReadCount(boardNo);
//System.out.println(result);
}
} //3) 종료
//4) 조회수 증가 성공시
// 쿠키가 적용 되는 경로 , 수명(당일 23 시 59분 59초) 지정
if(result > 0) {
//조회된 board 조회수와 DB 조회수 동기화
board.setReadCount(board.getReadCount()+ 1);
// 적용 경로 설정
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-08-24 12:17:40
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); // 응답 객체를 이용해서
// 클라이언트에게 전달
}
}
// foward 할 jsp경로
path = "board/boardDetail";
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) {
//System.out.println(paramMap);
return service.like(paramMap);
}
}
/** 좋아요 여부 확인
* @param map
* @return result
*/
int boardLikeCheck(Map<String, Object> map);
/** 좋아요 처리 서비스
* @param paramMap
* @return conut
*/
int like(Map<String, Integer> paramMap);
/** 조회수 증가 서비스
* @param boardNo
* @return readCount
*/
int updateReadCount(int boardNo);
// 좋아요 처리 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int like(Map<String, Integer> paramMap) {
int result = 0;
if(paramMap.get("check") == 0) { //좋아요 상태 X
// BOARD_LIKE 테이블 INSERT
result = dao.insertBoardLike(paramMap);
System.out.println(result);
}else { //좋아요 상태 O
// BOARD_LIKE 테이블 DELETE
result = dao.deleteBoardLike(paramMap);
}
// SQL 수행 결과가 0 == DB 또는 파라미터에 문제가 있다
// 1) 에러를 나타내는 임의의 값 반환 (-1)
if( result == 0) return -1;
// 현재 게시글의 좋아요 개수 다시 조회
int count = dao.countLike(paramMap.get("boardNo"));
return count;
}
// 조회수 증가 서비스
@Transactional(rollbackFor = Exception.class)
@Override
public int updateReadCount(int boardNo) {
return dao.updateReadCount(boardNo);
}
/**좋아요 여부 확인
* @param map
* @return result
*/
public int boardLikeCheck(Map<String, Object> map) {
return sqlSession.selectOne("boardMapper.boardLikeCheck", map);
}
/** 좋아요 테이블 삽입
* @param paramMap
* @return result
*/
public int insertBoardLike(Map<String, Integer> paramMap) {
return sqlSession.insert("boardMapper.insertBoardLike", paramMap);
}
/** 좋아요 테이블 삭제
* @param paramMap
* @return result
*/
public int deleteBoardLike(Map<String, Integer> paramMap) {
return sqlSession.delete("boardMapper.deleteBoardLike", paramMap);
}
/** 좋아요 개수 조회
* @param integer
* @return count
*/
public int countLike(Integer boardNo) {
return sqlSession.selectOne("boardMapper.countBoardLike", boardNo);
}
/** 조회수 증가
* @param boardNo
* @return result
*/
public int updateReadCount(int boardNo) {
return sqlSession.update("boardMapper.updateReadCount",boardNo);
}
<!-- 좋아요 여부 확인 -->
<select id="boardLikeCheck" resultType="_int">
SELECT COUNT(*) FROM BOARD_LIKE
WHERE BOARD_NO = #{boardNo}
AND MEMBER_NO = #{memberNo}
</select>
<!-- 좋아요 테이블 삽입 -->
<insert id="insertBoardLike">
INSERT INTO BOARD_LIKE
VALUES (#{boardNo}, #{memberNo})
</insert>
<!-- 좋아요 테이블 삭제 -->
<delete id="deleteBoardLike">
DELETE FROM BOARD_LIKE
WHERE MEMBER_NO = #{memberNo}
AND BOARD_NO = #{boardNo}
</delete>
<!-- 좋아요 개수 조회 -->
<select id="countBoardLike" resultType="_int">
SELECT COUNT(*) FROM BOARD_LIKE
WHERE BOARD_NO = #{boardNo}
</select>
<!-- 조회수 증가 -->
<update id="updateReadCount">
UPDATE BOARD SET
READ_COUNT= READ_COUNT +1
WHERE BOARD_NO = #{boardNo}
</update>