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
Expand Up @@ -6,6 +6,8 @@
import com.daramg.server.composer.dto.ComposerResponseDto;
import com.daramg.server.composer.repository.ComposerLikeRepository;
import com.daramg.server.composer.repository.ComposerRepository;
import com.daramg.server.post.dto.StoryPostStatsDto;
import com.daramg.server.post.repository.PostQueryRepository;
import com.daramg.server.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -14,6 +16,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -24,17 +27,20 @@ public class ComposerQueryService {

private final ComposerRepository composerRepository;
private final ComposerLikeRepository composerLikeRepository;
private final PostQueryRepository postQueryRepository;

public List<ComposerResponseDto> getAllComposers(User user, List<Era> eras, List<Continent> continents) {
Set<Long> likedComposerIds = getLikedComposerIds(user);
Map<Long, StoryPostStatsDto> statsMap = postQueryRepository.findStoryPostStatsByAllComposers();
List<Composer> allComposers = composerRepository.findAll();

return allComposers.stream()
.filter(composer -> eras == null || eras.isEmpty() || eras.contains(composer.getEra()))
.filter(composer -> continents == null || continents.isEmpty() || continents.contains(composer.getContinent()))
.map(composer -> ComposerResponseDto.from(
composer,
likedComposerIds.contains(composer.getId())
likedComposerIds.contains(composer.getId()),
statsMap.get(composer.getId())
))
.sorted(
Comparator.comparing(ComposerResponseDto::isLiked).reversed()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.daramg.server.composer.dto;

import com.daramg.server.composer.domain.Composer;
import com.daramg.server.post.dto.StoryPostStatsDto;
import com.fasterxml.jackson.annotation.JsonInclude;

import java.time.Instant;

public record ComposerResponseDto(
long composerId,
Expand All @@ -12,9 +16,12 @@ public record ComposerResponseDto(
Short birthYear,
Short deathYear,
String bio,
boolean isLiked
boolean isLiked,
long storyPostCount,
@JsonInclude(JsonInclude.Include.NON_NULL)
Instant lastStoryPostAt
) {
public static ComposerResponseDto from(Composer composer, boolean isLiked) {
public static ComposerResponseDto from(Composer composer, boolean isLiked, StoryPostStatsDto stats) {
return new ComposerResponseDto(
composer.getId(),
composer.getKoreanName(),
Expand All @@ -25,7 +32,9 @@ public static ComposerResponseDto from(Composer composer, boolean isLiked) {
composer.getBirthYear(),
composer.getDeathYear(),
composer.getBio(),
isLiked
isLiked,
stats != null ? stats.storyPostCount() : 0L,
stats != null ? stats.lastStoryPostAt() : null
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public PostDetailResponse getPostById(Long postId, User user) {
public ComposerWithPostsResponseDto getComposerWithPosts(Long composerId, PageRequestDto pageRequest, User user) {
Composer composer = entityUtils.getEntity(composerId, Composer.class);
boolean isLiked = user != null && composerLikeRepository.existsByComposerIdAndUserId(composerId, user.getId());
ComposerResponseDto composerDto = ComposerResponseDto.from(composer, isLiked);
ComposerResponseDto composerDto = ComposerResponseDto.from(composer, isLiked, null);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

ComposerResponseDto.from 메서드 시그니처 변경으로 인해 null을 전달하도록 수정하신 것으로 보입니다. 하지만 이렇게 되면 /composers/{composerId}/posts 엔드포인트의 응답에서 storyPostCount는 항상 0, lastStoryPostAt은 항상 null이 되어 /composers 목록 조회 API와 데이터 정합성이 맞지 않게 됩니다.

단일 작곡가에 대한 통계도 조회하여 ComposerResponseDto를 생성할 때 함께 전달하는 것이 좋겠습니다. PostQueryRepository에 단일 작곡가의 통계를 조회하는 메서드를 추가하여 해결할 수 있습니다.


List<Post> posts = postQueryRepository.getPostsByComposerIdWithPaging(composerId, pageRequest);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.daramg.server.post.dto;

import java.time.Instant;

public record StoryPostStatsDto(long storyPostCount, Instant lastStoryPostAt) {}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import com.daramg.server.post.domain.FreePost;
import com.daramg.server.post.domain.Post;
import com.daramg.server.post.domain.StoryPost;
import com.daramg.server.post.dto.StoryPostStatsDto;

import java.util.List;
import java.util.Map;

public interface PostQueryRepository {
List<FreePost> getAllFreePostsWithPaging(PageRequestDto pageRequest);
Expand All @@ -18,4 +20,5 @@ public interface PostQueryRepository {
List<Post> getUserDraftPostsWithPaging(Long userId, PageRequestDto pageRequest);
List<Post> getUserScrappedPostsWithPaging(Long userId, PageRequestDto pageRequest);
List<Post> getPostsByComposerIdWithPaging(Long composerId, PageRequestDto pageRequest);
Map<Long, StoryPostStatsDto> findStoryPostStatsByAllComposers();
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import com.daramg.server.post.domain.PostStatus;
import com.daramg.server.post.domain.QPost;
import com.daramg.server.post.domain.StoryPost;
import com.daramg.server.post.dto.StoryPostStatsDto;
import com.daramg.server.composer.domain.QComposer;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery;
Expand All @@ -21,6 +23,8 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.daramg.server.post.domain.QPost.post;
import static com.daramg.server.post.domain.QPostScrap.postScrap;
Expand Down Expand Up @@ -183,6 +187,31 @@ public List<Post> getPostsByComposerIdWithPaging(Long composerId, PageRequestDto
);
}

@Override
public Map<Long, StoryPostStatsDto> findStoryPostStatsByAllComposers() {
List<Tuple> results = queryFactory
.select(
storyPost.primaryComposer.id,
storyPost._super.id.count(),
storyPost._super.createdAt.max()
)
.from(storyPost)
.where(
storyPost._super.isBlocked.isFalse()
.and(storyPost._super.postStatus.eq(PostStatus.PUBLISHED))
)
.groupBy(storyPost.primaryComposer.id)
.fetch();

return results.stream().collect(Collectors.toMap(
tuple -> tuple.get(storyPost.primaryComposer.id),
tuple -> new StoryPostStatsDto(
tuple.get(storyPost._super.id.count()),
tuple.get(storyPost._super.createdAt.max())
)
));
Comment on lines +192 to +212

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 구현은 fetch()List<Tuple>을 가져온 후, stream을 사용해 Map으로 변환하고 있습니다. 이는 올바르게 동작하지만, QueryDSL의 transformgroupBy를 사용하면 더 간결하고 효율적인 코드를 작성할 수 있습니다. transform을 사용하면 중간 단계의 List 생성 없이 바로 Map을 생성할 수 있습니다.

아래와 같이 리팩토링하는 것을 고려해보세요.

Suggested change
List<Tuple> results = queryFactory
.select(
storyPost.primaryComposer.id,
storyPost._super.id.count(),
storyPost._super.createdAt.max()
)
.from(storyPost)
.where(
storyPost._super.isBlocked.isFalse()
.and(storyPost._super.postStatus.eq(PostStatus.PUBLISHED))
)
.groupBy(storyPost.primaryComposer.id)
.fetch();
return results.stream().collect(Collectors.toMap(
tuple -> tuple.get(storyPost.primaryComposer.id),
tuple -> new StoryPostStatsDto(
tuple.get(storyPost._super.id.count()),
tuple.get(storyPost._super.createdAt.max())
)
));
return queryFactory
.from(storyPost)
.where(
storyPost._super.isBlocked.isFalse()
.and(storyPost._super.postStatus.eq(PostStatus.PUBLISHED))
)
.groupBy(storyPost.primaryComposer.id)
.transform(com.querydsl.core.group.GroupBy.groupBy(storyPost.primaryComposer.id).as(
com.querydsl.core.types.Projections.constructor(StoryPostStatsDto.class,
storyPost._super.id.count(),
storyPost._super.createdAt.max()
)
));

}

private <T extends Post> List<T> getAllPostsWithPaging(
PageRequestDto pageRequest,
EntityPathBase<T> qEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import com.daramg.server.composer.dto.ComposerResponseDto;
import com.daramg.server.composer.repository.ComposerLikeRepository;
import com.daramg.server.composer.repository.ComposerRepository;
import com.daramg.server.post.domain.PostStatus;
import com.daramg.server.post.domain.StoryPost;
import com.daramg.server.post.domain.vo.PostCreateVo;
import com.daramg.server.post.repository.PostRepository;
import com.daramg.server.testsupport.support.ServiceTestSupport;
import com.daramg.server.user.domain.User;
import com.daramg.server.user.repository.UserRepository;
Expand Down Expand Up @@ -35,6 +39,9 @@ public class ComposerQueryServiceTest extends ServiceTestSupport {
@Autowired
private UserRepository userRepository;

@Autowired
private PostRepository postRepository;

private User user;
private Composer corelli; // 코렐리
private Composer vitali; // 비탈리
Expand Down Expand Up @@ -180,6 +187,33 @@ void filters_limitResultsByEraAndContinent() {
assertThat(result.getFirst().isLiked()).isFalse();
}

@Test
@DisplayName("스토리 게시글이 있는 작곡가는 storyPostCount와 lastStoryPostAt이 반환된다")
void storyPostStats_returnedCorrectly() {
// given - 비발디에 PUBLISHED 스토리 2개, 코렐리에 1개 저장
postRepository.save(StoryPost.from(new PostCreateVo.Story(user, "비발디 글1", "내용", PostStatus.PUBLISHED, List.of(), null, List.of(), vivaldi)));
postRepository.save(StoryPost.from(new PostCreateVo.Story(user, "비발디 글2", "내용", PostStatus.PUBLISHED, List.of(), null, List.of(), vivaldi)));
postRepository.save(StoryPost.from(new PostCreateVo.Story(user, "코렐리 글1", "내용", PostStatus.PUBLISHED, List.of(), null, List.of(), corelli)));
// DRAFT는 집계 제외
postRepository.save(StoryPost.from(new PostCreateVo.Story(user, "비탈리 초안", "내용", PostStatus.DRAFT, List.of(), null, List.of(), vitali)));

// when
List<ComposerResponseDto> result = composerQueryService.getAllComposers(null, null, null);

// then
ComposerResponseDto vivaldResult = result.stream().filter(r -> r.koreanName().equals("비발디")).findFirst().orElseThrow();
assertThat(vivaldResult.storyPostCount()).isEqualTo(2L);
assertThat(vivaldResult.lastStoryPostAt()).isNotNull();

ComposerResponseDto corelliResult = result.stream().filter(r -> r.koreanName().equals("코렐리")).findFirst().orElseThrow();
assertThat(corelliResult.storyPostCount()).isEqualTo(1L);
assertThat(corelliResult.lastStoryPostAt()).isNotNull();

ComposerResponseDto vitaliResult = result.stream().filter(r -> r.koreanName().equals("비탈리")).findFirst().orElseThrow();
assertThat(vitaliResult.storyPostCount()).isEqualTo(0L);
assertThat(vitaliResult.lastStoryPostAt()).isNull();
}

@Test
@DisplayName("빈 리스트 필터는 미적용으로 간주한다")
void emptyLists_areTreatedAsNoFilter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ public class ComposerQueryControllerTest extends ControllerTestSupport {
void 작곡가_목록을_조회한다() throws Exception {
// given
List<ComposerResponseDto> response = List.of(
new ComposerResponseDto(1L, "코렐리", "Arcangelo Corelli", "Arcangelo Corelli", "이탈리아", "MALE", (short) 1653, (short) 1713, "바이올린의 따스한 숨결이 서로를 감싸는 밤", true),
new ComposerResponseDto(2L, "비탈리", "Tomaso Antonio Vitali", "Tomaso Antonio Vitali", "이탈리아", "MALE", (short) 1663, (short) 1745, "서정이 흐르는 현의 떨림, 조용히 마음을 울리는 음악", false),
new ComposerResponseDto(3L, "A. 스카를라티", "Alessandro Scarlatti", "Alessandro Scarlatti", "이탈리아", "MALE", (short) 1660, (short) 1725, "수많은 이야기 속에 피어나는 이탈리아의 정열", true),
new ComposerResponseDto(4L, "D. 스카를라티", "Domenico Scarlatti", "Domenico Scarlatti", "이탈리아", "MALE", (short) 1685, (short) 1757, "하늘을 나는 건반 위 상상, 자유로운 영혼의 소나타", false),
new ComposerResponseDto(5L, "비발디", "Antonio Vivaldi", "Antonio Vivaldi", "이탈리아", "MALE", (short) 1678, (short) 1741, "빨간 머리의 계절처럼 쏟아지는 생명과 빛", true),
new ComposerResponseDto(6L, "타르티니", "Giuseppe Tartini", "Giuseppe Tartini", "이탈리아", "MALE", (short) 1692, (short) 1770, "악마도 울릴 만큼 깊은 꿈결, 신비로운 선율의 마법", false),
new ComposerResponseDto(7L, "파헬벨", "Johann Pachelbel", "Johann Pachelbel", "독일", "MALE", (short) 1653, (short) 1706, "시간 너머의 따뜻한 안식, 평화로운 하루의 시작", true),
new ComposerResponseDto(8L, "마테존", "Johann Mattheson", "Johann Mattheson", "독일", "MALE", (short) 1681, (short) 1764, "생각과 음악이 나란히 걷는 길, 새로움을 질문하는 순간", false)
new ComposerResponseDto(1L, "코렐리", "Arcangelo Corelli", "Arcangelo Corelli", "이탈리아", "MALE", (short) 1653, (short) 1713, "바이올린의 따스한 숨결이 서로를 감싸는 밤", true, 3L, Instant.parse("2024-03-01T12:00:00Z")),
new ComposerResponseDto(2L, "비탈리", "Tomaso Antonio Vitali", "Tomaso Antonio Vitali", "이탈리아", "MALE", (short) 1663, (short) 1745, "서정이 흐르는 현의 떨림, 조용히 마음을 울리는 음악", false, 0L, null),
new ComposerResponseDto(3L, "A. 스카를라티", "Alessandro Scarlatti", "Alessandro Scarlatti", "이탈리아", "MALE", (short) 1660, (short) 1725, "수많은 이야기 속에 피어나는 이탈리아의 정열", true, 1L, Instant.parse("2024-02-15T09:00:00Z")),
new ComposerResponseDto(4L, "D. 스카를라티", "Domenico Scarlatti", "Domenico Scarlatti", "이탈리아", "MALE", (short) 1685, (short) 1757, "하늘을 나는 건반 위 상상, 자유로운 영혼의 소나타", false, 0L, null),
new ComposerResponseDto(5L, "비발디", "Antonio Vivaldi", "Antonio Vivaldi", "이탈리아", "MALE", (short) 1678, (short) 1741, "빨간 머리의 계절처럼 쏟아지는 생명과 빛", true, 5L, Instant.parse("2024-03-10T18:30:00Z")),
new ComposerResponseDto(6L, "타르티니", "Giuseppe Tartini", "Giuseppe Tartini", "이탈리아", "MALE", (short) 1692, (short) 1770, "악마도 울릴 만큼 깊은 꿈결, 신비로운 선율의 마법", false, 2L, Instant.parse("2024-01-20T14:00:00Z")),
new ComposerResponseDto(7L, "파헬벨", "Johann Pachelbel", "Johann Pachelbel", "독일", "MALE", (short) 1653, (short) 1706, "시간 너머의 따뜻한 안식, 평화로운 하루의 시작", true, 0L, null),
new ComposerResponseDto(8L, "마테존", "Johann Mattheson", "Johann Mattheson", "독일", "MALE", (short) 1681, (short) 1764, "생각과 음악이 나란히 걷는 길, 새로움을 질문하는 순간", false, 4L, Instant.parse("2024-03-05T10:00:00Z"))
);
when(composerQueryService.getAllComposers(any(), any(), any())).thenReturn(response);

Expand Down Expand Up @@ -81,7 +81,9 @@ public class ComposerQueryControllerTest extends ControllerTestSupport {
fieldWithPath("[].birthYear").type(JsonFieldType.NUMBER).description("작곡가 출생년도").optional(),
fieldWithPath("[].deathYear").type(JsonFieldType.NUMBER).description("작곡가 사망년도").optional(),
fieldWithPath("[].bio").type(JsonFieldType.STRING).description("작곡가 소개").optional(),
fieldWithPath("[].isLiked").type(JsonFieldType.BOOLEAN).description("현재 유저의 좋아요 여부 (비로그인 시 false)")
fieldWithPath("[].isLiked").type(JsonFieldType.BOOLEAN).description("현재 유저의 좋아요 여부 (비로그인 시 false)"),
fieldWithPath("[].storyPostCount").type(JsonFieldType.NUMBER).description("스토리 게시글 수"),
fieldWithPath("[].lastStoryPostAt").type(JsonFieldType.STRING).description("가장 최근 스토리 게시글 작성 시각 (게시글 없으면 null)").optional()
)
.build()
)
Expand All @@ -94,7 +96,8 @@ public class ComposerQueryControllerTest extends ControllerTestSupport {
Long composerId = 1L;
ComposerResponseDto composerDto = new ComposerResponseDto(
1L, "코렐리", "Arcangelo Corelli", "Arcangelo Corelli", "이탈리아", "MALE",
(short) 1653, (short) 1713, "“바이올린의 따스한 숨결이 서로를 감싸는 밤”", true
(short) 1653, (short) 1713, "바이올린의 따스한 숨결이 서로를 감싸는 밤", true,
3L, Instant.parse("2024-01-15T10:30:00Z")
);

PostResponseDto post1 = new PostResponseDto(
Expand Down Expand Up @@ -176,6 +179,8 @@ public class ComposerQueryControllerTest extends ControllerTestSupport {
fieldWithPath("composer.deathYear").type(JsonFieldType.NUMBER).description("작곡가 사망년도").optional(),
fieldWithPath("composer.bio").type(JsonFieldType.STRING).description("작곡가 소개").optional(),
fieldWithPath("composer.isLiked").type(JsonFieldType.BOOLEAN).description("현재 유저의 좋아요 여부 (비로그인 시 false)"),
fieldWithPath("composer.storyPostCount").type(JsonFieldType.NUMBER).description("스토리 게시글 수"),
fieldWithPath("composer.lastStoryPostAt").type(JsonFieldType.STRING).description("가장 최근 스토리 게시글 작성 시각 (게시글 없으면 null)").optional(),
fieldWithPath("posts").type(JsonFieldType.OBJECT).description("포스트 목록 페이징 정보"),
fieldWithPath("posts.content").type(JsonFieldType.ARRAY).description("포스트 목록"),
fieldWithPath("posts.content[].id").type(JsonFieldType.NUMBER).description("포스트 ID"),
Expand Down
Loading