Skip to content

Commit

Permalink
Merge pull request #33 from CAKK-DEV/feature/#22
Browse files Browse the repository at this point in the history
Feature: 카테고리별 케이크 이미지 조회 API 구현
  • Loading branch information
lcomment authored May 23, 2024
2 parents ca96053 + 540d6d7 commit 5afd991
Show file tree
Hide file tree
Showing 23 changed files with 337 additions and 16 deletions.
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
) {
}
2 changes: 1 addition & 1 deletion cakk-api/src/main/java/com/cakk/api/mapper/ShopMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

import com.cakk.api.dto.request.shop.CreateShopRequest;
import com.cakk.common.enums.Days;
import com.cakk.domain.entity.cake.CakeShopOperation;
import com.cakk.domain.entity.shop.CakeShop;
import com.cakk.domain.entity.shop.CakeShopOperation;
import com.cakk.domain.entity.user.BusinessInformation;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class CakeService {

private final CakeReader cakeReader;

public CakeImageListResponse findCakeImagesByCursorAndCategory(CakeSearchByCategoryRequest dto) {
public CakeImageListResponse findCakeImagesByCursorAndCategory(final CakeSearchByCategoryRequest dto) {
return CakeImageListResponse.from(cakeReader.findCakeImagesByCursorAndCategory(dto.cakeId(), dto.category(), dto.pageSize()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import com.cakk.api.dto.request.shop.PromotionRequest;
import com.cakk.api.mapper.ShopMapper;
import com.cakk.domain.dto.param.user.CertificationParam;
import com.cakk.domain.entity.cake.CakeShopOperation;
import com.cakk.domain.entity.shop.CakeShop;
import com.cakk.domain.entity.shop.CakeShopOperation;
import com.cakk.domain.entity.user.BusinessInformation;
import com.cakk.domain.entity.user.User;
import com.cakk.domain.event.shop.CertificationEvent;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
package com.cakk.api.common.base;

import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.navercorp.fixturemonkey.FixtureMonkey;
import com.navercorp.fixturemonkey.api.introspector.BuilderArbitraryIntrospector;
import com.navercorp.fixturemonkey.api.introspector.ConstructorPropertiesArbitraryIntrospector;
import com.navercorp.fixturemonkey.api.introspector.FieldReflectionArbitraryIntrospector;

@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
@SpringBootTest(
properties = "spring.profiles.active: test",
properties = "spring.profiles.active=test",
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class IntegrationTest {

@Autowired
protected TestRestTemplate restTemplate;

@LocalServerPort
protected int port;

@Autowired
protected ObjectMapper objectMapper = new ObjectMapper();

protected static final String BASE_URL = "http://localhost:";

protected final FixtureMonkey getConstructorMonkey() {
return FixtureMonkey.builder()
.objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
Expand Down
10 changes: 10 additions & 0 deletions cakk-api/src/test/java/com/cakk/api/config/MockCustomUser.java
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,125 @@
package com.cakk.api.integration.cake;

import static org.junit.Assert.*;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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;
import com.cakk.domain.dto.param.cake.CakeImageResponseParam;
import com.cakk.domain.entity.cake.CakeCategory;
import com.cakk.domain.repository.reader.CakeCategoryReader;

@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";

@Autowired
private CakeCategoryReader cakeCategoryReader;

@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);

assertEquals(HttpStatusCode.valueOf(200), responseEntity.getStatusCode());
assertEquals(ReturnCode.SUCCESS.getCode(), response.getReturnCode());
assertEquals(ReturnCode.SUCCESS.getMessage(), response.getReturnMessage());

Long lastCakeId = data.cakeImages().stream().map(CakeImageResponseParam::cakeId).min(Long::compareTo).orElse(null);
assertEquals(lastCakeId, data.lastCakeId());
assertEquals(5, data.size());
data.cakeImages().forEach(cakeImage -> {
CakeCategory cakeCategory = cakeCategoryReader.findByCakeId(cakeImage.cakeId());
assertEquals(CakeDesignCategory.FLOWER, cakeCategory.getCakeDesignCategory());
});
}

@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);

assertEquals(HttpStatusCode.valueOf(200), responseEntity.getStatusCode());
assertEquals(ReturnCode.SUCCESS.getCode(), response.getReturnCode());
assertEquals(ReturnCode.SUCCESS.getMessage(), response.getReturnMessage());

Long lastCakeId = data.cakeImages().stream().map(CakeImageResponseParam::cakeId).min(Long::compareTo).orElse(null);
assertEquals(lastCakeId, data.lastCakeId());
assertEquals(5, data.size());
data.cakeImages().forEach(cakeImage -> {
CakeCategory cakeCategory = cakeCategoryReader.findByCakeId(cakeImage.cakeId());
assertEquals(CakeDesignCategory.FLOWER, cakeCategory.getCakeDesignCategory());
});
}

@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);

assertEquals(HttpStatusCode.valueOf(200), responseEntity.getStatusCode());
assertEquals(ReturnCode.SUCCESS.getCode(), response.getReturnCode());
assertEquals(ReturnCode.SUCCESS.getMessage(), response.getReturnMessage());

assertEquals(0, data.cakeImages().size());
assertNull(data.lastCakeId());
assertEquals(0, data.size());
}
}
13 changes: 13 additions & 0 deletions cakk-api/src/test/resources/sql/delete-all.sql
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;
27 changes: 27 additions & 0 deletions cakk-api/src/test/resources/sql/insert-cake.sql
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());
3 changes: 3 additions & 0 deletions cakk-api/src/test/resources/sql/insert-test-user.sql
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);
12 changes: 8 additions & 4 deletions cakk-common/src/main/java/com/cakk/common/enums/ReturnCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ public enum ReturnCode {
NOT_EXIST_USER("1201", "존재하지 않는 유저 입니다."),
ALREADY_EXIST_USER("1202", "이미 가입한 유저 입니다."),

// 케이크 샵 에러 (1300 ~ 1350)
NOT_EXIST_CAKE_SHOP("1200", "존재하지 않는 케이크 샵 입니다"),

// 케이크 에러 (1350 ~ 1400)
NOT_EXIST_CAKE("1350", "존재하지 않는 케이크 입니다"),
NOT_EXIST_CAKE_CATEGORY("1301", "존재하지 않는 케이크 카테고리 입니다"),

// 클라이언트 에러
WRONG_PARAMETER("9000", "잘못된 파라미터 입니다."),
METHOD_NOT_ALLOWED("9001", "허용되지 않은 메소드 입니다."),

// 서버 에러 (9998, 9999)
INTERNAL_SERVER_ERROR("9998", "내부 서버 에러 입니다."),
EXTERNAL_SERVER_ERROR("9999", "외부 서버 에러 입니다."),

// 케이크 샵 에러(1200 ~ 1210)
NOT_EXIST_CAKE_SHOP("1200", "존재하지 않는 케이크 샵입니다");
EXTERNAL_SERVER_ERROR("9999", "외부 서버 에러 입니다.");

private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.hibernate.annotations.ColumnDefault;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -32,6 +33,9 @@ public class Cake extends AuditEntity {
@Column(name = "cake_id", nullable = false)
private Long id;

@Column(name = "cake_name", nullable = false, length = 50)
private String cakeName;

@Column(name = "cake_image_url", nullable = false, length = 200)
private String cakeImageUrl;

Expand All @@ -46,7 +50,9 @@ public class Cake extends AuditEntity {
@Column(name = "deleted_at")
private LocalDateTime deletedAt;

public Cake(String cakeImageUrl, CakeShop cakeShop) {
@Builder
public Cake(String cakeName, String cakeImageUrl, CakeShop cakeShop) {
this.cakeName = cakeName;
this.cakeImageUrl = cakeImageUrl;
this.cakeShop = cakeShop;
this.likeCount = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class CakeShop extends AuditEntity {
private Integer likeCount;

@ColumnDefault("false")
@Column(name = "linkedFlag", nullable = false, columnDefinition = "TINYINT(1)")
@Column(name = "linked_flag", nullable = false, columnDefinition = "TINYINT(1)")
private Boolean linkedFlag;

@ColumnDefault("null")
Expand Down
Loading

0 comments on commit 5afd991

Please sign in to comment.