Skip to content

Commit

Permalink
Merge pull request #298 from sopt-makers/feat/#297
Browse files Browse the repository at this point in the history
[FEAT/#297] 배너 목록 조회 기능 구현
  • Loading branch information
yummygyudon authored Jan 14, 2025
2 parents 2fa4bd8 + af79539 commit 00b4814
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ public interface BannerApi {
)
ResponseEntity<BaseResponse<?>> getBannerDetail(Long bannerId);

@Operation(
summary = "배너 목록 조회 API",
responses = {
@ApiResponse(
responseCode = "200",
description = "배너 목록 조회 성공"
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청<br/><br/>1. 존재하지 않는 Parameter Key<br/>2. 존재하지 않는 Parameter Value"
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류"
)
}
)
ResponseEntity<BaseResponse<?>> getBanners(String status, String sort);

@Operation(
summary = "배너 삭제 API",
responses = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
import lombok.RequiredArgsConstructor;
import lombok.val;

import org.sopt.makers.operation.code.success.web.BannerSuccessCode;
import org.sopt.makers.operation.dto.BaseResponse;
import org.sopt.makers.operation.util.ApiResponseUtil;
import org.sopt.makers.operation.web.banner.dto.request.BannerRequest;
import org.sopt.makers.operation.web.banner.dto.request.BannerRequest.BannerCreate;
import org.sopt.makers.operation.web.banner.service.BannerService;
import org.sopt.makers.operation.web.banner.service.BannerService.FilterCriteria;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -17,11 +16,8 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_CREATE_BANNER;
import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_DELETE_BANNER;
import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_GET_BANNER_DETAIL;
import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_GET_BANNER_IMAGE_PRE_SIGNED_URL;
import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_GET_EXTERNAL_BANNERS;
import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.*;
import static org.sopt.makers.operation.web.banner.service.BannerService.*;

import org.springframework.web.bind.annotation.*;

Expand All @@ -41,6 +37,19 @@ public ResponseEntity<BaseResponse<?>> getBannerDetail(
return ApiResponseUtil.success(SUCCESS_GET_BANNER_DETAIL, response);
}

@Override
@GetMapping
public ResponseEntity<BaseResponse<?>> getBanners(
@RequestParam(value = "filter", required = false, defaultValue = "all") String filterCriteriaParameter,
@RequestParam(value = "sort", required = false, defaultValue = "status") String sortCriteriaParameter
) {
val progressStatus = FilterCriteria.fromParameter(filterCriteriaParameter);
val sortCriteria = SortCriteria.fromParameter(sortCriteriaParameter);

val response = bannerService.getBanners(progressStatus, sortCriteria);
return ApiResponseUtil.success(SUCCESS_GET_BANNER_LIST, response);
}

@Override
@DeleteMapping("/{bannerId}")
public ResponseEntity<BaseResponse<?>> deleteBanner(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@
@RequiredArgsConstructor(access = PRIVATE)
public final class BannerResponse {

@Builder(access = PRIVATE)
public record BannerSimple (
@JsonProperty("id") long bannerId,
@JsonProperty("status") String bannerStatus,
@JsonProperty("location") String bannerLocation,
@JsonProperty("content_type") String bannerType,
@JsonProperty("publisher") String publisher,
@JsonProperty("start_date") LocalDate startDate,
@JsonProperty("end_date") LocalDate endDate
) {
public static BannerSimple fromEntity(Banner banner) {
return BannerSimple.builder()
.bannerId(banner.getId())
.bannerStatus(banner.getPeriod().getPublishStatus(LocalDate.now()).getValue())
.bannerLocation(banner.getLocation().getValue())
.bannerType(banner.getContentType().getValue())
.publisher(banner.getPublisher())
.startDate(banner.getPeriod().getStartDate())
.endDate(banner.getPeriod().getEndDate())
.build();
}
}


@Builder(access = PRIVATE)
public record BannerDetail(
@JsonProperty("id") long bannerId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.sopt.makers.operation.web.banner.service;

import java.util.Arrays;
import java.util.List;

import org.sopt.makers.operation.code.failure.BannerFailureCode;
import org.sopt.makers.operation.exception.BannerException;
import org.sopt.makers.operation.web.banner.dto.request.*;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse.BannerImageUrl;
Expand All @@ -17,5 +20,47 @@ public interface BannerService {
BannerResponse.ImagePreSignedUrl getIssuedPreSignedUrlForPutImage(String contentName, String imageType, String imageExtension, String contentType);

BannerResponse.BannerDetail createBanner(BannerRequest.BannerCreate request);

List<BannerResponse.BannerSimple> getBanners(final FilterCriteria status, final SortCriteria sort);

enum FilterCriteria {
ALL("all"),
RESERVED("reserved"),
IN_PROGRESS("in_progress"),
DONE("done"),
;
private final String parameter;

FilterCriteria(String parameter) {
this.parameter = parameter;
}

public String getParameter() {
return this.parameter;
}

public static FilterCriteria fromParameter(String parameter) {
return Arrays.stream(FilterCriteria.values())
.filter(value -> value.parameter.equals(parameter))
.findAny().orElseThrow(() -> new BannerException(BannerFailureCode.INVALID_BANNER_PROGRESS_STATUS_PARAMETER));
}

}
enum SortCriteria {
START_DATE("start_date"),
END_DATE("end_date"),
STATUS("status")
;
private final String parameter;

SortCriteria(String parameter) {
this.parameter = parameter;
}
public static SortCriteria fromParameter(String parameter) {
return Arrays.stream(SortCriteria.values())
.filter(value -> value.parameter.equals(parameter))
.findAny().orElseThrow(() -> new BannerException(BannerFailureCode.INVALID_BANNER_SORT_CRITERIA_PARAMETER));
}
}

}
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package org.sopt.makers.operation.web.banner.service;

import java.time.Clock;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.sopt.makers.operation.banner.domain.Banner;
import org.sopt.makers.operation.banner.domain.PublishLocation;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.sopt.makers.operation.banner.domain.*;

import org.sopt.makers.operation.banner.repository.BannerRepository;
Expand All @@ -32,6 +31,7 @@ public class BannerServiceImpl implements BannerService {
private final BannerRepository bannerRepository;
private final S3Service s3Service;
private final ValueConfig valueConfig;
private final Clock clock;

@Override
public BannerResponse.BannerDetail getBannerDetail(final long bannerId) {
Expand All @@ -53,7 +53,7 @@ public List<BannerResponse.BannerImageUrl> getExternalBanners(final String image

List<String> list = bannerList.stream()
.map( banner -> banner.getImage().retrieveImageUrl(imageType))
.collect(Collectors.toUnmodifiableList());
.toList();

return BannerResponse.BannerImageUrl.fromEntity(list);
}
Expand All @@ -75,7 +75,7 @@ public BannerResponse.ImagePreSignedUrl getIssuedPreSignedUrlForPutImage(String
}

private String getBannerImageName(String location, String contentName, String imageType, String imageExtension) {
val today = LocalDate.now();
val today = LocalDate.now(clock);
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
val formattedDate = today.format(formatter);

Expand All @@ -100,6 +100,39 @@ public BannerDetail createBanner(BannerCreate request) {
return BannerResponse.BannerDetail.fromEntity(banner);
}

@Override
public List<BannerSimple> getBanners(FilterCriteria filter, SortCriteria sort) {
val allBanners = bannerRepository.findAll();
val filteredBanners = getFilteredBanners(allBanners, filter);
val resultBanners = getSortedBanners(filteredBanners, sort);
return resultBanners.stream()
.map(BannerSimple::fromEntity)
.toList();
}

private List<Banner> getFilteredBanners(List<Banner> banners, FilterCriteria filter) {
if (FilterCriteria.ALL.equals(filter)) {
return banners;
}
val targetStatus = PublishStatus.getByValue(filter.getParameter());
return banners.stream()
.filter(banner -> targetStatus.equals(banner.getPeriod().getPublishStatus(LocalDate.now(clock))))
.toList();
}

private List<Banner> getSortedBanners(List<Banner> banners, SortCriteria criteria) {
return switch (criteria) {
case STATUS, START_DATE -> banners.stream().sorted(
Comparator.comparing(Banner::getPeriod, (p1, p2) -> p2.getStartDate().compareTo(p1.getStartDate()))
.thenComparing(Banner::getPeriod, Comparator.comparing(PublishPeriod::getEndDate))
).toList();
case END_DATE -> banners.stream().sorted(
Comparator.comparing(Banner::getPeriod, Comparator.comparing(PublishPeriod::getEndDate))
.thenComparing(Banner::getPeriod, (p1, p2) -> p2.getStartDate().compareTo(p1.getStartDate()))
).toList();
};
}

private PublishPeriod getPublishPeriod(LocalDate startDate, LocalDate endDate) {
return PublishPeriod.builder()
.startDate(startDate)
Expand Down
57 changes: 57 additions & 0 deletions operation-api/src/main/resources/data/insert-get-banners-data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
CREATE TABLE if not exists banners (
id bigserial not null,
created_date timestamp(6),
last_modified_date timestamp(6),
content_type varchar(255) not null,
img_url_mobile varchar(255) not null,
img_url_pc varchar(255) not null,
link varchar(255),
location varchar(255) not null,
end_date date not null,
start_date date not null,
publisher varchar(255) not null,
primary key (id)
);

DELETE FROM banners;

INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2031-06-10', '2031-06-17','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2031-06-01', '2031-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2031-01-01', '2031-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2030-01-01', '2030-12-31','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2030-01-01', '2030-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2030-01-01', '2030-12-31','user');

INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2024-12-01', '2024-12-31','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2024-12-01', '2025-12-01','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2024-01-01', '2025-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2024-06-30', '2025-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2024-06-30', '2025-01-01','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2024-01-01', '2025-01-01','user');

INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2011-06-10', '2011-06-17','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2011-06-01', '2011-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2011-01-01', '2011-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2010-01-01', '2011-12-31','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2010-01-01', '2010-06-30','user');
INSERT INTO banners (content_type, img_url_mobile, img_url_pc, location, start_date, end_date, publisher)
VALUES ('PRODUCT', '','','PLAYGROUND_COMMUNITY','2010-01-01', '2010-12-31','user');


Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.sopt.makers.operation.web.banner.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.sopt.makers.operation.banner.domain.PublishPeriod;
import org.sopt.makers.operation.banner.domain.PublishStatus;
import org.sopt.makers.operation.banner.repository.BannerRepository;
import org.sopt.makers.operation.client.s3.S3Service;
import org.sopt.makers.operation.config.ValueConfig;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import java.time.LocalDate;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sopt.makers.operation.web.banner.service.BannerService.*;

@SpringBootTest
@ActiveProfiles("test")
@Sql(scripts = "classpath:data/insert-get-banners-data.sql")
class BannerServiceTest {
private static final Clock TEST_CLOCK = Clock.fixed(Instant.parse("2024-12-28T00:00:00Z"), ZoneId.systemDefault());
@Autowired
private BannerRepository bannerRepository;
@MockBean
private S3Service s3Service;
@Autowired
private ValueConfig valueConfig;

private BannerService bannerService;

@BeforeEach
void setBannerService() {
bannerService = new BannerServiceImpl(bannerRepository, s3Service, valueConfig, TEST_CLOCK);
}

@ParameterizedTest
@MethodSource("argsForGetBanners")
void getBanners(
FilterCriteria givenFilter,
SortCriteria givenSort,
int expectedSize,
PublishStatus expectedStatus
) {
List<BannerResponse.BannerSimple> banners = bannerService.getBanners(givenFilter, givenSort);

assertThat(banners)
.hasSize(expectedSize)
.extracting(info -> PublishPeriod.builder().startDate(info.startDate()).endDate(info.endDate()).build())
.allMatch(period -> {
if (givenFilter.equals(FilterCriteria.ALL)) {
return true;
}
return period.getPublishStatus(LocalDate.now(TEST_CLOCK)).equals(expectedStatus);
});
}
static Stream<Arguments> argsForGetBanners(){
return Stream.of(
Arguments.of(FilterCriteria.RESERVED, SortCriteria.START_DATE, 6, PublishStatus.RESERVED),
Arguments.of(FilterCriteria.ALL, SortCriteria.START_DATE, 18, null),
Arguments.of(FilterCriteria.IN_PROGRESS, SortCriteria.START_DATE, 6, PublishStatus.IN_PROGRESS),
Arguments.of(FilterCriteria.DONE, SortCriteria.START_DATE, 6, PublishStatus.DONE)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ public enum BannerFailureCode implements FailureCode {
NOT_FOUNT_BANNER(NOT_FOUND, "존재하지 않는 배너입니다."),
NOT_SUPPORTED_PLATFORM_TYPE(NOT_FOUND, "지원하지 않는 플랫폼 유형입니다."),
NOT_FOUND_BANNER(NOT_FOUND, "존재하지 않는 배너입니다."),
NOT_FOUND_BANNER_IMAGE(NOT_FOUND, "존재하지 않는 배너 이미지입니다.")
NOT_FOUND_BANNER_IMAGE(NOT_FOUND, "존재하지 않는 배너 이미지입니다."),

INVALID_BANNER_PROGRESS_STATUS_PARAMETER(BAD_REQUEST, "올바르지 않은 배너 진행 상태 조건입니다."),
INVALID_BANNER_SORT_CRITERIA_PARAMETER(BAD_REQUEST, "올바르지 않은 배너 정렬 조건입니다."),
;

private final HttpStatus status;
Expand Down
Loading

0 comments on commit 00b4814

Please sign in to comment.