diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 372975f..f126e70 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-log4j2") implementation("org.projectlombok:lombok:1.18.20") - implementation("io.springfox:springfox-swagger2:3.0.0") + implementation("io.springfox:springfox-boot-starter:3.0.0") implementation("io.springfox:springfox-swagger-ui:3.0.0") implementation("com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4") implementation("com.lmax:disruptor:3.4.2") diff --git a/server/src/main/java/com/ticketing/server/global/config/SwaggerConfig.java b/server/src/main/java/com/ticketing/server/global/config/SwaggerConfig.java index c451b62..7a1294f 100644 --- a/server/src/main/java/com/ticketing/server/global/config/SwaggerConfig.java +++ b/server/src/main/java/com/ticketing/server/global/config/SwaggerConfig.java @@ -1,14 +1,28 @@ package com.ticketing.server.global.config; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import springfox.documentation.builders.ApiInfoBuilder; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.service.ApiInfo; import springfox.documentation.service.ApiKey; import springfox.documentation.service.AuthorizationScope; import springfox.documentation.service.SecurityReference; @@ -22,27 +36,29 @@ import springfox.documentation.swagger.web.UiConfigurationBuilder; @RequiredArgsConstructor public class SwaggerConfig { - public static final String SECURITY_SCHEMA_NAME = "Authorization"; - public static final String AUTHORIZATION_SCOPE_GLOBAL = "global"; - public static final String AUTHORIZATION_SCOPE_GLOBAL_DESC = "accessEverything"; - @Bean public Docket api() { - return new Docket(DocumentationType.OAS_30) - .useDefaultResponseMessages(false) - .select() - .apis(RequestHandlerSelectors.any()) - .paths(PathSelectors.ant("/api/**")).build() - .apiInfo(apiInfo()) + return new Docket(DocumentationType.OAS_30).useDefaultResponseMessages(false).select() + .apis(RequestHandlerSelectors.any()).paths(PathSelectors.ant("/api/**")).build() .securityContexts(Arrays.asList(securityContext())) .securitySchemes(Arrays.asList(apiKey())); } - private ApiInfo apiInfo() { - return new ApiInfoBuilder() - .title("Ticketing REST API Document") - .version("v1") - .description("Ticketing REST API 문서").build(); + @Bean + public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties, Environment environment) { + List> allEndpoints = new ArrayList<>(); + Collection webEndpoints = webEndpointsSupplier.getEndpoints(); + allEndpoints.addAll(webEndpoints); + allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); + allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); + String basePath = webEndpointProperties.getBasePath(); + EndpointMapping endpointMapping = new EndpointMapping(basePath); + boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath); + return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null); + } + + private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) { + return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); } private ApiKey apiKey() { @@ -53,6 +69,10 @@ public class SwaggerConfig { return SecurityContext.builder().securityReferences(defaultAuth()).build(); } + public static final String SECURITY_SCHEMA_NAME = "Authorization"; + public static final String AUTHORIZATION_SCOPE_GLOBAL = "global"; + public static final String AUTHORIZATION_SCOPE_GLOBAL_DESC = "accessEverything"; + private List defaultAuth() { AuthorizationScope authorizationScope = new AuthorizationScope(AUTHORIZATION_SCOPE_GLOBAL, AUTHORIZATION_SCOPE_GLOBAL_DESC); diff --git a/server/src/main/java/com/ticketing/server/global/dto/repository/AbstractEntity.java b/server/src/main/java/com/ticketing/server/global/dto/repository/AbstractEntity.java index f2384f9..dfda9ce 100644 --- a/server/src/main/java/com/ticketing/server/global/dto/repository/AbstractEntity.java +++ b/server/src/main/java/com/ticketing/server/global/dto/repository/AbstractEntity.java @@ -28,4 +28,6 @@ public abstract class AbstractEntity { @LastModifiedDate private LocalDateTime updatedAt; + private LocalDateTime deletedAt; + } diff --git a/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java b/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java index 3e6a14e..2778e60 100644 --- a/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java +++ b/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java @@ -10,6 +10,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; @@ -55,10 +56,11 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .antMatchers(HttpMethod.POST, "/api/user/login").permitAll() .antMatchers(HttpMethod.POST, "/api/user/refresh").permitAll() .antMatchers(HttpMethod.POST, "/api/user").permitAll() + .antMatchers("/api/movies/**").permitAll() .antMatchers("/l7check").permitAll() - .antMatchers("/actuator/health").permitAll() + .antMatchers("/actuator/**").permitAll() + .antMatchers("/api/v3/", "/swagger-ui/**", "/swagger/", "/swagger-resources/**", "/v3/api-docs").permitAll() .anyRequest().authenticated() - .and() .apply(new JwtSecurityConfig(jwtFilter)); } diff --git a/server/src/main/java/com/ticketing/server/movie/application/MovieController.java b/server/src/main/java/com/ticketing/server/movie/application/MovieController.java index b1ef2f8..4c344c8 100644 --- a/server/src/main/java/com/ticketing/server/movie/application/MovieController.java +++ b/server/src/main/java/com/ticketing/server/movie/application/MovieController.java @@ -1,8 +1,30 @@ package com.ticketing.server.movie.application; +import com.ticketing.server.movie.application.response.MovieListResponse; +import com.ticketing.server.movie.service.interfaces.MovieService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController +@RequestMapping("/api/movies") +@Api(value = "Movie API", tags = {"Movie"}) +@RequiredArgsConstructor +@Slf4j public class MovieController { + private final MovieService movieService; + + @GetMapping() + @ApiOperation(value = "영화 목록 조회") + public ResponseEntity getMovies() { + return ResponseEntity.status(HttpStatus.OK).body(MovieListResponse.from(movieService.getMovies())); + } + } diff --git a/server/src/main/java/com/ticketing/server/movie/application/response/MovieListResponse.java b/server/src/main/java/com/ticketing/server/movie/application/response/MovieListResponse.java new file mode 100644 index 0000000..64c0d4a --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/application/response/MovieListResponse.java @@ -0,0 +1,23 @@ +package com.ticketing.server.movie.application.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.ticketing.server.movie.service.dto.MovieDto; +import io.swagger.annotations.ApiModelProperty; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class MovieListResponse { + + @ApiModelProperty(value = "영화 제목") + @JsonProperty + private List movieDtos; + + public static MovieListResponse from(List movieDtos) { + return new MovieListResponse(movieDtos); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/domain/Movie.java b/server/src/main/java/com/ticketing/server/movie/domain/Movie.java index 4c6e377..f2d6f64 100644 --- a/server/src/main/java/com/ticketing/server/movie/domain/Movie.java +++ b/server/src/main/java/com/ticketing/server/movie/domain/Movie.java @@ -1,6 +1,7 @@ package com.ticketing.server.movie.domain; import com.ticketing.server.global.dto.repository.AbstractEntity; +import com.ticketing.server.movie.service.dto.MovieDto; import javax.persistence.Column; import javax.persistence.Entity; import javax.validation.constraints.NotNull; @@ -21,4 +22,8 @@ public class Movie extends AbstractEntity { @NotNull private Integer runningTime; + public MovieDto toDto() { + return new MovieDto(this.title); + } + } diff --git a/server/src/main/java/com/ticketing/server/movie/domain/repository/MovieRepository.java b/server/src/main/java/com/ticketing/server/movie/domain/repository/MovieRepository.java index 630bd4f..c1888a2 100644 --- a/server/src/main/java/com/ticketing/server/movie/domain/repository/MovieRepository.java +++ b/server/src/main/java/com/ticketing/server/movie/domain/repository/MovieRepository.java @@ -1,8 +1,11 @@ package com.ticketing.server.movie.domain.repository; import com.ticketing.server.movie.domain.Movie; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository @@ -10,4 +13,9 @@ public interface MovieRepository extends JpaRepository { Optional findByTitle(String title); + @Query(value = "SELECT * " + + "FROM movie " + + "WHERE deleted_at IS NULL", nativeQuery = true) + List findValidMovies(); + } diff --git a/server/src/main/java/com/ticketing/server/movie/service/MovieServiceImpl.java b/server/src/main/java/com/ticketing/server/movie/service/MovieServiceImpl.java index c3d3e3a..6ff9de6 100644 --- a/server/src/main/java/com/ticketing/server/movie/service/MovieServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/movie/service/MovieServiceImpl.java @@ -1,9 +1,29 @@ package com.ticketing.server.movie.service; +import com.ticketing.server.movie.domain.Movie; +import com.ticketing.server.movie.domain.repository.MovieRepository; +import com.ticketing.server.movie.service.dto.MovieDto; import com.ticketing.server.movie.service.interfaces.MovieService; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor +@Slf4j public class MovieServiceImpl implements MovieService { + private final MovieRepository movieRepository; + + public List getMovies() { + List movies = movieRepository.findValidMovies(); + + return movies.stream() + .map(movie -> movie.toDto()) + .collect(Collectors.toList()); + + } + } diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/MovieDto.java b/server/src/main/java/com/ticketing/server/movie/service/dto/MovieDto.java new file mode 100644 index 0000000..92d1261 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/MovieDto.java @@ -0,0 +1,12 @@ +package com.ticketing.server.movie.service.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class MovieDto { + + @JsonProperty + private String title; + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/interfaces/MovieService.java b/server/src/main/java/com/ticketing/server/movie/service/interfaces/MovieService.java index 413335b..6294731 100644 --- a/server/src/main/java/com/ticketing/server/movie/service/interfaces/MovieService.java +++ b/server/src/main/java/com/ticketing/server/movie/service/interfaces/MovieService.java @@ -1,5 +1,10 @@ package com.ticketing.server.movie.service.interfaces; +import com.ticketing.server.movie.service.dto.MovieDto; +import java.util.List; + public interface MovieService { + List getMovies(); + } diff --git a/server/src/main/java/com/ticketing/server/user/application/UserController.java b/server/src/main/java/com/ticketing/server/user/application/UserController.java index 6cb72c5..3b31c7e 100644 --- a/server/src/main/java/com/ticketing/server/user/application/UserController.java +++ b/server/src/main/java/com/ticketing/server/user/application/UserController.java @@ -32,7 +32,6 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RequestMapping("/api/user") @Slf4j - public class UserController { private final UserServiceImpl userService; diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index d41cd67..adfede3 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -18,6 +18,10 @@ spring: maximum-pool-size: 10 # default 10 max-lifetime: 1800000 # default 30 minutes + mvc: + pathmatch: + matching-strategy: ant_path_matcher + jasypt: encryptor: bean: jasyptStringEncryptor @@ -29,3 +33,8 @@ jwt: secret-key: Zi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXktZi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXkK access-token-validity-in-seconds: 60 # 1분 refresh-token-validity-in-seconds: 259200 # 3일 + +springfox: + documentation: + swagger: + use-model-v3: false diff --git a/server/src/test/java/com/ticketing/server/movie/domain/repository/MovieRepositoryTest.java b/server/src/test/java/com/ticketing/server/movie/domain/repository/MovieRepositoryTest.java index df0ccb5..ac022fa 100644 --- a/server/src/test/java/com/ticketing/server/movie/domain/repository/MovieRepositoryTest.java +++ b/server/src/test/java/com/ticketing/server/movie/domain/repository/MovieRepositoryTest.java @@ -27,7 +27,7 @@ public class MovieRepositoryTest { @Order(1) @Test @Rollback(value = false) - @DisplayName("Movie Repository - test saving movie") + @DisplayName("Movie Repository Test - saving movie") void shouldAbleToSaveMovie() { // given Movie movie = new Movie("범죄도시 2", 106); diff --git a/server/src/test/java/com/ticketing/server/movie/service/MovieServiceImplTest.java b/server/src/test/java/com/ticketing/server/movie/service/MovieServiceImplTest.java index f3d75e6..116e0e7 100644 --- a/server/src/test/java/com/ticketing/server/movie/service/MovieServiceImplTest.java +++ b/server/src/test/java/com/ticketing/server/movie/service/MovieServiceImplTest.java @@ -1,9 +1,66 @@ package com.ticketing.server.movie.service; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import com.ticketing.server.movie.domain.Movie; +import com.ticketing.server.movie.domain.repository.MovieRepository; +import com.ticketing.server.movie.service.dto.MovieDto; +import java.util.ArrayList; +import java.util.Collections; +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.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class MovieServiceImplTest { + Movie movie; + MovieDto movieDto; + List movies = new ArrayList<>(); + List movieDtos = new ArrayList<>(); + + @Mock + MovieRepository movieRepository; + + @InjectMocks + MovieServiceImpl movieService; + + @Test + @DisplayName("Movie Service Test - get movies when there is no movie") + void shouldGetEmptyList() { + // given + when(movieRepository.findValidMovies()).thenReturn(Collections.emptyList()); + + // when + List movieDtoList = movieService.getMovies(); + + // then + assertTrue(movieDtoList.isEmpty()); + + } + + @Test + @DisplayName("Movie Service Test - get movies") + void shouldAbleToGetMovies() { + // given + movie = new Movie("범죄도시2", 106); + movieDto = movie.toDto(); + movies.add(movie); + movieDtos.add(movieDto); + + when(movieRepository.findValidMovies()).thenReturn(movies); + + // when + List movieDtoList = movieService.getMovies(); + + // then + assertTrue(!movieDtoList.isEmpty()); + } + }