Skip to content

Commit e303abf

Browse files
authored
Merge pull request #168 from YAPP-Github/feat/PRODUCT-256
[Feat] 자신이 응원한 가게 조회 API 구현
2 parents b8fa04b + 024f2cb commit e303abf

File tree

13 files changed

+247
-1
lines changed

13 files changed

+247
-1
lines changed

src/main/java/eatda/controller/store/StoreController.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ public ResponseEntity<StoreResponse> getStore(@PathVariable long storeId) {
4242
return ResponseEntity.ok(response);
4343
}
4444

45+
@GetMapping("/api/shops/cheered-member")
46+
public ResponseEntity<StoresInMemberResponse> getStoresByCheeredMember(LoginMember member) {
47+
StoresInMemberResponse response = storeService.getStoresByCheeredMember(member.id());
48+
return ResponseEntity.ok(response);
49+
}
50+
4551
@GetMapping("/api/shop/search")
4652
public ResponseEntity<StoreSearchResponses> searchStore(@RequestParam String query, LoginMember member) {
4753
List<StoreSearchResult> storeSearchResults = storeSearchService.searchStores(query);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package eatda.controller.store;
2+
3+
import eatda.domain.store.Store;
4+
5+
public record StoreInMemberResponse(
6+
long id,
7+
String name,
8+
String district,
9+
String neighborhood,
10+
long cheerCount
11+
) {
12+
public StoreInMemberResponse(Store store, int cheerCount) {
13+
this(
14+
store.getId(),
15+
store.getName(),
16+
store.getAddressDistrict(),
17+
store.getAddressNeighborhood(),
18+
cheerCount
19+
);
20+
}
21+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package eatda.controller.store;
2+
3+
import java.util.List;
4+
5+
public record StoresInMemberResponse(List<StoreInMemberResponse> stores) {
6+
}

src/main/java/eatda/repository/cheer/CheerRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@ public interface CheerRepository extends JpaRepository<Cheer, Long> {
3434

3535
int countByMember(Member member);
3636

37+
int countByStore(Store store);
38+
3739
boolean existsByMemberAndStoreKakaoId(Member member, String storeKakaoId);
3840
}

src/main/java/eatda/repository/store/StoreRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Optional;
99
import org.springframework.data.domain.Pageable;
1010
import org.springframework.data.jpa.repository.JpaRepository;
11+
import org.springframework.data.jpa.repository.Query;
1112

1213
public interface StoreRepository extends JpaRepository<Store, Long> {
1314

@@ -22,4 +23,12 @@ default Store getById(Long id) {
2223
List<Store> findAllByOrderByCreatedAtDesc(Pageable pageable);
2324

2425
List<Store> findAllByCategoryOrderByCreatedAtDesc(StoreCategory category, Pageable pageable);
26+
27+
@Query("""
28+
SELECT s FROM Store s
29+
JOIN Cheer c ON s.id = c.store.id
30+
WHERE c.member.id = :memberId
31+
ORDER BY c.createdAt DESC
32+
""")
33+
List<Store> findAllByCheeredMemberId(long memberId);
2534
}

src/main/java/eatda/service/store/StoreService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import static java.util.stream.Collectors.toList;
55

66
import eatda.controller.store.ImagesResponse;
7+
import eatda.controller.store.StoreInMemberResponse;
78
import eatda.controller.store.StorePreviewResponse;
89
import eatda.controller.store.StoreResponse;
10+
import eatda.controller.store.StoresInMemberResponse;
911
import eatda.controller.store.StoresResponse;
1012
import eatda.domain.store.Store;
1113
import eatda.domain.store.StoreCategory;
@@ -18,6 +20,7 @@
1820
import org.springframework.data.domain.PageRequest;
1921
import org.springframework.lang.Nullable;
2022
import org.springframework.stereotype.Service;
23+
import org.springframework.transaction.annotation.Transactional;
2124

2225
@Service
2326
@RequiredArgsConstructor
@@ -61,4 +64,13 @@ private Optional<String> getStoreImageUrl(Store store) {
6164
return cheerRepository.findRecentImageKey(store)
6265
.map(imageStorage::getPreSignedUrl);
6366
}
67+
68+
@Transactional(readOnly = true)
69+
public StoresInMemberResponse getStoresByCheeredMember(long memberId) {
70+
List<Store> stores = storeRepository.findAllByCheeredMemberId(memberId);
71+
List<StoreInMemberResponse> responses = stores.stream()
72+
.map(store -> new StoreInMemberResponse(store, cheerRepository.countByStore(store)))
73+
.toList(); // TODO : N+1 문제 해결 (특정 회원의 가게는 3명 제한이라 중요도 낮음)
74+
return new StoresInMemberResponse(responses);
75+
}
6476
}

src/test/java/eatda/controller/BaseControllerTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ protected final String accessToken() {
133133
return jwtManager.issueAccessToken(member.getId());
134134
}
135135

136+
protected final String accessToken(Member member) {
137+
return jwtManager.issueAccessToken(member.getId());
138+
}
139+
136140
protected final String refreshToken() {
137141
Member member = memberGenerator.generateByEmail(Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId()),
138142

src/test/java/eatda/controller/store/StoreControllerTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,34 @@ class GetStoreImages {
146146
}
147147
}
148148

149+
@Nested
150+
class GetStoresByCheeredMember {
151+
152+
@Test
153+
void 회원이_응원한_음식점_목록을_조회한다() {
154+
Member member = memberGenerator.generate("111");
155+
Store store1 = storeGenerator.generate("농민백암순대", "서울 강남구 대치동 896-33");
156+
Store store2 = storeGenerator.generate("홍콩반점", "서울 강남구 역삼동 123-45");
157+
LocalDateTime startAt = LocalDateTime.of(2025, 7, 26, 1, 0, 0);
158+
cheerGenerator.generate(member, store1, startAt);
159+
cheerGenerator.generate(member, store2, startAt.plusHours(1));
160+
161+
StoresInMemberResponse response = given()
162+
.header(HttpHeaders.AUTHORIZATION, accessToken(member))
163+
.when()
164+
.get("/api/shops/cheered-member")
165+
.then()
166+
.statusCode(200)
167+
.extract().as(StoresInMemberResponse.class);
168+
169+
assertAll(
170+
() -> assertThat(response.stores()).hasSize(2),
171+
() -> assertThat(response.stores().get(0).id()).isEqualTo(store2.getId()),
172+
() -> assertThat(response.stores().get(1).id()).isEqualTo(store1.getId())
173+
);
174+
}
175+
}
176+
149177
@Nested
150178
class SearchStores {
151179

src/test/java/eatda/document/store/CheerDocumentTest.java renamed to src/test/java/eatda/document/cheer/CheerDocumentTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package eatda.document.store;
1+
package eatda.document.cheer;
22

33
import static org.mockito.ArgumentMatchers.any;
44
import static org.mockito.ArgumentMatchers.anyLong;

src/test/java/eatda/document/store/StoreDocumentTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
1313

1414
import eatda.controller.store.ImagesResponse;
15+
import eatda.controller.store.StoreInMemberResponse;
1516
import eatda.controller.store.StorePreviewResponse;
1617
import eatda.controller.store.StoreResponse;
18+
import eatda.controller.store.StoresInMemberResponse;
1719
import eatda.controller.store.StoresResponse;
1820
import eatda.document.BaseDocumentTest;
1921
import eatda.document.RestDocsRequest;
@@ -223,6 +225,66 @@ class GetStoreImages {
223225

224226
}
225227

228+
@Nested
229+
class GetStoresByCheeredMember {
230+
231+
RestDocsRequest requestDocument = request()
232+
.tag(Tag.STORE_API)
233+
.summary("회원이 응원한 가게 목록 조회")
234+
.description("- 응답 순서 : 최신 응원순")
235+
.requestHeader(
236+
headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰")
237+
);
238+
239+
RestDocsResponse responseDocument = response()
240+
.responseBodyField(
241+
fieldWithPath("stores").type(ARRAY).description("응원한 음식점 목록"),
242+
fieldWithPath("stores[].id").type(NUMBER).description("음식점 ID"),
243+
fieldWithPath("stores[].name").type(STRING).description("음식점 이름"),
244+
fieldWithPath("stores[].district").type(STRING).description("음식점 주소 (구)"),
245+
fieldWithPath("stores[].neighborhood").type(STRING).description("음식점 주소 (동)"),
246+
fieldWithPath("stores[].cheerCount").type(NUMBER).description("해당 음식점 응원 횟수 (자신 포함)")
247+
);
248+
249+
@Test
250+
void 회원이_응원한_음식점_목록을_조회() {
251+
StoresInMemberResponse response = new StoresInMemberResponse(List.of(
252+
new StoreInMemberResponse(1L, "농민백암순대", "강남구", "대치동", 5L),
253+
new StoreInMemberResponse(2L, "홍콩반점", "강남구", "역삼동", 1L)
254+
));
255+
doReturn(response).when(storeService).getStoresByCheeredMember(anyLong());
256+
257+
var document = document("store/get-by-cheered-member", 200)
258+
.request(requestDocument)
259+
.response(responseDocument)
260+
.build();
261+
262+
given(document)
263+
.contentType(ContentType.JSON)
264+
.header(HttpHeaders.AUTHORIZATION, accessToken())
265+
.when().get("/api/shops/cheered-member")
266+
.then().statusCode(200);
267+
}
268+
269+
@EnumSource(value = BusinessErrorCode.class, names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN",
270+
"INVALID_MEMBER_ID"})
271+
@ParameterizedTest
272+
void 회원이_응원한_음식점_목록_조회_실패(BusinessErrorCode errorCode) {
273+
doThrow(new BusinessException(errorCode)).when(storeService).getStoresByCheeredMember(anyLong());
274+
275+
var document = document("store/get-by-cheered-member", errorCode)
276+
.request(requestDocument)
277+
.response(ERROR_RESPONSE)
278+
.build();
279+
280+
given(document)
281+
.contentType(ContentType.JSON)
282+
.header(HttpHeaders.AUTHORIZATION, accessToken())
283+
.when().get("/api/shops/cheered-member")
284+
.then().statusCode(errorCode.getStatus().value());
285+
}
286+
}
287+
226288
@Nested
227289
class SearchStores {
228290

0 commit comments

Comments
 (0)