@Getter @ToString
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Reaction {
private long reactionId;
private long boardNo;
private String account;
private ReactionType reactionType;
private LocalDateTime reactionDate;
}
public enum ReactionType {
LIKE, DISLIKE
}
package com.study.springstudy.springmvc.chap05.mapper;
import com.study.springstudy.springmvc.chap05.entity.Reaction;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.jmx.export.annotation.ManagedAttribute;
@Mapper
public interface ReactionMapper {
// 리액션 생성 - 좋아요, 싫어요 처음 찍었을 때
void save(Reaction reaction);
// 리엑션 삭제 - 좋아요, 싫어요 취소했을 때
void delete(@Param("boardNo") long boardNo,
@Param("account") String account);
// 리액션 단일 조회 - 사용자가 특정 게시물에 리액션을 했는지 확인
Reaction findOne(@Param("boardNo") long boardNo,
@Param("account") String account);
// 특정 게시물에 총 좋아요 수 조회
int countLikes(long boardNo);
// 특정 게시물에 총 싫어요 수 조회
int countDislikes(long boardNo);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.study.springstudy.springmvc.chap05.mapper.ReactionMapper">
<insert id="save">
INSERT INTO tbl_reaction
(board_no, account, reaction_type)
VALUES
(#{boardNo}, #{account}, #{reactionType})
</insert>
<delete id="delete">
DELETE FROM tbl_reaction
WHERE board_no = #{boardNo}
AND account = #{account}
</delete>
<select id="findOne" resultType="reaction">
SELECT
*
FROM tbl_reaction
WHERE board_no = #{boardNo}
AND account = #{account}
</select>
<select id="countLikes" resultType="int">
SELECT
COUNT(*)
FROM tbl_reaction
WHERE board_no = #{boardNo}
AND reaction_type = 'LIKE'
</select>
<select id="countDislikes" resultType="int">
SELECT
COUNT(*)
FROM tbl_reaction
WHERE board_no = #{boardNo}
AND reaction_type = 'DISLIKE'
</select>
</mapper>
@Service
@RequiredArgsConstructor
public class ReactionService {
private final ReactionMapper reactionMapper;
// 좋아요 중간처리
public void like(long boardNo, String account) {
// 1. 현재 사용자가 지금 게시물에 리액션을 했었는지 확인
// 1 - 1. 처음 리액션을 한다? -> 좋아요든 싫어요든 INSERT query
// 1 - 2. 근데 기존 리액션을 취소한다? -> 기존 데이터를 DELETE
// 1 - 3. 기존 리액션을 변경한다?
// -> 기존 리액션 데이터를 DELETE 후 새로운 리액션을 INSERT
// 좋아요를 처리해줄 새 라이크 객체 준비
Reaction newReaction = Reaction.builder()
.account(account)
.boardNo(boardNo)
.reactionType(ReactionType.LIKE)
.build();
Reaction existingReaction = reactionMapper.findOne(boardNo, account);
if (existingReaction != null) {
// 처음 리액션이 아닐 경우
if (existingReaction.getReactionType() == ReactionType.LIKE) {
// 동일한 리액션이기 때문에 취소로 봐야 함. (좋아요 두번 누르면 취소)
reactionMapper.delete(boardNo, account);
} else {
// 리액션 변경
reactionMapper.delete(boardNo, account); // 기존 리액션 (dislike) 취소
reactionMapper.save(newReaction); // 새 리액션 (like) 생성
}
} else {
// 처음 리액션을 한 경우
reactionMapper.save(newReaction); // 새 리액션 (like) 생성
}
}
// 싫어요 중간처리 (like와 반대로 간다)
public void dislike(long boardNo, String account) {
// 싫어요를 처리해줄 새 디스라이크 객체 준비
Reaction newReaction = Reaction.builder()
.account(account)
.boardNo(boardNo)
.reactionType(ReactionType.DISLIKE)
.build();
Reaction existingReaction = reactionMapper.findOne(boardNo, account);
if (existingReaction != null) {
// 처음 리액션이 아닐 경우
if (existingReaction.getReactionType() == ReactionType.DISLIKE) {
// 동일한 리액션이기 때문에 취소로 봐야 함. (좋아요 두번 누르면 취소)
reactionMapper.delete(boardNo, account);
} else {
// 리액션 변경
reactionMapper.delete(boardNo, account); // 기존 리액션 (like) 취소
reactionMapper.save(newReaction); // 새 리액션 (dislike) 생성
}
} else {
// 처음 리액션을 한 경우
reactionMapper.save(newReaction); // 새 리액션 (dislike) 생성
}
}
}
@Service
@RequiredArgsConstructor
public class ReactionService {
private final ReactionMapper reactionMapper;
// 공통 리액션 DB처리 메서드
private void handleReaction(long boardNo
, String account
, ReactionType newReactionType) {
// 처음 리액션을 한다? -> 좋아요든 싫어요든 INSERT
// 기존 리액션을 취소한다? -> 기존 데이터를 DELETE
// 기존 리액션을 변경한다?
// -> 기존 리액션 데이터를 DELETE 후 새로운 리액션을 INSERT
// 현재 게시물에 특정 사용자가 리액션을 했는지 확인
Reaction existingReaction = reactionMapper.findOne(boardNo, account);
// 새 라이크 리액션 객체
Reaction newReaction = Reaction.builder()
.account(account)
.boardNo(boardNo)
.reactionType(newReactionType)
.build();
if (existingReaction != null) { // 처음 리액션이 아닌 경우
if (existingReaction.getReactionType() == newReactionType) {
// 동일한 리액션이기 때문에 취소
reactionMapper.delete(boardNo, account);
} else {
// 리액션 변경
reactionMapper.delete(boardNo, account); // 기존 리액션 취소
reactionMapper.save(newReaction); // 새 리액션 생성
}
} else {
// 처음 리액션을 한 경우
reactionMapper.save(newReaction); // 새 리액션 생성
}
}
// 좋아요 중간처리
public void like(long boardNo, String account) {
handleReaction(boardNo, account, ReactionType.LIKE);
}
// 싫어요 중간처리 (like와 반대로 간다)
public void dislike(long boardNo, String account) {
handleReaction(boardNo, account, ReactionType.DISLIKE);
}
}
<script>
// 좋아요 클릭 이벤트
document.getElementById('like-btn').addEventListener('click', e => {
sendReaction('like');
});
// 싫어요 클릭 이벤트
document.getElementById('dislike-btn').addEventListener('click', e => {
sendReaction('dislike');
});
// 서버에 좋아요, 싫어요 요청을 보내는 함수
// reactionType을 파라미터로 받아야 함.
async function sendReaction(reactionType) {
console.log(reactionType)
const res = await fetch(``);
}
</script>
// 좋아요 요청 비동기 처리
@GetMapping("/like")
@ResponseBody
public ResponseEntity<?> like(long bno, HttpSession session) {
log.info("like async request");
String account = LoginUtil.getLoggedInUserAccount(session);
ReactionDto dto = reactionService.like(bno, account);// 좋아요 요청 처리
return ResponseEntity.ok().body(dto);
}
// 싫어요 요청 비동기 처리
@GetMapping("/dislike")
@ResponseBody
public ResponseEntity<?> dislike(long bno, HttpSession session) {
log.info("dislike async request");
String account = LoginUtil.getLoggedInUserAccount(session);
ReactionDto dto = reactionService.dislike(bno, account);// 싫어요 요청 처리
return ResponseEntity.ok().body(dto);
}
async function sendReaction(reactionType) {
console.log(reactionType)
const bno = document.getElementById('wrap').dataset.bno;
// jsp파일에서 \를 쓰면 js코드임을 알려줌 (jsp랑 구분)
const res = await fetch(`/board/\${reactionType}?bno=\${bno}`);
}
-> 여기까지 진행하면 사이트에서 좋아요, 싫어요를 눌렀을 때 정보가 DB로 들어옴
// 클라이언트에 필요한 정보들
@Getter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ReactionDto {
// 좋아요 처리를 위해 클라이언트에 보낼 JSON
private int likeCount; // 갱신된 총 좋아요 수
private int dislikeCount; // 갱신된 총 싫어요 수
private String userReaction; // 현재 리액션 상태 (안눌렀는지, 좋아요인지, 싫어요인지)
}
// 좋아요 중간처리
public ReactionDto like(long boardNo, String account) {
handleReaction(boardNo, account, ReactionType.LIKE);
return ReactionDto.builder()
.likeCount(reactionMapper.countLikes(boardNo))
.dislikeCount(reactionMapper.countDislikes(boardNo))
.build();
}
// 싫어요 중간처리 (like와 반대로 간다)
public ReactionDto dislike(long boardNo, String account) {
handleReaction(boardNo, account, ReactionType.DISLIKE);
return ReactionDto.builder()
.likeCount(reactionMapper.countLikes(boardNo))
.dislikeCount(reactionMapper.countDislikes(boardNo))
.build();
}
const { likeCount, dislikeCount, userReaction } = await res.json(); // 디스트럭쳐링
// active 클래스를 부여하기 위해 필요한 userReaction은 서버에서 받아와야 한다.
document.getElementById('like-count').textContent = likeCount;
document.getElementById('dislike-count').textContent = dislikeCount;
-> userReaction을 jsp로 보내주기 위함
public class ReactionService {
private final ReactionMapper reactionMapper;
// 공통 리액션 DB처리 메서드
private Reaction handleReaction(long boardNo
, String account
, ReactionType newReactionType) {
// 처음 리액션을 한다? -> 좋아요든 싫어요든 INSERT
// 기존 리액션을 취소한다? -> 기존 데이터를 DELETE
// 기존 리액션을 변경한다?
// -> 기존 리액션 데이터를 DELETE 후 새로운 리액션을 INSERT
// 현재 게시물에 특정 사용자가 리액션을 했는지 확인
Reaction existingReaction = reactionMapper.findOne(boardNo, account);
// 새 라이크 리액션 객체
Reaction newReaction = Reaction.builder()
.account(account)
.boardNo(boardNo)
.reactionType(newReactionType)
.build();
if (existingReaction != null) { // 처음 리액션이 아닌 경우
if (existingReaction.getReactionType() == newReactionType) {
// 동일한 리액션이기 때문에 취소
reactionMapper.delete(boardNo, account);
} else {
// 리액션 변경
reactionMapper.delete(boardNo, account); // 기존 리액션 취소
reactionMapper.save(newReaction); // 새 리액션 생성
}
} else {
// 처음 리액션을 한 경우
reactionMapper.save(newReaction); // 새 리액션 생성
}
↓ 추가
// 리액션 한 후 재조회를 통해 DB데이터 상태를 체크
// -> 좋아요를 누른 상태, 싫어요를 누른 상태, 아무것도 안누른 상태
return reactionMapper.findOne(boardNo, account);
}
private ReactionDto getReactionDto(long boardNo, Reaction reaction) {
String reactionType = null;
if (reaction != null) { // 좋아요, 싫어요를 누른 상태
reactionType = reaction.getReactionType().toString();
}
return ReactionDto.builder()
.likeCount(reactionMapper.countLikes(boardNo))
.dislikeCount(reactionMapper.countDislikes(boardNo))
.userReaction(reactionType)
.build();
}
// 좋아요 중간처리
public ReactionDto like(long boardNo, String account) {
Reaction reaction = handleReaction(boardNo, account, ReactionType.LIKE);
return getReactionDto(boardNo, reaction);
}
// 싫어요 중간처리
public ReactionDto dislike(long boardNo, String account) {
Reaction reaction = handleReaction(boardNo, account, ReactionType.DISLIKE);
return getReactionDto(boardNo, reaction);
}
}
<script>
// 서버에 좋아요, 싫어요 요청을 보내는 함수
async function sendReaction(reactionType) {
console.log(reactionType);
const bno = document.getElementById('wrap').dataset.bno;
// jsp파일에서 \를 쓰면 js코드임을 알려줌 (jsp랑 구분)
const res = await fetch(`/board/\${reactionType}?bno=\${bno}`);
const { likeCount, dislikeCount, userReaction } = await res.json();
document.getElementById('like-count').textContent = likeCount;
document.getElementById('dislike-count').textContent = dislikeCount;
// console.log(json);
// 버튼 활성화 스타일 처리
updateReactionButtons(userReaction);
}
// 좋아요, 싫어요 버튼 배경색 변경
function updateReactionButtons(userReaction) {
const $likeBtn = document.getElementById('like-btn');
const $dislikeBtn = document.getElementById('dislike-btn');
const ACTIVE = 'active';
// 좋아요 버튼이 눌렸을 경우
if (userReaction === 'LIKE') {
$likeBtn.classList.add(ACTIVE);
$dislikeBtn.classList.remove(ACTIVE);
} else if (userReaction === 'DISLIKE') { // 싫어요 버튼이 눌렸을 경우
$likeBtn.classList.remove(ACTIVE);
$dislikeBtn.classList.add(ACTIVE);
} else { // 둘다 안눌렀을 경우
$likeBtn.classList.remove(ACTIVE);
$dislikeBtn.classList.remove(ACTIVE);
}
}
// 좋아요 클릭 이벤트
document.getElementById('like-btn').addEventListener('click', e => {
sendReaction('like');
});
// 싫어요 클릭 이벤트
document.getElementById('dislike-btn').addEventListener('click', e => {
sendReaction('dislike');
});
</script>
// 좋아요 요청 비동기 처리
@GetMapping("/like")
@ResponseBody
public ResponseEntity<?> like(long bno, HttpSession session) {
↓ 추가
// 로그인 안했는데 좋아요 눌렀어? 로그인하고와
if (!LoginUtil.isLoggedIn(session)) {
return ResponseEntity.status(403)
.body("로그인이 필요합니다.");
}
log.info("like async request");
String account = LoginUtil.getLoggedInUserAccount(session);
ReactionDto dto = reactionService.like(bno, account);// 좋아요 요청 처리
return ResponseEntity.ok().body(dto);
}
// 싫어요 요청 비동기 처리
@GetMapping("/dislike")
@ResponseBody
public ResponseEntity<?> dislike(long bno, HttpSession session) {
↓ 추가
// 로그인 안했는데 싫어요 눌렀어? 로그인하고와
if (!LoginUtil.isLoggedIn(session)) {
return ResponseEntity.status(403)
.body("로그인이 필요합니다.");
}
log.info("dislike async request");
String account = LoginUtil.getLoggedInUserAccount(session);
ReactionDto dto = reactionService.dislike(bno, account);// 싫어요 요청 처리
return ResponseEntity.ok().body(dto);
}
@Getter
public class BoardDetailResponseDto {
private int boardNo;
private String writer;
private String title;
private String content;
private String regDateTime;
private String name;
private String account;
↓ 추가
@Setter
private int likeCount; // 총 좋아요 수
@Setter
private int dislikeCount; // 총 싫어요 수
@Setter
private String userReaction; // 현재 리액션 상태
// 상세 조회 요청 중간처리
public BoardDetailResponseDto detail(int bno,
HttpServletRequest request,
HttpServletResponse response) {
// 게시물 정보 조회
Board b = boardMapper.findOne(bno);
HttpSession session = request.getSession();
// 비회원이거나 본인 글이면 조회수 증가 방지
// 로그인 계정명
String currentUserAccount = getLoggedInUserAccount(session);
↓ 추가
// 상세조회시 초기렌더링에 그려질 데이터
BoardDetailResponseDto responseDto = new BoardDetailResponseDto(b);
responseDto.setLikeCount(reactionMapper.countLikes(bno));
responseDto.setDislikeCount(reactionMapper.countDislikes(bno));
Reaction reaction = reactionMapper.findOne(bno, currentUserAccount);
String type = null;
if (reaction != null) {
type = reaction.getReactionType().toString();
}
responseDto.setUserReaction(type);
if (!isLoggedIn(session) || isMine(b.getAccount(), currentUserAccount)) {
return responseDto; // 게시물만 반환 (조회수 X)
}
// 조회수가 올라가는 조건처리 (데이터베이스 버전)
// 1. 지금 조회하는 글이 기록에 있는지 확인
int boardNo = b.getBoardNo(); // 게시물 번호
ViewLog viewLog = viewLogMapper.findOne(currentUserAccount, boardNo);
boolean shouldIncrease = false; // 조회수 올려도 되는지??
// 조회 로그 기록 (누가, 어떤 게시글을, 언제 보았는지 기록함)
ViewLog viewLogEntity = ViewLog.builder()
.account(currentUserAccount)
.boardNo(boardNo)
.viewTime(LocalDateTime.now())
.build();
if (viewLog == null) {
// 2. 이 게시물이 이 회원에 의해 처음 조회됨
viewLogMapper.insertViewLog(viewLogEntity);
shouldIncrease = true;
} else {
// 3. 조회기록이 있는 경우 - 1시간 이내 인지
// 혹시 1시간이 지난 게시물인지 확인
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
if (viewLog.getViewTime().isBefore(oneHourAgo)) {
// 4. db에서 view_time 수정
viewLogMapper.updateViewLog(viewLogEntity);
shouldIncrease = true;
}
}
// 처음 조회되거나 조회한지 1시간이 지난 게시물일 경우 조회수 상승하고 게시물 반환
if (shouldIncrease) {
boardMapper.upViewCount(boardNo);
}
return responseDto;
}