-
Notifications
You must be signed in to change notification settings - Fork 0
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
Feature: 카테고리별 케이크 이미지 조회 API 구현 #33
Changes from 5 commits
4b05068
7763663
726b1b8
31704e8
bbe11b8
64496cf
540d6d7
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 |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.cakk.api.controller.cake; | ||
|
||
import jakarta.validation.Valid; | ||
|
||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.ModelAttribute; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
import com.cakk.api.dto.request.cake.CakeSearchByCategoryRequest; | ||
import com.cakk.api.dto.response.cake.CakeImageListResponse; | ||
import com.cakk.api.service.cake.CakeService; | ||
import com.cakk.common.response.ApiResponse; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/cakes") | ||
public class CakeController { | ||
|
||
private final CakeService cakeService; | ||
|
||
@GetMapping("/search/categories") | ||
public ApiResponse<CakeImageListResponse> listByCategory( | ||
@Valid @ModelAttribute CakeSearchByCategoryRequest request | ||
) { | ||
final CakeImageListResponse response = cakeService.findCakeImagesByCursorAndCategory(request); | ||
|
||
return ApiResponse.success(response); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,14 @@ | ||
package com.cakk.api.dto.request.cake; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
|
||
import com.cakk.common.enums.CakeDesignCategory; | ||
|
||
public record CakeSearchByCategoryRequest( | ||
Long cakeId, | ||
@NotNull | ||
CakeDesignCategory category, | ||
int pageSize | ||
@NotNull | ||
Integer pageSize | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.cakk.api.config; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import org.springframework.security.test.context.support.WithSecurityContext; | ||
|
||
@Retention(RetentionPolicy.RUNTIME) | ||
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) | ||
public @interface MockCustomUser { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.cakk.api.config; | ||
|
||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.context.SecurityContext; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.test.context.support.WithSecurityContextFactory; | ||
import org.springframework.stereotype.Component; | ||
|
||
import com.cakk.api.vo.OAuthUserDetails; | ||
import com.cakk.domain.entity.user.User; | ||
import com.cakk.domain.repository.reader.UserReader; | ||
|
||
@Component | ||
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<MockCustomUser> { | ||
|
||
@Autowired | ||
private UserReader userReader; | ||
|
||
@Override | ||
public SecurityContext createSecurityContext(MockCustomUser annotation) { | ||
final SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); | ||
|
||
final User user = userReader.findByUserId(1L); | ||
final OAuthUserDetails userDetails = new OAuthUserDetails(user); | ||
final UsernamePasswordAuthenticationToken authenticationToken = | ||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); | ||
|
||
securityContext.setAuthentication(authenticationToken); | ||
return securityContext; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package com.cakk.api.integration.cake; | ||
|
||
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.*; | ||
|
||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.http.HttpStatusCode; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.test.context.jdbc.Sql; | ||
import org.springframework.test.context.jdbc.SqlGroup; | ||
import org.springframework.web.util.UriComponents; | ||
import org.springframework.web.util.UriComponentsBuilder; | ||
|
||
import com.cakk.api.common.base.IntegrationTest; | ||
import com.cakk.api.dto.response.cake.CakeImageListResponse; | ||
import com.cakk.common.enums.CakeDesignCategory; | ||
import com.cakk.common.enums.ReturnCode; | ||
import com.cakk.common.response.ApiResponse; | ||
|
||
@SqlGroup({ | ||
@Sql(scripts = { | ||
"/sql/insert-test-user.sql", | ||
"/sql/insert-cake.sql" | ||
}, executionPhase = BEFORE_TEST_METHOD), | ||
@Sql(scripts = "/sql/delete-all.sql", executionPhase = AFTER_TEST_METHOD) | ||
}) | ||
class CakeIntegrationTest extends IntegrationTest { | ||
|
||
private static final String API_URL = "/api/v1/cakes"; | ||
|
||
@Test | ||
void 카테고리로_케이크_이미지_조회에_성공한다() { | ||
// given | ||
final String url = "%s%d%s/search/categories".formatted(BASE_URL, port, API_URL); | ||
final UriComponents uriComponents = UriComponentsBuilder | ||
.fromUriString(url) | ||
.queryParam("category", CakeDesignCategory.FLOWER) | ||
.queryParam("pageSize", 5) | ||
.build(); | ||
|
||
// when | ||
final ResponseEntity<ApiResponse> responseEntity = restTemplate.getForEntity(uriComponents.toUriString(), ApiResponse.class); | ||
|
||
// then | ||
final ApiResponse response = objectMapper.convertValue(responseEntity.getBody(), ApiResponse.class); | ||
final CakeImageListResponse data = objectMapper.convertValue(response.getData(), CakeImageListResponse.class); | ||
|
||
Assertions.assertEquals(HttpStatusCode.valueOf(200), responseEntity.getStatusCode()); | ||
Assertions.assertEquals(ReturnCode.SUCCESS.getCode(), response.getReturnCode()); | ||
Assertions.assertEquals(ReturnCode.SUCCESS.getMessage(), response.getReturnMessage()); | ||
Assertions.assertEquals(5, data.cakeImages().size()); | ||
Assertions.assertEquals(6L, data.lastCakeId()); | ||
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. lastCakeId가 6L에 대해 검증하는건 테스트 코드 조건에 따라 변경될 수 있는 부분 같은데 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. 카테고리에 대한 검증도 추가하겠습니다. 6L에 대한건 페이지네이션 검증이었습니다만, 정말 last id가 6L인지 검증을 추가하겠습니다. |
||
Assertions.assertEquals(5, data.size()); | ||
} | ||
|
||
@Test | ||
void 카테고리로_다음_페이지_케이크_이미지_조회에_성공한다() { | ||
// given | ||
final String url = "%s%d%s/search/categories".formatted(BASE_URL, port, API_URL); | ||
final UriComponents uriComponents = UriComponentsBuilder | ||
.fromUriString(url) | ||
.queryParam("cakeId", 6) | ||
.queryParam("category", CakeDesignCategory.FLOWER) | ||
.queryParam("pageSize", 5) | ||
.build(); | ||
|
||
// when | ||
final ResponseEntity<ApiResponse> responseEntity = restTemplate.getForEntity(uriComponents.toUriString(), ApiResponse.class); | ||
|
||
// then | ||
final ApiResponse response = objectMapper.convertValue(responseEntity.getBody(), ApiResponse.class); | ||
final CakeImageListResponse data = objectMapper.convertValue(response.getData(), CakeImageListResponse.class); | ||
|
||
Assertions.assertEquals(HttpStatusCode.valueOf(200), responseEntity.getStatusCode()); | ||
Assertions.assertEquals(ReturnCode.SUCCESS.getCode(), response.getReturnCode()); | ||
Assertions.assertEquals(ReturnCode.SUCCESS.getMessage(), response.getReturnMessage()); | ||
Assertions.assertEquals(5, data.cakeImages().size()); | ||
Assertions.assertEquals(1L, data.lastCakeId()); | ||
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. 이 부분도 Id 값에 대해 1L라는걸 검증하려면 테스트 queryParam에서 cakeId가 6이니까 제목에서 내림차순 쿼리 정렬이 진행되는게 표현되면 좋을 것 같습니다 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. 네, 확인했습니다. 제목 표기는 어떻게 생각하시나요? 태용님이 메서드명에 하시길래 일단 컨벤션을 맞춰보았는데, 아무래도 언더바만 사용한다는 점 등 표기에 제한이 있더라구요. 또 코드상으로 보기엔 너무 가독성이 떨어져서요. (테스트 코드는 문서라는 말이 많은데, 가독성이 떨어지면 문서 역할을 잘 못한다고 생각합니다) 현재 방식 유지가 좋을까요? 아니면 @DisplayName 어노테이션을 활용하는게 좋을까요?? 혹시 메서드 네임으로 하시면서 불편하시진 않았나요?? 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. DisplayName을 활용해볼까요?? |
||
Assertions.assertEquals(5, data.size()); | ||
} | ||
|
||
@Test | ||
void 카테고리로_케이크_이미지_조회_시_데이터가_없으면_빈_배열을_반환한다() { | ||
// given | ||
final String url = "%s%d%s/search/categories".formatted(BASE_URL, port, API_URL); | ||
final UriComponents uriComponents = UriComponentsBuilder | ||
.fromUriString(url) | ||
.queryParam("cakeId", 1) | ||
.queryParam("category", CakeDesignCategory.FLOWER) | ||
.queryParam("pageSize", 5) | ||
.build(); | ||
|
||
// when | ||
final ResponseEntity<ApiResponse> responseEntity = restTemplate.getForEntity(uriComponents.toUriString(), ApiResponse.class); | ||
|
||
// then | ||
final ApiResponse response = objectMapper.convertValue(responseEntity.getBody(), ApiResponse.class); | ||
final CakeImageListResponse data = objectMapper.convertValue(response.getData(), CakeImageListResponse.class); | ||
|
||
Assertions.assertEquals(HttpStatusCode.valueOf(200), responseEntity.getStatusCode()); | ||
Assertions.assertEquals(ReturnCode.SUCCESS.getCode(), response.getReturnCode()); | ||
Assertions.assertEquals(ReturnCode.SUCCESS.getMessage(), response.getReturnMessage()); | ||
Assertions.assertEquals(0, data.cakeImages().size()); | ||
Assertions.assertNull(data.lastCakeId()); | ||
Assertions.assertEquals(0, data.size()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
delete from cake_like; | ||
delete from cake_tag; | ||
delete from tag; | ||
delete from cake_category; | ||
delete from cake; | ||
|
||
delete from cake_shop_like; | ||
delete from cake_shop_link; | ||
delete from cake_shop_operation; | ||
delete from business_information; | ||
delete from cake_shop; | ||
|
||
delete from users; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
insert into cake_shop (shop_id, thumbnail_url, shop_name, shop_bio, shop_description, latitude, longitude, like_count, linked_flag, | ||
created_at, updated_at) | ||
values (1, 'thumbnail_url', '케이크 맛집', '케이크 맛집입니다.', '케이크 맛집입니다.', 37.123456, 127.123456, 0, false, now(), now()); | ||
|
||
insert into cake (cake_id, shop_id, cake_name, cake_image_url, like_count, created_at, updated_at) | ||
values (1, 1, '케이크1', 'cake_image_url1', 0, now(), now()), | ||
(2, 1, '케이크2', 'cake_image_url2', 0, now(), now()), | ||
(3, 1, '케이크3', 'cake_image_url3', 0, now(), now()), | ||
(4, 1, '케이크4', 'cake_image_url4', 0, now(), now()), | ||
(5, 1, '케이크5', 'cake_image_url5', 0, now(), now()), | ||
(6, 1, '케이크6', 'cake_image_url6', 0, now(), now()), | ||
(7, 1, '케이크7', 'cake_image_url7', 0, now(), now()), | ||
(8, 1, '케이크8', 'cake_image_url8', 0, now(), now()), | ||
(9, 1, '케이크9', 'cake_image_url9', 0, now(), now()), | ||
(10, 1, '케이크10', 'cake_image_url10', 0, now(), now()); | ||
|
||
insert into cake_category (cake_category_id, cake_id, cake_design_category, created_at) | ||
values (1, 1, 'FLOWER', now()), | ||
(2, 2, 'FLOWER', now()), | ||
(3, 3, 'FLOWER', now()), | ||
(4, 4, 'FLOWER', now()), | ||
(5, 5, 'FLOWER', now()), | ||
(6, 6, 'FLOWER', now()), | ||
(7, 7, 'FLOWER', now()), | ||
(8, 8, 'FLOWER', now()), | ||
(9, 9, 'FLOWER', now()), | ||
(10, 10, 'FLOWER', now()); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
insert into users (user_id, provider, provider_id, nickname, profile_image_url, email, gender, birthday, role, created_at, updated_at, | ||
deleted_at) | ||
values (1, 'GOOGLE', '123456', '테스트 유저', 'image_url', 'test@google.com', 'MALE', '1998-01-01', 'USER', now(), now(), null); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package com.cakk.domain.entity.cake; | ||
package com.cakk.domain.entity.shop; | ||
|
||
import java.time.LocalTime; | ||
|
||
|
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.
해당 애노테이션이 추상 클래스에서 선언되는건 안되던가요?? 기억이 잘 안나서 헷갈리네용
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.
스크립트 파일이 테스트마다 다를 것이기에 추상화를 못했습니다
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.
이해했습니다!