-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] 스토리 목록 조회 구현 #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e571d16
3bc497d
ed968af
d605310
90351ef
6978e06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,11 @@ | ||
| package eatda.repository.story; | ||
|
|
||
| import eatda.domain.story.Story; | ||
| import eatda.exception.BusinessErrorCode; | ||
| import eatda.exception.BusinessException; | ||
| import java.util.Optional; | ||
| import org.springframework.data.repository.Repository; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface StoryRepository extends Repository<Story, Long> { | ||
| public interface StoryRepository extends JpaRepository<Story, Long> { | ||
|
|
||
| Story save(Story story); | ||
|
|
||
| Optional<Story> findById(Long id); | ||
|
|
||
| default Story getById(Long id) { | ||
| return findById(id) | ||
| .orElseThrow(() -> new BusinessException(BusinessErrorCode.STORY_NOT_FOUND)); | ||
| } | ||
| Page<Story> findAllByOrderByCreatedAtDesc(Pageable pageable); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| import eatda.client.map.StoreSearchResult; | ||
| import eatda.controller.story.FilteredSearchResult; | ||
| import eatda.controller.story.StoriesResponse; | ||
| import eatda.controller.story.StoryRegisterRequest; | ||
| import eatda.domain.member.Member; | ||
| import eatda.domain.story.Story; | ||
|
|
@@ -14,13 +15,18 @@ | |
| import eatda.service.store.StoreService; | ||
| 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; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class StoryService { | ||
| private static final int PAGE_START_NUMBER = 0; | ||
| private static final int PAGE_SIZE = 5; | ||
|
Comment on lines
+28
to
+29
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [선택] 현재는 FE 분들과 size는 5개 고정하시기로 이야기 된거죠?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞습니다.. 5개로 고정해버리기로 이야기 했는데... |
||
|
|
||
| private final StoreService storeService; | ||
| private final ImageService imageService; | ||
|
|
@@ -59,4 +65,19 @@ private FilteredSearchResult filteredSearchResponse(List<StoreSearchResult> resp | |
| )) | ||
| .orElseThrow(() -> new BusinessException(BusinessErrorCode.STORE_NOT_FOUND)); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public StoriesResponse getPagedStoryPreviews() { | ||
| Pageable pageable = PageRequest.of(PAGE_START_NUMBER, PAGE_SIZE); | ||
| Page<Story> orderByPage = storyRepository.findAllByOrderByCreatedAtDesc(pageable); | ||
|
|
||
| return new StoriesResponse( | ||
| orderByPage.getContent().stream() | ||
| .map(story -> new StoriesResponse.StoryPreview( | ||
| story.getId(), | ||
| imageService.getPresignedUrl(story.getImageKey()) | ||
| )) | ||
| .toList() | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| INSERT INTO story (member_id, store_kakao_id, store_name, store_address, | ||
| store_category, description, image_key) | ||
| VALUES (1, '99999999999', '맛있는 한식집', '서울시 강남구 역삼동 123-45', 'KOREAN', '진짜 여기 곱창 맛집임. 다시 또 갈 듯!', 'story/preview/1.jpg'), | ||
| (2, '99999999998', '아름다운 양식집', '서울시 강남구 역삼동 67-89', 'WESTERN', '스테이크가 부드럽고 서비스도 좋아요.', 'story/preview/2.jpg'), | ||
| (3, '99999999997', '정통 중식당', '서울시 강남구 역삼동 101-112', 'CHINESE', '짜장면이 정통의 맛. 강력 추천.', 'story/preview/3.jpg'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| INSERT INTO story (member_id, store_kakao_id, store_name, store_address, | ||
| store_category, description, image_key) | ||
| VALUES (1, '99999999999', '맛있는 한식집', '서울시 강남구 역삼동 123-45', 'KOREAN', '진짜 여기 곱창 맛집임. 다시 또 갈 듯!', 'story/preview/1.jpg'), | ||
| (2, '99999999998', '아름다운 양식집', '서울시 강남구 역삼동 67-89', 'WESTERN', '스테이크가 부드럽고 서비스도 좋아요.', 'story/preview/2.jpg'), | ||
| (3, '99999999997', '정통 중식당', '서울시 강남구 역삼동 101-112', 'CHINESE', '짜장면이 정통의 맛. 강력 추천.', 'story/preview/3.jpg'), | ||
| (4, '99999999996', '고급 양식 레스토랑', '서울시 강남구 역삼동 131-415', 'WESTERN', '분위기가 연인 데이트하기 좋아요.', 'story/preview/4.jpg'), | ||
| (5, '99999999995', '달콤한 디저트 카페', '서울시 강남구 역삼동 161-718', 'ETC', '케이크가 촉촉하고 맛있어요.', 'story/preview/5.jpg'), | ||
| (6, '99999999994', '아늑한 카페', '서울시 강남구 역삼동 192-021', 'ETC', '조용해서 공부하기 좋아요.', 'story/preview/6.jpg'), | ||
| (7, '99999999993', '빠른 패스트푸드점', '서울시 강남구 역삼동 222-324', 'ETC', '햄버거 나오는데 3분도 안 걸림. 굿.', 'story/preview/7.jpg'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| package eatda.service.story; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
| import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; | ||
| import static org.mockito.Mockito.doReturn; | ||
|
|
@@ -9,12 +10,16 @@ | |
| import eatda.client.map.StoreSearchResult; | ||
| import eatda.controller.story.StoryRegisterRequest; | ||
| import eatda.domain.member.Member; | ||
| import eatda.domain.story.Story; | ||
| import eatda.exception.BusinessErrorCode; | ||
| import eatda.exception.BusinessException; | ||
| import eatda.repository.story.StoryRepository; | ||
| import eatda.service.BaseServiceTest; | ||
| import eatda.service.common.ImageDomain; | ||
| import eatda.service.store.StoreService; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Nested; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
|
|
@@ -24,6 +29,10 @@ public class StoryServiceTest extends BaseServiceTest { | |
|
|
||
| @Autowired | ||
| private StoryService storyService; | ||
| @Autowired | ||
| private StoryRepository storyRepository; | ||
| @Autowired | ||
| private StoreService storeService; | ||
|
|
||
| @Nested | ||
| class RegisterStory { | ||
|
|
@@ -58,4 +67,55 @@ class RegisterStory { | |
| .hasMessageContaining(BusinessErrorCode.STORE_NOT_FOUND.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @Nested | ||
| class GetPagedStoryPreviews extends BaseServiceTest { | ||
|
|
||
| private StoryService storyService; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| storyService = new StoryService(storeService, imageService, storyRepository, memberRepository); | ||
| } | ||
|
Comment on lines
+71
to
+79
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [제안] 제가 머지 작업 진행하면서 파일 위쪽을 바꿔 놓았는데, 머지하시기 전에 확인해주시면 좋을 것 같아요! |
||
|
|
||
| @Test | ||
| void 스토리_목록을_조회할_수_있다() { | ||
| Member member = memberGenerator.generate("12345"); | ||
|
|
||
| Story story1 = Story.builder() | ||
| .member(member) | ||
| .storeKakaoId("1") | ||
| .storeName("곱창집") | ||
| .storeAddress("서울시") | ||
| .storeCategory("한식") | ||
| .description("미쳤다 진짜") | ||
| .imageKey("image-key-1") | ||
| .build(); | ||
|
|
||
| Story story2 = Story.builder() | ||
| .member(member) | ||
| .storeKakaoId("2") | ||
| .storeName("순대국밥집") | ||
| .storeAddress("부산시") | ||
| .storeCategory("한식") | ||
| .description("뜨끈한 국밥 최고") | ||
| .imageKey("image-key-2") | ||
| .build(); | ||
|
|
||
| storyRepository.saveAll(List.of(story1, story2)); | ||
|
|
||
| when(imageService.getPresignedUrl("image-key-1")).thenReturn("https://s3.com/story1.jpg"); | ||
| when(imageService.getPresignedUrl("image-key-2")).thenReturn("https://s3.com/story2.jpg"); | ||
|
|
||
| var response = storyService.getPagedStoryPreviews(); | ||
|
|
||
| assertThat(response.stories()).hasSize(2); | ||
| assertThat(response.stories()) | ||
| .extracting("imageUrl") | ||
| .containsExactly( | ||
| "https://s3.com/story2.jpg", | ||
| "https://s3.com/story1.jpg" | ||
| ); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[제안] 이건 제가 이전 PR에서 JpaRepository로 잘못 작성했었네요;; 그래서 다시 Repository를 상속하게 하면 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
으악 이 부분은 제가 사용하면서도 의문이 들었던 지점인데요.
리포지토리 계층에서 도메인 예외를 던지는 게 맞나...? 라는 고민이 들었습니다.
서비스에서 orElseThrow로 처리하는 게 귀찮은 건 맞지만,
구조적으로 이게 올바른 방향인가? 라는 생각이 들어요.
특히 기본 메서드인 save, findById 등을 다시 선언하면서
Repository만 상속하는 방식이 실용적인가에 대한 의문도 들고요.
그래서 지금은 기능도 많지 않고 복잡하지 않으니
그냥 JpaRepository 상속 방식으로 가는 게 어떨까… 조심스럽게 의견 드려봅니다 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 내용에 동의하는 바에요! Repository에서 서비스 관련 예외를 던진다는게 조금 애매하다고 생각합니다.
저도 좋다고 생각합니다. 대신 제 몇가지 의견이 있어요
findById().orElseThrow()로직을 줄이기 위해서 서비스가 서비스를 참조하는 일은 없었으면 좋겠습니다. (생각보다 순환 참조가 종종 일어나더라구요...)findById().orElseThrow()가 여러 서비스 로직에서 중복적으로 일어날 거에요. 매번 "어 얘 없으면 던지는 에러 코드가 뭐였지?", "이거 에러 코드 바꿔야하는데 전부 어디에 쓰여 있지?" 등의 골치아픈 일이 일어날 수 있어요.이번 PR에서는 넘어가고, 한 번 이런 주제들을 모아서 시간 잡아서 의견 나눠보죠!
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
험... 그러게요
순환 참조 문제는 저도 공감합니다.
외부 API를 사용하지 않는 서비스라면, 다른 서비스를 직접 참조하는 건 최대한 피하는 방향으로 생각하고 있었어요.
흠 이거는 충분히 발생할수 있는일이긴 하네요...
뭔가 좀 절충점은 리포지토리마다 사이드카 느낌으로 헬퍼 클래스를 두는 방법도 있을것 같은데..
이건 또 너무 오버가 아닌가 싶기도 하고 어렵네요...