Skip to content

Commit 31145cf

Browse files
authored
Merge pull request #131 from YAPP-Github/develop
[Feat] 1차 스프린트 추가 api 배포
2 parents 4ce9e64 + ed43e3d commit 31145cf

File tree

8 files changed

+321
-18
lines changed

8 files changed

+321
-18
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/controller/story/StoryResponse.java

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

3+
import org.springframework.lang.Nullable;
4+
35
public record StoryResponse(
6+
@Nullable Long storeId,
47
String storeKakaoId,
58
String category,
69
String storeName,
@@ -11,4 +14,5 @@ public record StoryResponse(
1114
long memberId,
1215
String memberNickname
1316
) {
17+
1418
}

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: 21 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;
@@ -11,10 +12,12 @@
1112
import eatda.domain.ImageDomain;
1213
import eatda.domain.ImageKey;
1314
import eatda.domain.member.Member;
15+
import eatda.domain.store.Store;
1416
import eatda.domain.story.Story;
1517
import eatda.exception.BusinessErrorCode;
1618
import eatda.exception.BusinessException;
1719
import eatda.repository.member.MemberRepository;
20+
import eatda.repository.store.StoreRepository;
1821
import eatda.repository.story.StoryRepository;
1922
import eatda.storage.image.ImageStorage;
2023
import java.util.List;
@@ -35,6 +38,7 @@ public class StoryService {
3538
private final ImageStorage imageStorage;
3639
private final MapClient mapClient;
3740
private final StoryRepository storyRepository;
41+
private final StoreRepository storeRepository;
3842
private final MemberRepository memberRepository;
3943

4044
@Transactional
@@ -93,8 +97,12 @@ public StoriesResponse getPagedStoryPreviews(int size) {
9397
public StoryResponse getStory(long storyId) {
9498
Story story = storyRepository.findById(storyId)
9599
.orElseThrow(() -> new BusinessException(BusinessErrorCode.STORY_NOT_FOUND));
100+
Long storeId = storeRepository.findByKakaoId(story.getStoreKakaoId())
101+
.map(Store::getId)
102+
.orElse(null);
96103

97104
return new StoryResponse(
105+
storeId,
98106
story.getStoreKakaoId(),
99107
story.getStoreCategory().getCategoryName(),
100108
story.getStoreName(),
@@ -106,4 +114,17 @@ public StoryResponse getStory(long storyId) {
106114
story.getMember().getNickname()
107115
);
108116
}
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+
}
109130
}

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

Lines changed: 37 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;
@@ -90,6 +91,7 @@ class GetStory {
9091
long storyId = 1L;
9192

9293
doReturn(new StoryResponse(
94+
5L,
9395
"123456",
9496
"한식",
9597
"진또곱창집",
@@ -110,6 +112,7 @@ class GetStory {
110112
.extract().as(StoryResponse.class);
111113

112114
assertAll(
115+
() -> assertThat(response.storeId()).isEqualTo(5L),
113116
() -> assertThat(response.storeKakaoId()).isEqualTo("123456"),
114117
() -> assertThat(response.category()).isEqualTo("한식"),
115118
() -> assertThat(response.storeName()).isEqualTo("진또곱창집"),
@@ -137,4 +140,38 @@ class GetStory {
137140
.body("message", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getMessage()));
138141
}
139142
}
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+
}
140177
}

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

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +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;
9+
import static org.springframework.restdocs.payload.JsonFieldType.NUMBER;
10+
import static org.springframework.restdocs.payload.JsonFieldType.STRING;
811
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
912
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
1013
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
1114

15+
import eatda.controller.story.StoriesDetailResponse;
16+
import eatda.controller.story.StoriesDetailResponse.StoryDetailResponse;
1217
import eatda.controller.story.StoriesResponse;
1318
import eatda.controller.story.StoryRegisterRequest;
1419
import eatda.controller.story.StoryRegisterResponse;
@@ -26,6 +31,8 @@
2631
import java.util.List;
2732
import org.junit.jupiter.api.Nested;
2833
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.params.ParameterizedTest;
35+
import org.junit.jupiter.params.provider.EnumSource;
2936
import org.springframework.http.HttpHeaders;
3037
import org.springframework.restdocs.restassured.RestDocumentationFilter;
3138

@@ -162,21 +169,23 @@ class GetStory {
162169

163170
RestDocsResponse responseDocument = response()
164171
.responseBodyField(
165-
fieldWithPath("storeKakaoId").description("가게의 카카오 ID"),
166-
fieldWithPath("category").description("가게 카테고리"),
167-
fieldWithPath("storeName").description("가게 이름"),
168-
fieldWithPath("storeDistrict").description("가게 주소의 구"),
169-
fieldWithPath("storeNeighborhood").description("가게 주소의 동"),
170-
fieldWithPath("description").description("스토리 내용"),
171-
fieldWithPath("imageUrl").description("스토리 이미지 URL"),
172-
fieldWithPath("memberId").description("회원 ID"),
173-
fieldWithPath("memberNickname").description("회원 닉네임")
172+
fieldWithPath("storeId").type(NUMBER).description("가게의 카카오 ID (nullable)").optional(),
173+
fieldWithPath("storeKakaoId").type(STRING).description("가게의 카카오 ID"),
174+
fieldWithPath("category").type(STRING).description("가게 카테고리"),
175+
fieldWithPath("storeName").type(STRING).description("가게 이름"),
176+
fieldWithPath("storeDistrict").type(STRING).description("가게 주소의 구"),
177+
fieldWithPath("storeNeighborhood").type(STRING).description("가게 주소의 동"),
178+
fieldWithPath("description").type(STRING).description("스토리 내용"),
179+
fieldWithPath("imageUrl").type(STRING).description("스토리 이미지 URL"),
180+
fieldWithPath("memberId").type(NUMBER).description("회원 ID"),
181+
fieldWithPath("memberNickname").type(STRING).description("회원 닉네임")
174182
);
175183

176184
@Test
177185
void 스토리_상세_조회_성공() {
178186
long storyId = 1L;
179187
StoryResponse response = new StoryResponse(
188+
null,
180189
"123456",
181190
"한식",
182191
"진또곱창집",
@@ -223,4 +232,68 @@ class GetStory {
223232
.body("message", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getMessage()));
224233
}
225234
}
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+
}
226299
}

0 commit comments

Comments
 (0)