Skip to content

Commit

Permalink
Merge pull request #288 from sopt-makers/feat/#287
Browse files Browse the repository at this point in the history
[FEAT/#287] 단일 Banner 상세 조회 기능
  • Loading branch information
yummygyudon authored Dec 19, 2024
2 parents 469de96 + bcb8010 commit af4084f
Show file tree
Hide file tree
Showing 22 changed files with 863 additions and 29 deletions.
2 changes: 2 additions & 0 deletions operation-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly "io.jsonwebtoken:jjwt-impl:0.11.2"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:0.11.2"

testImplementation 'com.h2database:h2'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.sopt.makers.operation.web.banner.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

import org.sopt.makers.operation.dto.BaseResponse;

import org.springframework.http.ResponseEntity;

public interface BannerApi {
@Operation(
summary = "배너 상세 조회 API",
responses = {
@ApiResponse(
responseCode = "200",
description = "배너 상세 조회 성공"
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청"
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 배너 ID 요청"
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류"
)
}
)
ResponseEntity<BaseResponse<?>> getBannerDetail(Long bannerId);

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

import lombok.RequiredArgsConstructor;
import lombok.val;

import org.sopt.makers.operation.dto.BaseResponse;
import org.sopt.makers.operation.util.ApiResponseUtil;
import org.sopt.makers.operation.web.banner.service.BannerService;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.sopt.makers.operation.code.success.web.BannerSuccessCode.SUCCESS_GET_BANNER_DETAIL;

@RestController
@RequestMapping("/api/v1/banners")
@RequiredArgsConstructor
public class BannerApiController implements BannerApi {
private final BannerService bannerService;

@Override
@GetMapping("/{bannerId}")
public ResponseEntity<BaseResponse<?>> getBannerDetail(
@PathVariable("bannerId") Long bannerId
) {
val response = bannerService.getBannerDetail(bannerId);
return ApiResponseUtil.success(SUCCESS_GET_BANNER_DETAIL, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.sopt.makers.operation.web.banner.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import org.sopt.makers.operation.banner.domain.Banner;

import java.time.LocalDate;

import static lombok.AccessLevel.PRIVATE;

@RequiredArgsConstructor(access = PRIVATE)
public final class BannerResponse {

@Builder(access = PRIVATE)
public record BannerDetail(
@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,
@JsonProperty("image_url_pc") String pcImageUrl,
@JsonProperty("image_url_mobile") String mobileImageUrl
) {

public static BannerDetail fromEntity(Banner banner) {
return BannerDetail.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())
.pcImageUrl(banner.getImage().getPcImageUrl())
.mobileImageUrl(banner.getImage().getMobileImageUrl())
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.sopt.makers.operation.web.banner.service;

import org.sopt.makers.operation.web.banner.dto.response.BannerResponse;

public interface BannerService {

BannerResponse.BannerDetail getBannerDetail(final long bannerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.sopt.makers.operation.web.banner.service;

import lombok.RequiredArgsConstructor;
import lombok.val;
import org.sopt.makers.operation.banner.domain.Banner;
import org.sopt.makers.operation.banner.repository.BannerRepository;
import org.sopt.makers.operation.code.failure.BannerFailureCode;
import org.sopt.makers.operation.exception.BannerException;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class BannerServiceImpl implements BannerService {

private final BannerRepository bannerRepository;

@Override
public BannerResponse.BannerDetail getBannerDetail(final long bannerId) {
val banner = getBannerById(bannerId);
return BannerResponse.BannerDetail.fromEntity(banner);
}

private Banner getBannerById(final long id) {
return bannerRepository.findById(id)
.orElseThrow(() -> new BannerException(BannerFailureCode.NOT_FOUNT_BANNER));
}
}
82 changes: 82 additions & 0 deletions operation-api/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
spring:
config:
activate:
on-profile: test
datasource:
username: sa
password:
url: jdbc:h2:mem:test;DATABASE_TO_LOWER=true # ;DATABASE_TO_UPPER=false;MODE=PostgreSQL
jpa:
hibernate:
ddl-auto: update # create-drop 이나 create로 할경우, 자동으로 실행되는 schema & table drop 에서 DDL 오류 발생 (not exist -> not found)
properties:
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate:
format_sql: true
show_sql: true
show-sql: true
generate-ddl: true
jwt:
secretKey:
app: test
access: test
refresh: test
platform_code: test
secretKey:
playground: test

sopt:
current:
generation: 0
makers:
playground:
server: test
token: test
alarm:
message:
title_end: test
content_end: test

admin:
url:
prod: test
dev: test
local: test

notification:
url: test
key: test
arn: test

oauth:
apple:
aud: test
sub: test
key:
id: test
path: test
team:
id: test
google:
redirect:
url: test
client:
id: test
secret: test

cloud:
aws:
credentials:
accessKey: test
secretKey: test
eventBridge:
roleArn: test

logging:
level:
org:
hibernate:
SQL: DEBUG
type:
descriptor:
sql: trace
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.sopt.makers.operation.web.banner.api;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;

import org.sopt.makers.operation.code.success.web.BannerSuccessCode;
import org.sopt.makers.operation.filter.JwtAuthenticationFilter;
import org.sopt.makers.operation.filter.JwtExceptionFilter;
import org.sopt.makers.operation.jwt.JwtTokenProvider;
import org.sopt.makers.operation.web.banner.dto.response.BannerResponse;
import org.sopt.makers.operation.web.banner.service.BannerService;

import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;

import java.security.Principal;
import java.time.LocalDate;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(
controllers = {BannerApiController.class},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {JwtAuthenticationFilter.class, JwtExceptionFilter.class})},
excludeAutoConfiguration = {SecurityAutoConfiguration.class, JwtTokenProvider.class}
)
@DisplayName("[Web Layer Test] Banner API Controller")
class BannerApiControllerTest {
private static final long MOCK_BANNER_ID = 1L;

@MockBean
private BannerService bannerService;
@Autowired
private MockMvc mockMvc;

@BeforeEach
void setMockBanner() {
BannerResponse.BannerDetail mockBannerDetail = new BannerResponse.BannerDetail(
MOCK_BANNER_ID, "in_progress", "pg_community", "product", "publisher",
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 12, 31), "image-url-pc", "image-url-mobile"
);

when(bannerService.getBannerDetail(MOCK_BANNER_ID))
.thenReturn(mockBannerDetail);
}


@Test
@DisplayName("(GET) Banner Detail")
void getBannerDetail() throws Exception {
// given
BannerResponse.BannerDetail givenBannerDetail = bannerService.getBannerDetail(MOCK_BANNER_ID);

this.mockMvc.perform(
// when
get("/api/v1/banners/" + MOCK_BANNER_ID)
.contentType(MediaType.APPLICATION_JSON)
.principal(mock(Principal.class)))
// then
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value("true"))
.andExpect(jsonPath("$.message").value(BannerSuccessCode.SUCCESS_GET_BANNER_DETAIL.getMessage()))
.andExpect(jsonPath("$.data.id").value(givenBannerDetail.bannerId()))
.andExpect(jsonPath("$.data.status").value(givenBannerDetail.bannerStatus()))
.andExpect(jsonPath("$.data.location").value(givenBannerDetail.bannerLocation()))
.andExpect(jsonPath("$.data.content_type").value(givenBannerDetail.bannerType()))
.andExpect(jsonPath("$.data.publisher").value(givenBannerDetail.publisher()))
.andExpect(jsonPath("$.data.start_date").value(givenBannerDetail.startDate().toString()))
.andExpect(jsonPath("$.data.end_date").value(givenBannerDetail.endDate().toString()))
.andExpect(jsonPath("$.data.image_url_pc").value(givenBannerDetail.pcImageUrl()))
.andExpect(jsonPath("$.data.image_url_mobile").value(givenBannerDetail.mobileImageUrl()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.sopt.makers.operation.code.failure;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

import static org.springframework.http.HttpStatus.NOT_FOUND;

@RequiredArgsConstructor
@Getter
public enum BannerFailureCode implements FailureCode {
NOT_FOUND_STATUS(NOT_FOUND, "존재하지 않는 게시 상태입니다."),
NOT_FOUND_LOCATION(NOT_FOUND, "존재하지 않는 게시 위치입니다."),
NOT_FOUND_CONTENT_TYPE(NOT_FOUND, "존재하지 않는 게시 유형입니다."),
NOT_FOUNT_BANNER(NOT_FOUND, "존재하지 않는 배너입니다."),
;

private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.sopt.makers.operation.code.success.web;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.sopt.makers.operation.code.success.SuccessCode;
import org.springframework.http.HttpStatus;

import static lombok.AccessLevel.PRIVATE;

@Getter
@RequiredArgsConstructor(access = PRIVATE)
public enum BannerSuccessCode implements SuccessCode {
SUCCESS_GET_BANNER_DETAIL(HttpStatus.OK, "배너 상세 정보 조회 성공"),
;

private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.makers.operation.exception;

import lombok.Getter;
import org.sopt.makers.operation.code.failure.FailureCode;

@Getter
public class BannerException extends RuntimeException {
private final FailureCode failureCode;

public BannerException(FailureCode failureCode) {
super("[BannerException] : " + failureCode.getMessage());
this.failureCode = failureCode;
}

}
Loading

0 comments on commit af4084f

Please sign in to comment.