Spring MVC에서 구현 중입니다. (boot 아닙니다..)
예를들어 이런 게시글 폼이 있다고 했을때
하트 이미지를 누르면 오른쪽 추천 수가 증가되고 실제 게시글 테이블의 추천 수가 1 증가와 추천 테이블에 데이터가 삽입되도록 구현해보겠습니다!!
구현방식에는 다양한 방식이 있겠지만 추천기능은 거의 처음이라.. 간단하게 저는 추천을 눌렀을 때 데이터가 삽입되고, 추천을 취소했을 때 데이터가 삭제되도록 하고, 게시물 pk와 세션에 저장된 아이디로 게시물의 추천 여부를 불러오도록 하겠습니다.
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 를 외래키로 사용하고있습니다.
package com.project.dto;
import lombok.Data;
@Data
public class Likes {
private int likesIdx;
private String userinfoId;
private int postIdx;
}
DTO는 크게 다르지 않습니다!
public interface LikesDAO {
int insertPostLikes(Likes likes);
int deletePostLikes(Likes likes);
Likes selectPostLikes(@Param("postIdx") int postIdx, @Param("userinfoId") String userinfoId);
}
@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가지 종류의 쿼리를 수행할 수 있도록 만들어주었습니다.
public interface LikesMapper {
int insertPostLikes(Likes likes);
int deletePostLikes(Likes likes);
Likes selectPostLikes(@Param("postIdx") int postIdx, @Param("userinfoId") String userinfoId);
}
<?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>
public interface LikesService {
void addPostLikes(Likes likes);
void removePostLikes(Likes likes);
Likes getPostLikes(@Param("postIdx") int postIdx, @Param("userinfoId") String userinfoId);
}
@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);
}
}
두 가지 기능의 컨트롤러를 작성해보겠습니다.
✅ 추천 여부 확인
@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("오류가 발생했습니다.");
}
});
📌 이런식으로 추천 데이터가 없다면 빈하트를 출력하고 데이터가 있다면 꽉찬하트를 출력하도로 해줘었습니다.
✅ 추천 클릭 시 데이터 삽입, 삭제
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");
}
}
// 추천 기능
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