Skip to content

Commit fc2b85e

Browse files
committed
refact: 주차별 챌린지 댓글 조회 리팩토링
1 parent 54a3a3e commit fc2b85e

File tree

8 files changed

+247
-127
lines changed

8 files changed

+247
-127
lines changed
Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package targeter.aim.domain.challenge.controller;
22

33
import io.swagger.v3.oas.annotations.Operation;
4-
import io.swagger.v3.oas.annotations.Parameter;
54
import io.swagger.v3.oas.annotations.tags.Tag;
65
import jakarta.validation.Valid;
76
import lombok.RequiredArgsConstructor;
7+
import org.springdoc.core.annotations.ParameterObject;
8+
import org.springframework.data.domain.Pageable;
9+
import org.springframework.data.web.PageableDefault;
810
import org.springframework.http.MediaType;
911
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1012
import org.springframework.web.bind.annotation.*;
1113
import targeter.aim.domain.challenge.dto.WeeklyCommentDto;
12-
import targeter.aim.domain.challenge.repository.CommentSortType;
1314
import targeter.aim.domain.challenge.service.WeeklyCommentService;
1415
import targeter.aim.system.security.model.UserDetails;
1516

@@ -37,26 +38,18 @@ public WeeklyCommentDto.WeeklyCommentCreateResponse createWeeklyComment(
3738
);
3839
}
3940

40-
// @GetMapping
41-
// @Operation(
42-
// summary = "챌린지 주차별 댓글 목록 조회",
43-
// description = "챌린지의 특정 주차(weeks)에 작성된 댓글 목록(부모/대댓글)을 최신순으로 조회합니다."
44-
// )
45-
// public WeeklyCommentDto.WeeklyCommentListResponse getWeeklyComments(
46-
// @PathVariable Long challengeId,
47-
// @PathVariable Long weeksId,
48-
// @Parameter(description = "정렬 기준", example = "LATEST")
49-
// @RequestParam(defaultValue = "LATEST") CommentSortType sort,
50-
// @Parameter(description = "정렬 방향", example = "DESC")
51-
// @RequestParam(defaultValue = "DESC") SortOrder order,
52-
// @Parameter(description = "페이지 번호(0부터 시작)", example = "0")
53-
// @RequestParam(defaultValue = "0") int page,
54-
// @Parameter(description = "페이지 크기", example = "10")
55-
// @RequestParam(defaultValue = "10") int size,
56-
// @AuthenticationPrincipal UserDetails userDetails
57-
// ) {
58-
// return weeklyCommentService.getWeeklyComments(
59-
// challengeId, weeksId, sort, order, page, size, userDetails
60-
// );
61-
// }
41+
@GetMapping
42+
@Operation(
43+
summary = "챌린지 주차별 댓글 목록 조회",
44+
description = "챌린지 주차별 댓글 및 대댓글을 조회합니다."
45+
)
46+
public WeeklyCommentDto.WeeklyCommentPageResponse getWeeklyComments(
47+
@PathVariable Long challengeId,
48+
@PathVariable Long weeksId,
49+
@PageableDefault @ParameterObject Pageable pageable,
50+
@AuthenticationPrincipal UserDetails userDetails
51+
) {
52+
return weeklyCommentService.getWeeklyComments(challengeId, weeksId, pageable, userDetails);
53+
}
54+
6255
}

src/main/java/targeter/aim/domain/challenge/dto/WeeklyCommentDto.java

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package targeter.aim.domain.challenge.dto;
22

33
import io.swagger.v3.oas.annotations.media.Schema;
4-
import jakarta.validation.constraints.NotBlank;
54
import lombok.AllArgsConstructor;
65
import lombok.Builder;
76
import lombok.Data;
@@ -10,7 +9,8 @@
109
import org.springframework.web.multipart.MultipartFile;
1110
import targeter.aim.domain.challenge.entity.WeeklyComment;
1211
import targeter.aim.domain.file.dto.FileDto;
13-
import targeter.aim.domain.user.dto.UserDto;
12+
import targeter.aim.domain.user.dto.TierDto;
13+
import targeter.aim.domain.user.entity.User;
1414

1515
import java.time.LocalDateTime;
1616
import java.util.List;
@@ -23,28 +23,20 @@ public class WeeklyCommentDto {
2323
@Builder
2424
@Schema(description = "페이지 정보")
2525
public static class PageInfo {
26-
@Schema(description = "현재 페이지(0부터 시작)", example = "0")
2726
private int page;
2827

29-
@Schema(description = "페이지 크기", example = "10")
3028
private int size;
3129

32-
@Schema(description = "전체 요소 수", example = "123")
3330
private long totalElements;
3431

35-
@Schema(description = "전체 페이지 수", example = "13")
3632
private int totalPages;
3733

38-
@Schema(description = "다음 페이지 존재 여부", example = "true")
39-
private boolean hasNext;
40-
4134
public static PageInfo from(Page<?> page) {
4235
return PageInfo.builder()
4336
.page(page.getNumber())
4437
.size(page.getSize())
4538
.totalElements(page.getTotalElements())
4639
.totalPages(page.getTotalPages())
47-
.hasNext(page.hasNext())
4840
.build();
4941
}
5042
}
@@ -54,20 +46,51 @@ public static PageInfo from(Page<?> page) {
5446
@NoArgsConstructor
5547
@Builder
5648
@Schema(description = "챌린지 주차별 댓글 목록 조회 응답")
57-
public static class WeeklyCommentListResponse {
49+
public static class WeeklyCommentPageResponse {
5850
@Schema(description = "댓글 목록(부모 댓글 + childrenComments 포함)")
5951
private List<WeeklyCommentResponse> comments;
6052

6153
@Schema(description = "페이지 메타 정보")
6254
private PageInfo pageInfo;
6355

64-
public static WeeklyCommentListResponse of(
65-
Page<?> page,
66-
List<WeeklyCommentResponse> comments
67-
) {
68-
return WeeklyCommentListResponse.builder()
69-
.pageInfo(PageInfo.from(page))
70-
.comments(comments)
56+
public static WeeklyCommentPageResponse from(Page<WeeklyCommentResponse> page) {
57+
return new WeeklyCommentPageResponse(
58+
page.getContent(),
59+
new PageInfo(
60+
page.getSize(),
61+
page.getNumber(),
62+
page.getTotalElements(),
63+
page.getTotalPages()
64+
)
65+
);
66+
}
67+
}
68+
69+
@Data
70+
@NoArgsConstructor
71+
@AllArgsConstructor
72+
@Builder
73+
@Schema(description = "댓글 작성자 정보")
74+
public static class UserResponse {
75+
76+
@Schema(description = "유저 아이디", example = "1")
77+
private Long userId;
78+
79+
@Schema(description = "유저 닉네임", example = "닉네임")
80+
private String nickname;
81+
82+
@Schema(description = "티어명", example = "BRONZE")
83+
private TierDto.TierResponse tier;
84+
85+
@Schema(description = "프로필 이미지")
86+
private FileDto.FileResponse profileImage;
87+
88+
public static UserResponse from(User user) {
89+
return UserResponse.builder()
90+
.userId(user.getId())
91+
.nickname(user.getNickname())
92+
.tier(TierDto.TierResponse.from(user.getTier()))
93+
.profileImage(FileDto.FileResponse.from(user.getProfileImage()))
7194
.build();
7295
}
7396
}
@@ -84,8 +107,8 @@ public static class WeeklyCommentResponse {
84107
@Schema(description = "댓글/대댓글 깊이(1: 댓글, 2:대댓글)", example = "1")
85108
private Integer depth;
86109

87-
@Schema(description = "작성자 정보, 여기서 nickname, profileImage, tier 사용")
88-
private UserDto.UserResponse writerInfo;
110+
@Schema(description = "작성자 정보")
111+
private UserResponse writerInfo;
89112

90113
@Schema(description = "댓글 내용", example = "댓글 내용")
91114
private String content;
@@ -99,17 +122,14 @@ public static class WeeklyCommentResponse {
99122
@Schema(description = "댓글 작성 날짜", example = "ISO DateTime")
100123
private LocalDateTime createdAt;
101124

102-
@Schema(description = "최종 수정 날짜", example = "ISO DateTime")
103-
private LocalDateTime updatedAt;
104-
105125
@Schema(description = "자식 댓글 목록")
106126
private List<WeeklyCommentResponse> childrenComments;
107127

108128
public static WeeklyCommentResponse from(WeeklyComment weeklyComment) {
109129
return WeeklyCommentResponse.builder()
110130
.commentId(weeklyComment.getId())
111131
.depth(weeklyComment.getDepth())
112-
.writerInfo(UserDto.UserResponse.from(weeklyComment.getUser()))
132+
.writerInfo(UserResponse.from(weeklyComment.getUser()))
113133
.content(weeklyComment.getContent())
114134
.attachedImages(weeklyComment.getAttachedImages().stream()
115135
.map(FileDto.FileResponse::from)
@@ -118,7 +138,6 @@ public static WeeklyCommentResponse from(WeeklyComment weeklyComment) {
118138
.map(FileDto.FileResponse::from)
119139
.toList())
120140
.createdAt(weeklyComment.getCreatedAt())
121-
.updatedAt(weeklyComment.getLastModifiedAt())
122141
.childrenComments(List.of())
123142
.build();
124143
}

src/main/java/targeter/aim/domain/challenge/entity/WeeklyComment.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import targeter.aim.common.auditor.TimeStampedEntity;
77
import targeter.aim.domain.file.entity.ChallengeCommentAttachedFile;
88
import targeter.aim.domain.file.entity.ChallengeCommentImage;
9-
import targeter.aim.domain.file.entity.ChallengeProofAttachedFile;
10-
import targeter.aim.domain.file.entity.ChallengeProofImage;
119
import targeter.aim.domain.user.entity.User;
1210

1311
import java.util.ArrayList;
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package targeter.aim.domain.challenge.repository;
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.PageImpl;
7+
import org.springframework.data.domain.Pageable;
8+
import org.springframework.stereotype.Repository;
9+
import targeter.aim.domain.challenge.dto.WeeklyCommentDto;
10+
import targeter.aim.domain.challenge.entity.WeeklyComment;
11+
import targeter.aim.domain.file.dto.FileDto;
12+
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.stream.Collectors;
17+
import java.util.stream.Stream;
18+
19+
import static targeter.aim.domain.challenge.entity.QWeeklyComment.weeklyComment;
20+
import static targeter.aim.domain.challenge.entity.QWeeklyProgress.weeklyProgress;
21+
import static targeter.aim.domain.file.entity.QChallengeCommentAttachedFile.challengeCommentAttachedFile;
22+
import static targeter.aim.domain.file.entity.QChallengeCommentImage.challengeCommentImage;
23+
import static targeter.aim.domain.file.entity.QProfileImage.profileImage;
24+
import static targeter.aim.domain.user.entity.QTier.tier;
25+
import static targeter.aim.domain.user.entity.QUser.user;
26+
27+
@Repository
28+
@RequiredArgsConstructor
29+
public class WeeklyCommentQueryRepository {
30+
31+
private final JPAQueryFactory queryFactory;
32+
33+
public Page<WeeklyCommentDto.WeeklyCommentResponse> paginateByChallengeIdAndWeeksId(
34+
Long challengeId,
35+
Long weeksId,
36+
Pageable pageable
37+
) {
38+
List<WeeklyComment> parentComments = queryFactory
39+
.selectFrom(weeklyComment)
40+
.join(weeklyComment.weeklyProgress, weeklyProgress)
41+
.leftJoin(weeklyComment.user, user).fetchJoin()
42+
.leftJoin(user.tier, tier).fetchJoin()
43+
.leftJoin(user.profileImage, profileImage).fetchJoin()
44+
.where(
45+
weeklyProgress.challenge.id.eq(challengeId),
46+
weeklyProgress.id.eq(weeksId),
47+
weeklyComment.parentComment.isNull()
48+
)
49+
.orderBy(weeklyComment.createdAt.asc())
50+
.offset(pageable.getOffset())
51+
.limit(pageable.getPageSize())
52+
.fetch();
53+
54+
Long total = queryFactory
55+
.select(weeklyComment.count())
56+
.from(weeklyComment)
57+
.join(weeklyComment.weeklyProgress, weeklyProgress)
58+
.where(
59+
weeklyProgress.challenge.id.eq(challengeId),
60+
weeklyProgress.id.eq(weeksId),
61+
weeklyComment.parentComment.isNull()
62+
)
63+
.fetchOne();
64+
65+
if (parentComments.isEmpty()) {
66+
return new PageImpl<>(Collections.emptyList(), pageable, total != null ? total : 0);
67+
}
68+
69+
List<Long> parentIds = parentComments.stream()
70+
.map(WeeklyComment::getId)
71+
.toList();
72+
73+
List<WeeklyComment> childComments = queryFactory
74+
.selectFrom(weeklyComment)
75+
.leftJoin(weeklyComment.user, user).fetchJoin()
76+
.leftJoin(user.tier, tier).fetchJoin()
77+
.leftJoin(user.profileImage, profileImage).fetchJoin()
78+
.where(weeklyComment.parentComment.id.in(parentIds))
79+
.orderBy(weeklyComment.createdAt.asc())
80+
.fetch();
81+
82+
List<Long> allCommentIds = Stream.concat(parentComments.stream(), childComments.stream())
83+
.map(WeeklyComment::getId)
84+
.distinct()
85+
.toList();
86+
87+
Map<Long, List<FileDto.FileResponse>> imageMap = fetchImages(allCommentIds);
88+
Map<Long, List<FileDto.FileResponse>> fileMap = fetchFiles(allCommentIds);
89+
90+
List<WeeklyCommentDto.WeeklyCommentResponse> content = assembleHierarchy(
91+
parentComments,
92+
childComments,
93+
imageMap,
94+
fileMap
95+
);
96+
97+
return new PageImpl<>(content, pageable, total != null ? total : 0);
98+
}
99+
100+
private List<WeeklyCommentDto.WeeklyCommentResponse> assembleHierarchy(
101+
List<WeeklyComment> parents,
102+
List<WeeklyComment> children,
103+
Map<Long, List<FileDto.FileResponse>> imageMap,
104+
Map<Long, List<FileDto.FileResponse>> fileMap
105+
) {
106+
Map<Long, List<WeeklyComment>> childrenMap = children.stream()
107+
.collect(Collectors.groupingBy(c -> c.getParentComment().getId()));
108+
109+
return parents.stream()
110+
.map(parent -> {
111+
WeeklyCommentDto.WeeklyCommentResponse parentDto = mapToDto(parent, imageMap, fileMap);
112+
113+
List<WeeklyComment> myChildren = childrenMap.getOrDefault(parent.getId(), Collections.emptyList());
114+
115+
List<WeeklyCommentDto.WeeklyCommentResponse> childDtos = myChildren.stream()
116+
.map(child -> mapToDto(child, imageMap, fileMap))
117+
.toList();
118+
119+
parentDto.setChildrenComments(childDtos);
120+
return parentDto;
121+
})
122+
.toList();
123+
}
124+
125+
private WeeklyCommentDto.WeeklyCommentResponse mapToDto(
126+
WeeklyComment c,
127+
Map<Long, List<FileDto.FileResponse>> imageMap,
128+
Map<Long, List<FileDto.FileResponse>> fileMap
129+
) {
130+
List<FileDto.FileResponse> images = imageMap.getOrDefault(c.getId(), Collections.emptyList());
131+
List<FileDto.FileResponse> files = fileMap.getOrDefault(c.getId(), Collections.emptyList());
132+
133+
return WeeklyCommentDto.WeeklyCommentResponse.builder()
134+
.commentId(c.getId())
135+
.depth(c.getDepth())
136+
.writerInfo(WeeklyCommentDto.UserResponse.from(c.getUser()))
137+
.content(c.getContent())
138+
.attachedImages(images)
139+
.attachedFiles(files)
140+
.createdAt(c.getCreatedAt())
141+
.childrenComments(Collections.emptyList())
142+
.build();
143+
}
144+
145+
private Map<Long, List<FileDto.FileResponse>> fetchImages(List<Long> commentIds) {
146+
return queryFactory
147+
.selectFrom(challengeCommentImage)
148+
.where(challengeCommentImage.weeklyComment.id.in(commentIds))
149+
.fetch()
150+
.stream()
151+
.collect(Collectors.groupingBy(
152+
img -> img.getWeeklyComment().getId(),
153+
Collectors.mapping(FileDto.FileResponse::from, Collectors.toList())
154+
));
155+
}
156+
157+
private Map<Long, List<FileDto.FileResponse>> fetchFiles(List<Long> commentIds) {
158+
return queryFactory
159+
.selectFrom(challengeCommentAttachedFile)
160+
.where(challengeCommentAttachedFile.weeklyComment.id.in(commentIds))
161+
.fetch()
162+
.stream()
163+
.collect(Collectors.groupingBy(
164+
file -> file.getWeeklyComment().getId(),
165+
Collectors.mapping(FileDto.FileResponse::from, Collectors.toList())
166+
));
167+
}
168+
}

0 commit comments

Comments
 (0)