-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: 댓글 조회 및 댓글, 대댓글 추가 기능 구현 (#287)
* feat: comment API 중간 커밋 * feat: 댓글 추가 및 조회 * fix: DTO Swagger 정의 * fix: children -> replyComments * refactor: 개선 및 주석 * fix: @dbscks97 리뷰 반영
- Loading branch information
Showing
15 changed files
with
321 additions
and
4 deletions.
There are no files selected for viewing
41 changes: 41 additions & 0 deletions
41
src/main/java/com/depromeet/stonebed/domain/comment/api/CommentController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.depromeet.stonebed.domain.comment.api; | ||
|
||
import com.depromeet.stonebed.domain.comment.application.CommentService; | ||
import com.depromeet.stonebed.domain.comment.dto.request.CommentCreateRequest; | ||
import com.depromeet.stonebed.domain.comment.dto.response.CommentCreateResponse; | ||
import com.depromeet.stonebed.domain.comment.dto.response.CommentFindResponse; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@Tag(name = "9. [댓글]", description = "댓글 관련 API입니다.") | ||
@RequestMapping("/comments") | ||
@RestController | ||
@RequiredArgsConstructor | ||
public class CommentController { | ||
|
||
private final CommentService commentService; | ||
|
||
@Operation(summary = "댓글 작성", description = "댓글을 작성합니다.") | ||
@PostMapping | ||
public ResponseEntity<CommentCreateResponse> commentCreate( | ||
@RequestBody @Valid CommentCreateRequest request) { | ||
return ResponseEntity.status(HttpStatus.CREATED) | ||
.body(commentService.createComment(request)); | ||
} | ||
|
||
@Operation(summary = "댓글 조회", description = "댓글을 조회합니다.") | ||
@GetMapping | ||
public CommentFindResponse commentFind(@RequestParam Long recordId) { | ||
return commentService.findCommentsByRecordId(recordId); | ||
} | ||
} |
152 changes: 152 additions & 0 deletions
152
src/main/java/com/depromeet/stonebed/domain/comment/application/CommentService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package com.depromeet.stonebed.domain.comment.application; | ||
|
||
import com.depromeet.stonebed.domain.comment.dao.CommentRepository; | ||
import com.depromeet.stonebed.domain.comment.domain.Comment; | ||
import com.depromeet.stonebed.domain.comment.dto.request.CommentCreateRequest; | ||
import com.depromeet.stonebed.domain.comment.dto.response.CommentCreateResponse; | ||
import com.depromeet.stonebed.domain.comment.dto.response.CommentFindOneResponse; | ||
import com.depromeet.stonebed.domain.comment.dto.response.CommentFindResponse; | ||
import com.depromeet.stonebed.domain.member.domain.Member; | ||
import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository; | ||
import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; | ||
import com.depromeet.stonebed.global.error.ErrorCode; | ||
import com.depromeet.stonebed.global.error.exception.CustomException; | ||
import com.depromeet.stonebed.global.util.MemberUtil; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Transactional | ||
public class CommentService { | ||
|
||
private final MemberUtil memberUtil; | ||
private final CommentRepository commentRepository; | ||
private final MissionRecordRepository missionRecordRepository; | ||
private static final Long ROOT_COMMENT_PARENT_ID = -1L; | ||
|
||
/** | ||
* 댓글을 생성합니다. | ||
* | ||
* @param request 댓글 생성 요청 객체 (content, recordId, parentId 포함) | ||
* @return 생성된 댓글의 ID를 포함한 응답 객체 | ||
*/ | ||
public CommentCreateResponse createComment(CommentCreateRequest request) { | ||
final Member member = memberUtil.getCurrentMember(); | ||
final MissionRecord missionRecord = findMissionRecordById(request.recordId()); | ||
|
||
// 부모 댓글이 존재하는 경우 | ||
|
||
final Comment comment = | ||
request.parentId() != null | ||
? Comment.createComment( | ||
missionRecord, | ||
member, | ||
request.content(), | ||
findCommentById(request.parentId())) | ||
: Comment.createComment(missionRecord, member, request.content(), null); | ||
|
||
Comment savedComment = commentRepository.save(comment); | ||
|
||
return CommentCreateResponse.of(savedComment.getId()); | ||
} | ||
|
||
/** | ||
* 특정 기록 ID에 대한 모든 댓글을 조회합니다. | ||
* | ||
* @param recordId 조회할 기록의 ID | ||
* @return 조회된 댓글 목록을 포함한 응답 객체 | ||
*/ | ||
@Transactional(readOnly = true) | ||
public CommentFindResponse findCommentsByRecordId(Long recordId) { | ||
final MissionRecord missionRecord = findMissionRecordById(recordId); | ||
final List<Comment> allComments = | ||
commentRepository.findAllCommentsByMissionRecord(missionRecord); | ||
|
||
// 댓글을 부모 ID로 그룹화, 부모 ID가 null인 경우 -1L로 처리 | ||
Map<Long, List<Comment>> commentsByParentId = | ||
allComments.stream() | ||
.collect( | ||
Collectors.groupingBy( | ||
comment -> { | ||
Comment parent = comment.getParent(); | ||
return (parent != null) | ||
? parent.getId() | ||
: ROOT_COMMENT_PARENT_ID; | ||
}, | ||
Collectors.toList())); | ||
|
||
// 부모 댓글 (부모 댓글이 없는 댓글) 조회 | ||
List<Comment> rootComments = | ||
commentsByParentId.getOrDefault(ROOT_COMMENT_PARENT_ID, List.of()); | ||
|
||
// 부모 댓글을 CommentFindOneResponse로 변환 | ||
List<CommentFindOneResponse> rootResponses = | ||
rootComments.stream() | ||
.map( | ||
comment -> | ||
convertToCommentFindOneResponse( | ||
comment, commentsByParentId)) | ||
.collect(Collectors.toList()); | ||
|
||
return CommentFindResponse.of(rootResponses); | ||
} | ||
|
||
/** | ||
* Comment 객체를 CommentFindOneResponse 객체로 변환합니다. | ||
* | ||
* @param comment 변환할 댓글 객체 | ||
* @param commentsByParentId 부모 ID로 그룹화된 댓글 key-value 형태의 Map | ||
* @return 변환된 댓글 응답 객체 | ||
*/ | ||
private CommentFindOneResponse convertToCommentFindOneResponse( | ||
Comment comment, Map<Long, List<Comment>> commentsByParentId) { | ||
List<CommentFindOneResponse> replyCommentsResponses = | ||
commentsByParentId.getOrDefault(comment.getId(), List.of()).stream() | ||
.map( | ||
childComment -> | ||
convertToCommentFindOneResponse( | ||
childComment, commentsByParentId)) | ||
.collect(Collectors.toList()); | ||
|
||
return CommentFindOneResponse.of( | ||
comment.getParent() != null ? comment.getParent().getId() : null, | ||
comment.getId(), | ||
comment.getContent(), | ||
comment.getWriter().getId(), | ||
comment.getWriter().getProfile().getNickname(), | ||
comment.getWriter().getProfile().getProfileImageUrl(), | ||
comment.getCreatedAt().toString(), | ||
replyCommentsResponses); | ||
} | ||
|
||
/** | ||
* MissionRecord를 조회 | ||
* | ||
* @param recordId 조회할 기록의 ID | ||
* @return 조회된 MissionRecord 객체 | ||
* @throws CustomException 기록을 찾을 수 없는 경우 예외 발생 | ||
*/ | ||
private MissionRecord findMissionRecordById(Long recordId) { | ||
return missionRecordRepository | ||
.findById(recordId) | ||
.orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND)); | ||
} | ||
|
||
/** | ||
* Comment를 조회 | ||
* | ||
* @param commentId 조회할 댓글의 ID | ||
* @return 조회된 Comment 객체 | ||
* @throws CustomException 댓글을 찾을 수 없는 경우 예외 발생 | ||
*/ | ||
private Comment findCommentById(Long commentId) { | ||
return commentRepository | ||
.findById(commentId) | ||
.orElseThrow(() -> new CustomException(ErrorCode.COMMENT_NOT_FOUND)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryCustom.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.depromeet.stonebed.domain.comment.dao; | ||
|
||
import com.depromeet.stonebed.domain.comment.domain.Comment; | ||
import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; | ||
import java.util.List; | ||
|
||
public interface CommentRepositoryCustom { | ||
List<Comment> findAllCommentsByMissionRecord(MissionRecord missionRecord); | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/depromeet/stonebed/domain/comment/dao/CommentRepositoryImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.depromeet.stonebed.domain.comment.dao; | ||
|
||
import static com.depromeet.stonebed.domain.comment.domain.QComment.comment; | ||
|
||
import com.depromeet.stonebed.domain.comment.domain.Comment; | ||
import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord; | ||
import com.querydsl.jpa.impl.JPAQueryFactory; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@RequiredArgsConstructor | ||
public class CommentRepositoryImpl implements CommentRepositoryCustom { | ||
|
||
private final JPAQueryFactory queryFactory; | ||
|
||
@Override | ||
public List<Comment> findAllCommentsByMissionRecord(MissionRecord missionRecord) { | ||
return queryFactory | ||
.selectFrom(comment) | ||
.leftJoin(comment.parent) | ||
.fetchJoin() | ||
.leftJoin(comment.replyComments) | ||
.fetchJoin() | ||
.where(comment.missionRecord.eq(missionRecord)) | ||
.fetch(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
src/main/java/com/depromeet/stonebed/domain/comment/dto/request/CommentCreateRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.depromeet.stonebed.domain.comment.dto.request; | ||
|
||
import io.swagger.v3.oas.annotations.media.Schema; | ||
|
||
public record CommentCreateRequest( | ||
@Schema(description = "댓글 내용", example = "너무 이쁘자나~") String content, | ||
@Schema(description = "기록 ID", example = "1") Long recordId, | ||
@Schema(description = "부모 댓글 ID", example = "1") Long parentId) {} |
9 changes: 9 additions & 0 deletions
9
src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentCreateResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.depromeet.stonebed.domain.comment.dto.response; | ||
|
||
import io.swagger.v3.oas.annotations.media.Schema; | ||
|
||
public record CommentCreateResponse(@Schema(description = "댓글 ID", example = "1") Long commentId) { | ||
public static CommentCreateResponse of(Long commentId) { | ||
return new CommentCreateResponse(commentId); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindOneResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package com.depromeet.stonebed.domain.comment.dto.response; | ||
|
||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import java.util.List; | ||
|
||
public record CommentFindOneResponse( | ||
@Schema(description = "부모 댓글 ID", example = "1") Long parentId, | ||
@Schema(description = "댓글 ID", example = "1") Long commentId, | ||
@Schema(description = "댓글 내용", example = "너무 이쁘자나~") String content, | ||
@Schema(description = "작성자 ID", example = "1") Long writerId, | ||
@Schema(description = "작성자 닉네임", example = "왈왈대장") String writerNickname, | ||
@Schema(description = "작성자 프로필 이미지 URL", example = "https://default.walwal/profile.jpg") | ||
String writerProfileImageUrl, | ||
@Schema(description = "작성일", example = "2021-10-01T00:00:00") String createdAt, | ||
@Schema(description = "자식 댓글 목록") List<CommentFindOneResponse> replyComments) { | ||
public static CommentFindOneResponse of( | ||
Long parentId, | ||
Long commentId, | ||
String content, | ||
Long writerId, | ||
String writerNickname, | ||
String writerProfileImageUrl, | ||
String createdAt, | ||
List<CommentFindOneResponse> replyComments) { | ||
return new CommentFindOneResponse( | ||
parentId, | ||
commentId, | ||
content, | ||
writerId, | ||
writerNickname, | ||
writerProfileImageUrl, | ||
createdAt, | ||
replyComments); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/com/depromeet/stonebed/domain/comment/dto/response/CommentFindResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.depromeet.stonebed.domain.comment.dto.response; | ||
|
||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import java.util.List; | ||
|
||
public record CommentFindResponse( | ||
@Schema(description = "댓글 목록") List<CommentFindOneResponse> comments) { | ||
public static CommentFindResponse of(List<CommentFindOneResponse> comments) { | ||
return new CommentFindResponse(comments); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters