Skip to content

Commit e21ba4e

Browse files
authored
Merge pull request #171 from YAPP-Github/feat/PRODUCT-258
[Feat] 내가 올린 스트리 조회 API 구현
2 parents e303abf + 764485c commit e21ba4e

File tree

11 files changed

+236
-29
lines changed

11 files changed

+236
-29
lines changed

src/main/java/eatda/controller/cheer/CheerController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import lombok.RequiredArgsConstructor;
1313
import org.springframework.http.HttpStatus;
1414
import org.springframework.http.ResponseEntity;
15+
import org.springframework.validation.annotation.Validated;
1516
import org.springframework.web.bind.annotation.GetMapping;
1617
import org.springframework.web.bind.annotation.PathVariable;
1718
import org.springframework.web.bind.annotation.PostMapping;
@@ -20,6 +21,7 @@
2021
import org.springframework.web.bind.annotation.RestController;
2122
import org.springframework.web.multipart.MultipartFile;
2223

24+
@Validated
2325
@RestController
2426
@RequiredArgsConstructor
2527
public class CheerController {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
import java.util.List;
1111
import lombok.RequiredArgsConstructor;
1212
import org.springframework.http.ResponseEntity;
13+
import org.springframework.validation.annotation.Validated;
1314
import org.springframework.web.bind.annotation.GetMapping;
1415
import org.springframework.web.bind.annotation.PathVariable;
1516
import org.springframework.web.bind.annotation.RequestParam;
1617
import org.springframework.web.bind.annotation.RestController;
1718

19+
@Validated
1820
@RestController
1921
@RequiredArgsConstructor
2022
public class StoreController {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package eatda.controller.story;
2+
3+
import java.util.List;
4+
5+
public record StoriesInMemberResponse(List<StoryInMemberResponse> stories) {
6+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import lombok.RequiredArgsConstructor;
1313
import org.springframework.http.HttpStatus;
1414
import org.springframework.http.ResponseEntity;
15+
import org.springframework.validation.annotation.Validated;
1516
import org.springframework.web.bind.annotation.GetMapping;
1617
import org.springframework.web.bind.annotation.PathVariable;
1718
import org.springframework.web.bind.annotation.PostMapping;
@@ -20,6 +21,7 @@
2021
import org.springframework.web.bind.annotation.RestController;
2122
import org.springframework.web.multipart.MultipartFile;
2223

24+
@Validated
2325
@RestController
2426
@RequiredArgsConstructor
2527
public class StoryController {
@@ -54,6 +56,17 @@ public ResponseEntity<StoryResponse> getStory(@PathVariable long storyId) {
5456
.body(storyService.getStory(storyId));
5557
}
5658

59+
@GetMapping("/api/stories/member")
60+
public ResponseEntity<StoriesInMemberResponse> getStoriesByMemberId(
61+
@RequestParam(defaultValue = "0") @Min(0) int page,
62+
@RequestParam(defaultValue = "5") @Min(1) @Max(50) int size,
63+
LoginMember member
64+
) {
65+
StoriesInMemberResponse response = storyService.getPagedStoryByMemberId(member.id(), page, size);
66+
return ResponseEntity.status(HttpStatus.OK)
67+
.body(response);
68+
}
69+
5770
@GetMapping("/api/stories/kakao/{kakaoId}")
5871
public ResponseEntity<StoriesDetailResponse> getStoriesByKakaoId(
5972
@PathVariable String kakaoId,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package eatda.controller.story;
2+
3+
import eatda.domain.story.Story;
4+
5+
public record StoryInMemberResponse(
6+
Long id,
7+
String imageUrl,
8+
String storeName
9+
) {
10+
11+
public StoryInMemberResponse(Story story, String imageUrl) {
12+
this(story.getId(), imageUrl, story.getStoreName());
13+
}
14+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import eatda.domain.story.Story;
44
import org.springframework.data.domain.Page;
55
import org.springframework.data.domain.Pageable;
6+
import org.springframework.data.jpa.repository.EntityGraph;
67
import org.springframework.data.jpa.repository.JpaRepository;
78

89
public interface StoryRepository extends JpaRepository<Story, Long> {
910

1011
Page<Story> findAllByOrderByCreatedAtDesc(Pageable pageable);
1112

13+
Page<Story> findAllByMemberIdOrderByCreatedAtDesc(Long memberId, Pageable pageable);
14+
15+
@EntityGraph(attributePaths = {"member"})
1216
Page<Story> findAllByStoreKakaoIdOrderByCreatedAtDesc(String storeKakaoId, Pageable pageable);
1317
}

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package eatda.service.story;
22

33
import eatda.controller.story.StoriesDetailResponse;
4+
import eatda.controller.story.StoriesInMemberResponse;
45
import eatda.controller.story.StoriesResponse;
6+
import eatda.controller.story.StoriesResponse.StoryPreview;
7+
import eatda.controller.story.StoryInMemberResponse;
58
import eatda.controller.story.StoryRegisterRequest;
69
import eatda.controller.story.StoryRegisterResponse;
710
import eatda.controller.story.StoryResponse;
@@ -18,7 +21,6 @@
1821
import eatda.storage.image.ImageStorage;
1922
import java.util.List;
2023
import lombok.RequiredArgsConstructor;
21-
import org.springframework.data.domain.Page;
2224
import org.springframework.data.domain.PageRequest;
2325
import org.springframework.data.domain.Pageable;
2426
import org.springframework.stereotype.Service;
@@ -58,16 +60,12 @@ public StoryRegisterResponse registerStory(StoryRegisterRequest request,
5860
@Transactional(readOnly = true)
5961
public StoriesResponse getPagedStoryPreviews(int size) {
6062
Pageable pageable = PageRequest.of(PAGE_START_NUMBER, size);
61-
Page<Story> orderByPage = storyRepository.findAllByOrderByCreatedAtDesc(pageable);
63+
List<Story> stories = storyRepository.findAllByOrderByCreatedAtDesc(pageable).getContent();
6264

63-
return new StoriesResponse(
64-
orderByPage.getContent().stream()
65-
.map(story -> new StoriesResponse.StoryPreview(
66-
story.getId(),
67-
imageStorage.getPreSignedUrl(story.getImageKey())
68-
))
69-
.toList()
70-
);
65+
List<StoryPreview> responses = stories.stream()
66+
.map(story -> new StoryPreview(story.getId(), imageStorage.getPreSignedUrl(story.getImageKey())))
67+
.toList();
68+
return new StoriesResponse(responses);
7169
}
7270

7371
@Transactional(readOnly = true)
@@ -101,7 +99,18 @@ public StoriesDetailResponse getPagedStoryDetails(String kakaoId, int size) {
10199
List<StoriesDetailResponse.StoryDetailResponse> responses = stories.stream()
102100
.map(story -> new StoriesDetailResponse.StoryDetailResponse(
103101
story, imageStorage.getPreSignedUrl(story.getImageKey())))
104-
.toList(); // TODO: N+1 문제 해결
102+
.toList();
105103
return new StoriesDetailResponse(responses);
106104
}
105+
106+
@Transactional(readOnly = true)
107+
public StoriesInMemberResponse getPagedStoryByMemberId(long memberId, int page, int size) {
108+
List<Story> stories = storyRepository
109+
.findAllByMemberIdOrderByCreatedAtDesc(memberId, PageRequest.of(page, size))
110+
.getContent();
111+
List<StoryInMemberResponse> responses = stories.stream()
112+
.map(story -> new StoryInMemberResponse(story, imageStorage.getPreSignedUrl(story.getImageKey())))
113+
.toList();
114+
return new StoriesInMemberResponse(responses);
115+
}
107116
}

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

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.time.LocalDateTime;
1414
import org.junit.jupiter.api.Nested;
1515
import org.junit.jupiter.api.Test;
16+
import org.springframework.http.HttpHeaders;
1617

1718
public class StoryControllerTest extends BaseControllerTest {
1819

@@ -109,33 +110,63 @@ class GetStory {
109110
}
110111

111112
@Nested
112-
class GetPagedStoryDetailsByKakaoId {
113+
class GetStoriesByMemberId {
113114

114115
@Test
115-
void 카카오ID로_스토리_목록을_조회할_수_있다() {
116-
String kakaoId = "123456";
116+
void 회원의_스토리_목록을_조회할_수_있다() {
117+
Member member = memberGenerator.generateRegisteredMember("nickname", "[email protected]", "123", "01012345679");
117118
LocalDateTime startAt = LocalDateTime.of(2025, 8, 5, 12, 0, 0);
118-
Member member = memberGenerator.generateRegisteredMember("test", "[email protected]", "812", "01081231234");
119-
Story story1 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt);
120-
Story story2 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt.plusHours(1));
121-
122-
StoriesDetailResponse response = given()
123-
.pathParam("kakaoId", kakaoId)
124-
.queryParam("size", 5)
119+
Story story1 = storyGenerator.generate(member, "123456", "진또곱창집", startAt);
120+
Story story2 = storyGenerator.generate(member, "654321", "또진곱창집", startAt.plusHours(1));
121+
int page = 0;
122+
int size = 5;
123+
124+
StoriesInMemberResponse response = given()
125+
.header(HttpHeaders.AUTHORIZATION, accessToken(member))
126+
.queryParam("page", page)
127+
.queryParam("size", size)
125128
.when()
126-
.get("/api/stories/kakao/{kakaoId}")
129+
.get("/api/stories/member")
127130
.then().statusCode(200)
128-
.extract().as(StoriesDetailResponse.class);
131+
.extract().as(StoriesInMemberResponse.class);
129132

130133
assertAll(
131134
() -> assertThat(response.stories()).hasSize(2),
132-
() -> assertThat(response.stories().getFirst().storyId()).isEqualTo(story2.getId()),
133-
() -> assertThat(response.stories().getFirst().memberId()).isEqualTo(member.getId()),
134-
() -> assertThat(response.stories().getFirst().memberNickname()).isEqualTo(member.getNickname()),
135-
() -> assertThat(response.stories().get(1).storyId()).isEqualTo(story1.getId()),
136-
() -> assertThat(response.stories().get(1).memberId()).isEqualTo(member.getId()),
137-
() -> assertThat(response.stories().get(1).memberNickname()).isEqualTo(member.getNickname())
135+
() -> assertThat(response.stories().get(0).id()).isEqualTo(story2.getId()),
136+
() -> assertThat(response.stories().get(1).id()).isEqualTo(story1.getId())
138137
);
139138
}
139+
140+
@Nested
141+
class GetStoriesByKakaoId {
142+
143+
@Test
144+
void 카카오ID로_스토리_목록을_조회할_수_있다() {
145+
String kakaoId = "123456";
146+
LocalDateTime startAt = LocalDateTime.of(2025, 8, 5, 12, 0, 0);
147+
Member member = memberGenerator.generateRegisteredMember("test", "[email protected]", "812",
148+
"01081231234");
149+
Story story1 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt);
150+
Story story2 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt.plusHours(1));
151+
152+
StoriesDetailResponse response = given()
153+
.pathParam("kakaoId", kakaoId)
154+
.queryParam("size", 5)
155+
.when()
156+
.get("/api/stories/kakao/{kakaoId}")
157+
.then().statusCode(200)
158+
.extract().as(StoriesDetailResponse.class);
159+
160+
assertAll(
161+
() -> assertThat(response.stories()).hasSize(2),
162+
() -> assertThat(response.stories().get(0).storyId()).isEqualTo(story2.getId()),
163+
() -> assertThat(response.stories().get(0).memberId()).isEqualTo(member.getId()),
164+
() -> assertThat(response.stories().get(0).memberNickname()).isEqualTo(member.getNickname()),
165+
() -> assertThat(response.stories().get(1).storyId()).isEqualTo(story1.getId()),
166+
() -> assertThat(response.stories().get(1).memberId()).isEqualTo(member.getId()),
167+
() -> assertThat(response.stories().get(1).memberNickname()).isEqualTo(member.getNickname())
168+
);
169+
}
170+
}
140171
}
141172
}

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.hamcrest.Matchers.equalTo;
44
import static org.mockito.ArgumentMatchers.any;
55
import static org.mockito.ArgumentMatchers.anyLong;
6+
import static org.mockito.ArgumentMatchers.eq;
67
import static org.mockito.Mockito.doReturn;
78
import static org.mockito.Mockito.doThrow;
89
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
@@ -15,7 +16,9 @@
1516

1617
import eatda.controller.story.StoriesDetailResponse;
1718
import eatda.controller.story.StoriesDetailResponse.StoryDetailResponse;
19+
import eatda.controller.story.StoriesInMemberResponse;
1820
import eatda.controller.story.StoriesResponse;
21+
import eatda.controller.story.StoryInMemberResponse;
1922
import eatda.controller.story.StoryRegisterRequest;
2023
import eatda.controller.story.StoryRegisterResponse;
2124
import eatda.controller.story.StoryResponse;
@@ -233,6 +236,76 @@ class GetStory {
233236
}
234237
}
235238

239+
@Nested
240+
class GetStoriesByMemberId {
241+
242+
RestDocsRequest requestDocument = request()
243+
.tag(Tag.STORY_API)
244+
.summary("회원 ID로 스토리 목록 조회")
245+
.requestHeader(
246+
headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰")
247+
)
248+
.queryParameter(
249+
parameterWithName("page").description("페이지 번호 (기본값: 0) (최소값: 0)").optional(),
250+
parameterWithName("size").description("스토리 개수 (기본값: 5) (최소값: 1, 최대값: 50)").optional()
251+
);
252+
253+
RestDocsResponse responseDocument = response()
254+
.responseBodyField(
255+
fieldWithPath("stories").type(ARRAY).description("스토리 리스트"),
256+
fieldWithPath("stories[].id").type(NUMBER).description("스토리 ID"),
257+
fieldWithPath("stories[].imageUrl").type(STRING).description("스토리 이미지 URL"),
258+
fieldWithPath("stories[].storeName").type(STRING).description("가게 이름")
259+
);
260+
261+
@Test
262+
void 회원_ID로_스토리_목록_조회_성공() {
263+
int page = 0;
264+
int size = 5;
265+
StoriesInMemberResponse response = new StoriesInMemberResponse(List.of(
266+
new StoryInMemberResponse(1L, "https://dummy-s3.com/story1.png", "백암순대"),
267+
new StoryInMemberResponse(2L, "https://dummy-s3.com/story2.png", "맥도날드")
268+
));
269+
doReturn(response).when(storyService).getPagedStoryByMemberId(anyLong(), eq(page), eq(size));
270+
271+
var document = document("story/get-stories-by-member-id", 200)
272+
.request(requestDocument)
273+
.response(responseDocument)
274+
.build();
275+
276+
given(document)
277+
.queryParam("page", page)
278+
.queryParam("size", size)
279+
.header(HttpHeaders.AUTHORIZATION, accessToken())
280+
.when().get("/api/stories/member")
281+
.then().statusCode(200);
282+
}
283+
284+
@EnumSource(value = BusinessErrorCode.class, names = {
285+
"UNAUTHORIZED_MEMBER",
286+
"EXPIRED_TOKEN",
287+
"INVALID_MEMBER_ID"})
288+
@ParameterizedTest
289+
void 회원_ID로_스토리_목록_조회_실패(BusinessErrorCode errorCode) {
290+
int page = 0;
291+
int size = 5;
292+
doThrow(new BusinessException(errorCode))
293+
.when(storyService).getPagedStoryByMemberId(anyLong(), eq(page), eq(size));
294+
295+
var document = document("story/get-stories-by-member-id", errorCode)
296+
.request(requestDocument)
297+
.response(ERROR_RESPONSE)
298+
.build();
299+
300+
given(document)
301+
.queryParam("page", page)
302+
.queryParam("size", size)
303+
.header(HttpHeaders.AUTHORIZATION, accessToken())
304+
.when().get("/api/stories/member")
305+
.then().statusCode(errorCode.getStatus().value());
306+
}
307+
}
308+
236309
@Nested
237310
class GetStoriesByKakaoId {
238311

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import eatda.fixture.CheerGenerator;
1111
import eatda.fixture.MemberGenerator;
1212
import eatda.fixture.StoreGenerator;
13+
import eatda.fixture.StoryGenerator;
1314
import eatda.repository.cheer.CheerRepository;
1415
import eatda.repository.cheer.CheerTagRepository;
1516
import eatda.repository.member.MemberRepository;
@@ -56,6 +57,9 @@ public abstract class BaseServiceTest {
5657
@Autowired
5758
protected CheerGenerator cheerGenerator;
5859

60+
@Autowired
61+
protected StoryGenerator storyGenerator;
62+
5963
@Autowired
6064
protected MemberRepository memberRepository;
6165

0 commit comments

Comments
 (0)