Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.project200.undabang.comment.dto.response;

import com.project200.undabang.comment.entity.Comment;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

Expand All @@ -15,33 +12,7 @@ public record CommentResponse(
String memberThumbnailUrl,
String content,
Integer likesCount,
Boolean commentIsLiked,
LocalDateTime createdAt,
List<CommentResponse> children) {

public static CommentResponse from(Comment comment) {
return of(comment, new ArrayList<>());
}

public static CommentResponse of(Comment comment, List<CommentResponse> children) {
String profileImageUrl = null;
String thumbnailUrl = null;

if (comment.getMember().getMemberPicture() != null) {
profileImageUrl = comment.getMember().getMemberPicture().getPicture().getPictureUrl();
if (comment.getMember().getMemberPicture().getPicture() != null) {
thumbnailUrl = null; // 썸네일은 추후 개발 예정
}
}

return new CommentResponse(
comment.getId(),
comment.getMember().getMemberId(),
comment.getMember().getMemberNickname(),
profileImageUrl,
thumbnailUrl,
comment.getContent(),
comment.getLikesCount(),
comment.getCreatedAt(),
children);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.project200.undabang.comment.repository;

import com.project200.undabang.comment.dto.response.CommentResponse;
import com.project200.undabang.member.entity.Member;

import java.util.List;

public interface CommentRepositoryCustom {

List<CommentResponse> findCommentsWithChildrenByFeedId(Long feedId);
List<CommentResponse> findCommentsWithChildrenByFeedId(Long feedId, Member currentMember);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package com.project200.undabang.comment.repository.impl;

import com.project200.undabang.comment.dto.response.CommentResponse;
import com.project200.undabang.comment.entity.Comment;
import com.project200.undabang.comment.entity.QComment;
import com.project200.undabang.comment.repository.CommentRepositoryCustom;
import com.project200.undabang.common.entity.QPicture;
import com.project200.undabang.like.entity.QCommentLike;
import com.project200.undabang.member.entity.Member;
import com.project200.undabang.member.entity.QMember;
import com.project200.undabang.member.entity.QMemberPicture;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand All @@ -22,19 +28,31 @@ public class CommentRepositoryImpl implements CommentRepositoryCustom {

private final JPAQueryFactory queryFactory;

@Override
public List<CommentResponse> findCommentsWithChildrenByFeedId(Long feedId) {
QComment comment = QComment.comment;
QMember member = QMember.member;
QMemberPicture memberPicture = QMemberPicture.memberPicture;
QPicture picture = QPicture.picture;
private final QComment comment = QComment.comment;
private final QMember member = QMember.member;
private final QMemberPicture memberPicture = QMemberPicture.memberPicture;
private final QPicture picture = QPicture.picture;
private final QCommentLike commentLike = QCommentLike.commentLike;

@Override
public List<CommentResponse> findCommentsWithChildrenByFeedId(Long feedId, Member currentMember) {
// 1. 부모 댓글 조회 (parent가 null인 것만)
List<Comment> parentComments = queryFactory
.selectFrom(comment)
.leftJoin(comment.member, member).fetchJoin()
.leftJoin(member.memberPicture, memberPicture).fetchJoin()
.leftJoin(memberPicture.picture, picture).fetchJoin()
List<CommentResponse> parentComments = queryFactory
.select(Projections.constructor(CommentResponse.class,
comment.id,
member.memberId,
member.memberNickname,
memberPicture.memberPicturesUrl,
Expressions.nullExpression(String.class), // 썸네일은 추후 개발 예정
comment.content,
comment.likesCount,
isCommentLikedExpression(currentMember),
comment.createdAt,
Expressions.constant(Collections.<CommentResponse>emptyList())))
.from(comment)
.leftJoin(comment.member, member)
.leftJoin(member.memberPicture, memberPicture)
.leftJoin(memberPicture.picture, picture)
.where(
comment.feed.id.eq(feedId),
comment.parent.isNull(),
Expand All @@ -48,32 +66,90 @@ public List<CommentResponse> findCommentsWithChildrenByFeedId(Long feedId) {

// 2. 부모 댓글 ID 목록 추출
List<Long> parentIds = parentComments.stream()
.map(Comment::getId)
.map(CommentResponse::commentId)
.collect(Collectors.toList());

// 3. 대댓글 조회
List<Comment> childComments = queryFactory
.selectFrom(comment)
.leftJoin(comment.member, member).fetchJoin()
.leftJoin(member.memberPicture, memberPicture).fetchJoin()
.leftJoin(memberPicture.picture, picture).fetchJoin()
List<CommentResponse> childComments = queryFactory
.select(Projections.constructor(CommentResponse.class,
comment.id,
member.memberId,
member.memberNickname,
memberPicture.memberPicturesUrl,
Expressions.nullExpression(String.class),
comment.content,
comment.likesCount,
isCommentLikedExpression(currentMember),
comment.createdAt,
Expressions.constant(Collections.<CommentResponse>emptyList())))
.from(comment)
.leftJoin(comment.member, member)
.leftJoin(member.memberPicture, memberPicture)
.leftJoin(memberPicture.picture, picture)
.where(
comment.parent.id.in(parentIds),
comment.deletedAt.isNull())
.orderBy(comment.createdAt.asc())
.fetch();

// 4. 부모 ID별 대댓글 그룹핑
Map<Long, List<CommentResponse>> childrenMap = childComments.stream()
.collect(Collectors.groupingBy(
c -> c.getParent().getId(),
Collectors.mapping(CommentResponse::from, Collectors.toList())));
// 4. 대댓글의 부모 ID를 가져오기 위한 매핑 쿼리
Map<Long, List<CommentResponse>> childrenMap = buildChildrenMap(childComments, parentIds);

// 5. 부모 댓글에 대댓글 조립
return parentComments.stream()
.map(parent -> CommentResponse.of(parent,
childrenMap.getOrDefault(parent.getId(), new ArrayList<>())))
.map(parent -> new CommentResponse(
parent.commentId(),
parent.memberId(),
parent.memberNickname(),
parent.memberProfileImageUrl(),
parent.memberThumbnailUrl(),
parent.content(),
parent.likesCount(),
parent.commentIsLiked(),
parent.createdAt(),
childrenMap.getOrDefault(parent.commentId(), new ArrayList<>())))
.collect(Collectors.toList());
}

/**
* 대댓글을 부모 댓글 ID별로 그룹핑합니다.
*/
private Map<Long, List<CommentResponse>> buildChildrenMap(List<CommentResponse> childComments,
List<Long> parentIds) {
if (childComments.isEmpty()) {
return Collections.emptyMap();
}

// 대댓글의 부모 ID 매핑을 위해 별도 조회
QComment c = QComment.comment;
Map<Long, Long> childToParentMap = queryFactory
.select(c.id, c.parent.id)
.from(c)
.where(c.parent.id.in(parentIds), c.deletedAt.isNull())
.fetch()
.stream()
.collect(Collectors.toMap(
tuple -> tuple.get(c.id),
tuple -> tuple.get(c.parent.id)));

return childComments.stream()
.collect(Collectors.groupingBy(
child -> childToParentMap.getOrDefault(child.commentId(), -1L)));
}

/**
* 현재 사용자가 특정 댓글을 좋아요 했는지 확인하는 조건식을 생성합니다.
*/
private BooleanExpression isCommentLikedExpression(Member currentMember) {
if (currentMember == null) {
return Expressions.FALSE;
}

return JPAExpressions
.selectOne()
.from(commentLike)
.where(commentLike.comment.id.eq(comment.id)
.and(commentLike.member.memberId.eq(currentMember.getMemberId())))
.exists();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import com.project200.undabang.comment.dto.response.CommentResponse;
import com.project200.undabang.comment.repository.CommentRepository;
import com.project200.undabang.comment.service.CommentQueryService;
import com.project200.undabang.common.context.UserContextHolder;
import com.project200.undabang.common.web.exception.CustomException;
import com.project200.undabang.common.web.exception.ErrorCode;
import com.project200.undabang.feed.repository.FeedRepository;
import com.project200.undabang.member.entity.Member;
import com.project200.undabang.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.UUID;

@Service
@RequiredArgsConstructor
Expand All @@ -19,6 +23,7 @@ public class CommentQueryServiceImpl implements CommentQueryService {

private final CommentRepository commentRepository;
private final FeedRepository feedRepository;
private final MemberRepository memberRepository;

@Override
public List<CommentResponse> getComments(Long feedId) {
Expand All @@ -27,6 +32,12 @@ public List<CommentResponse> getComments(Long feedId) {
throw new CustomException(ErrorCode.FEED_NOT_FOUND);
}

return commentRepository.findCommentsWithChildrenByFeedId(feedId);
// 현재 사용자 조회 (비로그인 시 null)
UUID currentUserId = UserContextHolder.getUserId();
Member currentMember = memberRepository
.findByMemberIdAndMemberDeletedAtNull(currentUserId)
.orElse(null);

return commentRepository.findCommentsWithChildrenByFeedId(feedId, currentMember);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ private List<CommentResponse> createSampleComments() {
null,
"대댓글 내용입니다.",
3,
false,
LocalDateTime.now().minusMinutes(30),
new ArrayList<>());

Expand All @@ -60,6 +61,7 @@ private List<CommentResponse> createSampleComments() {
null,
"부모 댓글 내용입니다.",
5,
true,
LocalDateTime.now().minusHours(1),
List.of(reply));

Expand All @@ -81,10 +83,11 @@ void getComments_success() throws Exception {
BDDMockito.given(commentQueryService.getComments(feedId)).willReturn(comments);

// when
String response = mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.headers(getCommonApiHeaders(testMemberId)))
String response = mockMvc
.perform(MockMvcRequestBuilders.get("/api/v1/feeds/{feedId}/comments", feedId)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.headers(getCommonApiHeaders(testMemberId)))
.andExpect(status().isOk())
.andDo(document.document(
requestHeaders(HEADER_ACCESS_TOKEN),
Expand All @@ -98,6 +101,7 @@ void getComments_success() throws Exception {
fieldWithPath("data[].memberThumbnailUrl").type(JsonFieldType.STRING).description("댓글 작성자 프로필 썸네일 URL입니다.").optional(),
fieldWithPath("data[].content").type(JsonFieldType.STRING).description("댓글 내용입니다."),
fieldWithPath("data[].likesCount").type(JsonFieldType.NUMBER).description("댓글 좋아요 수 입니다."),
fieldWithPath("data[].commentIsLiked").type(JsonFieldType.BOOLEAN).description("현재 사용자의 댓글 좋아요 여부입니다."),
fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("댓글이 작성된 시간입니다."),
fieldWithPath("data[].children").type(JsonFieldType.ARRAY).description("대댓글 목록입니다."),
fieldWithPath("data[].children[].commentId").type(JsonFieldType.NUMBER).description("대댓글 고유 식별자(ID) 입니다."),
Expand All @@ -107,6 +111,7 @@ void getComments_success() throws Exception {
fieldWithPath("data[].children[].memberThumbnailUrl").type(JsonFieldType.STRING).description("대댓글 작성자 프로필 썸네일 URL").optional(),
fieldWithPath("data[].children[].content").type(JsonFieldType.STRING).description("대댓글 내용"),
fieldWithPath("data[].children[].likesCount").type(JsonFieldType.NUMBER).description("대댓글 좋아요 수"),
fieldWithPath("data[].children[].commentIsLiked").type(JsonFieldType.BOOLEAN).description("현재 사용자의 대댓글 좋아요 여부"),
fieldWithPath("data[].children[].createdAt").type(JsonFieldType.STRING).description("대댓글 작성 시간"),
fieldWithPath("data[].children[].children").type(JsonFieldType.ARRAY).description("대댓글의 대댓글 (현재 미지원)")))))
.andReturn().getResponse().getContentAsString();
Expand Down
Loading