72일차 (1) - 좋아요, 싫어요 기능

Yohan·2024년 6월 4일
0

코딩기록

목록 보기
110/156
post-custom-banner

좋아요, 싫어요

  • 최초 게시물에 진입 시 좋아요, 싫어요 수 : 서버 사이드 렌더링 (SSR)
  • 좋아요, 싫어요 클릭 이벤트 시 : 클라이언트 사이드 렌더링으로 실시간 처리 (CSR)

DB

  • 테이블 생성
  • reaction_id와 reaction_date는 자동으로 값이 들어가게 설정 (xml에 따로 안넣어도됨)
  • board_no, account -> 몇 번 게시물에 좋아요를 눌렀는지!
  • reaction_date같은 시간 데이터는 관례상 적어줌

Entity 클래스

  • DB와 1대1로 매칭되는 클래스
@Getter @ToString
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Reaction {
    private long reactionId;
    private long boardNo;
    private String account;
    private ReactionType reactionType;
    private LocalDateTime reactionDate;
}

ENUM

public enum ReactionType {
    LIKE, DISLIKE
}

Mapper 생성

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);
}

Mapper.xml

<?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

@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 리팩토링

@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);
    }
}

detail.jsp (클릭 이벤트 처리)

<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>

BoardController

    // 좋아요 요청 비동기 처리
    @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);
    }

detail.jsp에 url 추가

    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로 들어옴


ReactionDto

// 클라이언트에 필요한 정보들
@Getter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ReactionDto {
    
    // 좋아요 처리를 위해 클라이언트에 보낼 JSON
    private int likeCount; // 갱신된 총 좋아요 수
    private int dislikeCount; // 갱신된 총 싫어요 수
    private String userReaction; // 현재 리액션 상태 (안눌렀는지, 좋아요인지, 싫어요인지)
}

ReactionService 에서 메서드 수정

  • dto를 return
    // 좋아요 중간처리
    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();
    }

detail.jsp 에서 데이터 받기

const { likeCount, dislikeCount, userReaction } = await res.json(); // 디스트럭쳐링
// active 클래스를 부여하기 위해 필요한 userReaction은 서버에서 받아와야 한다.


document.getElementById('like-count').textContent = likeCount;
        document.getElementById('dislike-count').textContent = dislikeCount;

Service 코드에 리액션 한 후 재조회를 통해 DB데이터 상태를 체크

-> 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);
    }
}

detail.jsp

  • userReaction을 반영하여 버튼 배경색 변경까지 완료
            <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>

최초 진입 시 좋아요, 싫어요 수 서버 사이드 렌더링 (게시물에 좋아요, 싫어요 유지)

BoardController

    // 좋아요 요청 비동기 처리
    @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);
    }

BoardDetailResponseDto

  • BoardController에서 상세 조회를 담당하는 ('/detail') 메서드에서 화면에 렌더링하는 BoardDetailResponseDto에 필드를 추가 -> 화면에 좋아요, 싫어요, 리액션 상태를 렌더링 하기위해서
@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; // 현재 리액션 상태

BoardService

  • 상세조회시 초기렌더링에 그려질 데이터에 DB에 저장되어있는 좋아요, 싷어요, 리액션 상태를 보낸다.
// 상세 조회 요청 중간처리
    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;

    }
profile
백엔드 개발자
post-custom-banner

0개의 댓글