[Spring] 추천(좋아요) 기능 만들기

yunSeok·2023년 11월 30일
0

사이드 프로젝트

목록 보기
3/14

Spring MVC에서 구현 중입니다. (boot 아닙니다..)

예를들어 이런 게시글 폼이 있다고 했을때
하트 이미지를 누르면 오른쪽 추천 수가 증가되고 실제 게시글 테이블의 추천 수가 1 증가와 추천 테이블에 데이터가 삽입되도록 구현해보겠습니다!!

구현방식에는 다양한 방식이 있겠지만 추천기능은 거의 처음이라.. 간단하게 저는 추천을 눌렀을 때 데이터가 삽입되고, 추천을 취소했을 때 데이터가 삭제되도록 하고, 게시물 pk와 세션에 저장된 아이디로 게시물의 추천 여부를 불러오도록 하겠습니다.


테이블 (MySQL ver.)

CREATE TABLE likes (
	likes_idx int AUTO_INCREMENT primary key
    , userinfo_id varchar(50)
    , post_idx int
    
    , FOREIGN KEY (userinfo_id) REFERENCES userinfo(id) ON DELETE CASCADE ON UPDATE CASCADE
    , FOREIGN KEY (post_idx) REFERENCES post(post_idx) ON DELETE CASCADE ON UPDATE CASCADE
);

userinfo_id 컬럼은 user 테이블의 id,
post_idx 컬럼은 post 테이블의 post_idx 를 외래키로 사용하고있습니다.


DTO

package com.project.dto;

import lombok.Data;

@Data
public class Likes {
	private int likesIdx;
	private String userinfoId;
	private int postIdx;

}

DTO는 크게 다르지 않습니다!


DAO

DAO

public interface LikesDAO {
	int insertPostLikes(Likes likes);
	int deletePostLikes(Likes likes);
	
	Likes selectPostLikes(@Param("postIdx") int postIdx, @Param("userinfoId") String userinfoId);

}

DAOImpl

@Repository
@RequiredArgsConstructor
public class LikesDAOImpl implements LikesDAO{
	
	private final SqlSession sqlSession;

	@Override
	public int insertPostLikes(Likes likes) {
		return sqlSession.getMapper(LikesMapper.class).insertPostLikes(likes);
	}

	@Override
	public int deletePostLikes(Likes likes) {
		return sqlSession.getMapper(LikesMapper.class).deletePostLikes(likes);
	}

	@Override
	public Likes selectPostLikes(int postIdx, String userinfoId) {
		return sqlSession.getMapper(LikesMapper.class).selectPostLikes(postIdx, userinfoId);
	}

}

삽입, 삭제, 추천 여부 조회의 3가지 종류의 쿼리를 수행할 수 있도록 만들어주었습니다.


Mapper

Mapper.java

public interface LikesMapper {
	int insertPostLikes(Likes likes);
	int deletePostLikes(Likes likes);
	
	Likes selectPostLikes(@Param("postIdx") int postIdx, @Param("userinfoId") String userinfoId);

}

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.project.mapper.LikesMapper">
	<insert id="insertPostLikes" parameterType="com.project.dto.Likes">
		INSERT INTO likes(
	        likes_idx
            , userinfo_id 
            , post_idx
	        )
	    VALUES(
	        #{likesIdx}
	        , #{userinfoId}
	        , #{postIdx} 
	        )
	</insert>
	
	<delete id="deletePostLikes" parameterType="com.project.dto.Likes">
		DELETE FROM	likes
	    WHERE post_idx=#{postIdx} 
	    	   AND userinfo_id=#{userinfoId}
	</delete>
	
	<select id="selectPostLikes" parameterType="com.project.dto.Likes">
		SELECT  
   			likes_idx
   			, userinfo_id
   			, post_idx
    	FROM likes
    	WHERE 	post_idx=#{postIdx}
    			AND userinfo_id=#{userinfoId}
	</select>
</mapper>

Service

Service

public interface LikesService {
	void addPostLikes(Likes likes);
	void removePostLikes(Likes likes);
	
	Likes getPostLikes(@Param("postIdx") int postIdx, @Param("userinfoId") String userinfoId);

}

ServiceImpl

@Service
@RequiredArgsConstructor
public class LikesServiceImpl implements LikesService{
	
	private final LikesDAO likesDAO;

	@Override
	public void addPostLikes(Likes likes) {
		likesDAO.insertPostLikes(likes);
	}

	@Override
	public void removePostLikes(Likes likes) {
		likesDAO.deletePostLikes(likes);
	}

	@Override
	public Likes getPostLikes(int postIdx, String userinfoId) {
		return likesDAO.selectPostLikes(postIdx, userinfoId);
	}

}

Controller

  1. 게시글을 불러올때 추천 여부를 확인
  2. 추천 클릭 시 데이터 삽입, 삭제

두 가지 기능의 컨트롤러를 작성해보겠습니다.

1. 게시글 Controller

✅ 추천 여부 확인

@GetMapping("/detail/{postIdx}")
	public ResponseEntity<CollectionModel<Map<String, Object>>> getPost(
			@PathVariable("postIdx") int postIdx
			, @RequestParam String userinfoId) {
                      .
                      .
                      
     Post post = postService.getSelectPost(postIdx);          
     Likes likes = likesService.getPostLikes(postIdx, userinfoId); 
     
     Map<String, Object> resultMap = new HashMap<String, Object>();
     resultMap.put("post", post);
     resultMap.put("likes", likes);

Map 객체에 추천이 체크되어 있는지 담아주겠습니다.

게시글 페이지에서는

var userinfoId = "${sessionScope.userinfoId}";

세션에 저장된 로그인 아이디를

 $.ajax({
        method: "GET",
        url: "<c:url value='/post/detail'/>/" + postIdx,
        data: {"postIdx": postIdx
        	   , "userinfoId": userinfoId
        	},
                   .
                   .

ajax data에 넣어서 보내주고있습니다.

$.ajax({
        method: "GET",
        url: "<c:url value='/post/detail'/>/" + postIdx,
        data: {"postIdx": postIdx
        	   , "userinfoId": userinfoId
        	},
        dataType: "json",
        success: function(result) {
        	
        	var post = result.content[0].post;
        	var likes = result.content[0].likes;
        	console.log(likes);

ajax 요청 성공시 console.log를 통해 확인해보면

이런식으로 로그인을 하지 않은 상태이거나 로그인한 사용자가 추천을 누르지 않았다면 null이 반환되어서 아래처럼 해당하는 이미지로 출력되도록 해주었습니다.

추천 전 하트 이미지

<img role="button" src="${pageContext.request.contextPath}/assets/images/heart_before.jpg" id="postLike" class="heart-image">

추천 후 하트 이미지

<img role="button" src="${pageContext.request.contextPath}/assets/images/heart_after.jpg" id="postLike" class="heart-image">
$.ajax({
        method: "GET",
        url: "<c:url value='/post/detail'/>/" + postIdx,
        data: {"postIdx": postIdx
        	   , "userinfoId": userinfoId
        	},
        dataType: "json",
        success: function(result) {
        	
        	var post = result.content[0].post;
        	var likes = result.content[0].likes;
        	
            if(likes === null) {
        		$("#postLike").attr("src", "${pageContext.request.contextPath}/assets/images/heart_before.jpg");
        	} else {
        		$("#postLike").attr("src", "${pageContext.request.contextPath}/assets/images/heart_after.jpg");
        	}
         },
        error: function(error) {
            alert("오류가 발생했습니다.");
        }
	});

📌 이런식으로 추천 데이터가 없다면 빈하트를 출력하고 데이터가 있다면 꽉찬하트를 출력하도로 해줘었습니다.

2. 추천 기능 Controller

✅ 추천 클릭 시 데이터 삽입, 삭제

ajax 요청으로 게시글 번호(postIdx)로그인 아이디(userinfoId)를 보내면 컨트롤러에서 응답받게 됩니다.

// 추천 체크
@PostMapping("/likesCheck")
public ResponseEntity<String> likesCheck(@RequestParam("postIdx") int postIdx
		, @RequestParam("userinfoId") String userinfoId) {
	try {
		Likes likes = new Likes();
		likes.setPostIdx(postIdx);
		likes.setUserinfoId(userinfoId);
        
		likesService.addPostLikes(likes);
		postService.getPostLikesCheck(postIdx);
		return ResponseEntity.ok("ok");
	} catch (Exception e) {
		return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("fail");
	}
	
}

// 추천 취소
@PostMapping("/likesCancel")
public ResponseEntity<String> likesCancel(@RequestParam int postIdx
		, @RequestParam String userinfoId) {
	try {
		Likes likes = new Likes();
		likes.setPostIdx(postIdx);
		likes.setUserinfoId(userinfoId);
        
		likesService.removePostLikes(likes);
		postService.getPostLikesCancel(postIdx);
		return ResponseEntity.ok("ok");
	} catch (Exception e) {
		return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("fail");
	}
	
}

Ajax 요청과 응답

// 추천 기능
function likesCheck() {   
	
	var postLikecnt = document.querySelector('#postLikecnt').textContent;
	
       $.ajax({
           type: "POST",
           url: "<c:url value='/post/likesCheck'/>/",
           data: {"postIdx" : postIdx
        	      , "userinfoId": userinfoId
        	   },
           success: function(response) {
               if (response == "ok") {
            	   $("#postLike").attr("src", "${pageContext.request.contextPath}/assets/images/heart_after.jpg");
            	   $("#postLike").attr("onclick", "likesCancel()");
            	   
            	   postLikecnt = parseInt(postLikecnt) + 1;
            	   $("#postLikecnt").text(postLikecnt);
               }  
           },
           error: function(error) {
           	console.log(error);
   	       }
      });
}

// 추천 취소
function likesCancel() {  
	
	var postLikecnt = document.querySelector('#postLikecnt').textContent;

       $.ajax({
           type: "POST",
           url: "<c:url value='/post/likesCancel'/>/",
           data: {'postIdx' : postIdx
     	      , "userinfoId": userinfoId
    	   },
    	   success: function(response) {
               if (response == "ok") {
            	   $("#postLike").attr("src", "${pageContext.request.contextPath}/assets/images/heart_before.jpg");
            	   $("#postLike").attr("onclick", "likesCheck()");
            	   
            	   postLikecnt = parseInt(postLikecnt) - 1;
            	   $("#postLikecnt").text(postLikecnt);
               } 
           },
           error: function(error) {
           	console.log(error);
           }
      });
}
var postLikecnt = document.querySelector('#postLikecnt').textContent;

응답이 성공된다면 이미지를 바꿔주고
postLikecnt로 현재 추천수를 가져와서 추천수를 1 증가 또는 감소시켜주도록 구현했습니다. (DB에 저장이되는 작업은 아닙니다.)


수정 전 구현 화면

📌 구현은 되었지만 이미지와 숫자가 업데이트 되는게 ajax 응답 후라서 사용자가 느끼기에 반응이 조금 느렸습니다..

그래서 이미지 클릭시 바로 이미지와 숫자를 업데이트 해주고,
만약 오류가 발생한다면 다시 원래대로 해주는 방향으로 수정했습니다.

❗️❗️ 이미지를 클릭할때마다 쿼리가 바로바로 실행되기 때문에 클릭을 단시간에 많이 하게되면 오류가 발생하게 될겁니다... 자바스크립트에서 setTimeout 기능으로 버튼 클릭을 일정시간(0.1초 정도로 설정했습니다.) 동안 비활성화하는 방식으로 적용했습니다.

수정 후

// 추천 기능
function likesCheck() {   
	
	var postLikecnt = document.querySelector('#postLikecnt').textContent;
	postLikecnt = parseInt(postLikecnt) + 1;
	$("#postLikecnt").text(postLikecnt);
	
	$("#postLike").attr("src", "${pageContext.request.contextPath}/assets/images/heart_after.jpg");
	$("#postLike").removeAttr("onclick");
	setTimeout(function() {
        $("#postLike").attr("onclick", "likesCancel()");
    }, 1000);
	
       $.ajax({
           type: "POST",
           url: "<c:url value='/post/likesCheck'/>/",
           data: {"postIdx" : postIdx
        	      , "userinfoId": userinfoId
        	   },
           success: function(response) {
           },
           error: function(error) {
           	console.log(error);
           	postLikecnt = parseInt(postLikecnt) - 1;
     	    $("#postLikecnt").text(postLikecnt);
   	       }
      });
}

// 추천 취소
function likesCancel() {  
	
	var postLikecnt = document.querySelector('#postLikecnt').textContent;
	postLikecnt = parseInt(postLikecnt) - 1;
	$("#postLikecnt").text(postLikecnt);
	
	$("#postLike").attr("src", "${pageContext.request.contextPath}/assets/images/heart_before.jpg");
	$("#postLike").removeAttr("onclick");
	setTimeout(function() {
        $("#postLike").attr("onclick", "likesCheck()");
    }, 1000);

       $.ajax({
           type: "POST",
           url: "<c:url value='/post/likesCancel'/>/",
           data: {'postIdx' : postIdx
     	        , "userinfoId": userinfoId
    	      },
    	   success: function(response) {
           },
           error: function(error) {
           	console.log(error);
           	postLikecnt = parseInt(postLikecnt) + 1;
     	    $("#postLikecnt").text(postLikecnt);
   	       }
      });
}

수정 후 구현 화면


이미지 출처
작가 juicy_fish 출처 Freepik

0개의 댓글