학습 목표

정방향 인서트JPA에서 엔티티 간의 연관 관계를 설정할 때, 연관 관계의 주인(owner) 엔티티를 통해 데이터를 저장하는 것을 의미합니다. 이는 양방향 매핑에서 특히 중요하며, 연관 관계의 주인 측에서 데이터를 추가하고 저장해야 연관 관계가 올바르게 맵핑됩니다.

댓글 등록 화면 측 코드 수정

    <!-- 댓글 -->
    <div class="card mt-3">
        <!-- 댓글등록 -->
        <div class="card-body">
            <form action="/reply/save" method="post">
                <input type="hidden" name="boardId" value="{{board.id}}">
                <textarea class="form-control" rows="2" name="comment"></textarea>
                <div class="d-flex justify-content-end">
                    <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
                </div>
            </form>
        </div>

ReplyDTO 만들기

package com.tenco.blog_v2.reply;

import com.tenco.blog_v2.board.Board;
import com.tenco.blog_v2.user.User;
import lombok.Getter;
import lombok.Setter;

public class ReplyDTO {

    @Getter
    @Setter
    public static class SaveDTO {
        private Integer boardId;
        private String comment;

        // DTO --> JPA 영속성 컨텍스트로 저장 한다.. 엔티티로 변환 해야 한다.
        public Reply toEntity(User sessionUser, Board board) {
            return Reply.builder()
                    .comment(comment)
                    .board(board)
                    .user(sessionUser)
                    .build();
        }

    }
}

ReplyJPARepository 생성

package com.tenco.blog_v2.reply;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

// 어노테이션 생략 가능 -> IoC 처리 됨
public interface ReplyJPARepository extends JpaRepository<Reply, Integer> {
    // 기본적인 주요 메서드 제공 받음 (구현체를 만들어 준다)

    // 1. 커스텀 쿼리를 만들어 본다. 어노테이션 사용
    // boardId 를 통해서 리플정보를 조회하는 기능
    @Query("select r from Reply r where r.board.id = :boardId")
    List<Reply> findByBoardId(@Param("boardId") Integer boardId); // 알아서 메서드의 바디를 만들어 준다.
    
}

ReplyService 생성

package com.tenco.blog_v2.reply;

import com.tenco.blog_v2.board.Board;
import com.tenco.blog_v2.board.BoardJPARepository;
import com.tenco.blog_v2.common.errors.Exception403;
import com.tenco.blog_v2.common.errors.Exception404;
import com.tenco.blog_v2.user.User;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class ReplyService {

    private final BoardJPARepository boardJPARepository;
    private final ReplyJPARepository replyJPARepository;

    // 댓글 쓰기
    @Transactional
    public void saveReply(ReplyDTO.SaveDTO reDto, User sessionUser) {
        // 댓글 작성시 게시글 존재 여부 반드시 확인
        Board board =  boardJPARepository
                .findById(reDto.getBoardId()).orElseThrow(() -> new Exception404("없는 게시글에 댓글을 작성 못해요"));

        Reply reply = reDto.toEntity(sessionUser, board);
        replyJPARepository.save(reply);
    }

    // 댓글 삭제
    @Transactional
    public void deleteReply(Integer replyId, Integer sessionUserId, Integer boardId) {
        // 댓글 존재 여부 확인
        Reply reply = replyJPARepository
                .findById(replyId).orElseThrow(() -> new Exception404("없는 댓글을 삭제 못해요"));
        // 권한 처리 확인
        if(!reply.getUser().getId().equals(sessionUserId)) {
            throw new Exception403("댓글 삭제 권한이 없어요");
        }

        if (!reply.getBoard().getId().equals(boardId)) {
            throw new Exception403("해당 게시글의 댓글이 아닙니다");
        }
        replyJPARepository.deleteById(replyId);
    }
}

ReplyController 생성

package com.tenco.blog_v2.reply;

import com.tenco.blog_v2.user.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

@RequiredArgsConstructor
@Controller
public class ReplyController {

    private final ReplyService replyService;
    private final HttpSession session;

    // 댓글 생성 기능 만들기
    @PostMapping("/reply/save")
    public String save(ReplyDTO.SaveDTO reqDTO) {
        // 로그인 여부 확인
        User sessionUser = (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            return "redirect:/login-form";
        }
        replyService.saveReply(reqDTO, sessionUser);
        return "redirect:/board/"+ reqDTO.getBoardId();
    }

    // 댓글 삭제

    // @DeleteMapping("")
    @PostMapping("/baord/{boardId}/reply/{replyId}/delete")
    public String delete(@PathVariable(name = "boardId") Integer boardId, @PathVariable(name = "replyId") Integer replyId) {
        // 삭제도 권한 확인
        User sessionUser = (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            return "redirect:/login-from";
        }
        replyService.deleteReply(replyId, sessionUser.getId(), boardId);
        return "redirect:/board/" + boardId;
    }

}

게시글 상세 보기 화면 수정

{{#replyOwner}}
<form action="/baord/{{board.id}}/reply/{{id}}/delete" method="post">
    <button class="btn">🗑</button>
</form>
{{/replyOwner}}

전체 코드

{{> layout/header}}

<div class="container p-5">

    <!-- 수정, 삭제버튼 -->
    {{# isOwner}}
    <div class="d-flex justify-content-end">
        <a href="/board/{{board.id}}/update-form" class="btn btn-warning me-1">수정</a>
        <form action="/board/{{board.id}}/delete" method="post">
            <button class="btn btn-danger">삭제</button>
        </form>
    </div>
    {{/ isOwner}}


    <div class="d-flex justify-content-end">
        <b>작성자</b> : {{ board.user.username }}
    </div>

    <!-- 게시글내용 -->
    <div>
        <h2><b>{{board.title}}</b></h2>
        <hr />
        <div class="m-4 p-2">
            {{board.content}}
        </div>
    </div>

    <!-- 댓글 -->
    <div class="card mt-3">
        <!-- 댓글등록 -->
        <div class="card-body">
            <form action="/reply/save" method="post">
                <input type="hidden" name="boardId" value="{{board.id}}">
                <textarea class="form-control" rows="2" name="comment"></textarea>
                <div class="d-flex justify-content-end">
                    <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
                </div>
            </form>
        </div>

        <!-- 댓글목록 -->
        <div class="card-footer">
            <b>댓글리스트</b>
        </div>
        <div class="list-group">
            {{#board.replies}}
            <!-- 댓글아이템  2-->
            <div class="list-group-item d-flex justify-content-between align-items-center">
                <div class="d-flex">
                    <div class="px-1 me-1 bg-primary text-white rounded">{{user.username}}</div>
                    <div>{{comment}}</div>
                </div>

                {{#replyOwner}}
                <form action="/baord/{{board.id}}/reply/{{id}}/delete" method="post">
                    <button class="btn">🗑</button>
                </form>
                {{/replyOwner}}

            </div>
            {{/board.replies}}
        </div>
    </div>
</div>

{{> layout/footer}}