Skip to content

Commit 0d32ca1

Browse files
authored
Merge pull request #84 from YAPP-Github/feat/PRODUCT-149
[Feat] 스토리 목록 조회 구현
2 parents 4f238d9 + 6978e06 commit 0d32ca1

File tree

10 files changed

+173
-19
lines changed

10 files changed

+173
-19
lines changed

src/main/java/eatda/controller/story/StoryController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import lombok.RequiredArgsConstructor;
66
import org.springframework.http.HttpStatus;
77
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.GetMapping;
89
import org.springframework.web.bind.annotation.PostMapping;
910
import org.springframework.web.bind.annotation.RequestPart;
1011
import org.springframework.web.bind.annotation.RestController;
@@ -25,4 +26,10 @@ public ResponseEntity<Void> registerStory(
2526
storyService.registerStory(request, image, member.id());
2627
return ResponseEntity.status(HttpStatus.CREATED).build();
2728
}
29+
30+
@GetMapping("api/stories")
31+
public ResponseEntity<StoriesResponse> getStories() {
32+
return ResponseEntity.status(HttpStatus.OK)
33+
.body(storyService.getPagedStoryPreviews());
34+
}
2835
}
Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
package eatda.repository.story;
22

33
import eatda.domain.story.Story;
4-
import eatda.exception.BusinessErrorCode;
5-
import eatda.exception.BusinessException;
6-
import java.util.Optional;
7-
import org.springframework.data.repository.Repository;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.Pageable;
6+
import org.springframework.data.jpa.repository.JpaRepository;
87

9-
public interface StoryRepository extends Repository<Story, Long> {
8+
public interface StoryRepository extends JpaRepository<Story, Long> {
109

11-
Story save(Story story);
12-
13-
Optional<Story> findById(Long id);
14-
15-
default Story getById(Long id) {
16-
return findById(id)
17-
.orElseThrow(() -> new BusinessException(BusinessErrorCode.STORY_NOT_FOUND));
18-
}
10+
Page<Story> findAllByOrderByCreatedAtDesc(Pageable pageable);
1911
}

src/main/java/eatda/service/story/StoryService.java

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

33
import eatda.client.map.StoreSearchResult;
44
import eatda.controller.story.FilteredSearchResult;
5+
import eatda.controller.story.StoriesResponse;
56
import eatda.controller.story.StoryRegisterRequest;
67
import eatda.domain.member.Member;
78
import eatda.domain.story.Story;
@@ -14,13 +15,18 @@
1415
import eatda.service.store.StoreService;
1516
import java.util.List;
1617
import lombok.RequiredArgsConstructor;
18+
import org.springframework.data.domain.Page;
19+
import org.springframework.data.domain.PageRequest;
20+
import org.springframework.data.domain.Pageable;
1721
import org.springframework.stereotype.Service;
1822
import org.springframework.transaction.annotation.Transactional;
1923
import org.springframework.web.multipart.MultipartFile;
2024

2125
@Service
2226
@RequiredArgsConstructor
2327
public class StoryService {
28+
private static final int PAGE_START_NUMBER = 0;
29+
private static final int PAGE_SIZE = 5;
2430

2531
private final StoreService storeService;
2632
private final ImageService imageService;
@@ -59,4 +65,19 @@ private FilteredSearchResult filteredSearchResponse(List<StoreSearchResult> resp
5965
))
6066
.orElseThrow(() -> new BusinessException(BusinessErrorCode.STORE_NOT_FOUND));
6167
}
68+
69+
@Transactional(readOnly = true)
70+
public StoriesResponse getPagedStoryPreviews() {
71+
Pageable pageable = PageRequest.of(PAGE_START_NUMBER, PAGE_SIZE);
72+
Page<Story> orderByPage = storyRepository.findAllByOrderByCreatedAtDesc(pageable);
73+
74+
return new StoriesResponse(
75+
orderByPage.getContent().stream()
76+
.map(story -> new StoriesResponse.StoryPreview(
77+
story.getId(),
78+
imageService.getPresignedUrl(story.getImageKey())
79+
))
80+
.toList()
81+
);
82+
}
6283
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
INSERT INTO story (member_id, store_kakao_id, store_name, store_address,
2+
store_category, description, image_key)
3+
VALUES (1, '99999999999', '맛있는 한식집', '서울시 강남구 역삼동 123-45', 'KOREAN', '진짜 여기 곱창 맛집임. 다시 또 갈 듯!', 'story/preview/1.jpg'),
4+
(2, '99999999998', '아름다운 양식집', '서울시 강남구 역삼동 67-89', 'WESTERN', '스테이크가 부드럽고 서비스도 좋아요.', 'story/preview/2.jpg'),
5+
(3, '99999999997', '정통 중식당', '서울시 강남구 역삼동 101-112', 'CHINESE', '짜장면이 정통의 맛. 강력 추천.', 'story/preview/3.jpg');
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
INSERT INTO story (member_id, store_kakao_id, store_name, store_address,
2+
store_category, description, image_key)
3+
VALUES (1, '99999999999', '맛있는 한식집', '서울시 강남구 역삼동 123-45', 'KOREAN', '진짜 여기 곱창 맛집임. 다시 또 갈 듯!', 'story/preview/1.jpg'),
4+
(2, '99999999998', '아름다운 양식집', '서울시 강남구 역삼동 67-89', 'WESTERN', '스테이크가 부드럽고 서비스도 좋아요.', 'story/preview/2.jpg'),
5+
(3, '99999999997', '정통 중식당', '서울시 강남구 역삼동 101-112', 'CHINESE', '짜장면이 정통의 맛. 강력 추천.', 'story/preview/3.jpg'),
6+
(4, '99999999996', '고급 양식 레스토랑', '서울시 강남구 역삼동 131-415', 'WESTERN', '분위기가 연인 데이트하기 좋아요.', 'story/preview/4.jpg'),
7+
(5, '99999999995', '달콤한 디저트 카페', '서울시 강남구 역삼동 161-718', 'ETC', '케이크가 촉촉하고 맛있어요.', 'story/preview/5.jpg'),
8+
(6, '99999999994', '아늑한 카페', '서울시 강남구 역삼동 192-021', 'ETC', '조용해서 공부하기 좋아요.', 'story/preview/6.jpg'),
9+
(7, '99999999993', '빠른 패스트푸드점', '서울시 강남구 역삼동 222-324', 'ETC', '햄버거 나오는데 3분도 안 걸림. 굿.', 'story/preview/7.jpg');

src/main/resources/db/seed/prod/V4__local_add_story_data.sql

Whitespace-only changes.

src/test/java/eatda/controller/story/StoryControllerTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package eatda.controller.story;
22

3+
import static org.hamcrest.Matchers.equalTo;
34
import static org.mockito.ArgumentMatchers.any;
45
import static org.mockito.ArgumentMatchers.eq;
56
import static org.mockito.Mockito.doNothing;
@@ -9,6 +10,7 @@
910
import eatda.service.common.ImageDomain;
1011
import io.restassured.response.Response;
1112
import java.nio.charset.StandardCharsets;
13+
import java.util.List;
1214
import org.junit.jupiter.api.BeforeEach;
1315
import org.junit.jupiter.api.Nested;
1416
import org.junit.jupiter.api.Test;
@@ -48,4 +50,26 @@ class SearchStores {
4850
response.then().statusCode(201);
4951
}
5052
}
53+
54+
@Test
55+
void 스토리_목록을_조회할_수_있다() {
56+
StoriesResponse mockResponse = new StoriesResponse(List.of(
57+
new StoriesResponse.StoryPreview(1L, "https://dummy-s3.com/story1.png"),
58+
new StoriesResponse.StoryPreview(2L, "https://dummy-s3.com/story2.png")
59+
));
60+
61+
doReturn(mockResponse)
62+
.when(storyService)
63+
.getPagedStoryPreviews();
64+
65+
Response response = given()
66+
.when()
67+
.get("/api/stories");
68+
69+
response.then()
70+
.statusCode(200)
71+
.body("stories.size()", equalTo(2))
72+
.body("stories[0].storyId", equalTo(1))
73+
.body("stories[0].imageUrl", equalTo("https://dummy-s3.com/story1.png"));
74+
}
5175
}

src/test/java/eatda/document/story/StoryDocumentTest.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import static org.mockito.Mockito.doReturn;
66
import static org.mockito.Mockito.doThrow;
77
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
8+
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
89

10+
import eatda.controller.story.StoriesResponse;
911
import eatda.document.BaseDocumentTest;
1012
import eatda.document.RestDocsRequest;
1113
import eatda.document.RestDocsResponse;
@@ -16,6 +18,7 @@
1618
import eatda.service.common.ImageDomain;
1719
import io.restassured.response.Response;
1820
import java.nio.charset.StandardCharsets;
21+
import java.util.List;
1922
import org.junit.jupiter.api.Nested;
2023
import org.junit.jupiter.api.Test;
2124
import org.springframework.http.HttpHeaders;
@@ -127,10 +130,46 @@ class RegisterStory {
127130
.multiPart("image", "image.txt", invalidImage, "text/plain")
128131
.when().post("/api/stories");
129132

130-
System.out.println("응답 상태코드 >>> " + response.statusCode());
131-
System.out.println("응답 바디 >>> " + response.asString());
132-
133133
response.then().statusCode(BusinessErrorCode.INVALID_IMAGE_TYPE.getStatus().value());
134134
}
135135
}
136+
137+
@Nested
138+
class GetStories {
139+
140+
RestDocsRequest requestDocument = request()
141+
.tag(Tag.STORY_API)
142+
.summary("스토리 목록 조회")
143+
.description("스토리 목록을 페이지네이션하여 조회합니다.");
144+
145+
RestDocsResponse responseDocument = response()
146+
.responseBodyField(
147+
fieldWithPath("stories").description("스토리 프리뷰 리스트"),
148+
fieldWithPath("stories[].storyId").description("스토리 ID"),
149+
fieldWithPath("stories[].imageUrl").description("스토리 이미지 URL")
150+
);
151+
152+
@Test
153+
void 스토리_목록_조회_성공() {
154+
StoriesResponse mockResponse = new StoriesResponse(List.of(
155+
new StoriesResponse.StoryPreview(1L, "https://dummy-s3.com/story1.png"),
156+
new StoriesResponse.StoryPreview(2L, "https://dummy-s3.com/story2.png")
157+
));
158+
159+
doReturn(mockResponse)
160+
.when(storyService)
161+
.getPagedStoryPreviews();
162+
163+
RestDocumentationFilter document = document("story/get-stories", 200)
164+
.request(requestDocument)
165+
.response(responseDocument)
166+
.build();
167+
168+
Response response = given(document)
169+
.header(HttpHeaders.AUTHORIZATION, accessToken())
170+
.when().get("/api/stories");
171+
172+
response.then().statusCode(200);
173+
}
174+
}
136175
}

src/test/java/eatda/service/BaseServiceTest.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
import eatda.repository.store.StoreRepository;
1616
import eatda.service.common.ImageService;
1717
import org.junit.jupiter.api.BeforeEach;
18-
import eatda.repository.story.StoryRepository;
19-
import eatda.service.common.ImageService;
20-
import eatda.service.store.StoreService;
2118
import org.junit.jupiter.api.extension.ExtendWith;
2219
import org.springframework.beans.factory.annotation.Autowired;
2320
import org.springframework.boot.test.context.SpringBootTest;

src/test/java/eatda/service/story/StoryServiceTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package eatda.service.story;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
34
import static org.assertj.core.api.Assertions.assertThatThrownBy;
45
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
56
import static org.mockito.Mockito.doReturn;
@@ -9,12 +10,16 @@
910
import eatda.client.map.StoreSearchResult;
1011
import eatda.controller.story.StoryRegisterRequest;
1112
import eatda.domain.member.Member;
13+
import eatda.domain.story.Story;
1214
import eatda.exception.BusinessErrorCode;
1315
import eatda.exception.BusinessException;
16+
import eatda.repository.story.StoryRepository;
1417
import eatda.service.BaseServiceTest;
1518
import eatda.service.common.ImageDomain;
19+
import eatda.service.store.StoreService;
1620
import java.util.Collections;
1721
import java.util.List;
22+
import org.junit.jupiter.api.BeforeEach;
1823
import org.junit.jupiter.api.Nested;
1924
import org.junit.jupiter.api.Test;
2025
import org.springframework.beans.factory.annotation.Autowired;
@@ -24,6 +29,10 @@ public class StoryServiceTest extends BaseServiceTest {
2429

2530
@Autowired
2631
private StoryService storyService;
32+
@Autowired
33+
private StoryRepository storyRepository;
34+
@Autowired
35+
private StoreService storeService;
2736

2837
@Nested
2938
class RegisterStory {
@@ -58,4 +67,55 @@ class RegisterStory {
5867
.hasMessageContaining(BusinessErrorCode.STORE_NOT_FOUND.getMessage());
5968
}
6069
}
70+
71+
@Nested
72+
class GetPagedStoryPreviews extends BaseServiceTest {
73+
74+
private StoryService storyService;
75+
76+
@BeforeEach
77+
void setUp() {
78+
storyService = new StoryService(storeService, imageService, storyRepository, memberRepository);
79+
}
80+
81+
@Test
82+
void 스토리_목록을_조회할_수_있다() {
83+
Member member = memberGenerator.generate("12345");
84+
85+
Story story1 = Story.builder()
86+
.member(member)
87+
.storeKakaoId("1")
88+
.storeName("곱창집")
89+
.storeAddress("서울시")
90+
.storeCategory("한식")
91+
.description("미쳤다 진짜")
92+
.imageKey("image-key-1")
93+
.build();
94+
95+
Story story2 = Story.builder()
96+
.member(member)
97+
.storeKakaoId("2")
98+
.storeName("순대국밥집")
99+
.storeAddress("부산시")
100+
.storeCategory("한식")
101+
.description("뜨끈한 국밥 최고")
102+
.imageKey("image-key-2")
103+
.build();
104+
105+
storyRepository.saveAll(List.of(story1, story2));
106+
107+
when(imageService.getPresignedUrl("image-key-1")).thenReturn("https://s3.com/story1.jpg");
108+
when(imageService.getPresignedUrl("image-key-2")).thenReturn("https://s3.com/story2.jpg");
109+
110+
var response = storyService.getPagedStoryPreviews();
111+
112+
assertThat(response.stories()).hasSize(2);
113+
assertThat(response.stories())
114+
.extracting("imageUrl")
115+
.containsExactly(
116+
"https://s3.com/story2.jpg",
117+
"https://s3.com/story1.jpg"
118+
);
119+
}
120+
}
61121
}

0 commit comments

Comments
 (0)