Skip to content
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

[FEAT/#287] 단일 Banner 상세 조회 기능 #288

Merged
merged 17 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d3b556f
[FEAT] 배너 상세 조회 API Response DTO 정의
yummygyudon Dec 11, 2024
6260e89
[FEAT] Banner 상세 조회 연산 Service 인터페이스 정의
yummygyudon Dec 11, 2024
a2459f0
[FEAT] Banner 상세 조회 API 정의
yummygyudon Dec 11, 2024
d47498c
[FEAT] Banner 예외 객체 구현 (Exception & Failure Code Enum)
yummygyudon Dec 11, 2024
e198d1c
[FEAT] Banner 구성 도메인 요소 Enum 구현
yummygyudon Dec 11, 2024
cd1c4de
[FEAT] Embeddable 도메인 엔티티 정의 및 구현
yummygyudon Dec 11, 2024
8bd342c
[TEST] PublishPeriod 객체 PublishStatus 연산 기능 테스트
yummygyudon Dec 11, 2024
a45154f
[STYLE] 값 변경 메서드명 수정 (`change` -> `update`)
yummygyudon Dec 11, 2024
94c15e0
[FEAT] Test DatabaseCleaner 내 신규 테이블 정보 반영
yummygyudon Dec 11, 2024
a2821a9
[FEAT] Banner 도메인 Entity/Repository 정의 및 변경 메서드 정의
yummygyudon Dec 11, 2024
147bb27
[TEST] Banner 도메인 Entity 단위 테스트
yummygyudon Dec 11, 2024
e1ae1fa
[CHORE] Unused import 및 와일드 카드 import 개선
yummygyudon Dec 11, 2024
e76c71d
[FEAT] Entity -> DTO 변환 메서드 정의 및 배너 상세 조회 Service 메서드 구현
yummygyudon Dec 11, 2024
3b7680a
[CHORE] `test` profile Spring Boot Test를 위한 Test H2 DB Driver Class 의…
yummygyudon Dec 12, 2024
1b86ee3
[FEAT] Banner API Controller Test - GET `/api/v1/banners/{bannerId}`
yummygyudon Dec 12, 2024
7ae8616
[REFACTOR] PublishPeriod 배포 상태 연산 조건문 개선
yummygyudon Dec 19, 2024
bcb8010
[STYLE] 코드 스타일 개선
yummygyudon Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
yummygyudon marked this conversation as resolved.
Show resolved Hide resolved
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
Loading