Skip to content

Commit 812640d

Browse files
authored
[feat/#87] 좋아요한 기록 조회 api 구현 (#114)
* feat : 구현중 * feat : 좋아요한 기록 조회 구현 * refactor : 페이징 방식 리팩토링 * fix : test 수정
1 parent 707263d commit 812640d

8 files changed

Lines changed: 289 additions & 2 deletions

File tree

clokey-api/src/main/java/org/clokey/domain/history/repository/HistoryImageRepository.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,22 @@
33
import java.util.List;
44
import org.clokey.history.entity.HistoryImage;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
68

79
public interface HistoryImageRepository extends JpaRepository<HistoryImage, Long> {
10+
@Query(
11+
"""
12+
SELECT hi.history.id, hi.imageUrl
13+
FROM HistoryImage hi
14+
WHERE hi.history.id IN :historyIds
15+
AND hi.id = (
16+
SELECT MIN(h2.id)
17+
FROM HistoryImage h2
18+
WHERE h2.history.id = hi.history.id
19+
)
20+
""")
21+
List<Object[]> getFirstImageUrlsWithHistoryId(@Param("historyIds") List<Long> historyIds);
22+
823
List<HistoryImage> findByHistoryId(Long historyId);
924
}

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

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

33
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
45
import io.swagger.v3.oas.annotations.tags.Tag;
56
import lombok.RequiredArgsConstructor;
67
import org.clokey.code.GlobalBaseSuccessCode;
8+
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
79
import org.clokey.domain.like.service.LikeService;
10+
import org.clokey.global.annotation.PageSize;
811
import org.clokey.response.BaseResponse;
12+
import org.clokey.response.SliceResponse;
913
import org.springframework.validation.annotation.Validated;
1014
import org.springframework.web.bind.annotation.*;
15+
import org.springframework.web.bind.annotation.GetMapping;
16+
import org.springframework.web.bind.annotation.RequestMapping;
17+
import org.springframework.web.bind.annotation.RequestParam;
18+
import org.springframework.web.bind.annotation.RestController;
1119

1220
@RestController
1321
@RequestMapping("/likes")
@@ -18,6 +26,20 @@ public class LikeController {
1826

1927
private final LikeService likeService;
2028

29+
@GetMapping("/histories")
30+
@Operation(summary = "좋아요한 기록 조회", description = "사용자가 좋아요한 기록을 조회합니다.")
31+
public BaseResponse<SliceResponse<LikedHistoriesResponse.LikedHistoryPreview>>
32+
getLikedHistories(
33+
@Parameter(description = "이전 페이지의 좋아요 ID (첫 요청 시 생략)")
34+
@RequestParam(required = false)
35+
Long lastLikeId,
36+
@Parameter(description = "페이지당 조회할 개수") @RequestParam @PageSize Integer size) {
37+
SliceResponse<LikedHistoriesResponse.LikedHistoryPreview> response =
38+
likeService.getLikedHistories(lastLikeId, size);
39+
40+
return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response);
41+
}
42+
2143
@PostMapping
2244
@Operation(operationId = "Like_toggleLike", summary = "좋아요 생성", description = "기록에 좋아요를 추가합니다")
2345
public BaseResponse<Void> toggleLike(@RequestParam("historyId") Long historyId) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.clokey.domain.like.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import java.util.List;
5+
6+
@Schema(description = "좋아요 히스토리 조회 결과 (Slice 기반)")
7+
public record LikedHistoriesResponse(
8+
@Schema(description = "히스토리 미리보기 목록") List<LikedHistoryPreview> historyPreviews,
9+
@Schema(description = "마지막 페이지 여부", example = "false") boolean isLast) {
10+
11+
@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) {}
19+
}

clokey-api/src/main/java/org/clokey/domain/like/repository/MemberLikeRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99

1010
public interface MemberLikeRepository extends JpaRepository<MemberLike, Long> {
1111

12+
<<<<<<< HEAD
13+
=======
1214
long countByHistoryId(Long historyId);
1315

16+
>>>>>>> 707263d5afbbd10030259d946f7748710380a71c
1417
@Query(
1518
"""
1619
SELECT ml
@@ -21,6 +24,8 @@ public interface MemberLikeRepository extends JpaRepository<MemberLike, Long> {
2124
""")
2225
List<MemberLike> findLikedHistoriesByMemberId(
2326
Long memberId, Long lastLikeId, Pageable pageable);
27+
<<<<<<< HEAD
28+
=======
2429

2530
@Query(
2631
"""
@@ -33,4 +38,5 @@ List<MemberLike> findLikedHistoriesByMemberId(
3338
List<MemberLike> findLikeMembersByHistoryId(Long historyId, Long lastLikeId, Pageable pageable);
3439

3540
Optional<MemberLike> findByMemberIdAndHistoryId(Long memberId, Long historyId);
41+
>>>>>>> 707263d5afbbd10030259d946f7748710380a71c
3642
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package org.clokey.domain.like.service;
22

3+
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
4+
import org.clokey.response.SliceResponse;
5+
36
public interface LikeService {
7+
SliceResponse<LikedHistoriesResponse.LikedHistoryPreview> getLikedHistories(
8+
Long lastLikedId, Integer size);
9+
410
void toggleLike(Long historyId);
511
}

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
package org.clokey.domain.like.service;
22

3+
import java.util.List;
4+
import java.util.Map;
35
import java.util.Optional;
6+
import java.util.stream.Collectors;
47
import lombok.RequiredArgsConstructor;
58
import org.clokey.domain.history.exception.HistoryErrorCode;
9+
import org.clokey.domain.history.repository.HistoryImageRepository;
610
import org.clokey.domain.history.repository.HistoryRepository;
11+
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
712
import org.clokey.domain.like.repository.MemberLikeRepository;
813
import org.clokey.domain.member.repository.BlockRepository;
914
import org.clokey.exception.BaseCustomException;
1015
import org.clokey.global.util.MemberUtil;
1116
import org.clokey.history.entity.History;
1217
import org.clokey.like.entity.MemberLike;
1318
import org.clokey.member.entity.Member;
19+
import org.clokey.response.SliceResponse;
20+
import org.springframework.data.domain.PageRequest;
21+
import org.springframework.data.domain.Pageable;
1422
import org.springframework.stereotype.Service;
1523
import org.springframework.transaction.annotation.Transactional;
1624

@@ -20,11 +28,61 @@
2028
public class LikeServiceImpl implements LikeService {
2129

2230
private final MemberUtil memberUtil;
23-
2431
private final MemberLikeRepository memberLikeRepository;
32+
private final HistoryImageRepository historyImageRepository;
2533
private final HistoryRepository historyRepository;
2634
private final BlockRepository blockRepository;
2735

36+
@Override
37+
public SliceResponse<LikedHistoriesResponse.LikedHistoryPreview> getLikedHistories(
38+
Long lastLikeId, Integer size) {
39+
40+
Member currentMember = memberUtil.getCurrentMember();
41+
42+
// limit + 1 조회
43+
Pageable pageable = PageRequest.of(0, size + 1);
44+
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()) {
56+
return new SliceResponse<>(List.of(), true);
57+
}
58+
59+
List<Long> historyIds = likes.stream().map(like -> like.getHistory().getId()).toList();
60+
61+
Map<Long, String> imageMap = findFirstImagesByHistoryIds(historyIds);
62+
63+
List<LikedHistoriesResponse.LikedHistoryPreview> previews =
64+
likes.stream()
65+
.map(
66+
like ->
67+
new LikedHistoriesResponse.LikedHistoryPreview(
68+
like.getHistory().getId(),
69+
imageMap.get(like.getHistory().getId())))
70+
.toList();
71+
72+
return new SliceResponse<>(previews, isLast);
73+
}
74+
75+
private Map<Long, String> findFirstImagesByHistoryIds(List<Long> historyIds) {
76+
if (historyIds.isEmpty()) return Map.of();
77+
78+
List<Object[]> rows = historyImageRepository.getFirstImageUrlsWithHistoryId(historyIds);
79+
80+
return rows.stream()
81+
.collect(
82+
Collectors.toMap(
83+
row -> ((Number) row[0]).longValue(), row -> (String) row[1]));
84+
}
85+
2886
@Override
2987
@Transactional
3088
public void toggleLike(Long historyId) {

clokey-api/src/test/java/org/clokey/domain/like/controller/LikeControllerTest.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package org.clokey.domain.like.controller;
22

3+
import static org.mockito.ArgumentMatchers.any;
4+
import static org.mockito.ArgumentMatchers.anyInt;
5+
import static org.mockito.BDDMockito.given;
36
import static org.mockito.BDDMockito.willDoNothing;
47
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
58
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
69
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
710

811
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import java.util.List;
13+
import org.clokey.domain.like.dto.response.LikedHistoriesResponse;
914
import org.clokey.domain.like.service.LikeService;
15+
import org.clokey.response.SliceResponse;
1016
import org.junit.jupiter.api.Nested;
1117
import org.junit.jupiter.api.Test;
1218
import org.springframework.beans.factory.annotation.Autowired;
@@ -48,4 +54,80 @@ class 좋아요_요청_시 {
4854
.andExpect(jsonPath("$.message").value("요청 성공 및 반환값 없음"));
4955
}
5056
}
57+
58+
@Nested
59+
class 좋아요한_기록_조회_시 {
60+
@Test
61+
void 유효한_요청이면_좋아요한_기록을_반환한다() throws Exception {
62+
// given
63+
List<LikedHistoriesResponse.LikedHistoryPreview> previews =
64+
List.of(
65+
new LikedHistoriesResponse.LikedHistoryPreview(
66+
1L, "https://img.com/img1.jpg"),
67+
new LikedHistoriesResponse.LikedHistoryPreview(
68+
2L, "https://img.com/img2.jpg"));
69+
70+
SliceResponse<LikedHistoriesResponse.LikedHistoryPreview> sliceResponse =
71+
new SliceResponse<>(previews, true);
72+
73+
given(likeService.getLikedHistories(any(), anyInt())).willReturn(sliceResponse);
74+
75+
ResultActions perform =
76+
mockMvc.perform(
77+
get("/likes/histories")
78+
.param("size", "10")
79+
.contentType(MediaType.APPLICATION_JSON));
80+
81+
// then
82+
perform.andExpect(status().isOk())
83+
.andExpect(jsonPath("$.code").value("COMMON200"))
84+
.andExpect(jsonPath("$.message").value("성공입니다."))
85+
.andExpect(jsonPath("$.result.content[0].id").value(1L))
86+
.andExpect(
87+
jsonPath("$.result.content[0].imageUrl")
88+
.value("https://img.com/img1.jpg"))
89+
.andExpect(jsonPath("$.result.content[1].id").value(2L))
90+
.andExpect(
91+
jsonPath("$.result.content[1].imageUrl")
92+
.value("https://img.com/img2.jpg"))
93+
.andExpect(jsonPath("$.result.isLast").value(true));
94+
}
95+
96+
@Test
97+
void 마지막_페이지가_아닌_경우_isLast를_false로_응답한다() throws Exception {
98+
// given
99+
List<LikedHistoriesResponse.LikedHistoryPreview> previews =
100+
List.of(
101+
new LikedHistoriesResponse.LikedHistoryPreview(
102+
1L, "https://img.com/img1.jpg"),
103+
new LikedHistoriesResponse.LikedHistoryPreview(
104+
2L, "https://img.com/img2.jpg"));
105+
106+
SliceResponse<LikedHistoriesResponse.LikedHistoryPreview> sliceResponse =
107+
new SliceResponse<>(previews, false);
108+
109+
given(likeService.getLikedHistories(any(), anyInt())).willReturn(sliceResponse);
110+
111+
// when
112+
ResultActions perform =
113+
mockMvc.perform(
114+
get("/likes/histories")
115+
.param("size", "10")
116+
.contentType(MediaType.APPLICATION_JSON));
117+
118+
// then
119+
perform.andExpect(status().isOk())
120+
.andExpect(jsonPath("$.code").value("COMMON200"))
121+
.andExpect(jsonPath("$.message").value("성공입니다."))
122+
.andExpect(jsonPath("$.result.content[0].id").value(1L))
123+
.andExpect(
124+
jsonPath("$.result.content[0].imageUrl")
125+
.value("https://img.com/img1.jpg"))
126+
.andExpect(jsonPath("$.result.content[1].id").value(2L))
127+
.andExpect(
128+
jsonPath("$.result.content[1].imageUrl")
129+
.value("https://img.com/img2.jpg"))
130+
.andExpect(jsonPath("$.result.isLast").value(false));
131+
}
132+
}
51133
}

0 commit comments

Comments
 (0)