Skip to content

Commit ed43e3d

Browse files
authored
Merge pull request #130 from YAPP-Github/feat/get-stories-kakaoId
[Feat] 카카오 ID를 통한 스토리 조회 API 구현
2 parents 0db5d62 + 1491297 commit ed43e3d

File tree

7 files changed

+246
-1
lines changed

7 files changed

+246
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package eatda.controller.story;
2+
3+
import eatda.domain.story.Story;
4+
import java.util.List;
5+
6+
public record StoriesDetailResponse(List<StoryDetailResponse> stories) {
7+
8+
public record StoryDetailResponse(
9+
long storyId,
10+
String imageUrl,
11+
long memberId,
12+
String memberNickname
13+
) {
14+
15+
public StoryDetailResponse(Story story, String imageUrl) {
16+
this(
17+
story.getId(),
18+
imageUrl,
19+
story.getMember().getId(),
20+
story.getMember().getNickname()
21+
);
22+
}
23+
}
24+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,13 @@ public ResponseEntity<StoryResponse> getStory(@PathVariable long storyId) {
4242
return ResponseEntity.status(HttpStatus.OK)
4343
.body(storyService.getStory(storyId));
4444
}
45+
46+
@GetMapping("/api/stories/kakao/{kakaoId}")
47+
public ResponseEntity<StoriesDetailResponse> getStoriesByKakaoId(
48+
@PathVariable String kakaoId,
49+
@RequestParam(defaultValue = "5") @Min(1) @Max(50) int size
50+
) {
51+
return ResponseEntity.status(HttpStatus.OK)
52+
.body(storyService.getPagedStoryDetails(kakaoId, size));
53+
}
4554
}

src/main/java/eatda/repository/story/StoryRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
public interface StoryRepository extends JpaRepository<Story, Long> {
99

1010
Page<Story> findAllByOrderByCreatedAtDesc(Pageable pageable);
11+
12+
Page<Story> findAllByStoreKakaoIdOrderByCreatedAtDesc(String storeKakaoId, Pageable pageable);
1113
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import eatda.client.map.MapClient;
44
import eatda.client.map.StoreSearchResult;
55
import eatda.controller.story.FilteredSearchResult;
6+
import eatda.controller.story.StoriesDetailResponse;
67
import eatda.controller.story.StoriesResponse;
78
import eatda.controller.story.StoryRegisterRequest;
89
import eatda.controller.story.StoryRegisterResponse;
@@ -113,4 +114,17 @@ public StoryResponse getStory(long storyId) {
113114
story.getMember().getNickname()
114115
);
115116
}
117+
118+
@Transactional(readOnly = true)
119+
public StoriesDetailResponse getPagedStoryDetails(String kakaoId, int size) {
120+
List<Story> stories = storyRepository
121+
.findAllByStoreKakaoIdOrderByCreatedAtDesc(kakaoId, PageRequest.of(PAGE_START_NUMBER, size))
122+
.getContent();
123+
124+
List<StoriesDetailResponse.StoryDetailResponse> responses = stories.stream()
125+
.map(story -> new StoriesDetailResponse.StoryDetailResponse(
126+
story, imageStorage.getPreSignedUrl(story.getImageKey())))
127+
.toList(); // TODO: N+1 문제 해결
128+
return new StoriesDetailResponse(responses);
129+
}
116130
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.mockito.Mockito.doThrow;
99

1010
import eatda.controller.BaseControllerTest;
11+
import eatda.controller.story.StoriesDetailResponse.StoryDetailResponse;
1112
import eatda.exception.BusinessErrorCode;
1213
import eatda.exception.BusinessException;
1314
import eatda.util.ImageUtils;
@@ -139,4 +140,38 @@ class GetStory {
139140
.body("message", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getMessage()));
140141
}
141142
}
143+
144+
@Nested
145+
class GetPagedStoryDetailsByKakaoId {
146+
147+
@Test
148+
void 카카오ID로_스토리_목록을_조회할_수_있다() {
149+
String kakaoId = "123456";
150+
List<StoryDetailResponse> mockDetails = List.of(
151+
new StoryDetailResponse(1L, "https://s3.bucket.com/story/dummy/1.jpg", 5L, "커찬"),
152+
new StoryDetailResponse(2L, "https://s3.bucket.com/story/dummy/2.jpg", 2L, "지민")
153+
);
154+
155+
doReturn(new StoriesDetailResponse(mockDetails))
156+
.when(storyService)
157+
.getPagedStoryDetails(kakaoId, 5);
158+
159+
StoriesDetailResponse response = given()
160+
.pathParam("kakaoId", kakaoId)
161+
.queryParam("size", 5)
162+
.when()
163+
.get("/api/stories/kakao/{kakaoId}")
164+
.then().statusCode(200)
165+
.extract().as(StoriesDetailResponse.class);
166+
167+
assertAll(
168+
() -> assertThat(response.stories()).hasSize(2),
169+
() -> assertThat(response.stories().getFirst().storyId()).isEqualTo(1L),
170+
() -> assertThat(response.stories().getFirst().imageUrl()).isEqualTo(
171+
"https://s3.bucket.com/story/dummy/1.jpg"),
172+
() -> assertThat(response.stories().getFirst().memberId()).isEqualTo(5L),
173+
() -> assertThat(response.stories().getFirst().memberNickname()).isEqualTo("커찬")
174+
);
175+
}
176+
}
142177
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
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.JsonFieldType.ARRAY;
89
import static org.springframework.restdocs.payload.JsonFieldType.NUMBER;
910
import static org.springframework.restdocs.payload.JsonFieldType.STRING;
1011
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
1112
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
1213
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
1314

15+
import eatda.controller.story.StoriesDetailResponse;
16+
import eatda.controller.story.StoriesDetailResponse.StoryDetailResponse;
1417
import eatda.controller.story.StoriesResponse;
1518
import eatda.controller.story.StoryRegisterRequest;
1619
import eatda.controller.story.StoryRegisterResponse;
@@ -28,6 +31,8 @@
2831
import java.util.List;
2932
import org.junit.jupiter.api.Nested;
3033
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.EnumSource;
3136
import org.springframework.http.HttpHeaders;
3237
import org.springframework.restdocs.restassured.RestDocumentationFilter;
3338

@@ -227,4 +232,68 @@ class GetStory {
227232
.body("message", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getMessage()));
228233
}
229234
}
235+
236+
@Nested
237+
class GetStoriesByKakaoId {
238+
239+
RestDocsRequest requestDocument = request()
240+
.tag(Tag.STORY_API)
241+
.summary("카카오 ID로 스토리 목록 조회")
242+
.description("특정 카카오 ID에 해당하는 스토리 목록을 페이지네이션하여 조회합니다.")
243+
.pathParameter(
244+
parameterWithName("kakaoId").description("가게의 카카오 ID")
245+
)
246+
.queryParameter(
247+
parameterWithName("size").description("스토리 개수 (기본값: 5) (최소값: 1, 최대값: 50)").optional()
248+
);
249+
250+
RestDocsResponse responseDocument = response()
251+
.responseBodyField(
252+
fieldWithPath("stories").type(ARRAY).description("스토리 상세 리스트"),
253+
fieldWithPath("stories[].storyId").type(NUMBER).description("스토리 ID"),
254+
fieldWithPath("stories[].imageUrl").type(STRING).description("스토리 이미지 URL"),
255+
fieldWithPath("stories[].memberId").type(NUMBER).description("회원 ID"),
256+
fieldWithPath("stories[].memberNickname").type(STRING).description("회원 닉네임")
257+
);
258+
259+
@Test
260+
void 카카오_ID로_스토리_목록_조회_성공() {
261+
String kakaoId = "123456";
262+
int size = 5;
263+
StoriesDetailResponse response = new StoriesDetailResponse(List.of(
264+
new StoryDetailResponse(1L, "https://dummy-s3.com/story1.png", 1L, "커찬"),
265+
new StoryDetailResponse(2L, "https://dummy-s3.com/story2.png", 2L, "준환")
266+
));
267+
doReturn(response).when(storyService).getPagedStoryDetails(kakaoId, size);
268+
269+
var document = document("story/get-stories-by-kakao-id", 200)
270+
.request(requestDocument)
271+
.response(responseDocument)
272+
.build();
273+
274+
given(document)
275+
.queryParam("size", size)
276+
.header(HttpHeaders.AUTHORIZATION, accessToken())
277+
.when().get("/api/stories/kakao/{kakaoId}", kakaoId)
278+
.then().statusCode(200);
279+
}
280+
281+
@EnumSource(value = BusinessErrorCode.class, names = {"PRESIGNED_URL_GENERATION_FAILED"})
282+
@ParameterizedTest
283+
void 카카오_ID로_스토리_목록_조회_실패(BusinessErrorCode errorCode) {
284+
String kakaoId = "nonexistent";
285+
int size = 5;
286+
doThrow(new BusinessException(errorCode)).when(storyService).getPagedStoryDetails(kakaoId, size);
287+
288+
var document = document("story/get-stories-by-kakao-id", errorCode)
289+
.request(requestDocument)
290+
.response(ERROR_RESPONSE)
291+
.build();
292+
293+
given(document)
294+
.queryParam("size", size)
295+
.when().get("/api/stories/kakao/{kakaoId}", kakaoId)
296+
.then().statusCode(errorCode.getStatus().value());
297+
}
298+
}
230299
}

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

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.mockito.Mockito.when;
99

1010
import eatda.client.map.StoreSearchResult;
11+
import eatda.controller.story.StoriesDetailResponse;
1112
import eatda.controller.story.StoriesResponse.StoryPreview;
1213
import eatda.controller.story.StoryRegisterRequest;
1314
import eatda.controller.story.StoryResponse;
@@ -27,7 +28,7 @@
2728
import org.springframework.mock.web.MockMultipartFile;
2829
import org.springframework.web.multipart.MultipartFile;
2930

30-
public class StoryServiceTest extends BaseServiceTest {
31+
class StoryServiceTest extends BaseServiceTest {
3132

3233
@Autowired
3334
private StoryService storyService;
@@ -187,4 +188,95 @@ class GetStory {
187188
.hasMessageContaining(BusinessErrorCode.STORY_NOT_FOUND.getMessage());
188189
}
189190
}
191+
192+
@Nested
193+
class GetPagedStoryDetails {
194+
195+
@Test
196+
void 카카오ID로_스토리_목록을_조회할_수_있다() {
197+
String kakaoId = "123456";
198+
Member member = memberGenerator.generate("12345");
199+
Story story1 = Story.builder()
200+
.member(member)
201+
.storeKakaoId(kakaoId)
202+
.storeName("곱창집")
203+
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
204+
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
205+
.storeCategory(StoreCategory.KOREAN)
206+
.description("미쳤다 진짜")
207+
.imageKey(new ImageKey("image-key-1"))
208+
.build();
209+
Story story2 = Story.builder()
210+
.member(member)
211+
.storeKakaoId(kakaoId)
212+
.storeName("순대국밥집")
213+
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
214+
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
215+
.storeCategory(StoreCategory.KOREAN)
216+
.description("뜨끈한 국밥 최고")
217+
.imageKey(new ImageKey("image-key-2"))
218+
.build();
219+
storyRepository.save(story1);
220+
storyRepository.save(story2);
221+
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-1")))
222+
.thenReturn("https://s3.bucket.com/story/dummy/1.jpg");
223+
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-2")))
224+
.thenReturn("https://s3.bucket.com/story/dummy/2.jpg");
225+
226+
var response = storyService.getPagedStoryDetails(kakaoId, 5);
227+
228+
assertThat(response.stories())
229+
.hasSize(2)
230+
.extracting(StoriesDetailResponse.StoryDetailResponse::storyId)
231+
.containsExactlyInAnyOrder(story2.getId(), story1.getId());
232+
}
233+
234+
@Test
235+
void 카카오ID로_스토리_목록을_조회할_때_특정_스토리만_반환한다() {
236+
String kakaoId = "123456";
237+
Member member = memberGenerator.generate("12345");
238+
Story story1 = Story.builder()
239+
.member(member)
240+
.storeKakaoId(kakaoId)
241+
.storeName("곱창집")
242+
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
243+
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
244+
.storeCategory(StoreCategory.KOREAN)
245+
.description("미쳤다 진짜")
246+
.imageKey(new ImageKey("image-key-1"))
247+
.build();
248+
Story story2 = Story.builder()
249+
.member(member)
250+
.storeKakaoId("different-kakao-id")
251+
.storeName("순대국밥집")
252+
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
253+
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
254+
.storeCategory(StoreCategory.KOREAN)
255+
.description("뜨끈한 국밥 최고")
256+
.imageKey(new ImageKey("image-key-2"))
257+
.build();
258+
storyRepository.save(story1);
259+
storyRepository.save(story2);
260+
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-1")))
261+
.thenReturn("https://s3.bucket.com/story/dummy/1.jpg");
262+
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-2")))
263+
.thenReturn("https://s3.bucket.com/story/dummy/2.jpg");
264+
265+
var response = storyService.getPagedStoryDetails(kakaoId, 5);
266+
267+
assertThat(response.stories())
268+
.hasSize(1)
269+
.extracting(StoriesDetailResponse.StoryDetailResponse::storyId)
270+
.containsExactlyInAnyOrder(story1.getId());
271+
}
272+
}
273+
274+
@Test
275+
void 존재하지_않는_카카오ID로_조회하면_빈_목록을_반환한다() {
276+
String nonExistentKakaoId = "non-existent";
277+
278+
var response = storyService.getPagedStoryDetails(nonExistentKakaoId, 5);
279+
280+
assertThat(response.stories()).isEmpty();
281+
}
190282
}

0 commit comments

Comments
 (0)