Skip to content

Commit 153a16c

Browse files
authored
[Feat] 가게 조회 API 구현
2 parents 0d32ca1 + 7afaf90 commit 153a16c

File tree

13 files changed

+291
-16
lines changed

13 files changed

+291
-16
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import eatda.controller.web.auth.LoginMember;
44
import eatda.service.store.StoreService;
5+
import jakarta.validation.constraints.Max;
6+
import jakarta.validation.constraints.Min;
57
import lombok.RequiredArgsConstructor;
68
import org.springframework.http.ResponseEntity;
79
import org.springframework.web.bind.annotation.GetMapping;
@@ -14,6 +16,11 @@ public class StoreController {
1416

1517
private final StoreService storeService;
1618

19+
@GetMapping("/api/shops")
20+
public ResponseEntity<StoresResponse> getStores(@RequestParam @Min(1) @Max(50) int size) {
21+
return ResponseEntity.ok(storeService.getStores(size));
22+
}
23+
1724
@GetMapping("/api/shop/search")
1825
public ResponseEntity<StoreSearchResponses> searchStore(@RequestParam String query, LoginMember member) {
1926
StoreSearchResponses response = storeService.searchStores(query);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package eatda.controller.store;
2+
3+
import eatda.domain.store.Store;
4+
5+
public record StorePreviewResponse(
6+
long id,
7+
String imageUrl,
8+
String name,
9+
String district,
10+
String neighborhood,
11+
String category
12+
) {
13+
14+
public StorePreviewResponse(Store store, String imageUrl) {
15+
this(
16+
store.getId(),
17+
imageUrl,
18+
store.getName(),
19+
store.getAddressDistrict(),
20+
store.getAddressNeighborhood(),
21+
store.getCategory().getCategoryName()
22+
);
23+
}
24+
}
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 StoresResponse(List<StorePreviewResponse> stores) {
6+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
package eatda.repository.store;
22

33
import eatda.domain.store.Cheer;
4+
import eatda.domain.store.Store;
45
import java.util.List;
6+
import java.util.Optional;
57
import org.springframework.data.domain.Pageable;
8+
import org.springframework.data.jpa.repository.Query;
69
import org.springframework.data.repository.Repository;
710

811
public interface CheerRepository extends Repository<Cheer, Long> {
912

1013
Cheer save(Cheer cheer);
1114

1215
List<Cheer> findAllByOrderByCreatedAtDesc(Pageable pageable);
16+
17+
@Query("""
18+
SELECT c.imageKey FROM Cheer c
19+
WHERE c.store = :store AND c.imageKey IS NOT NULL
20+
ORDER BY c.createdAt DESC
21+
LIMIT 1""")
22+
Optional<String> findRecentImageKey(Store store);
1323
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package eatda.repository.store;
22

33
import eatda.domain.store.Store;
4+
import java.util.List;
5+
import org.springframework.data.domain.Pageable;
46
import org.springframework.data.repository.Repository;
57

68
public interface StoreRepository extends Repository<Store, Long> {
79

810
Store save(Store store);
11+
12+
List<Store> findAllByOrderByCreatedAtDesc(Pageable pageable);
913
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import eatda.service.common.ImageService;
88
import java.util.List;
99
import lombok.RequiredArgsConstructor;
10-
import org.springframework.data.domain.PageRequest;
10+
import org.springframework.data.domain.Pageable;
1111
import org.springframework.stereotype.Service;
1212
import org.springframework.transaction.annotation.Transactional;
1313

@@ -20,8 +20,7 @@ public class CheerService {
2020

2121
@Transactional(readOnly = true)
2222
public CheersResponse getCheers(int size) {
23-
PageRequest pageRequest = PageRequest.of(0, size);
24-
List<Cheer> cheers = cheerRepository.findAllByOrderByCreatedAtDesc(pageRequest);
23+
List<Cheer> cheers = cheerRepository.findAllByOrderByCreatedAtDesc(Pageable.ofSize(size));
2524
return toCheersResponse(cheers);
2625
}
2726

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
package eatda.service.store;
22

3+
import static java.util.stream.Collectors.collectingAndThen;
4+
import static java.util.stream.Collectors.toList;
5+
36
import eatda.client.map.MapClient;
47
import eatda.client.map.StoreSearchResult;
8+
import eatda.controller.store.StorePreviewResponse;
59
import eatda.controller.store.StoreSearchResponses;
10+
import eatda.controller.store.StoresResponse;
11+
import eatda.domain.store.Store;
12+
import eatda.repository.store.CheerRepository;
13+
import eatda.repository.store.StoreRepository;
14+
import eatda.service.common.ImageService;
615
import java.util.List;
16+
import java.util.Optional;
717
import lombok.RequiredArgsConstructor;
18+
import org.springframework.data.domain.Pageable;
819
import org.springframework.stereotype.Service;
920

1021
@Service
@@ -13,6 +24,22 @@ public class StoreService {
1324

1425
private final MapClient mapClient;
1526
private final StoreSearchFilter storeSearchFilter;
27+
private final StoreRepository storeRepository;
28+
private final CheerRepository cheerRepository;
29+
private final ImageService imageService;
30+
31+
// TODO : N+1 문제 해결
32+
public StoresResponse getStores(int size) {
33+
return storeRepository.findAllByOrderByCreatedAtDesc(Pageable.ofSize(size))
34+
.stream()
35+
.map(store -> new StorePreviewResponse(store, getStoreImageUrl(store).orElse(null)))
36+
.collect(collectingAndThen(toList(), StoresResponse::new));
37+
}
38+
39+
private Optional<String> getStoreImageUrl(Store store) {
40+
return cheerRepository.findRecentImageKey(store)
41+
.map(imageService::getPresignedUrl);
42+
}
1643

1744
public StoreSearchResponses searchStores(String query) {
1845
List<StoreSearchResult> searchResults = mapClient.searchShops(query);

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,48 @@
11
package eatda.controller.store;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertAll;
45

56
import eatda.controller.BaseControllerTest;
7+
import eatda.domain.member.Member;
8+
import eatda.domain.store.Store;
69
import org.junit.jupiter.api.Nested;
710
import org.junit.jupiter.api.Test;
811
import org.springframework.http.HttpHeaders;
912

1013
class StoreControllerTest extends BaseControllerTest {
1114

15+
@Nested
16+
class GetStores {
17+
18+
@Test
19+
void 음식점_목록을_최신순으로_조회한다() {
20+
Member member = memberGenerator.generate("111");
21+
Store store1 = storeGenerator.generate("111", "서울 강남구 대치동 896-33");
22+
Store store2 = storeGenerator.generate("222", "서울 강남구 대치동 896-34");
23+
Store store3 = storeGenerator.generate("333", "서울 강남구 대치동 896-35");
24+
cheerGenerator.generateCommon(member, store1, "image-key-1");
25+
cheerGenerator.generateCommon(member, store2, "image-key-2");
26+
cheerGenerator.generateCommon(member, store3, "image-key-3");
27+
28+
int size = 2;
29+
30+
StoresResponse response = given()
31+
.queryParam("size", size)
32+
.when()
33+
.get("/api/shops")
34+
.then()
35+
.statusCode(200)
36+
.extract().as(StoresResponse.class);
37+
38+
assertAll(
39+
() -> assertThat(response.stores()).hasSize(size),
40+
() -> assertThat(response.stores().get(0).id()).isEqualTo(store3.getId()),
41+
() -> assertThat(response.stores().get(1).id()).isEqualTo(store2.getId())
42+
);
43+
}
44+
}
45+
1246
@Nested
1347
class SearchStores {
1448

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package eatda.document.store;
22

33

4+
import static org.mockito.ArgumentMatchers.anyInt;
45
import static org.mockito.ArgumentMatchers.anyString;
56
import static org.mockito.Mockito.doReturn;
67
import static org.mockito.Mockito.doThrow;
78
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
89
import static org.springframework.restdocs.payload.JsonFieldType.ARRAY;
10+
import static org.springframework.restdocs.payload.JsonFieldType.NUMBER;
911
import static org.springframework.restdocs.payload.JsonFieldType.STRING;
1012
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
1113
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
1214

15+
import eatda.controller.store.StorePreviewResponse;
1316
import eatda.controller.store.StoreSearchResponse;
1417
import eatda.controller.store.StoreSearchResponses;
18+
import eatda.controller.store.StoresResponse;
1519
import eatda.document.BaseDocumentTest;
1620
import eatda.document.RestDocsRequest;
1721
import eatda.document.RestDocsResponse;
@@ -28,6 +32,67 @@
2832

2933
public class StoreDocumentTest extends BaseDocumentTest {
3034

35+
@Nested
36+
class GetStores {
37+
38+
RestDocsRequest requestDocument = request()
39+
.tag(Tag.STORE_API)
40+
.summary("음식점 목록 조회")
41+
.queryParameter(
42+
parameterWithName("size").description("조회할 음식점 개수 (최소 1, 최대 50)")
43+
);
44+
45+
RestDocsResponse responseDocument = response()
46+
.responseBodyField(
47+
fieldWithPath("stores").type(ARRAY).description("음식점 목록"),
48+
fieldWithPath("stores[].id").type(NUMBER).description("음식점 ID"),
49+
fieldWithPath("stores[].imageUrl").type(STRING).description("음식점 대표 이미지 URL"),
50+
fieldWithPath("stores[].name").type(STRING).description("음식점 이름"),
51+
fieldWithPath("stores[].district").type(STRING).description("음식점 주소 (구)"),
52+
fieldWithPath("stores[].neighborhood").type(STRING).description("음식점 주소 (동)"),
53+
fieldWithPath("stores[].category").type(STRING).description("음식점 카테고리")
54+
);
55+
56+
@Test
57+
void 음식점_목록_최신순으로_조회() {
58+
StoresResponse response = new StoresResponse(List.of(
59+
new StorePreviewResponse(2L, "https://example.image", "농민백암순대", "강남구", "대치동", "한식"),
60+
new StorePreviewResponse(1L, "https://example.image", "석관동떡볶이", "성북구", "석관동", "한식")
61+
));
62+
doReturn(response).when(storeService).getStores(anyInt());
63+
64+
int size = 2;
65+
var document = document("store/get", 200)
66+
.request(requestDocument)
67+
.response(responseDocument)
68+
.build();
69+
70+
given(document)
71+
.contentType(ContentType.JSON)
72+
.queryParam("size", size)
73+
.when().get("/api/shops")
74+
.then().statusCode(200);
75+
}
76+
77+
@EnumSource(value = BusinessErrorCode.class, names = {"PRESIGNED_URL_GENERATION_FAILED"})
78+
@ParameterizedTest
79+
void 음식점_목록_조회_실패(BusinessErrorCode errorCode) {
80+
doThrow(new BusinessException(errorCode)).when(storeService).getStores(anyInt());
81+
82+
int size = 2;
83+
var document = document("store/get", errorCode)
84+
.request(requestDocument)
85+
.response(ERROR_RESPONSE)
86+
.build();
87+
88+
given(document)
89+
.contentType(ContentType.JSON)
90+
.queryParam("size", size)
91+
.when().get("/api/shops")
92+
.then().statusCode(errorCode.getStatus().value());
93+
}
94+
}
95+
3196
@Nested
3297
class SearchStores {
3398

src/test/java/eatda/fixture/CheerGenerator.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ public CheerGenerator(CheerRepository cheerRepository) {
1919
}
2020

2121
public Cheer generateAdmin(Member member, Store store) {
22-
Cheer cheer = new Cheer(member, store, DEFAULT_IMAGE_KEY, DEFAULT_DESCRIPTION, true);
22+
Cheer cheer = new Cheer(member, store, DEFAULT_DESCRIPTION, DEFAULT_IMAGE_KEY, true);
2323
return cheerRepository.save(cheer);
2424
}
2525

2626
public Cheer generateCommon(Member member, Store store) {
27-
Cheer cheer = new Cheer(member, store, DEFAULT_IMAGE_KEY, DEFAULT_DESCRIPTION, false);
27+
return generateCommon(member, store, DEFAULT_IMAGE_KEY);
28+
}
29+
30+
public Cheer generateCommon(Member member, Store store, String imageKey) {
31+
Cheer cheer = new Cheer(member, store, DEFAULT_DESCRIPTION, imageKey, false);
2832
return cheerRepository.save(cheer);
2933
}
3034
}

0 commit comments

Comments
 (0)