11package com .dmu .debug_visual .community .service ;
22
3- import com .dmu .debug_visual .community .dto .*;
4- import com .dmu .debug_visual .community .entity .*;
5- import com .dmu .debug_visual .community .repository .*;
3+ import com .dmu .debug_visual .community .dto .CommentRequestDTO ;
4+ import com .dmu .debug_visual .community .dto .CommentResponseDTO ;
5+ import com .dmu .debug_visual .community .entity .Comment ;
6+ import com .dmu .debug_visual .community .entity .Post ;
7+ import com .dmu .debug_visual .community .repository .CommentRepository ;
8+ import com .dmu .debug_visual .community .repository .PostRepository ;
69import com .dmu .debug_visual .user .User ;
710import lombok .RequiredArgsConstructor ;
811import org .springframework .stereotype .Service ;
1114import java .util .List ;
1215import java .util .stream .Collectors ;
1316
17+ /**
18+ * 게시글 댓글 관련 비즈니스 로직을 처리하는 서비스 클래스.
19+ */
1420@ Service
1521@ RequiredArgsConstructor
1622public class CommentService {
@@ -19,89 +25,122 @@ public class CommentService {
1925 private final PostRepository postRepository ;
2026 private final NotificationService notificationService ;
2127
22- // ★ [수정] 싱글톤 서비스에서 상태를 가지는 필드는 스레드 충돌을 일으키므로 삭제
23- // Comment parent = null;
24-
28+ /**
29+ * 새로운 댓글 또는 대댓글을 생성하고 저장합니다.
30+ * 댓글 생성 시 게시글 작성자에게, 대댓글 생성 시 부모 댓글 작성자에게 알림을 전송합니다.
31+ *
32+ * @param dto 댓글 생성 요청 정보 (postId, content, parentId)
33+ * @param user 댓글 작성자
34+ * @return 생성된 댓글의 ID
35+ */
2536 @ Transactional
2637 public Long createComment (CommentRequestDTO dto , User user ) {
27- // ★ [수정] postRepository.findById() -> findByIdWithWriter()로 변경
38+ // Fetch Join을 사용하여 Post 조회 시 writer 정보도 함께 로드
2839 Post post = postRepository .findByIdWithWriter (dto .getPostId ())
29- .orElseThrow (() -> new RuntimeException ("게시글 없음" ));
40+ .orElseThrow (() -> new RuntimeException ("게시글을 찾을 수 없습니다. ID: " + dto . getPostId () ));
3041
3142 Comment .CommentBuilder builder = Comment .builder ()
3243 .post (post )
3344 .writer (user )
3445 .content (dto .getContent ());
3546
36- // 메서드 내의 지역 변수로 사용하는 것이 올바른 방법입니다.
37- Comment parent = null ;
38-
47+ // 대댓글 처리
3948 if (dto .getParentId () != null && dto .getParentId () != 0 ) {
40- // ★ [수정] commentRepository.findById() -> findByIdWithWriter()로 변경
41- parent = commentRepository .findByIdWithWriter (dto .getParentId ())
42- .orElseThrow (() -> new RuntimeException ("상위 댓글 없음" ));
49+ // Fetch Join을 사용하여 부모 댓글 조회 시 writer 정보도 함께 로드
50+ Comment parent = commentRepository .findByIdWithWriter (dto .getParentId ())
51+ .orElseThrow (() -> new RuntimeException ("상위 댓글을 찾을 수 없습니다. ID: " + dto . getParentId () ));
4352
53+ // 대댓글의 대댓글은 허용하지 않음
4454 if (parent .getParent () != null ) {
4555 throw new IllegalArgumentException ("대댓글에는 답글을 달 수 없습니다." );
4656 }
4757
4858 builder .parent (parent );
4959
60+ // 대댓글 작성 시, 부모 댓글 작성자와 현재 작성자가 다르면 알림 전송
5061 if (!user .getUserNum ().equals (parent .getWriter ().getUserNum ())) {
51- notificationService .notify (
52- parent .getWriter (),
53- user .getName () + "님이 댓글에 답글을 남겼습니다." ,
54- post .getId ()
55- );
62+ String message = user .getName () + "님이 회원님의 댓글에 답글을 남겼습니다." ;
63+ notificationService .notify (parent .getWriter (), message , post .getId ());
5664 }
5765 }
5866
67+ // 댓글 저장
68+ Comment savedComment = commentRepository .save (builder .build ());
69+
70+ // 댓글 작성 시, 게시글 작성자와 현재 작성자가 다르면 알림 전송
71+ // (대댓글인 경우에도 게시글 작성자에게 알림 전송)
5972 if (!user .getUserNum ().equals (post .getWriter ().getUserNum ())) {
60- notificationService .notify (
61- post .getWriter (),
62- user .getName () + "님이 게시글에 댓글을 남겼습니다." ,
63- post .getId ()
64- );
73+ String message = user .getName () + "님이 회원님의 게시글에 댓글을 남겼습니다." ;
74+ notificationService .notify (post .getWriter (), message , post .getId ());
6575 }
6676
67- return commentRepository . save ( builder . build ()) .getId ();
77+ return savedComment .getId ();
6878 }
6979
70-
80+ /**
81+ * 특정 게시글의 댓글 목록을 조회합니다 (대댓글 포함).
82+ *
83+ * @param postId 댓글 목록을 조회할 게시글 ID
84+ * @return 댓글 DTO 리스트 (계층 구조)
85+ */
86+ @ Transactional (readOnly = true ) // 읽기 전용 트랜잭션 명시
7187 public List <CommentResponseDTO > getComments (Long postId ) {
88+ // 게시글 존재 여부 확인 (writer 정보는 필요 없으므로 기본 findById 사용)
7289 Post post = postRepository .findById (postId )
73- .orElseThrow (() -> new RuntimeException ("게시글 없음" ));
90+ .orElseThrow (() -> new RuntimeException ("게시글을 찾을 수 없습니다. ID: " + postId ));
7491
75- // ★ [수정] N+1 문제를 일부 해결하기 위해 writer를 함께 조회하는 메서드 사용
92+ // 최상위 댓글 목록 조회 (writer 정보 포함)
7693 List <Comment > rootComments = commentRepository .findByPostAndParentIsNullWithWriter (post );
7794
7895 return rootComments .stream ()
79- .map (this ::mapToDTO )
96+ .map (this ::mapToDTO ) // DTO 변환 메소드 재귀 호출
8097 .collect (Collectors .toList ());
8198 }
8299
100+ /**
101+ * 특정 댓글을 논리적으로 삭제 처리합니다.
102+ *
103+ * @param commentId 삭제할 댓글 ID
104+ * @param user 현재 로그인한 사용자 (권한 확인용)
105+ */
106+ @ Transactional // 데이터 변경이 있으므로 트랜잭션 명시
107+ public void deleteComment (Long commentId , User user ) {
108+ // 댓글 조회 (writer 정보는 권한 확인에 필요 없으므로 기본 findById 사용)
109+ Comment comment = commentRepository .findById (commentId )
110+ .orElseThrow (() -> new RuntimeException ("댓글을 찾을 수 없습니다. ID: " + commentId ));
111+
112+ // 댓글 작성자와 현재 사용자가 일치하는지 확인
113+ if (!comment .getWriter ().getUserNum ().equals (user .getUserNum ())) {
114+ throw new RuntimeException ("댓글 삭제 권한이 없습니다. (댓글 ID: " + commentId + ")" );
115+ }
116+
117+ // 논리 삭제 처리
118+ comment .setDeleted (true );
119+ // @Transactional 환경에서는 명시적인 save 호출 없이 더티 체킹으로 업데이트 가능
120+ // commentRepository.save(comment);
121+ }
122+
123+ // --- Private Helper Methods ---
124+
125+ /**
126+ * Comment 엔티티를 CommentResponseDTO로 변환합니다 (재귀 호출 지원).
127+ *
128+ * @param comment 변환할 Comment 엔티티
129+ * @return 변환된 CommentResponseDTO
130+ */
83131 private CommentResponseDTO mapToDTO (Comment comment ) {
132+ // 재귀 호출 시 children의 writer 로딩으로 N+1 발생 가능성 있음
133+ // 성능 최적화가 필요하다면 EntityGraph 또는 별도 조회 쿼리 고려
84134 return CommentResponseDTO .builder ()
85135 .id (comment .getId ())
136+ // writer 필드는 getComments에서 Fetch Join 되었으므로 getName() 호출 가능
86137 .writer (comment .getWriter ().getName ())
87138 .content (comment .isDeleted () ? "삭제된 댓글입니다." : comment .getContent ())
88139 .createdAt (comment .getCreatedAt ())
89- // 참고: 이 부분(children)은 여전히 N+1 문제가 발생할 수 있습니다.
90140 .replies (comment .getChildren ().stream ()
141+ .filter (child -> !child .isDeleted ()) // 논리 삭제된 대댓글은 제외 (선택사항)
91142 .map (this ::mapToDTO )
92143 .collect (Collectors .toList ()))
93144 .build ();
94145 }
95-
96- public void deleteComment (Long commentId , User user ) {
97- Comment comment = commentRepository .findById (commentId )
98- .orElseThrow (() -> new RuntimeException ("댓글 없음" ));
99-
100- if (!comment .getWriter ().getUserNum ().equals (user .getUserNum ())) {
101- throw new RuntimeException ("댓글 삭제 권한 없음" );
102- }
103-
104- comment .setDeleted (true );
105- commentRepository .save (comment );
106- }
107146}
0 commit comments