SpringBoot 프로젝트

Spring Boot - 댓글 CRUD기능 구현하기 (1)

orin602 2024. 11. 21. 15:30

1. 클래스 생성

package com.demo.domain;

import java.util.Date;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

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 jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@Builder
@ToString
@Entity
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
public class Reply {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int replySeq; // 고유번호 시퀀스 자동생성 및 1씩 증가

    @ManyToOne
    @JoinColumn(name="id")
    private Member member;

    @ManyToOne
    @JoinColumn(name="review_seq")
    private Review review;

    @Temporal(value=TemporalType.TIMESTAMP)
    @ColumnDefault("sysdate")
    private Date reply_date; // 게시글작성 시간 저장(sysdate)

    private int likes; // 댓글의 좋아요 수
    private String content;	// 댓글 내용
}

 

  • 어노테이션:
    • @Getter와 @Setter: Lombok 어노테이션으로, 클래스의 모든 필드에 대해 자동으로 getter와 setter 메서드를 생성해줍니다. 이로 인해 코드가 간결해집니다.
    • @Builder: Lombok의 @Builder는 빌더 패턴을 적용해, 클래스 인스턴스를 생성할 때 특정 필드 값을 손쉽게 설정할 수 있게 합니다.
    • @ToString: 클래스의 toString 메서드를 자동으로 생성하여, 객체를 문자열로 출력할 때 클래스의 모든 필드를 포함시킵니다.
    • @Entity: 이 클래스는 JPA 엔티티임을 나타내며, 데이터베이스 테이블과 매핑됩니다.
    • @NoArgsConstructor와 @AllArgsConstructor: 각각 기본 생성자와 모든 필드를 파라미터로 받는 생성자를 생성합니다.
    • @DynamicInsert와 @DynamicUpdate: Hibernate의 어노테이션으로, 삽입 시 기본값이 설정된 필드를 제외한 값만 업데이트하거나 삽입하게 도와줍니다.
  • 필드 설명:
    • @Id와 @GeneratedValue(strategy = GenerationType.IDENTITY): replySeq 필드는 댓글의 고유 번호로 자동으로 생성되고, 1씩 증가하는 방식입니다.
    • @ManyToOne과 @JoinColumn: Member와 Review 객체와의 관계를 나타냅니다. Member는 댓글을 작성한 사용자, Review는 댓글이 속한 리뷰를 의미합니다.
    • @Temporal(value=TemporalType.TIMESTAMP): reply_date는 댓글이 작성된 날짜와 시간을 저장합니다. sysdate를 기본값으로 설정하여 자동으로 현재 시간으로 설정되도록 합니다.

 

 

2. Repository 생성

package com.demo.persistence;

import org.springframework.data.jpa.repository.JpaRepository;

import com.demo.domain.Reply;

public interface ReplyRepository extends JpaRepository<Reply, Integer> {
	// 기본적인 CRUD 메서드는 JpaRepository가 제공하므로 별도의 추가 메서드는 작성하지 않음
    // 추가 기능은 따로 작성
}

public interface ReplyRepository extends JpaRepository<Reply, Integer> : JpaRepository는 Reply 엔티티를 다루며, 기본적인 CRUD 기능을 제공합니다. 여기서 Reply는 엔티티 타입이고, Integer는 Reply 엔티티의 id(primary key)의 타입입니다.

 

3. Service 생성

package com.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;

import com.demo.domain.Reply;
import com.demo.persistence.ReplyRepository;

@Service
public class ReplyService {

    @Autowired
    private ReplyRepository replyRepo;

    // 댓글 저장 (C)
    @Override
    public void saveReply(Reply vo) {
        replyRepo.save(vo);
    }

    // 댓글 수정 (U)
    @Override
    @Transactional
    public void updateReply(int replySeq, Reply vo) {
        Optional<Reply> beforeReply = replyRepo.findById(replySeq);
        if (beforeReply.isPresent()) {
            Reply reply = beforeReply.get();
            reply.setContent(vo.getContent());
            replyRepo.save(reply);
        } else {
            throw new IllegalArgumentException("잘못된 댓글 번호: " + replySeq);
        }
    }

    // 댓글 조회 (R)
    @Override
    public Reply getReplyBySeq(int replySeq) {
        return replyRepo.findById(replySeq)
                .orElseThrow(() -> new RuntimeException("해당 댓글을 찾을 수 없습니다."));
    }

    // 댓글 삭제 (D)
    @Override
    public void deleteReply(int replySeq) {
        // 댓글 존재 여부 체크 후 삭제
        if (!replyRepo.existsById(replySeq)) {
            throw new RuntimeException("해당 댓글을 찾을 수 없습니다.");
        }
        replyRepo.deleteById(replySeq);
    }
}

 

  • @Service: 이 어노테이션은 Spring의 서비스 클래스임을 나타냅니다. 이 클래스는 비즈니스 로직을 처리하며, 다른 계층(예: 컨트롤러)에서 주입받아 사용됩니다.
  • @Autowired: ReplyRepository를 자동으로 주입합니다. 이 레포지토리는 Reply 엔티티와 관련된 데이터베이스 작업을 처리합니다.
  1. 댓글 저장 (saveReply)
    • saveReply 메서드는 ReplyRepository의 save 메서드를 호출하여 댓글을 데이터베이스에 저장합니다.
  2. 댓글 수정 (updateReply)
    • 댓글을 수정하는 로직으로, 주어진 replySeq로 댓글을 조회한 후, 댓글이 존재하면 내용(content)을 수정하고 저장합니다.
    • 댓글이 존재하지 않으면 IllegalArgumentException을 발생시켜 잘못된 댓글 번호에 대해 오류를 처리합니다.
    • @Transactional을 사용해 트랜잭션을 관리합니다.
  3. 댓글 조회 (getReplyBySeq)
    • 댓글을 조회하는 메서드로, findById를 사용하여 댓글을 찾습니다. 댓글이 존재하지 않으면 RuntimeException을 던져 오류를 처리합니다.
  4. 댓글 삭제 (deleteReply)
    • 댓글을 삭제하는 메서드

 

4. html

<div class="review-reply">
    <h2>댓글</h2>
    <div class="reply-box" th:each="reply : ${reply}">
        <div class="reply-content">
            <h5><span th:text="${reply.member.name}"></span> : 
            <span th:text="${reply.content}"></span><br>
            <span th:text="${#dates.format(reply.reply_date, 'yyyy-MM-dd HH:mm')}"></span></h5>
            <button type="button" th:attr="data-reply-seq=${reply.replySeq}" 
            	class="detail-replylike-btn" onclick="replyLike(this)">좋아요</button>&nbsp;
            <span>좋아요 수 : <span th:text="${reply.likes}"></span></span>&nbsp;
            <button type="button" th:attr="data-reply-seq=${reply.replySeq}" 
            	class="detail-replydelete-btn" onclick="replyDelete(this)">댓글삭제</button>
            <!-- th:attr은 속성 이름과 값을 동적으로 설정할 수 있게 한다. (JavaScript와 상호작용할 때 유용) -->
        </div>
    </div>
    <div class="reply-content">
        <form class="reply-write-form" id="reply-write-form" method="post">
            <input type="hidden" name="review_seq" th:value="${review.review_seq}">
            <input type="hidden" name="member_id" th:value="${loginUser.id}">
            <input type="text" id="content" name="content" placeholder="댓글을 작성하세요">
        </form>
        <button class="detail-reply-btn"type="button" onclick="reply_write()">댓글 작성</button>
	</div>
</div>

 

  • th:each="reply : ${reply}" 는 Thymeleaf의 반복문 기능입니다. 이 속성은 reply 객체 배열(리스트)을 반복해서 출력합니다.
  • 반복문이 실행될 때, 각 reply 객체를 하나씩 순차적으로 받아서 그에 해당하는 HTML을 생성합니다.
  • 댓글 작성자${reply.member.name}, 댓글 내용${reply.content}, 댓글 작성 시간${#dates.format(reply.reply_date, 'yyyy-MM-dd HH:mm')} 이 span 태그로 출력됩니다. (작성 시간은 날짜 포맷을 지정하고 있습니다.)
  • 좋아요 버튼과 댓글 삭제 버튼은 각각 replyLike(this)와 replyDelete(this) JavaScript 함수를 호출하여 댓글에 대해 좋아요 기능 및 삭제 기능을 수행합니다.
  • 각 버튼은 data-reply-seq 속성에 댓글의 고유번호(reply.replySeq)를 저장해 두어, JavaScript에서 이 정보를 활용할 수 있도록 합니다.

+ reviewDetail.html의 (th:each="reply : ${reply}" 는 Thymeleaf의 반복문 기능입니다. 이 속성은 reply 객체 배열(리스트)을 반복해서 출력합니다.)를 위해

// 댓글
List<Reply> reply = replyService.getReplyByReview(review_seq);
model.addAttribute("reply", reply);
  • List<Reply>reply=replyService.getReplyByReview(review_seq);: replyService를 통해 리뷰에 달린 댓글 목록을 가져옵니다.
  • model.addAttribute("reply", reply): 댓글 리스트인 reply를 모델에 추가하여, 해당 데이터를 reviewDetail.html에서 사용할 수 있도록 합니다.
  • 위의 코딩을 reviewDetail.html 텡플릿을반환하는 메서드에 작성해줘야 한다.

 

5. JavaScript: 댓글 작성 요청 보내기

// 댓글 작성
function reply_write() {
    var content = $("#content").val();
    var reviewSeq = $("input[name='review_seq']").val();
    var memberId = $("input[name='member_id']").val();
    
    if (content.trim() === "") {
        swal.fire({
            title: '댓글 작성 실패',
            text: '댓글을 입력해 주세요.',
            icon: 'error',
            confirmButtonText: '확인'
        });
        $("#content").focus();
        return;
    }

    var reviewSeq = $("input[name='review_seq']").val();

    // 댓글 작성 요청
    $.ajax({
        url: '/write-reply',
        type: 'POST',
        contentType: 'application/json',
        data: JSON.stringify({
            review_seq: reviewSeq,
            member_id: memberId,
            content: content
        }),
        success: function(response) {
            swal.fire({
                title: '댓글 작성 성공',
                text: response,
                icon: 'success',
                confirmButtonText: '확인'
            }).then(() => {
                location.reload(); // 댓글 작성 후 페이지 새로고침
            });
        },
        error: function(xhr, status, error) {
            swal.fire({
                title: '댓글 작성 실패',
                text: '서버에서 오류가 발생했습니다. 다시 시도해 주세요.',
                icon: 'error',
                confirmButtonText: '확인'
            });
        }
    });
}

 

  • 사용자가 댓글을 작성하고 댓글 작성 버튼을 클릭하면 reply_write() 함수가 호출됩니다.
  • 댓글 내용, 리뷰 번호, 사용자 ID를 가져와 서버로 AJAX 요청을 보냅니다.
  • 서버의 응답을 swal.fire()로 처리하며, 댓글 작성 성공 시 페이지를 새로고침합니다.

 

6. Controller : 댓글 작성 처리

// 댓글 작성
@PostMapping("/write-reply")
@ResponseBody
public ResponseEntity<String> writeReply(@RequestBody Map<String, Object> requestBody) {
    try {
        // requestBody에서 review_seq와 member_id 추출
        int reviewSeq = Integer.parseInt((String) requestBody.get("review_seq"));
        String memberId = (String) requestBody.get("member_id");
        String content = (String) requestBody.get("content");

        // Review 객체와 Member 객체를 데이터베이스에서 조회
        Review review = reviewService.getReviewBySeq(reviewSeq);
        Member member = memberService.getMember(memberId);

        // Reply 객체 생성
        Reply reply = Reply.builder()
                            .content(content)
                            .review(review)
                            .member(member)
                            .reply_date(new Date()) // 현재 날짜 및 시간 설정
                            .build();

        // 댓글 저장 서비스 호출
        replyService.saveReply(reply);

        return ResponseEntity.ok("댓글이 성공적으로 작성되었습니다.");
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("댓글 작성 실패: " + e.getMessage());
    }
}

 

  • @RequestBody Map<String, Object> requestBody 를 통해 댓글 작성에 필요한 데이터(리뷰 번호, 사용자 ID, 댓글 내용)를 받아옵니다.
  • reviewService와 memberService를 이용해 Review와 Member 객체를 조회합니다.
  • Reply reply = Reply.builder() 객체를 생성하고, replyService.saveReply(reply) 로 댓글을 저장합니다.
  • 성공적인 댓글 작성 후 "댓글이 성공적으로 작성되었습니다."라는 응답을 클라이언트로 보냅니다. 오류 발생 시 예외 메시지를 포함한 500 에러 응답을 보냅니다.

 

테스트 및 db

댓글 작성 + 성공 alert 창
반영된 html
반영된 db

이번 글에서는 Spring Boot를 사용하여 리뷰 상세 페이지에 댓글을 작성하고, 해당 댓글을 데이터베이스에서 조회하여 출력하는 방법에 대해 설명했습니다. 댓글 작성, 조회 기능을 구현하여, 사용자가 리뷰에 대한 의견을 남기고, 다른 사용자들이 이를 확인할 수 있도록 했습니다.

다음 글에서는 댓글 좋아요, 수정, 삭제 기능을 다룰 예정입니다.