Skip to content

Commit

Permalink
Merge pull request #106 from cvs-go/feature#105
Browse files Browse the repository at this point in the history
프로모션 조회 기능 추가
  • Loading branch information
feel-coding authored Jan 11, 2024
2 parents 8c86b7c + e5a5e91 commit ed32e18
Show file tree
Hide file tree
Showing 13 changed files with 369 additions and 5 deletions.
7 changes: 6 additions & 1 deletion sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,14 @@ create table product_like (

create table promotion (
id bigint not null auto_increment,
name varchar(50) not null unique,
image_url varchar(255) not null,
landing_url varchar(255),
priority integer,
start_at datetime,
end_at datetime,
created_at datetime,
modified_at datetime,
image_url varchar(255),
primary key (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Expand Down
9 changes: 9 additions & 0 deletions src/docs/asciidoc/api-doc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,12 @@ include::{snippets}/notice-controller-test/respond_200_when_read_notice_successf

| `404 NOT FOUND` | `NOT_FOUND_NOTICE` | 해당하는 공지사항이 없는 경우
|===

== 7. 프로모션
=== 7-1. 프로모션 목록 조회
==== Sample Request
include::{snippets}/promotion-controller-test/respond_200_when_read_promotion_list_successfully/http-request.adoc[]
==== Response Fields
include::{snippets}/promotion-controller-test/respond_200_when_read_promotion_list_successfully/response-fields.adoc[]
==== Sample Response
include::{snippets}/promotion-controller-test/respond_200_when_read_promotion_list_successfully/http-response.adoc[]
4 changes: 2 additions & 2 deletions src/main/java/com/cvsgo/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void addInterceptors(InterceptorRegistry registry) {
.addPathPatterns("/**")
.excludePathPatterns("/", "/docs/**", "/*.ico", "/api/auth/login", "/api/users",
"/api/tags", "/api/users/emails/*/exists", "/api/users/nicknames/*/exists",
"/api/products", "/api/products/*", "/api/products/*/tags", "/api/users/*/reviews",
"/error");
"/api/promotions", "/api/products", "/api/products/*", "/api/products/*/tags",
"/api/users/*/reviews","/error");
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/cvsgo/controller/PromotionController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.cvsgo.controller;

import com.cvsgo.dto.SuccessResponse;
import com.cvsgo.dto.promotion.ReadPromotionResponseDto;
import com.cvsgo.service.PromotionService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/promotions")
public class PromotionController {

private final PromotionService promotionService;

@GetMapping
private SuccessResponse<List<ReadPromotionResponseDto>> readPromotionList() {
return SuccessResponse.from(promotionService.readPromotionList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.cvsgo.dto.promotion;

import com.cvsgo.entity.Promotion;
import lombok.Getter;

@Getter
public class ReadPromotionResponseDto {

private final Long id;

private final String imageUrl;

private final String landingUrl;

public ReadPromotionResponseDto(Promotion promotion) {
this.id = promotion.getId();
this.imageUrl = promotion.getImageUrl();
this.landingUrl = promotion.getLandingUrl();
}

public static ReadPromotionResponseDto from(Promotion promotion) {
return new ReadPromotionResponseDto(promotion);
}
}
24 changes: 23 additions & 1 deletion src/main/java/com/cvsgo/entity/Promotion.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.cvsgo.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -18,11 +21,30 @@ public class Promotion extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
@Column(unique = true)
private String name;

@NotNull
private String imageUrl;

private String landingUrl;

private Integer priority;

private LocalDateTime startAt;

private LocalDateTime endAt;

@Builder
public Promotion(Long id, String imageUrl) {
public Promotion(Long id, String name, String imageUrl, String landingUrl, Integer priority,
LocalDateTime startAt, LocalDateTime endAt) {
this.id = id;
this.name = name;
this.imageUrl = imageUrl;
this.landingUrl = landingUrl;
this.priority = priority;
this.startAt = startAt;
this.endAt = endAt;
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/cvsgo/repository/PromotionCustomRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.cvsgo.repository;

import com.cvsgo.entity.Promotion;
import java.time.LocalDateTime;
import java.util.List;

public interface PromotionCustomRepository {

List<Promotion> findActivePromotions(LocalDateTime now);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.cvsgo.repository;

import static com.cvsgo.entity.QPromotion.promotion;

import com.cvsgo.entity.Promotion;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class PromotionCustomRepositoryImpl implements PromotionCustomRepository {

private final JPAQueryFactory queryFactory;

public List<Promotion> findActivePromotions(LocalDateTime now) {
return queryFactory.selectFrom(promotion)
.where(promotion.startAt.loe(now).and(promotion.endAt.goe(now)))
.orderBy(promotion.priority.asc())
.fetch();
}

}
7 changes: 6 additions & 1 deletion src/main/java/com/cvsgo/repository/PromotionRepository.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.cvsgo.repository;

import com.cvsgo.entity.Promotion;
import java.time.LocalDateTime;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface PromotionRepository extends JpaRepository<Promotion, Long> {
public interface PromotionRepository extends JpaRepository<Promotion, Long>, PromotionCustomRepository {

}
30 changes: 30 additions & 0 deletions src/main/java/com/cvsgo/service/PromotionService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.cvsgo.service;

import com.cvsgo.dto.promotion.ReadPromotionResponseDto;
import com.cvsgo.repository.PromotionRepository;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class PromotionService {

private final PromotionRepository promotionRepository;

/**
* 현재 로컬 날짜 및 시간 기준 활성된 프로모션 목록을 우선순위 순으로 조회한다.
*
* @return 프로모션 목록
*/
@Transactional(readOnly = true)
public List<ReadPromotionResponseDto> readPromotionList() {
List<ReadPromotionResponseDto> promotionResponseDtos = promotionRepository.findActivePromotions(LocalDateTime.now()).stream()
.map(ReadPromotionResponseDto::from).toList();
return promotionResponseDtos;
}

}
97 changes: 97 additions & 0 deletions src/test/java/com/cvsgo/controller/PromotionControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.cvsgo.controller;

import static com.cvsgo.ApiDocumentUtils.documentIdentifier;
import static com.cvsgo.ApiDocumentUtils.getDocumentRequest;
import static com.cvsgo.ApiDocumentUtils.getDocumentResponse;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer.sharedHttpSession;

import com.cvsgo.argumentresolver.LoginUserArgumentResolver;
import com.cvsgo.config.WebConfig;
import com.cvsgo.dto.promotion.ReadPromotionResponseDto;
import com.cvsgo.entity.Promotion;
import com.cvsgo.interceptor.AuthInterceptor;
import com.cvsgo.service.PromotionService;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;

@ExtendWith(RestDocumentationExtension.class)
@WebMvcTest(PromotionController.class)
class PromotionControllerTest {

@MockBean
LoginUserArgumentResolver loginUserArgumentResolver;

@MockBean
WebConfig webConfig;

@MockBean
AuthInterceptor authInterceptor;

@MockBean
private PromotionService promotionService;

private MockMvc mockMvc;

@BeforeEach
void setup(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.apply(sharedHttpSession())
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.build();
}

@Test
@DisplayName("프로모션 목록을 정상적으로 조회하면 HTTP 200을 응답한다")
void respond_200_when_read_promotion_list_successfully() throws Exception {
List<ReadPromotionResponseDto> responseDto = List.of(new ReadPromotionResponseDto(promotion1), new ReadPromotionResponseDto(promotion2));
given(promotionService.readPromotionList()).willReturn(responseDto);

mockMvc.perform(get("/api/promotions").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print())
.andDo(document(documentIdentifier,
getDocumentRequest(),
getDocumentResponse(),
relaxedResponseFields(
fieldWithPath("data.[].id").type(JsonFieldType.NUMBER).description("프로모션 ID"),
fieldWithPath("data.[].imageUrl").type(JsonFieldType.STRING).description("프로모션 이미지 url"),
fieldWithPath("data.[].landingUrl").type(JsonFieldType.STRING).description("프로모션 랜딩 url")
)
));
}

Promotion promotion1 = Promotion.builder()
.id(1L)
.imageUrl("imageUrl1")
.landingUrl("landindUrl1")
.build();

Promotion promotion2 = Promotion.builder()
.id(2L)
.imageUrl("imageUrl2")
.landingUrl("landindUrl2")
.build();
}
55 changes: 55 additions & 0 deletions src/test/java/com/cvsgo/repository/PromotionRepositoryTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.cvsgo.repository;

import static org.assertj.core.api.Assertions.assertThat;

import com.cvsgo.config.TestConfig;
import com.cvsgo.entity.Promotion;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;

@Import(TestConfig.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class PromotionRepositoryTest {

@Autowired
PromotionRepository promotionRepository;

@BeforeEach
void initData() {
promotion1 = Promotion.builder().id(1L).name("프로모션1").imageUrl("imageUrl1").landingUrl("landindUrl1")
.priority(3).startAt(LocalDateTime.now().minusDays(3)).endAt(LocalDateTime.now().minusDays(1)).build();
promotion2 = Promotion.builder().id(2L).name("프로모션2").imageUrl("imageUrl2").landingUrl("landindUrl2")
.priority(2).startAt(LocalDateTime.now().minusDays(1)).endAt(LocalDateTime.now().plusDays(1)).build();
promotion3 = Promotion.builder().id(3L).name("프로모션3").imageUrl("imageUrl3").landingUrl("landindUrl3")
.priority(1).startAt(LocalDateTime.now().minusDays(5)).endAt(LocalDateTime.now().plusDays(6)).build();
promotion4 = Promotion.builder().id(2L).name("프로모션4").imageUrl("imageUrl2").landingUrl("landindUrl2")
.priority(1).startAt(LocalDateTime.now().plusDays(1)).endAt(LocalDateTime.now().plusDays(3)).build();
promotionRepository.saveAll(List.of(promotion1, promotion2, promotion3, promotion4));
}

@Test
@DisplayName("활성된 프로모션을 조회한다")
void find_active_promotions() {
// when
LocalDateTime now = LocalDateTime.now();
List<Promotion> foundPromotions = promotionRepository.findActivePromotions(now);

// then
assertThat(foundPromotions).hasSize(2);
assertThat(foundPromotions.get(0).getPriority()).isEqualTo(1);
assertThat(foundPromotions.get(1).getPriority()).isEqualTo(2);
}

private Promotion promotion1;
private Promotion promotion2;
private Promotion promotion3;
private Promotion promotion4;
}
Loading

0 comments on commit ed32e18

Please sign in to comment.