Skip to content

Commit 144902c

Browse files
authored
[feat/#137] 기록에 좋아요한 유저 목록을 조회 (#138)
* init : 파일 생성 * feat : 좋아요한 유저 조회 구현 * fix : projection 적용, n+1 문제 해결 * chore : 코드 정리 * fix : 코드 정리
1 parent e14492b commit 144902c

10 files changed

Lines changed: 465 additions & 29 deletions

File tree

clokey-api/src/main/java/org/clokey/domain/like/controller/LikeController.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import lombok.RequiredArgsConstructor;
77
import org.clokey.code.GlobalBaseSuccessCode;
88
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
9+
import org.clokey.domain.like.dto.response.LikedMembersResponse;
910
import org.clokey.domain.like.service.LikeService;
1011
import org.clokey.global.annotation.PageSize;
1112
import org.clokey.response.BaseResponse;
@@ -26,8 +27,27 @@ public class LikeController {
2627

2728
private final LikeService likeService;
2829

30+
@GetMapping("/users")
31+
@Operation(
32+
operationId = "Like_getLikedMembers",
33+
summary = "좋아요한 유저 조회",
34+
description = "내 기록을 좋아요한 유저를 조회합니다")
35+
public BaseResponse<SliceResponse<LikedMembersResponse.LikedMemberPreview>> getLikedMembers(
36+
@Parameter(description = "기록 ID") @RequestParam Long historyId,
37+
@Parameter(description = "이전 페이지의 좋아요 ID (첫 요청 시 생략)") @RequestParam(required = false)
38+
Long lastLikeId,
39+
@Parameter(description = "페이지당 조회할 개수") @RequestParam @PageSize Integer size) {
40+
SliceResponse<LikedMembersResponse.LikedMemberPreview> response =
41+
likeService.getLikedMembers(historyId, lastLikeId, size);
42+
43+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response);
44+
}
45+
2946
@GetMapping("/histories")
30-
@Operation(summary = "좋아요한 기록 조회", description = "사용자가 좋아요한 기록을 조회합니다.")
47+
@Operation(
48+
operationId = "Like_getLikedHistories",
49+
summary = "좋아요한 기록 조회",
50+
description = "사용자가 좋아요한 기록을 조회합니다.")
3151
public BaseResponse<SliceResponse<LikedHistoriesResponse.LikedHistoryPreview>>
3252
getLikedHistories(
3353
@Parameter(description = "이전 페이지의 좋아요 ID (첫 요청 시 생략)")

clokey-api/src/main/java/org/clokey/domain/like/dto/response/LikedHistoriesResponse.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,40 @@ public record LikedHistoriesResponse(
99
@Schema(description = "마지막 페이지 여부", example = "false") boolean isLast) {
1010

1111
@Schema(description = "히스토리 미리보기 DTO")
12-
public record LikedHistoryPreview(
13-
@Schema(description = "히스토리 ID", example = "30") Long id,
14-
@Schema(
15-
description = "히스토리 대표 이미지 URL",
16-
example =
17-
"https://clokeybucket.s3.ap-northeast-2.amazonaws.com/example.jpg")
18-
String imageUrl) {}
12+
public static class LikedHistoryPreview {
13+
@Schema(description = "히스토리 ID", example = "30")
14+
private final Long id;
15+
16+
@Schema(
17+
description = "히스토리 대표 이미지 URL",
18+
example = "https://clokeybucket.s3.ap-northeast-2.amazonaws.com/example.jpg")
19+
private String imageUrl;
20+
21+
@Schema(description = "다음 페이지 조회를 위한 커서 ID (MemberLike ID)", example = "100")
22+
private final Long lastLikeId;
23+
24+
public LikedHistoryPreview(Long id, Long lastLikeId) {
25+
this.id = id;
26+
this.lastLikeId = lastLikeId;
27+
this.imageUrl = null;
28+
}
29+
30+
public LikedHistoryPreview(Long id, String imageUrl) {
31+
this.id = id;
32+
this.imageUrl = imageUrl;
33+
this.lastLikeId = null;
34+
}
35+
36+
public Long getId() {
37+
return id;
38+
}
39+
40+
public String getImageUrl() {
41+
return imageUrl;
42+
}
43+
44+
public Long getLastLikeId() {
45+
return lastLikeId;
46+
}
47+
}
1948
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.clokey.domain.like.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import java.util.List;
5+
import lombok.Getter;
6+
7+
@Schema(description = "좋아요 유저 조회 결과")
8+
public record LikedMembersResponse(
9+
@Schema(description = "유저 미리보기 목록") List<LikedMemberPreview> memberPreviews,
10+
@Schema(description = "마지막 페이지 여부", example = "false") boolean isLast) {
11+
12+
@Schema(description = "유저 미리보기 DTO")
13+
@Getter
14+
public static class LikedMemberPreview {
15+
@Schema(description = "유저 ID", example = "30")
16+
private final Long id;
17+
18+
@Schema(description = "클로키 ID", example = "@Clokey_USER1")
19+
private final String codiveId;
20+
21+
@Schema(description = "프로필 이미지 URL")
22+
private final String imageUrl;
23+
24+
@Schema(description = "닉네임")
25+
private final String nickname;
26+
27+
@Schema(description = "팔로우 여부")
28+
private final boolean followStatus;
29+
30+
@Schema(description = "다음 페이지 조회를 위한 커서 ID (MemberLike ID)", example = "100")
31+
private final Long lastLikeId;
32+
33+
public LikedMemberPreview(
34+
Long id, String codiveId, String imageUrl, String nickname, Long lastLikeId) {
35+
this.id = id;
36+
this.codiveId = codiveId;
37+
this.imageUrl = imageUrl;
38+
this.nickname = nickname;
39+
this.lastLikeId = lastLikeId;
40+
this.followStatus = false;
41+
}
42+
43+
public LikedMemberPreview(
44+
Long id, String codiveId, String imageUrl, String nickname, boolean followStatus) {
45+
this.id = id;
46+
this.codiveId = codiveId;
47+
this.imageUrl = imageUrl;
48+
this.nickname = nickname;
49+
this.followStatus = followStatus;
50+
this.lastLikeId = null;
51+
}
52+
}
53+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.clokey.domain.like.repository;
2+
3+
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
4+
import org.clokey.domain.like.dto.response.LikedMembersResponse;
5+
import org.springframework.data.domain.Slice;
6+
7+
public interface MemberLikeRepositoryCustom {
8+
9+
Slice<LikedHistoriesResponse.LikedHistoryPreview> findLikedHistoriesSliceByMemberId(
10+
Long memberId, Long lastLikeId, Integer size);
11+
12+
Slice<LikedMembersResponse.LikedMemberPreview> findLikedMembersSliceByHistoryId(
13+
Long historyId, Long lastLikeId, Integer size);
14+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.clokey.domain.like.repository;
2+
3+
import static org.clokey.like.entity.QMemberLike.memberLike;
4+
import static org.clokey.member.entity.QMember.member;
5+
6+
import com.querydsl.core.types.Projections;
7+
import com.querydsl.core.types.dsl.BooleanExpression;
8+
import com.querydsl.jpa.impl.JPAQueryFactory;
9+
import java.util.List;
10+
import lombok.RequiredArgsConstructor;
11+
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
12+
import org.clokey.domain.like.dto.response.LikedMembersResponse;
13+
import org.clokey.global.paging.SortDirection;
14+
import org.springframework.data.domain.PageRequest;
15+
import org.springframework.data.domain.Slice;
16+
import org.springframework.data.domain.SliceImpl;
17+
import org.springframework.stereotype.Repository;
18+
19+
@Repository
20+
@RequiredArgsConstructor
21+
public class MemberLikeRepositoryImpl implements MemberLikeRepositoryCustom {
22+
23+
private final JPAQueryFactory queryFactory;
24+
25+
private final SortDirection DEFAULT_SORT = SortDirection.DESC;
26+
27+
@Override
28+
public Slice<LikedHistoriesResponse.LikedHistoryPreview> findLikedHistoriesSliceByMemberId(
29+
Long memberId, Long lastLikeId, Integer size) {
30+
31+
List<LikedHistoriesResponse.LikedHistoryPreview> results =
32+
queryFactory
33+
.select(
34+
Projections.constructor(
35+
LikedHistoriesResponse.LikedHistoryPreview.class,
36+
memberLike.history.id,
37+
memberLike.id))
38+
.from(memberLike)
39+
.where(
40+
memberLike.member.id.eq(memberId),
41+
lastLikeIdCondition(lastLikeId, DEFAULT_SORT))
42+
.limit(size + 1)
43+
.orderBy(memberLike.id.desc())
44+
.fetch();
45+
46+
return checkLastPage(size, results);
47+
}
48+
49+
@Override
50+
public Slice<LikedMembersResponse.LikedMemberPreview> findLikedMembersSliceByHistoryId(
51+
Long historyId, Long lastLikeId, Integer size) {
52+
53+
List<LikedMembersResponse.LikedMemberPreview> results =
54+
queryFactory
55+
.select(
56+
Projections.constructor(
57+
LikedMembersResponse.LikedMemberPreview.class,
58+
member.id,
59+
member.clokeyId,
60+
member.profileImageUrl,
61+
member.nickname,
62+
memberLike.id))
63+
.from(memberLike)
64+
.join(memberLike.member, member)
65+
.where(
66+
memberLike.history.id.eq(historyId),
67+
lastLikeIdCondition(lastLikeId, DEFAULT_SORT))
68+
.limit(size + 1)
69+
.orderBy(memberLike.id.desc())
70+
.fetch();
71+
72+
return checkLastPage(size, results);
73+
}
74+
75+
private BooleanExpression lastLikeIdCondition(Long likeId, SortDirection direction) {
76+
if (likeId == null) {
77+
return null;
78+
}
79+
return memberLike.id.lt(likeId);
80+
}
81+
82+
private <T> Slice<T> checkLastPage(int pageSize, List<T> results) {
83+
boolean hasNext = false;
84+
85+
if (results.size() > pageSize) {
86+
hasNext = true;
87+
results.remove(pageSize);
88+
}
89+
90+
return new SliceImpl<>(results, PageRequest.of(0, pageSize), hasNext);
91+
}
92+
}

clokey-api/src/main/java/org/clokey/domain/like/service/LikeService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package org.clokey.domain.like.service;
22

33
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
4+
import org.clokey.domain.like.dto.response.LikedMembersResponse;
45
import org.clokey.response.SliceResponse;
56

67
public interface LikeService {
8+
SliceResponse<LikedMembersResponse.LikedMemberPreview> getLikedMembers(
9+
Long historyId, Long lastLikedId, Integer size);
10+
711
SliceResponse<LikedHistoriesResponse.LikedHistoryPreview> getLikedHistories(
812
Long lastLikedId, Integer size);
913

clokey-api/src/main/java/org/clokey/domain/like/service/LikeServiceImpl.java

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
package org.clokey.domain.like.service;
22

3+
import java.util.HashSet;
34
import java.util.List;
45
import java.util.Map;
56
import java.util.Optional;
7+
import java.util.Set;
68
import java.util.stream.Collectors;
79
import lombok.RequiredArgsConstructor;
810
import org.clokey.domain.history.exception.HistoryErrorCode;
911
import org.clokey.domain.history.repository.HistoryImageRepository;
1012
import org.clokey.domain.history.repository.HistoryRepository;
1113
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
14+
import org.clokey.domain.like.dto.response.LikedMembersResponse;
1215
import org.clokey.domain.like.repository.MemberLikeRepository;
16+
import org.clokey.domain.like.repository.MemberLikeRepositoryCustom;
1317
import org.clokey.domain.member.repository.BlockRepository;
18+
import org.clokey.domain.member.repository.FollowRepository;
1419
import org.clokey.exception.BaseCustomException;
1520
import org.clokey.global.util.MemberUtil;
1621
import org.clokey.history.entity.History;
1722
import org.clokey.like.entity.MemberLike;
1823
import org.clokey.member.entity.Member;
1924
import org.clokey.response.SliceResponse;
20-
import org.springframework.data.domain.PageRequest;
21-
import org.springframework.data.domain.Pageable;
25+
import org.springframework.data.domain.Slice;
2226
import org.springframework.stereotype.Service;
2327
import org.springframework.transaction.annotation.Transactional;
2428

@@ -32,44 +36,39 @@ public class LikeServiceImpl implements LikeService {
3236
private final HistoryImageRepository historyImageRepository;
3337
private final HistoryRepository historyRepository;
3438
private final BlockRepository blockRepository;
39+
private final FollowRepository followRepository;
40+
private final MemberLikeRepositoryCustom memberLikeRepositoryCustom;
3541

3642
@Override
3743
public SliceResponse<LikedHistoriesResponse.LikedHistoryPreview> getLikedHistories(
3844
Long lastLikeId, Integer size) {
3945

4046
Member currentMember = memberUtil.getCurrentMember();
4147

42-
// limit + 1 조회
43-
Pageable pageable = PageRequest.of(0, size + 1);
48+
Slice<LikedHistoriesResponse.LikedHistoryPreview> likedHistoriesSlice =
49+
memberLikeRepositoryCustom.findLikedHistoriesSliceByMemberId(
50+
currentMember.getId(), lastLikeId, size);
4451

45-
List<MemberLike> likes =
46-
memberLikeRepository.findLikedHistoriesByMemberId(
47-
currentMember.getId(), lastLikeId, pageable);
48-
49-
boolean isLast = likes.size() <= size;
50-
51-
if (!isLast) {
52-
likes = likes.subList(0, size);
53-
}
54-
55-
if (likes.isEmpty()) {
52+
if (likedHistoriesSlice.isEmpty()) {
5653
return new SliceResponse<>(List.of(), true);
5754
}
5855

59-
List<Long> historyIds = likes.stream().map(like -> like.getHistory().getId()).toList();
56+
List<Long> historyIds =
57+
likedHistoriesSlice.getContent().stream()
58+
.map(LikedHistoriesResponse.LikedHistoryPreview::getId)
59+
.toList();
6060

6161
Map<Long, String> imageMap = findFirstImagesByHistoryIds(historyIds);
6262

6363
List<LikedHistoriesResponse.LikedHistoryPreview> previews =
64-
likes.stream()
64+
likedHistoriesSlice.getContent().stream()
6565
.map(
66-
like ->
66+
preview ->
6767
new LikedHistoriesResponse.LikedHistoryPreview(
68-
like.getHistory().getId(),
69-
imageMap.get(like.getHistory().getId())))
68+
preview.getId(), imageMap.get(preview.getId())))
7069
.toList();
7170

72-
return new SliceResponse<>(previews, isLast);
71+
return new SliceResponse<>(previews, likedHistoriesSlice.isLast());
7372
}
7473

7574
private Map<Long, String> findFirstImagesByHistoryIds(List<Long> historyIds) {
@@ -115,4 +114,42 @@ private boolean isBlockedByOrBlocking(Long fromId, Long toId) {
115114
fromId, toId,
116115
toId, fromId);
117116
}
117+
118+
@Override
119+
public SliceResponse<LikedMembersResponse.LikedMemberPreview> getLikedMembers(
120+
Long historyId, Long lastLikeId, Integer size) {
121+
122+
Member currentMember = memberUtil.getCurrentMember();
123+
124+
Slice<LikedMembersResponse.LikedMemberPreview> likedMembersSlice =
125+
memberLikeRepositoryCustom.findLikedMembersSliceByHistoryId(
126+
historyId, lastLikeId, size);
127+
128+
if (likedMembersSlice.isEmpty()) {
129+
return new SliceResponse<>(List.of(), true);
130+
}
131+
132+
List<Long> memberIds =
133+
likedMembersSlice.getContent().stream()
134+
.map(LikedMembersResponse.LikedMemberPreview::getId)
135+
.toList();
136+
137+
Set<Long> followedIdSet =
138+
new HashSet<>(
139+
followRepository.findFollowedMemberIds(currentMember.getId(), memberIds));
140+
141+
List<LikedMembersResponse.LikedMemberPreview> previews =
142+
likedMembersSlice.getContent().stream()
143+
.map(
144+
preview ->
145+
new LikedMembersResponse.LikedMemberPreview(
146+
preview.getId(),
147+
preview.getCodiveId(),
148+
preview.getImageUrl(),
149+
preview.getNickname(),
150+
followedIdSet.contains(preview.getId())))
151+
.toList();
152+
153+
return new SliceResponse<>(previews, likedMembersSlice.isLast());
154+
}
118155
}

0 commit comments

Comments
 (0)