Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 2 additions & 2 deletions .github/workflows/automatic-erd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: ERD to GitHub Pages

on:
pull_request:
branches: [ "**" ]
paths: [ "**" ]
branches: [ "main", "develop" ]
paths: [ "src/main/resources/db/migration/**" ]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Java
uses: actions/setup-java@v3
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/terraform-plan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ name: Terraform Plan on PR

on:
pull_request:
branches:
- main
- develop
branches: [ "main", "develop" ]
paths: [ "terraform/**", "terraform-bootstrap/**" ]

permissions:
contents: read
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/eatda/controller/story/StoryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import eatda.controller.web.auth.LoginMember;
import eatda.service.story.StoryService;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -28,8 +32,14 @@ public ResponseEntity<Void> registerStory(
}

@GetMapping("api/stories")
public ResponseEntity<StoriesResponse> getStories() {
public ResponseEntity<StoriesResponse> getStories(@RequestParam(defaultValue = "5") @Min(1) @Max(50) int size) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return ResponseEntity.status(HttpStatus.OK)
.body(storyService.getPagedStoryPreviews());
.body(storyService.getPagedStoryPreviews(size));
}

@GetMapping("/api/stories/{storyId}")
public ResponseEntity<StoryResponse> getStory(@PathVariable long storyId) {
return ResponseEntity.status(HttpStatus.OK)
.body(storyService.getStory(storyId));
}
}
2 changes: 1 addition & 1 deletion src/main/java/eatda/exception/BusinessErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public enum BusinessErrorCode {
INVALID_STORY_IMAGE_KEY("STY002", "스토리 이미지 Key는 필수입니다."),
STORY_MEMBER_REQUIRED("STY003", "스토리 작성 시 회원 정보는 필수입니다."),
STORY_STORE_REQUIRED("STY004", "스토리 작성 시 가게 정보는 필수입니다."),
STORY_NOT_FOUND("STY005", "스토리를 찾을 수 없습니다."),
STORY_NOT_FOUND("STY005", "스토리를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
INVALID_STORE_ID("STY006", "유효하지 않은 가게 ID입니다."),
INVALID_STORE_KAKAO_ID("STY007", "스토어 Kakao ID는 필수입니다."),
INVALID_STORE_NAME("STY008", "스토어 이름은 필수입니다."),
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/eatda/service/story/StoryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import eatda.controller.story.FilteredSearchResult;
import eatda.controller.story.StoriesResponse;
import eatda.controller.story.StoryRegisterRequest;
import eatda.controller.story.StoryResponse;
import eatda.domain.member.Member;
import eatda.domain.story.Story;
import eatda.exception.BusinessErrorCode;
Expand All @@ -26,7 +27,6 @@
@RequiredArgsConstructor
public class StoryService {
private static final int PAGE_START_NUMBER = 0;
private static final int PAGE_SIZE = 5;

private final StoreService storeService;
private final ImageService imageService;
Expand Down Expand Up @@ -67,8 +67,8 @@ private FilteredSearchResult filteredSearchResponse(List<StoreSearchResult> resp
}

@Transactional(readOnly = true)
public StoriesResponse getPagedStoryPreviews() {
Pageable pageable = PageRequest.of(PAGE_START_NUMBER, PAGE_SIZE);
public StoriesResponse getPagedStoryPreviews(int size) {
Pageable pageable = PageRequest.of(PAGE_START_NUMBER, size);
Comment on lines +72 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안] Pageable에 이런 게 있더라구요!

Suggested change
public StoriesResponse getPagedStoryPreviews(int size) {
Pageable pageable = PageRequest.of(PAGE_START_NUMBER, size);
public StoriesResponse getPagedStoryPreviews(int size) {
Pageable pageable = PageRequest.of(Pageable.ofSize(size));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 제안 감사합니다!
컴파일 에러가 나는데 어엄.. 이 방식을 말씀하신거겠죠??
Pageable pageable = Pageable.ofSize(size);

1차 배포후에 다른 페이지가 필요하다고 할수도 있으니... 지금은 이대로 가겠습니다!

Page<Story> orderByPage = storyRepository.findAllByOrderByCreatedAtDesc(pageable);

return new StoriesResponse(
Expand All @@ -80,4 +80,19 @@ public StoriesResponse getPagedStoryPreviews() {
.toList()
);
}

@Transactional(readOnly = true)
public StoryResponse getStory(long storyId) {
Story story = storyRepository.findById(storyId)
.orElseThrow(() -> new BusinessException(BusinessErrorCode.STORY_NOT_FOUND));

return new StoryResponse(
story.getStoreKakaoId(),
story.getStoreCategory(),
story.getStoreName(),
story.getStoreAddress(),
story.getDescription(),
imageService.getPresignedUrl(story.getImageKey())
);
}
}
20 changes: 10 additions & 10 deletions src/main/resources/db/seed/dev/V2__dev_init_data.sql
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • V2 파일 바뀌었으니, 머지하지 전에 개발 DB 리셋 한 번 부탁드려요!
  • 그리고 개발 DB 리셋하면 FE 한테도 알려주면 좋을 것 같아요!

Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ VALUES (1, '99999999999', 'KOREAN', '01012345678', '맛있는 한식집', 'https
'서울시 강남구 역삼동 222-324', 37.5036675804016, 127.05305858911);

INSERT INTO cheer (id, member_id, store_id, description, image_key, is_admin)
VALUES (1, 1, 1, '정말 맛있어요! 강추합니다!', 'default.jpg', true),
(2, 2, 2, '서비스가 훌륭해요!', 'default.jpg', true),
(3, 3, 3, '여기 음식이 정말 맛있어요!', 'default.jpg', true),
(4, 4, 4, '분위기가 너무 좋아요!', 'default.jpg', true),
(5, 5, 5, '디저트가 정말 맛있어요!', 'default.jpg', true),
(6, 6, 6, '커피가 정말 맛있어요!', 'default.jpg', false),
(7, 7, 7, '패스트푸드가 빠르고 맛있어요!', 'default.jpg', false);
VALUES (1, 1, 1, '정말 맛있어요! 강추합니다!', 'cheer/dummy/1.jpg', true),
(2, 2, 2, '서비스가 훌륭해요!', 'cheer/dummy/2.jpg', true),
(3, 3, 3, '여기 음식이 정말 맛있어요!', 'cheer/dummy/3.jpg', true),
(4, 4, 4, '분위기가 너무 좋아요!', 'cheer/dummy/4.jpg', true),
(5, 5, 5, '디저트가 정말 맛있어요!', 'cheer/dummy/5.jpg', true),
(6, 6, 6, '커피가 정말 맛있어요!', 'cheer/dummy/6.jpg', false),
(7, 7, 7, '패스트푸드가 빠르고 맛있어요!', 'cheer/dummy/7.jpg', false);

INSERT INTO article (id, title, subtitle, article_url, image_key)
VALUES (1, '첫 번째 기사', '서브타이틀 1', 'https://example.com/article1', 'default.jpg'),
(2, '두 번째 기사', '서브타이틀 2', 'https://example.com/article2', 'default.jpg'),
(3, '세 번째 기사', '서브타이틀 3', 'https://example.com/article3', 'default.jpg');
VALUES (1, '첫 번째 기사', '서브타이틀 1', 'https://example.com/article1', 'article/dummy/1.jpg'),
(2, '두 번째 기사', '서브타이틀 2', 'https://example.com/article2', 'article/dummy/2.jpg'),
(3, '세 번째 기사', '서브타이틀 3', 'https://example.com/article3', 'article/dummy/3.jpg');
6 changes: 3 additions & 3 deletions src/main/resources/db/seed/dev/V4__dev_add_story_data.sql
Original file line number Diff line number Diff line change
@@ -1,5 +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');
VALUES (1, '99999999999', '맛있는 한식집', '서울시 강남구 역삼동 123-45', 'KOREAN', '진짜 여기 곱창 맛집임. 다시 또 갈 듯!', 'story/dummy/1.jpg'),
(2, '99999999998', '아름다운 양식집', '서울시 강남구 역삼동 67-89', 'WESTERN', '스테이크가 부드럽고 서비스도 좋아요.', 'story/dummy/2.jpg'),
(3, '99999999997', '정통 중식당', '서울시 강남구 역삼동 101-112', 'CHINESE', '짜장면이 정통의 맛. 강력 추천.', 'story/dummy/3.jpg');
20 changes: 10 additions & 10 deletions src/main/resources/db/seed/local/V2__local_init_data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ VALUES (1, '99999999999', 'KOREAN', '01012345678', '맛있는 한식집', 'https
'서울시 강남구 역삼동 222-324', 37.5036675804016, 127.05305858911);

INSERT INTO cheer (id, member_id, store_id, description, image_key, is_admin)
VALUES (1, 1, 1, '정말 맛있어요! 강추합니다!', 'default.jpg', true),
(2, 2, 2, '서비스가 훌륭해요!', 'default.jpg', true),
(3, 3, 3, '여기 음식이 정말 맛있어요!', 'default.jpg', true),
(4, 4, 4, '분위기가 너무 좋아요!', 'default.jpg', true),
(5, 5, 5, '디저트가 정말 맛있어요!', 'default.jpg', true),
(6, 6, 6, '커피가 정말 맛있어요!', 'default.jpg', false),
(7, 7, 7, '패스트푸드가 빠르고 맛있어요!', 'default.jpg', false);
VALUES (1, 1, 1, '정말 맛있어요! 강추합니다!', 'cheer/dummy/1.jpg', true),
(2, 2, 2, '서비스가 훌륭해요!', 'cheer/dummy/2.jpg', true),
(3, 3, 3, '여기 음식이 정말 맛있어요!', 'cheer/dummy/3.jpg', true),
(4, 4, 4, '분위기가 너무 좋아요!', 'cheer/dummy/4.jpg', true),
(5, 5, 5, '디저트가 정말 맛있어요!', 'cheer/dummy/5.jpg', true),
(6, 6, 6, '커피가 정말 맛있어요!', 'cheer/dummy/6.jpg', false),
(7, 7, 7, '패스트푸드가 빠르고 맛있어요!', 'cheer/dummy/7.jpg', false);

INSERT INTO article (id, title, subtitle, article_url, image_key)
VALUES (1, '첫 번째 기사', '서브타이틀 1', 'https://example.com/article1', 'default.jpg'),
(2, '두 번째 기사', '서브타이틀 2', 'https://example.com/article2', 'default.jpg'),
(3, '세 번째 기사', '서브타이틀 3', 'https://example.com/article3', 'default.jpg');
VALUES (1, '첫 번째 기사', '서브타이틀 1', 'https://example.com/article1', 'article/dummy/1.jpg'),
(2, '두 번째 기사', '서브타이틀 2', 'https://example.com/article2', 'article/dummy/2.jpg'),
(3, '세 번째 기사', '서브타이틀 3', 'https://example.com/article3', 'article/dummy/3.jpg');
14 changes: 7 additions & 7 deletions src/main/resources/db/seed/local/V4__local_add_story_data.sql
Original file line number Diff line number Diff line change
@@ -1,9 +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');
VALUES (1, '99999999999', '맛있는 한식집', '서울시 강남구 역삼동 123-45', 'KOREAN', '진짜 여기 곱창 맛집임. 다시 또 갈 듯!', 'story/dummy/1.jpg'),
(2, '99999999998', '아름다운 양식집', '서울시 강남구 역삼동 67-89', 'WESTERN', '스테이크가 부드럽고 서비스도 좋아요.', 'story/dummy/2.jpg'),
(3, '99999999997', '정통 중식당', '서울시 강남구 역삼동 101-112', 'CHINESE', '짜장면이 정통의 맛. 강력 추천.', 'story/dummy/3.jpg'),
(4, '99999999996', '고급 양식 레스토랑', '서울시 강남구 역삼동 131-415', 'WESTERN', '분위기가 연인 데이트하기 좋아요.', 'story/dummy/4.jpg'),
(5, '99999999995', '달콤한 디저트 카페', '서울시 강남구 역삼동 161-718', 'ETC', '케이크가 촉촉하고 맛있어요.', 'story/dummy/5.jpg'),
(6, '99999999994', '아늑한 카페', '서울시 강남구 역삼동 192-021', 'ETC', '조용해서 공부하기 좋아요.', 'story/dummy/6.jpg'),
(7, '99999999993', '빠른 패스트푸드점', '서울시 강남구 역삼동 222-324', 'ETC', '햄버거 나오는데 3분도 안 걸림. 굿.', 'story/dummy/7.jpg');
96 changes: 76 additions & 20 deletions src/test/java/eatda/controller/story/StoryControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;

import eatda.controller.BaseControllerTest;
import eatda.service.common.ImageDomain;
import eatda.exception.BusinessErrorCode;
import eatda.exception.BusinessException;
import io.restassured.response.Response;
import java.nio.charset.StandardCharsets;
import java.util.List;
Expand All @@ -25,7 +26,7 @@ void setUpMock() {
}

@Nested
class SearchStores {
class RegisterStory {

@Test
void 스토리를_등록할_수_있다() {
Expand All @@ -51,25 +52,80 @@ class SearchStores {
}
}

@Test
void 스토리_목록을_조회할_수_있다() {
StoriesResponse mockResponse = new StoriesResponse(List.of(
new StoriesResponse.StoryPreview(1L, "https://dummy-s3.com/story1.png"),
new StoriesResponse.StoryPreview(2L, "https://dummy-s3.com/story2.png")
));
@Nested
class GetStories {

doReturn(mockResponse)
.when(storyService)
.getPagedStoryPreviews();
@Test
void 스토리_목록을_조회할_수_있다() {
StoriesResponse mockResponse = new StoriesResponse(List.of(
new StoriesResponse.StoryPreview(1L, "https://s3.bucket.com/story/dummy/1.jpg"),
new StoriesResponse.StoryPreview(2L, "https://s3.bucket.com/story/dummy/2.jpg")
));

doReturn(mockResponse)
.when(storyService)
.getPagedStoryPreviews(5);

Response response = given()
.queryParam("size", 5)
.when()
.get("/api/stories");

response.then()
.statusCode(200)
.body("stories.size()", equalTo(2))
.body("stories[0].storyId", equalTo(1))
.body("stories[0].imageUrl", equalTo("https://s3.bucket.com/story/dummy/1.jpg"));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 방식도 있어요!

Suggested change
Response response = given()
.queryParam("size", 5)
.when()
.get("/api/stories");
response.then()
.statusCode(200)
.body("stories.size()", equalTo(2))
.body("stories[0].storyId", equalTo(1))
.body("stories[0].imageUrl", equalTo("https://s3.bucket.com/story/dummy/1.jpg"));
}
StoriesResponse response = given()
.queryParam("size", 5)
.when()
.get("/api/stories")
.then().statusCode(200)
.extract().as(StoriesResponse.class);
assertAll(
() -> assertThat(response.stories()).hasSize(2),
....
);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 DTO로 역직렬화 해서 사용하는 방식이군요!
이게 더 깔끔하고 좋은것 같아요!!

}

@Nested
class GetStory {

@Test
void 해당_스토리를_상세_조회할_수_있다() {
long storyId = 1L;

doReturn(new StoryResponse(
"123456",
"한식",
"진또곱창집",
"서울특별시 성동구 성수동1가",
"곱창은 여기",
"https://s3.bucket.com/story1.jpg"
)).when(storyService).getStory(storyId);

Response response = given()
.when()
.get("/api/stories");
Response response = given()
.pathParam("storyId", storyId)
.when()
.get("/api/stories/{storyId}");

response.then()
.statusCode(200)
.body("stories.size()", equalTo(2))
.body("stories[0].storyId", equalTo(1))
.body("stories[0].imageUrl", equalTo("https://dummy-s3.com/story1.png"));
response.then()
.statusCode(200)
.body("storeKakaoId", equalTo("123456"))
.body("category", equalTo("한식"))
.body("storeName", equalTo("진또곱창집"))
.body("storeAddress", equalTo("서울특별시 성동구 성수동1가"))
.body("description", equalTo("곱창은 여기"))
.body("imageUrl", equalTo("https://s3.bucket.com/story1.jpg"));
}

@Test
void 존재하지_않는_스토리를_조회하면_404_응답한다() {
long nonexistentId = 999L;

doThrow(new BusinessException(BusinessErrorCode.STORY_NOT_FOUND))
.when(storyService).getStory(nonexistentId);

Response response = given()
.pathParam("storyId", nonexistentId)
.when()
.get("/api/stories/{storyId}");

response.then()
.statusCode(404)
.body("errorCode", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getCode()))
.body("message", equalTo(BusinessErrorCode.STORY_NOT_FOUND.getMessage()));
}
}
}
Loading