목차
💡 학습 목표
정방향 인서트는 JPA에서 엔티티 간의 연관 관계를 설정할 때, 연관 관계의 주인(owner) 엔티티를 통해 데이터를 저장하는 것을 의미한다. 이는 양방향 매핑에서 특히 중요하며, 연관 관계의 주인 측에서 데이터를 추가하고 저장해야 연관 관계가 올바르게 맵핑된다.
1. 코드 수정
댓글 등록 화면 측 코드 수정
<!-- 댓글 -->
<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_v1.reply;
import com.tenco.blog_v1.board.Board;
import com.tenco.blog_v1.user.User;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "reply_tb")
@ToString(exclude = {"user","board"}) // 연관된 엔티티를 제외하여 순환 참조 방지 및 보안 강화 때문에 사용한다.
public class Reply {
// 일반적으로 id는 Long 타입을 사용하는 것을 권장한다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
// null 값이 들어올 수 없어 ! - 기본 값을 null 허용
@Column(nullable = false)
private String comment;
// 단방향 관계 설계 -> User 엔티티에는 Reply 정보가 없다!!!
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "user_id")
private User user;
// 양방향 매핑 (FK 주인은 댓글(Reply 이다)
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "board_id")
private Board board;
// JPA 엔티티에서 데이터베이스에 저장할 필요가 없는 필드를 정의할 때 사용한다.
@Transient
private boolean replyOwner;
@Builder.Default
private String status = "ACTIVE"; // 댓글 상태 (ACTIVE, DELETED)
@CreationTimestamp // 엔티티가 생성될 때 자동으로 현재 시간으로 설정
@Column(name = "created_at")
private LocalDateTime createdAt;
/**
* 엔티티가 데이터베이스에 영속화 되기 전에 호출 되는 메서드가 있다면 사용한다
* @PrePersist 어노테이션은 JPA 라이프 사이클 이벤트 중 하나로 엔티티가 영속화 되기 전에 실행된다
*/
@PrePersist
protected void onCreate() {
if (this.status == null) {
this.status = "ACTIVE";
}
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
}
}
ReplyJPARepository 생성
package com.tenco.blog_v1.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;
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_v1.reply;
import com.tenco.blog_v1.board.Board;
import com.tenco.blog_v1.board.BoardJPARepository;
import com.tenco.blog_v1.common.errors.Exception403;
import com.tenco.blog_v1.common.errors.Exception404;
import com.tenco.blog_v1.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_v1.reply;
import com.tenco.blog_v1.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;
// 댓글 생성 기능 만들기
@PostMapping("/reply/save")
public String save(ReplyDTO.SaveDTO reqDTO, HttpSession session) {
// 로그인 여부 확인
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
return "redirect:/login-form";
}
replyService.saveReply(reqDTO, sessionUser);
return "redirect:/board/" + reqDTO.getBoardId();
}
// 댓글 삭제
@PostMapping("/board/{boardId}/reply/{replyId}/delete")
public String delete(@PathVariable(name = "boardId") Integer boardId, @PathVariable(name = "replyId") Integer replyId, HttpSession session) {
// 삭제도 권한 확인
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}}
2. 인터셉터 적용
/**
* 인터셉터를 등록하고 적용할 URL 패턴을 설정하는 메서드이다.
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> loginAddPath = new ArrayList<>();
loginAddPath.add("/board/**");
loginAddPath.add("/user/**");
loginAddPath.add("/reply/**");
List<String> loginExculdePath = new ArrayList<>();
loginExculdePath.add("/board/{id:\\d+}");
registry.addInterceptor(loginInterceptor)
.addPathPatterns(loginAddPath) // 인터셉터를 적용할 경로 패턴 설정
.excludePathPatterns(loginExculdePath); // 인터셉터를 제외할 경로 패턴 설정
registry.addInterceptor(adminInterceptor)
.addPathPatterns("/admin/**");
}
'Spring Boot > Blog 프로젝트 만들기(JPA)' 카테고리의 다른 글
RestAPI 주소 설계 규칙 (0) | 2024.10.21 |
---|---|
CORS(Cross-Origin Resource Sharing) 이란 뭘까? (1) | 2024.10.21 |
게시글 삭제 오류 해결 (0) | 2024.10.17 |
댓글 목록 보기 (0) | 2024.10.17 |
댓글 테이블 설계 (엔티티) (1) | 2024.10.16 |