Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/main/java/eatda/controller/story/StoriesDetailResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package eatda.controller.story;

import eatda.domain.story.Story;
import java.util.List;

public record StoriesDetailResponse(List<StoryDetailResponse> stories) {

public record StoryDetailResponse(
long storyId,
String imageUrl,
long memberId,
String memberNickname
) {

public StoryDetailResponse(Story story, String imageUrl) {
this(
story.getId(),
imageUrl,
story.getMember().getId(),
story.getMember().getNickname()
);
}
}
}
9 changes: 9 additions & 0 deletions src/main/java/eatda/controller/story/StoryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,13 @@ public ResponseEntity<StoryResponse> getStory(@PathVariable long storyId) {
return ResponseEntity.status(HttpStatus.OK)
.body(storyService.getStory(storyId));
}

@GetMapping("/api/stories/kakao/{kakaoId}")
public ResponseEntity<StoriesDetailResponse> getStoriesByKakaoId(
@PathVariable String kakaoId,
@RequestParam(defaultValue = "5") @Min(1) @Max(50) int size
) {
return ResponseEntity.status(HttpStatus.OK)
.body(storyService.getPagedStoryDetails(kakaoId, size));
}
}
2 changes: 2 additions & 0 deletions src/main/java/eatda/repository/story/StoryRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
public interface StoryRepository extends JpaRepository<Story, Long> {

Page<Story> findAllByOrderByCreatedAtDesc(Pageable pageable);

Page<Story> findAllByStoreKakaoIdOrderByCreatedAtDesc(String storeKakaoId, Pageable pageable);
}
14 changes: 14 additions & 0 deletions src/main/java/eatda/service/story/StoryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import eatda.client.map.MapClient;
import eatda.client.map.StoreSearchResult;
import eatda.controller.story.FilteredSearchResult;
import eatda.controller.story.StoriesDetailResponse;
import eatda.controller.story.StoriesResponse;
import eatda.controller.story.StoryRegisterRequest;
import eatda.controller.story.StoryRegisterResponse;
Expand Down Expand Up @@ -113,4 +114,17 @@ public StoryResponse getStory(long storyId) {
story.getMember().getNickname()
);
}

@Transactional(readOnly = true)
public StoriesDetailResponse getPagedStoryDetails(String kakaoId, int size) {
List<Story> stories = storyRepository
.findAllByStoreKakaoIdOrderByCreatedAtDesc(kakaoId, PageRequest.of(PAGE_START_NUMBER, size))
.getContent();

List<StoriesDetailResponse.StoryDetailResponse> responses = stories.stream()
.map(story -> new StoriesDetailResponse.StoryDetailResponse(
story, imageStorage.getPreSignedUrl(story.getImageKey())))
.toList(); // TODO: N+1 문제 해결
return new StoriesDetailResponse(responses);
}
}
35 changes: 35 additions & 0 deletions src/test/java/eatda/controller/story/StoryControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.mockito.Mockito.doThrow;

import eatda.controller.BaseControllerTest;
import eatda.controller.story.StoriesDetailResponse.StoryDetailResponse;
import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import eatda.util.ImageUtils;
Expand Down Expand Up @@ -139,4 +140,38 @@ class GetStory {
.body("message", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getMessage()));
}
}

@Nested
class GetPagedStoryDetailsByKakaoId {

@Test
void 카카오ID로_스토리_목록을_조회할_수_있다() {
String kakaoId = "123456";
List<StoryDetailResponse> mockDetails = List.of(
new StoryDetailResponse(1L, "https://s3.bucket.com/story/dummy/1.jpg", 5L, "커찬"),
new StoryDetailResponse(2L, "https://s3.bucket.com/story/dummy/2.jpg", 2L, "지민")
);

doReturn(new StoriesDetailResponse(mockDetails))
.when(storyService)
.getPagedStoryDetails(kakaoId, 5);

StoriesDetailResponse response = given()
.pathParam("kakaoId", kakaoId)
.queryParam("size", 5)
.when()
.get("/api/stories/kakao/{kakaoId}")
.then().statusCode(200)
.extract().as(StoriesDetailResponse.class);

assertAll(
() -> assertThat(response.stories()).hasSize(2),
() -> assertThat(response.stories().getFirst().storyId()).isEqualTo(1L),
() -> assertThat(response.stories().getFirst().imageUrl()).isEqualTo(
"https://s3.bucket.com/story/dummy/1.jpg"),
() -> assertThat(response.stories().getFirst().memberId()).isEqualTo(5L),
() -> assertThat(response.stories().getFirst().memberNickname()).isEqualTo("커찬")
);
}
}
}
69 changes: 69 additions & 0 deletions src/test/java/eatda/document/story/StoryDocumentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.payload.JsonFieldType.ARRAY;
import static org.springframework.restdocs.payload.JsonFieldType.NUMBER;
import static org.springframework.restdocs.payload.JsonFieldType.STRING;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;

import eatda.controller.story.StoriesDetailResponse;
import eatda.controller.story.StoriesDetailResponse.StoryDetailResponse;
import eatda.controller.story.StoriesResponse;
import eatda.controller.story.StoryRegisterRequest;
import eatda.controller.story.StoryRegisterResponse;
Expand All @@ -28,6 +31,8 @@
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.restassured.RestDocumentationFilter;

Expand Down Expand Up @@ -227,4 +232,68 @@ class GetStory {
.body("message", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getMessage()));
}
}

@Nested
class GetStoriesByKakaoId {

RestDocsRequest requestDocument = request()
.tag(Tag.STORY_API)
.summary("카카오 ID로 스토리 목록 조회")
.description("특정 카카오 ID에 해당하는 스토리 목록을 페이지네이션하여 조회합니다.")
.pathParameter(
parameterWithName("kakaoId").description("가게의 카카오 ID")
)
.queryParameter(
parameterWithName("size").description("스토리 개수 (기본값: 5) (최소값: 1, 최대값: 50)").optional()
);

RestDocsResponse responseDocument = response()
.responseBodyField(
fieldWithPath("stories").type(ARRAY).description("스토리 상세 리스트"),
fieldWithPath("stories[].storyId").type(NUMBER).description("스토리 ID"),
fieldWithPath("stories[].imageUrl").type(STRING).description("스토리 이미지 URL"),
fieldWithPath("stories[].memberId").type(NUMBER).description("회원 ID"),
fieldWithPath("stories[].memberNickname").type(STRING).description("회원 닉네임")
);

@Test
void 카카오_ID로_스토리_목록_조회_성공() {
String kakaoId = "123456";
int size = 5;
StoriesDetailResponse response = new StoriesDetailResponse(List.of(
new StoryDetailResponse(1L, "https://dummy-s3.com/story1.png", 1L, "커찬"),
new StoryDetailResponse(2L, "https://dummy-s3.com/story2.png", 2L, "준환")
));
doReturn(response).when(storyService).getPagedStoryDetails(kakaoId, size);

var document = document("story/get-stories-by-kakao-id", 200)
.request(requestDocument)
.response(responseDocument)
.build();

given(document)
.queryParam("size", size)
.header(HttpHeaders.AUTHORIZATION, accessToken())
.when().get("/api/stories/kakao/{kakaoId}", kakaoId)
.then().statusCode(200);
}

@EnumSource(value = BusinessErrorCode.class, names = {"PRESIGNED_URL_GENERATION_FAILED"})
@ParameterizedTest
void 카카오_ID로_스토리_목록_조회_실패(BusinessErrorCode errorCode) {
String kakaoId = "nonexistent";
int size = 5;
doThrow(new BusinessException(errorCode)).when(storyService).getPagedStoryDetails(kakaoId, size);

var document = document("story/get-stories-by-kakao-id", errorCode)
.request(requestDocument)
.response(ERROR_RESPONSE)
.build();

given(document)
.queryParam("size", size)
.when().get("/api/stories/kakao/{kakaoId}", kakaoId)
.then().statusCode(errorCode.getStatus().value());
}
}
}
94 changes: 93 additions & 1 deletion src/test/java/eatda/service/story/StoryServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.mockito.Mockito.when;

import eatda.client.map.StoreSearchResult;
import eatda.controller.story.StoriesDetailResponse;
import eatda.controller.story.StoriesResponse.StoryPreview;
import eatda.controller.story.StoryRegisterRequest;
import eatda.controller.story.StoryResponse;
Expand All @@ -27,7 +28,7 @@
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

public class StoryServiceTest extends BaseServiceTest {
class StoryServiceTest extends BaseServiceTest {

@Autowired
private StoryService storyService;
Expand Down Expand Up @@ -187,4 +188,95 @@ class GetStory {
.hasMessageContaining(BusinessErrorCode.STORY_NOT_FOUND.getMessage());
}
}

@Nested
class GetPagedStoryDetails {

@Test
void 카카오ID로_스토리_목록을_조회할_수_있다() {
String kakaoId = "123456";
Member member = memberGenerator.generate("12345");
Story story1 = Story.builder()
.member(member)
.storeKakaoId(kakaoId)
.storeName("곱창집")
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
.storeCategory(StoreCategory.KOREAN)
.description("미쳤다 진짜")
.imageKey(new ImageKey("image-key-1"))
.build();
Story story2 = Story.builder()
.member(member)
.storeKakaoId(kakaoId)
.storeName("순대국밥집")
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
.storeCategory(StoreCategory.KOREAN)
.description("뜨끈한 국밥 최고")
.imageKey(new ImageKey("image-key-2"))
.build();
storyRepository.save(story1);
storyRepository.save(story2);
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-1")))
.thenReturn("https://s3.bucket.com/story/dummy/1.jpg");
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-2")))
.thenReturn("https://s3.bucket.com/story/dummy/2.jpg");

var response = storyService.getPagedStoryDetails(kakaoId, 5);

assertThat(response.stories())
.hasSize(2)
.extracting(StoriesDetailResponse.StoryDetailResponse::storyId)
.containsExactlyInAnyOrder(story2.getId(), story1.getId());
}

@Test
void 카카오ID로_스토리_목록을_조회할_때_특정_스토리만_반환한다() {
String kakaoId = "123456";
Member member = memberGenerator.generate("12345");
Story story1 = Story.builder()
.member(member)
.storeKakaoId(kakaoId)
.storeName("곱창집")
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
.storeCategory(StoreCategory.KOREAN)
.description("미쳤다 진짜")
.imageKey(new ImageKey("image-key-1"))
.build();
Story story2 = Story.builder()
.member(member)
.storeKakaoId("different-kakao-id")
.storeName("순대국밥집")
.storeRoadAddress("서울시 성동구 왕십리로 1길 12")
.storeLotNumberAddress("서울시 성동구 성수동1가 685-12")
.storeCategory(StoreCategory.KOREAN)
.description("뜨끈한 국밥 최고")
.imageKey(new ImageKey("image-key-2"))
.build();
storyRepository.save(story1);
storyRepository.save(story2);
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-1")))
.thenReturn("https://s3.bucket.com/story/dummy/1.jpg");
when(externalImageStorage.getPreSignedUrl(new ImageKey("image-key-2")))
.thenReturn("https://s3.bucket.com/story/dummy/2.jpg");

var response = storyService.getPagedStoryDetails(kakaoId, 5);

assertThat(response.stories())
.hasSize(1)
.extracting(StoriesDetailResponse.StoryDetailResponse::storyId)
.containsExactlyInAnyOrder(story1.getId());
}
}

@Test
void 존재하지_않는_카카오ID로_조회하면_빈_목록을_반환한다() {
String nonExistentKakaoId = "non-existent";

var response = storyService.getPagedStoryDetails(nonExistentKakaoId, 5);

assertThat(response.stories()).isEmpty();
}
}