앞서 소개한 댓글 CRUD 기능 구현에 이어, 오늘은 댓글 좋아요 기능을 구현하려고 합니다.
부족하더라도 좋게 봐주세요 :)
+ 댓글의 좋아요 기능을 위해 테스트용 댓글 작성
1. 클래스 생성
package com.demo.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class ReplyLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int userId;
@ManyToOne
@JoinColumn(name="id")
private Member member;
@ManyToOne
@JoinColumn(name="replySeq")
private Reply reply;
}
- @Entity: JPA가 이 클래스를 엔티티로 인식하도록 지정합니다.
- @Id 및 @GeneratedValue(strategy = GenerationType.IDENTITY): userId 필드는 기본 키로 설정되며, 데이터베이스에서 자동으로 증가하는 방식(IDENTITY)으로 값을 생성합니다.
- @ManyToOne 및 @JoinColumn:
- Member member: 좋아요를 누른 사용자와 관계를 설정합니다. @ManyToOne 어노테이션으로 다대일 관계를 정의하며, @JoinColumn(name="id")으로 연결된 Member 엔티티의 id와 외래 키로 연결합니다.
- Reply reply: 좋아요가 눌린 댓글과 관계를 설정합니다. @ManyToOne 어노테이션으로 다대일 관계를 정의하며, @JoinColumn(name="replySeq")로 연결된 Reply 엔티티의 replySeq와 외래 키로 연결합니다.
2. Repository 작성
package com.demo.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
import com.demo.domain.ReplyLike;
public interface ReplyLikeRepository extends JpaRepository<ReplyLike, Integer> {
boolean existsByMemberIdAndReplyReplySeq(String userId, int replySeq);
void deleteByMemberIdAndReplyReplySeq(String userId, int replySeq);
}
- boolean existsByMemberIdAndReplyReplySeq(String userId, int replySeq) : 특정 사용자가 특정 댓글에 이미 좋아요를 눌렀는지 확인합니다.
- void deleteByMemberIdAndReplyReplySeq(String userId, int replySeq) : 특정 사용자가 특정 댓글에 대해 눌렀던 좋아요를 삭제합니다.
package com.demo.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import com.demo.domain.Reply;
public interface ReplyRepository extends JpaRepository<Reply, Integer> {
// 댓글 좋아요 수 증가
@Modifying
@Transactional
@Query("UPDATE Reply r SET r.likes = r.likes + 1 WHERE r.replySeq = :replySeq")
void incrementLikes(int replySeq);
// 댓글 좋아요 수 감소
@Modifying
@Query("UPDATE Reply r SET r.likes = r.likes - 1 WHERE r.replySeq = :replySeq")
void decrementLikes(int replySeq);
}
- @Modifying은 @Query와 함께 사용되며, 데이터베이스에 수정, 삭제, 삽입 작업을 수행할 때 사용됩니다. 여기서는 댓글의 "좋아요" 수를 증가하거나 감소시키기 위해 UPDATE 쿼리를 사용하므로 이 작업이 데이터 수정 작업임을 알리기 위해 @Modifying이 필요합니다.
- @Transactional은 메서드나 클래스 수준에서 트랜잭션 관리를 활성화하기 위해 사용됩니다. 트랜잭션이란, 작업이 완전히 성공하거나 실패하는 경우 데이터의 일관성을 유지하기 위해 하나의 작업 단위로 묶이는 것을 의미합니다. 여기서는 "좋아요" 수를 증가시키는 작업이 수행 도중 오류 없이 완전히 끝나야만 데이터베이스에 반영되도록 하기 위해 사용됩니다.(행 중 오류가 발생하면 데이터베이스 변경 사항이 자동으로 롤백)
- @Query는 JPQL(Java Persistence Query Language) 쿼리를 직접 작성하여 실행하기 위해 사용합니다. 쿼리는 엔티티에 기반하여 데이터베이스와 상호작용합니다.
3. Service 메서드 작성
@Autowired
private ReplyRepository replyRepo;
@Autowired
private ReplyLikeRepository replyLikeRepo;
// 댓글 좋아요 수 증가
@Override
@Transactional
public boolean toggleLike(String userId, int replySeq) throws Exception {
boolean alreadyLiked = replyLikeRepo.existsByMemberIdAndReplyReplySeq(userId, replySeq);
if (alreadyLiked) {
// 좋아요 취소
replyLikeRepo.deleteByMemberIdAndReplyReplySeq(userId, replySeq);
replyRepo.decrementLikes(replySeq);
return false; // 좋아요가 취소됨
} else {
// 좋아요 추가
Reply reply = replyRepo.findById(replySeq)
.orElseThrow(() -> new Exception("댓글을 찾을 수 없습니다."));
Member member = Member.builder().id(userId).build();
ReplyLike replyLike = ReplyLike.builder()
.member(member)
.reply(reply)
.build();
replyLikeRepo.save(replyLike);
replyRepo.incrementLikes(replySeq);
return true; // 좋아요가 추가됨
}
}
- @Transactional 이 메서드는 트랜잭션으로 묶여 있습니다. 메서드 내의 작업이 모두 성공해야만 최종적으로 데이터베이스에 반영됩니다. 만약 중간에 오류가 발생하면 전체 작업이 롤백됩니다. 따라서 메서드 실행 중 문제가 발생하면 좋아요 수와 관련된 데이터가 일관성을 유지할 수 있습니다.
- boolean alreadyLiked = replyLikeRepo.existsByMemberIdAndReplyReplySeq(userId, replySeq) : 메서드를 호출하여 해당 사용자(userId)가 특정 댓글(replySeq)에 이미 좋아요를 눌렀는지 확인합니다. alreadyLiked가 true면 이미 좋아요가 눌린 상태이고, false면 좋아요가 눌리지 않은 상태입니다.
- if (alreadyLiked) {...} (좋아요가 이미 눌려 있는 경우) : replyLikeRepo.deleteByMemberIdAndReplyReplySeq(userId, replySeq);를 호출하여 좋아요 정보를 삭제합니다.이어서 replyRepo.decrementLikes(replySeq);를 호출해 해당 댓글의 좋아요 수를 1 감소시킵니다. 이 작업이 완료되면 return false;를 반환하여 좋아요가 취소되었음을 알립니다.
- else {...} (좋아요가 눌리지 않은 경우) replyRepo.findById(replySeq)를 사용하여 해당 댓글을 찾아옵니다. 댓글이 없다면 .orElseThrow(() -> new Exception("댓글을 찾을 수 없습니다.")) 예외를 던집니다.
- 다음으로, Member member = Member.builder().id(userId).build(); : Member 객체를 생성해 사용자 ID를 설정하고, ReplyLike replyLike = ReplyLike.builder().member(member).reply(reply).build(); : 댓글과 사용자 정보를 포함하는 ReplyLike 객체를 생성합니다.
- replyLikeRepo.save(replyLike)를 통해 새로운 좋아요 정보를 데이터베이스에 저장하고, replyRepo.incrementLikes(replySeq)를 통해 해당 댓글의 좋아요 수를 1 증가시킵니다. 작업이 완료되면 return true;를 반환하여 좋아요가 추가되었음을 알립니다.
4. Html 버튼 만들기
<button type="button" th:attr="data-reply-seq=${reply.replySeq}"
class="detail-replylike-btn" onclick="replyLike(this)">좋아요
</button>
- th:attr는 Thymeleaf가 HTML의 속성을 렌더링할 때 동적으로 값을 넣도록 합니다. 여기서 data-reply-seq라는 속성에 reply.replySeq(댓글 고유번호)의 값을 동적으로 할당하고 있습니다.
- data-* 속성은 HTML5에서 제공하는 사용자 정의 데이터 속성으로, HTML 요소에 개발자가 직접 데이터를 추가할 수 있습니다. 이 데이터를 JavaScript를 통해 쉽게 접근할 수 있으며, 속성 이름을 자유롭게 지정할 수 있습니다. 여기서(data-reply-seq처럼 data- 뒤에 원하는 이름을 붙여 사용할 수 있습니다.)
- onclick="replyLike(this)" : onclick은 HTML 요소의 클릭 이벤트 발생 시 특정 JavaScript 코드를 실행하도록 하는 속성입니다. replyLike 함수를 호출하며, 클릭된 버튼 요소를 this로 참조하여 해당 요소를 함수에 전달합니다. (여기서 this는 현재 클릭된 요소, 즉 클릭 이벤트가 발생한 <button> 요소를 참조하게 됩니다.)
5. JavaScript 함수 작성
// 댓글 좋아요 처리
function replyLike(buttonElement) {
var replySeq = $(buttonElement).data('reply-seq');
$.ajax({
url: `/replies/${replySeq}/like`,
method: 'POST',
success: function(response) {
// 서버 응답에 따라 메시지 설정
var title = '좋아요 성공';
var text = '좋아요가 성공적으로 추가되었습니다.';
if (response.includes("취소되었습니다")) {
title = '좋아요 취소';
text = '좋아요가 취소되었습니다.';
}
swal.fire({
title: title,
text: text,
icon: 'success',
confirmButtonText: '확인'
}).then(() => {
location.reload(); // 페이지 새로 고침
});
},
error: function(xhr, status, error) {
swal.fire({
title: '에러',
text: '좋아요 처리 중 오류가 발생했습니다.',
icon: 'error',
confirmButtonText: '확인'
});
}
});
}
- var replySeq = $(buttonElement).data('reply-seq') : buttonElement는 "좋아요" 버튼 요소를 나타내고, data-reply-seq 속성을 통해 이 버튼이 참조하는 댓글의 고유 번호 (replySeq)를 가져옵니다.
- $.ajax({ : jQuery의 비동기 요청 함수. 여기서 url: `/replies/${replySeq}/like`, 로 설정하여 특정 댓글에 "좋아요" 요청을 method: 'POST',POST 요청을 통해 서버로 전달합니다.
- 응답 메시지 설정: if (response.includes("취소되었습니다")) { 조건문을 통해 서버 응답이 "취소"인지 확인하고, 그에 따라 메시지(title, text)를 설정합니다.
- 알림 표시: swal.fire()를 사용해 SweetAlert 라이브러리로 사용자에게 메시지를 표시합니다.
6. Controller 구현
// 댓글 좋아요 처리
@PostMapping("/replies/{replySeq}/like")
@ResponseBody
public ResponseEntity<String> likeReply(@PathVariable int replySeq, HttpSession session) {
// 로그인한 사용자 확인
Member loginUser = (Member) session.getAttribute("loginUser");
if (loginUser == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("로그인 후 이용할 수 있습니다.");
}
String userId = loginUser.getId();
try {
// 좋아요가 추가되었는지 취소되었는지 확인
boolean liked = replyService.toggleLike(userId, replySeq);
if (liked) {
return ResponseEntity.ok("좋아요가 성공적으로 추가되었습니다.");
} else {
return ResponseEntity.ok("좋아요가 취소되었습니다.");
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("좋아요 처리 중 오류가 발생했습니다.");
}
}
- @PostMapping("/replies/{replySeq}/like") : POST 요청을 받고 URL 경로에서 {replySeq}에 따라 특정 댓글을 식별합니다.
- @ResponseBody : 메서드가 JSON 형식의 데이터를 응답 본문으로 반환하게 합니다.
- boolean liked = replyService.toggleLike(userId, replySeq); : toggleLike() 메서드는 사용자가 해당 댓글에 대해 이미 좋아요를 눌렀는지 여부에 따라 좋아요를 추가하거나 취소하고, 그 결과로 true(좋아요 추가) 또는 false(좋아요 취소)를 반환합니다.
- if (liked) {...} else {...} liked가 true면 "좋아요가 성공적으로 추가되었습니다."라는 메시지를 반환합니다. false면 "좋아요가 취소되었습니다."라는 메시지를 반환합니다.
테스트
좋아요 +
좋아요 -
'SpringBoot 프로젝트' 카테고리의 다른 글
Spring Boot - Q&A 페이지 구현하기 (2) (2) | 2024.11.25 |
---|---|
Spring Boot - Q&A 페이지 구현하기 (1) (1) | 2024.11.23 |
Spring Boot - 댓글 CRUD기능 구현하기 (3) (0) | 2024.11.22 |
Spring Boot - 댓글 CRUD기능 구현하기 (2) (1) | 2024.11.22 |
Spring Boot - 댓글 CRUD기능 구현하기 (1) (3) | 2024.11.21 |