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
2 changes: 2 additions & 0 deletions src/main/java/eatda/controller/cheer/CheerController.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -20,6 +21,7 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Validated
@RestController
@RequiredArgsConstructor
public class CheerController {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/eatda/controller/store/StoreController.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequiredArgsConstructor
public class StoreController {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package eatda.controller.story;

import java.util.List;

public record StoriesInMemberResponse(List<StoryInMemberResponse> stories) {
}
13 changes: 13 additions & 0 deletions src/main/java/eatda/controller/story/StoryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -20,6 +21,7 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@Validated
@RestController
@RequiredArgsConstructor
public class StoryController {
Expand Down Expand Up @@ -54,6 +56,17 @@ public ResponseEntity<StoryResponse> getStory(@PathVariable long storyId) {
.body(storyService.getStory(storyId));
}

@GetMapping("/api/stories/member")
public ResponseEntity<StoriesInMemberResponse> getStoriesByMemberId(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "5") @Min(1) @Max(50) int size,
LoginMember member
) {
StoriesInMemberResponse response = storyService.getPagedStoryByMemberId(member.id(), page, size);
return ResponseEntity.status(HttpStatus.OK)
.body(response);
}

@GetMapping("/api/stories/kakao/{kakaoId}")
public ResponseEntity<StoriesDetailResponse> getStoriesByKakaoId(
@PathVariable String kakaoId,
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/eatda/controller/story/StoryInMemberResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package eatda.controller.story;

import eatda.domain.story.Story;

public record StoryInMemberResponse(
Long id,
String imageUrl,
String storeName
) {

public StoryInMemberResponse(Story story, String imageUrl) {
this(story.getId(), imageUrl, story.getStoreName());
}
}
4 changes: 4 additions & 0 deletions src/main/java/eatda/repository/story/StoryRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
import eatda.domain.story.Story;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StoryRepository extends JpaRepository<Story, Long> {

Page<Story> findAllByOrderByCreatedAtDesc(Pageable pageable);

Page<Story> findAllByMemberIdOrderByCreatedAtDesc(Long memberId, Pageable pageable);

@EntityGraph(attributePaths = {"member"})
Page<Story> findAllByStoreKakaoIdOrderByCreatedAtDesc(String storeKakaoId, Pageable pageable);
}
31 changes: 20 additions & 11 deletions src/main/java/eatda/service/story/StoryService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package eatda.service.story;

import eatda.controller.story.StoriesDetailResponse;
import eatda.controller.story.StoriesInMemberResponse;
import eatda.controller.story.StoriesResponse;
import eatda.controller.story.StoriesResponse.StoryPreview;
import eatda.controller.story.StoryInMemberResponse;
import eatda.controller.story.StoryRegisterRequest;
import eatda.controller.story.StoryRegisterResponse;
import eatda.controller.story.StoryResponse;
Expand All @@ -18,7 +21,6 @@
import eatda.storage.image.ImageStorage;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -58,16 +60,12 @@ public StoryRegisterResponse registerStory(StoryRegisterRequest request,
@Transactional(readOnly = true)
public StoriesResponse getPagedStoryPreviews(int size) {
Pageable pageable = PageRequest.of(PAGE_START_NUMBER, size);
Page<Story> orderByPage = storyRepository.findAllByOrderByCreatedAtDesc(pageable);
List<Story> stories = storyRepository.findAllByOrderByCreatedAtDesc(pageable).getContent();

return new StoriesResponse(
orderByPage.getContent().stream()
.map(story -> new StoriesResponse.StoryPreview(
story.getId(),
imageStorage.getPreSignedUrl(story.getImageKey())
))
.toList()
);
List<StoryPreview> responses = stories.stream()
.map(story -> new StoryPreview(story.getId(), imageStorage.getPreSignedUrl(story.getImageKey())))
.toList();
return new StoriesResponse(responses);
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -101,7 +99,18 @@ public StoriesDetailResponse getPagedStoryDetails(String kakaoId, int size) {
List<StoriesDetailResponse.StoryDetailResponse> responses = stories.stream()
.map(story -> new StoriesDetailResponse.StoryDetailResponse(
story, imageStorage.getPreSignedUrl(story.getImageKey())))
.toList(); // TODO: N+1 문제 해결
.toList();
return new StoriesDetailResponse(responses);
}

@Transactional(readOnly = true)
public StoriesInMemberResponse getPagedStoryByMemberId(long memberId, int page, int size) {
List<Story> stories = storyRepository
.findAllByMemberIdOrderByCreatedAtDesc(memberId, PageRequest.of(page, size))
.getContent();
List<StoryInMemberResponse> responses = stories.stream()
.map(story -> new StoryInMemberResponse(story, imageStorage.getPreSignedUrl(story.getImageKey())))
.toList();
return new StoriesInMemberResponse(responses);
}
}
4 changes: 4 additions & 0 deletions src/test/java/eatda/controller/BaseControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ protected final String accessToken() {
return jwtManager.issueAccessToken(member.getId());
}

protected final String accessToken(Member member) {
return jwtManager.issueAccessToken(member.getId());
}

protected final String refreshToken() {
Member member = memberGenerator.generateByEmail(Long.toString(DEFAULT_OAUTH_MEMBER_INFO.socialId()),
"[email protected]");
Expand Down
67 changes: 49 additions & 18 deletions src/test/java/eatda/controller/story/StoryControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.time.LocalDateTime;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;

public class StoryControllerTest extends BaseControllerTest {

Expand Down Expand Up @@ -109,33 +110,63 @@ class GetStory {
}

@Nested
class GetPagedStoryDetailsByKakaoId {
class GetStoriesByMemberId {

@Test
void 카카오ID로_스토리_목록을_조회할_수_있다() {
String kakaoId = "123456";
void 회원의_스토리_목록을_조회할_수_있다() {
Member member = memberGenerator.generateRegisteredMember("nickname", "[email protected]", "123", "01012345679");
LocalDateTime startAt = LocalDateTime.of(2025, 8, 5, 12, 0, 0);
Member member = memberGenerator.generateRegisteredMember("test", "[email protected]", "812", "01081231234");
Story story1 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt);
Story story2 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt.plusHours(1));

StoriesDetailResponse response = given()
.pathParam("kakaoId", kakaoId)
.queryParam("size", 5)
Story story1 = storyGenerator.generate(member, "123456", "진또곱창집", startAt);
Story story2 = storyGenerator.generate(member, "654321", "또진곱창집", startAt.plusHours(1));
int page = 0;
int size = 5;

StoriesInMemberResponse response = given()
.header(HttpHeaders.AUTHORIZATION, accessToken(member))
.queryParam("page", page)
.queryParam("size", size)
.when()
.get("/api/stories/kakao/{kakaoId}")
.get("/api/stories/member")
.then().statusCode(200)
.extract().as(StoriesDetailResponse.class);
.extract().as(StoriesInMemberResponse.class);

assertAll(
() -> assertThat(response.stories()).hasSize(2),
() -> assertThat(response.stories().getFirst().storyId()).isEqualTo(story2.getId()),
() -> assertThat(response.stories().getFirst().memberId()).isEqualTo(member.getId()),
() -> assertThat(response.stories().getFirst().memberNickname()).isEqualTo(member.getNickname()),
() -> assertThat(response.stories().get(1).storyId()).isEqualTo(story1.getId()),
() -> assertThat(response.stories().get(1).memberId()).isEqualTo(member.getId()),
() -> assertThat(response.stories().get(1).memberNickname()).isEqualTo(member.getNickname())
() -> assertThat(response.stories().get(0).id()).isEqualTo(story2.getId()),
() -> assertThat(response.stories().get(1).id()).isEqualTo(story1.getId())
);
}

@Nested
class GetStoriesByKakaoId {

@Test
void 카카오ID로_스토리_목록을_조회할_수_있다() {
String kakaoId = "123456";
LocalDateTime startAt = LocalDateTime.of(2025, 8, 5, 12, 0, 0);
Member member = memberGenerator.generateRegisteredMember("test", "[email protected]", "812",
"01081231234");
Story story1 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt);
Story story2 = storyGenerator.generate(member, kakaoId, "진또곱창집", startAt.plusHours(1));

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().get(0).storyId()).isEqualTo(story2.getId()),
() -> assertThat(response.stories().get(0).memberId()).isEqualTo(member.getId()),
() -> assertThat(response.stories().get(0).memberNickname()).isEqualTo(member.getNickname()),
() -> assertThat(response.stories().get(1).storyId()).isEqualTo(story1.getId()),
() -> assertThat(response.stories().get(1).memberId()).isEqualTo(member.getId()),
() -> assertThat(response.stories().get(1).memberNickname()).isEqualTo(member.getNickname())
);
}
}
}
}
73 changes: 73 additions & 0 deletions src/test/java/eatda/document/story/StoryDocumentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
Expand All @@ -15,7 +16,9 @@

import eatda.controller.story.StoriesDetailResponse;
import eatda.controller.story.StoriesDetailResponse.StoryDetailResponse;
import eatda.controller.story.StoriesInMemberResponse;
import eatda.controller.story.StoriesResponse;
import eatda.controller.story.StoryInMemberResponse;
import eatda.controller.story.StoryRegisterRequest;
import eatda.controller.story.StoryRegisterResponse;
import eatda.controller.story.StoryResponse;
Expand Down Expand Up @@ -233,6 +236,76 @@ class GetStory {
}
}

@Nested
class GetStoriesByMemberId {

RestDocsRequest requestDocument = request()
.tag(Tag.STORY_API)
.summary("회원 ID로 스토리 목록 조회")
.requestHeader(
headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰")
)
.queryParameter(
parameterWithName("page").description("페이지 번호 (기본값: 0) (최소값: 0)").optional(),
parameterWithName("size").description("스토리 개수 (기본값: 5) (최소값: 1, 최대값: 50)").optional()
);

RestDocsResponse responseDocument = response()
.responseBodyField(
fieldWithPath("stories").type(ARRAY).description("스토리 리스트"),
fieldWithPath("stories[].id").type(NUMBER).description("스토리 ID"),
fieldWithPath("stories[].imageUrl").type(STRING).description("스토리 이미지 URL"),
fieldWithPath("stories[].storeName").type(STRING).description("가게 이름")
);

@Test
void 회원_ID로_스토리_목록_조회_성공() {
int page = 0;
int size = 5;
StoriesInMemberResponse response = new StoriesInMemberResponse(List.of(
new StoryInMemberResponse(1L, "https://dummy-s3.com/story1.png", "백암순대"),
new StoryInMemberResponse(2L, "https://dummy-s3.com/story2.png", "맥도날드")
));
doReturn(response).when(storyService).getPagedStoryByMemberId(anyLong(), eq(page), eq(size));

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

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

@EnumSource(value = BusinessErrorCode.class, names = {
"UNAUTHORIZED_MEMBER",
"EXPIRED_TOKEN",
"INVALID_MEMBER_ID"})
@ParameterizedTest
void 회원_ID로_스토리_목록_조회_실패(BusinessErrorCode errorCode) {
int page = 0;
int size = 5;
doThrow(new BusinessException(errorCode))
.when(storyService).getPagedStoryByMemberId(anyLong(), eq(page), eq(size));

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

given(document)
.queryParam("page", page)
.queryParam("size", size)
.header(HttpHeaders.AUTHORIZATION, accessToken())
.when().get("/api/stories/member")
.then().statusCode(errorCode.getStatus().value());
}
}

@Nested
class GetStoriesByKakaoId {

Expand Down
4 changes: 4 additions & 0 deletions src/test/java/eatda/service/BaseServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import eatda.fixture.CheerGenerator;
import eatda.fixture.MemberGenerator;
import eatda.fixture.StoreGenerator;
import eatda.fixture.StoryGenerator;
import eatda.repository.cheer.CheerRepository;
import eatda.repository.cheer.CheerTagRepository;
import eatda.repository.member.MemberRepository;
Expand Down Expand Up @@ -56,6 +57,9 @@ public abstract class BaseServiceTest {
@Autowired
protected CheerGenerator cheerGenerator;

@Autowired
protected StoryGenerator storyGenerator;

@Autowired
protected MemberRepository memberRepository;

Expand Down
Loading