diff --git a/build.gradle.kts b/build.gradle.kts index ad4e990..66899f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,13 +3,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "3.0.1" id("io.spring.dependency-management") version "1.1.0" + id("org.springdoc.openapi-gradle-plugin") version "1.6.0" kotlin("jvm") version "1.7.22" kotlin("plugin.spring") version "1.7.22" kotlin("plugin.jpa") version "1.7.22" kotlin("kapt") version "1.7.22" } -group = "com.nexters" +group = "pimo" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_17 @@ -54,8 +55,11 @@ dependencies { runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") - implementation("com.infobip:infobip-spring-data-r2dbc-querydsl-boot-starter:6.2.0") - kapt("com.infobip:infobip-spring-data-jdbc-annotation-processor-common:6.2.0") + implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.0.2") // spring boot 3.0에서 쓰려면 무조건 "2.0.2"버전으로 해야됨!!!! +} + +tasks.getByName("jar") { + enabled = false } tasks.withType { diff --git a/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/AccountRouter.kt b/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/AccountRouter.kt index c46f63b..5ff0327 100644 --- a/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/AccountRouter.kt +++ b/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/AccountRouter.kt @@ -1,8 +1,25 @@ package com.nexters.pimo.account.adapter.`in`.web +import com.nexters.pimo.account.adapter.`in`.web.dto.AccountInput +import com.nexters.pimo.account.application.dto.AccountDto +import com.nexters.pimo.common.dto.BaseResponse +import com.nexters.pimo.follow.adapter.`in`.web.dto.RegisterInput +import com.nexters.pimo.follow.application.dto.FollowAccountDto +import com.nexters.pimo.follow.application.dto.FollowCntDto +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.ExampleObject +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import org.springdoc.core.annotations.RouterOperation +import org.springdoc.core.annotations.RouterOperations import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.reactive.function.server.RouterFunction import org.springframework.web.reactive.function.server.ServerResponse import org.springframework.web.reactive.function.server.router @@ -16,6 +33,253 @@ class AccountRouter( private val accountHandler: AccountHandler ) { @Bean + @RouterOperations( + value = [ + RouterOperation( + path = "/users", + method = [RequestMethod.GET], + beanClass = AccountHandler::class, + beanMethod = "findMe", + operation = Operation( + tags = ["계정관리"], + summary = "나의 계정정보 조회", + operationId = "findMe", + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "나의 계정정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = AccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users/exists", + method = [RequestMethod.GET], + beanClass = AccountHandler::class, + beanMethod = "existUser", + operation = Operation( + tags = ["계정관리"], + summary = "나의 계정정보 존재여부 조회", + operationId = "existUser", + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "나의 계정정보 존재여부", + responseCode = "200", + content = [Content(schema = Schema(implementation = Boolean::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users/exists/nickname", + method = [RequestMethod.GET], + beanClass = AccountHandler::class, + beanMethod = "existsNickName", + operation = Operation( + tags = ["계정관리"], + summary = "닉네임 존재여부 조회(중복체크)", + operationId = "existsNickName", + parameters = [ + Parameter(name = "nickname", description = "닉네임", example = "admin", required = true), + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "닉네임 존재여부", + responseCode = "200", + content = [Content(schema = Schema(implementation = Boolean::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users/search", + method = [RequestMethod.GET], + beanClass = AccountHandler::class, + beanMethod = "findUser", + operation = Operation( + tags = ["계정관리"], + summary = "사용자 계정정보 조회", + operationId = "findUser", + parameters = [ + Parameter(name = "userId", description = "사용자 ID", example = "admin", required = true), + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "조회한 사용자 계정정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = AccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users/search/nickname", + method = [RequestMethod.GET], + beanClass = AccountHandler::class, + beanMethod = "searchUser", + operation = Operation( + tags = ["계정관리"], + summary = "사용자 닉네임 조회", + operationId = "searchUser", + parameters = [ + Parameter(name = "nickname", description = "닉네임", example = "admin", required = true), + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "조회한 사용자 계정정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = AccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users", + method = [RequestMethod.POST], + beanClass = AccountHandler::class, + beanMethod = "saveUser", + operation = Operation( + tags = ["계정관리"], + summary = "사용자 정보 저장", + operationId = "saveUser", + requestBody = RequestBody( + content = [Content(schema = Schema(implementation = AccountInput::class, required = true))] + ), + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "저장한 사용자 계정정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = AccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users/nickname", + method = [RequestMethod.PATCH], + beanClass = AccountHandler::class, + beanMethod = "updateUser", + operation = Operation( + tags = ["계정관리"], + summary = "사용자 닉네임 정보 업데이트", + operationId = "updateUser", + parameters = [ + Parameter(name = "nickname", description = "닉네임", example = "admin", required = true), + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "업데이트한 사용자 계정정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = AccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users/profile", + method = [RequestMethod.PATCH], + beanClass = AccountHandler::class, + beanMethod = "updateUser", + operation = Operation( + tags = ["계정관리"], + summary = "사용자 프로필사진링크 정보 업데이트", + operationId = "updateUser", + parameters = [ + Parameter(name = "profile", description = "프로필사진링크", example = "http://localhost:8080", required = true), + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "업데이트한 사용자 계정정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = AccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/users", + method = [RequestMethod.DELETE], + beanClass = AccountHandler::class, + beanMethod = "deleteUser", + operation = Operation( + tags = ["계정관리"], + summary = "사용자 정보 삭제", + operationId = "deleteUser", + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "사용자 삭제 성공여부", + responseCode = "200", + content = [Content(schema = Schema(implementation = Boolean::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + ] + ) fun accountRouterFunction(): RouterFunction = router { accept(MediaType.APPLICATION_JSON).nest { diff --git a/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/dto/AccountInput.kt b/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/dto/AccountInput.kt index 98328bf..6fa8aa9 100644 --- a/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/dto/AccountInput.kt +++ b/src/main/kotlin/com/nexters/pimo/account/adapter/in/web/dto/AccountInput.kt @@ -1,5 +1,6 @@ package com.nexters.pimo.account.adapter.`in`.web.dto +import io.swagger.v3.oas.annotations.media.Schema import java.io.Serializable /** @@ -7,6 +8,8 @@ import java.io.Serializable * @since 2023.02.14 */ data class AccountInput ( + @Schema(description = "닉네임", example = "admin1") val nickName: String, + @Schema(description = "프로필사진링크", example = "https://localhost:8080") val profileImgUrl: String ): Serializable \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/account/application/dto/AccountDto.kt b/src/main/kotlin/com/nexters/pimo/account/application/dto/AccountDto.kt index faf8896..e37a8ab 100644 --- a/src/main/kotlin/com/nexters/pimo/account/application/dto/AccountDto.kt +++ b/src/main/kotlin/com/nexters/pimo/account/application/dto/AccountDto.kt @@ -1,14 +1,22 @@ package com.nexters.pimo.account.application.dto +import io.swagger.v3.oas.annotations.media.Schema + /** * @author yoonho * @since 2023.02.15 */ data class AccountDto( + @Schema(description = "사용자ID", example = "admin1") val userId: String, + @Schema(description = "닉네임", example = "admin1") var nickName: String, + @Schema(description = "프로필사진링크", example = "https://localhost:8080") var profileImgUrl: String, + @Schema(description = "계정상태", example = "0") var status: String, + @Schema(description = "계정 업데이트 일자", example = "yyyyMMddHHmmss") var updatedAt: String, + @Schema(description = "계정 생성 일자", example = "yyyyMMddHHmmss") val createdAt: String ) \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/common/dto/BaseResponse.kt b/src/main/kotlin/com/nexters/pimo/common/dto/BaseResponse.kt index 639fdb5..cb8158b 100644 --- a/src/main/kotlin/com/nexters/pimo/common/dto/BaseResponse.kt +++ b/src/main/kotlin/com/nexters/pimo/common/dto/BaseResponse.kt @@ -1,5 +1,6 @@ package com.nexters.pimo.common.dto +import io.swagger.v3.oas.annotations.media.Schema import org.springframework.http.HttpStatus import org.springframework.web.reactive.function.BodyInserters import org.springframework.web.reactive.function.server.ServerResponse @@ -9,9 +10,13 @@ import java.io.Serializable * @author yoonho * @since 2023.01.16 */ +@Schema(description = "공통 Response") data class BaseResponse ( + @Schema(description = "응답 메세지", example = "Success") val message: String?, + @Schema(description = "응답 HTTP Status", example = "200") val status: HttpStatus, + @Schema(description = "응답 데이터") val data: Any? ): Serializable { constructor(): this(message = "Success", status = HttpStatus.OK, null) diff --git a/src/main/kotlin/com/nexters/pimo/common/dto/BaseTokenInfo.kt b/src/main/kotlin/com/nexters/pimo/common/dto/BaseTokenInfo.kt index bdc342a..c72cf45 100644 --- a/src/main/kotlin/com/nexters/pimo/common/dto/BaseTokenInfo.kt +++ b/src/main/kotlin/com/nexters/pimo/common/dto/BaseTokenInfo.kt @@ -1,5 +1,6 @@ package com.nexters.pimo.common.dto +import io.swagger.v3.oas.annotations.media.Schema import java.io.Serializable /** @@ -7,6 +8,8 @@ import java.io.Serializable * @since 2023.02.14 */ data class BaseTokenInfo ( + @Schema(description = "소셜로그인 구분", example = "kakao") val provider: String, + @Schema(description = "토큰", example = "0") val accessToken: String ): Serializable \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/common/exception/ThirdPartyServerException.kt b/src/main/kotlin/com/nexters/pimo/common/exception/ThirdPartyServerException.kt new file mode 100644 index 0000000..327df98 --- /dev/null +++ b/src/main/kotlin/com/nexters/pimo/common/exception/ThirdPartyServerException.kt @@ -0,0 +1,10 @@ +package com.nexters.pimo.common.exception + +/** + * @author yoonho + * @since 2023.02.21 + */ +class ThirdPartyServerException: RuntimeException { + constructor(msg: String?): super(msg) + constructor(): super() +} \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/common/exception/UnAuthorizationException.kt b/src/main/kotlin/com/nexters/pimo/common/exception/UnAuthorizationException.kt new file mode 100644 index 0000000..d4b2d85 --- /dev/null +++ b/src/main/kotlin/com/nexters/pimo/common/exception/UnAuthorizationException.kt @@ -0,0 +1,10 @@ +package com.nexters.pimo.common.exception + +/** + * @author yoonho + * @since 2023.02.19 + */ +class UnAuthorizationException: RuntimeException { + constructor(msg: String?): super(msg) + constructor(): super() +} \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/common/filter/AuthorizationFilter.kt b/src/main/kotlin/com/nexters/pimo/common/filter/AuthorizationFilter.kt index 1696f54..bfe4eaf 100644 --- a/src/main/kotlin/com/nexters/pimo/common/filter/AuthorizationFilter.kt +++ b/src/main/kotlin/com/nexters/pimo/common/filter/AuthorizationFilter.kt @@ -3,6 +3,7 @@ package com.nexters.pimo.common.filter import com.nexters.pimo.common.constants.CommCode import com.nexters.pimo.common.dto.BaseTokenInfo import com.nexters.pimo.common.exception.BadRequestException +import com.nexters.pimo.common.exception.UnAuthorizationException import com.nexters.pimo.common.utils.CipherUtil import com.nexters.pimo.login.application.port.`in`.JwtTokenUseCase import org.springframework.http.HttpHeaders @@ -27,12 +28,19 @@ class AuthorizationFilter( ): WebFilter { override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { val path = exchange.request.path.pathWithinApplication().value() + + // 로그인 관련 API 제외처리 if(Regex("/login*") in path) { return chain.filter(exchange) } + // Swagger 관련 API 제외처리 + if(Regex("/swagger*") in path || Regex("/v3/api-docs*") in path) { + return chain.filter(exchange) + } + val authorization = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION) - ?: throw BadRequestException("Authorization 헤더가 존재하지 않습니다.") + ?: throw UnAuthorizationException("Authorization 헤더가 존재하지 않습니다.") if(DEV_TOKEN_PREFIX in authorization) { val userId = authorization.substringAfter(DEV_TOKEN_PREFIX) @@ -50,7 +58,7 @@ class AuthorizationFilter( accessToken.startsWith(CommCode.Social.APPLE.prefix) -> { BaseTokenInfo(provider = CommCode.Social.APPLE.code, accessToken = accessToken.substring(1)) } - else -> throw BadRequestException("유효한 토큰이 아닙니다.") + else -> throw UnAuthorizationException("유효한 토큰이 아닙니다.") } return jwtTokenUseCase.authToken(tokenInfo) @@ -60,7 +68,7 @@ class AuthorizationFilter( } } - throw BadRequestException("필수값을 누락하였습니다.") + throw UnAuthorizationException("필수값을 누락하였습니다.") } } diff --git a/src/main/kotlin/com/nexters/pimo/common/handler/ExceptionHandler.kt b/src/main/kotlin/com/nexters/pimo/common/handler/ExceptionHandler.kt index 09f5557..d34507d 100644 --- a/src/main/kotlin/com/nexters/pimo/common/handler/ExceptionHandler.kt +++ b/src/main/kotlin/com/nexters/pimo/common/handler/ExceptionHandler.kt @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.nexters.pimo.common.dto.BaseResponse import com.nexters.pimo.common.exception.BadRequestException import com.nexters.pimo.common.exception.NotFoundDataException +import com.nexters.pimo.common.exception.ThirdPartyServerException +import com.nexters.pimo.common.exception.UnAuthorizationException import org.slf4j.LoggerFactory import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler import org.springframework.core.annotation.Order @@ -49,6 +51,17 @@ class ExceptionHandler: ErrorWebExceptionHandler { ) }) } + is UnAuthorizationException -> { + log.warn(" >>> [handle] UnAuthorizationException occurs - message: {}", ex.message) + return exchange.response.writeWith(Mono.fromSupplier { + val bufferFactory = exchange.response.bufferFactory() + exchange.response.statusCode = HttpStatus.UNAUTHORIZED + exchange.response.headers.contentType = MediaType.APPLICATION_JSON + return@fromSupplier bufferFactory.wrap( + objectMapper.writeValueAsBytes(BaseResponse(ex.message, HttpStatus.UNAUTHORIZED, null)) + ) + }) + } is IllegalArgumentException -> { log.warn(" >>> [handle] IllegalArgumentException occurs - message: {}", ex.message) return exchange.response.writeWith(Mono.fromSupplier { @@ -60,6 +73,17 @@ class ExceptionHandler: ErrorWebExceptionHandler { ) }) } + is ThirdPartyServerException -> { + log.warn(" >>> [handle] ThirdPartyServerException occurs - message: {}", ex.message) + return exchange.response.writeWith(Mono.fromSupplier { + val bufferFactory = exchange.response.bufferFactory() + exchange.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR + exchange.response.headers.contentType = MediaType.APPLICATION_JSON + return@fromSupplier bufferFactory.wrap( + objectMapper.writeValueAsBytes(BaseResponse(ex.message, HttpStatus.INTERNAL_SERVER_ERROR, null)) + ) + }) + } else -> { return exchange.response.writeWith(Mono.fromSupplier { val bufferFactory = exchange.response.bufferFactory() diff --git a/src/main/kotlin/com/nexters/pimo/config/OpenApiConfig.kt b/src/main/kotlin/com/nexters/pimo/config/OpenApiConfig.kt new file mode 100644 index 0000000..fd0b58c --- /dev/null +++ b/src/main/kotlin/com/nexters/pimo/config/OpenApiConfig.kt @@ -0,0 +1,29 @@ +package com.nexters.pimo.config + +import io.swagger.v3.oas.annotations.OpenAPIDefinition +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType +import io.swagger.v3.oas.annotations.info.Info +import io.swagger.v3.oas.annotations.security.SecurityScheme +import org.springframework.context.annotation.Configuration + +/** + * @author yoonho + * @since 2023.02.19 + */ +@OpenAPIDefinition( + info = Info( + title = "감성팔이소녀 PIMO API 규격서", + description = "

감성팔이소녀팀의 PIMO API 규격서입니다.

" + + "

아래 API 문서 참고부탁드리겠습니다.

" + + "

문의사항은 언제든 말씀해주세요.

", + version = "0.0.1" + ) +) +@SecurityScheme( + name = "Bearer Authentication", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + scheme = "bearer" +) +@Configuration +class OpenApiConfig \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/config/WebClientConfig.kt b/src/main/kotlin/com/nexters/pimo/config/WebClientConfig.kt new file mode 100644 index 0000000..22f2957 --- /dev/null +++ b/src/main/kotlin/com/nexters/pimo/config/WebClientConfig.kt @@ -0,0 +1,53 @@ +package com.nexters.pimo.config + +import io.netty.channel.ChannelOption +import io.netty.handler.timeout.ReadTimeoutHandler +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.http.client.reactive.ReactorResourceFactory +import org.springframework.web.reactive.function.client.ExchangeStrategies +import org.springframework.web.reactive.function.client.WebClient +import reactor.netty.http.client.HttpClient +import java.util.concurrent.TimeUnit + +/** + * @author yoonho + * @since 2023.02.21 + */ +@Configuration +class WebClientConfig { + + @Value("\${auth.url}") + private lateinit var baseUrl: String + @Value("\${auth.conn.connect-timeout:0.0}") + private val connectionTimeout: Int? = null + @Value("\${auth.conn.read-timeout:0.0}") + private val readTimeout: Long? = null + @Value("\${auth.conn.user-agent}") + private lateinit var userAgent: String + + @Bean + fun defaultWebClient(reactorResourceFactory: ReactorResourceFactory): WebClient { + val mapper: (HttpClient) -> HttpClient = { + it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout) + .doOnConnected { + it.addHandlerLast(ReadTimeoutHandler(readTimeout!!, TimeUnit.MILLISECONDS)) + } + } + + val customExchangeStrategies = ExchangeStrategies.builder() + .codecs { it.defaultCodecs().maxInMemorySize(1024 * 1024 * 10) } + .build() + + return WebClient.builder() + .baseUrl(baseUrl) + .defaultHeaders { + it.set("User-Agent", userAgent) + } + .clientConnector(ReactorClientHttpConnector(reactorResourceFactory, mapper)) + .exchangeStrategies(customExchangeStrategies) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowHandler.kt b/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowHandler.kt index f429d5d..01bce33 100644 --- a/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowHandler.kt +++ b/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowHandler.kt @@ -10,6 +10,7 @@ import com.nexters.pimo.follow.application.dto.FollowCntDto import com.nexters.pimo.follow.application.port.`in`.DeleteUseCase import com.nexters.pimo.follow.application.port.`in`.FindUseCase import com.nexters.pimo.follow.application.port.`in`.RegisterUseCase +import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.web.reactive.function.server.ServerRequest import org.springframework.web.reactive.function.server.ServerResponse diff --git a/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowRouter.kt b/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowRouter.kt index 5e9ea9f..b7bd336 100644 --- a/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowRouter.kt +++ b/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/FollowRouter.kt @@ -1,8 +1,25 @@ package com.nexters.pimo.follow.adapter.`in`.web +import com.nexters.pimo.common.dto.BaseResponse +import com.nexters.pimo.follow.adapter.`in`.web.dto.RegisterInput +import com.nexters.pimo.follow.application.dto.FollowAccountDto +import com.nexters.pimo.follow.application.dto.FollowCntDto +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.ExampleObject +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.models.examples.Example +import org.springdoc.core.annotations.RouterOperation +import org.springdoc.core.annotations.RouterOperations import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.reactive.function.server.RouterFunction import org.springframework.web.reactive.function.server.ServerResponse import org.springframework.web.reactive.function.server.router @@ -15,8 +32,203 @@ import org.springframework.web.reactive.function.server.router class FollowRouter( private val followHandler: FollowHandler ) { - @Bean + @RouterOperations( + value = [ + RouterOperation( + path = "/follows", + method = [RequestMethod.POST], + beanClass = FollowHandler::class, + beanMethod = "register", + operation = Operation( + tags = ["친구관리"], + summary = "친구 등록", + operationId = "register", + requestBody = RequestBody( + content = [Content(schema = Schema(implementation = RegisterInput::class, required = true))] + ), + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "친구등록 성공여부", + responseCode = "200", + content = [Content(schema = Schema(implementation = Boolean::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/follows", + method = [RequestMethod.DELETE], + beanClass = FollowHandler::class, + beanMethod = "delete", + operation = Operation( + tags = ["친구관리"], + summary = "친구 삭제", + operationId = "delete", + requestBody = RequestBody( + content = [Content(schema = Schema(implementation = RegisterInput::class, required = true))] + ), + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "친구삭제 성공여부", + responseCode = "200", + content = [Content(schema = Schema(implementation = Boolean::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/follows/count", + method = [RequestMethod.GET], + beanClass = FollowHandler::class, + beanMethod = "count", + operation = Operation( + tags = ["친구관리"], + summary = "친구 목록수 조회", + operationId = "count", + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "친구 목록수", + responseCode = "200", + content = [Content(schema = Schema(implementation = FollowCntDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/follows/follower", + method = [RequestMethod.GET], + beanClass = FollowHandler::class, + beanMethod = "list", + operation = Operation( + tags = ["친구관리"], + summary = "나만 친구 목록조회", + operationId = "list", + parameters = [ + Parameter( + name = "sort", + description = "정렬 방식 구분", + examples = [ + ExampleObject(name = "0", value = "0", description = "친구 등록한 순서 정렬"), + ExampleObject(name = "1", value = "1", description = "닉네임 가나다순 정렬"), + ] + ), + Parameter(name = "start", description = "페이징 시작Index", example = "0"), + Parameter(name = "size", description = "페이징 조회할 갯수", example = "10") + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "나만 친구 목록", + responseCode = "200", + content = [Content(schema = Schema(implementation = FollowAccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/follows/followee", + method = [RequestMethod.GET], + beanClass = FollowHandler::class, + beanMethod = "list", + operation = Operation( + tags = ["친구관리"], + summary = "상대방만 친구 목록조회", + operationId = "list", + parameters = [ + Parameter( + name = "sort", + description = "정렬 방식 구분", + examples = [ + ExampleObject(name = "0", value = "0", description = "친구 등록한 순서 정렬"), + ExampleObject(name = "1", value = "1", description = "닉네임 가나다순 정렬"), + ] + ), + Parameter(name = "start", description = "페이징 시작Index", example = "0"), + Parameter(name = "size", description = "페이징 조회할 갯수", example = "10") + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "상대방만 친구 목록", + responseCode = "200", + content = [Content(schema = Schema(implementation = FollowAccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ), + RouterOperation( + path = "/follows/followforfollow", + method = [RequestMethod.GET], + beanClass = FollowHandler::class, + beanMethod = "list", + operation = Operation( + tags = ["친구관리"], + summary = "서로 친구 목록조회", + operationId = "list", + parameters = [ + Parameter( + name = "sort", + description = "정렬 방식 구분", + examples = [ + ExampleObject(name = "0", value = "0", description = "친구 등록한 순서 정렬"), + ExampleObject(name = "1", value = "1", description = "닉네임 가나다순 정렬"), + ] + ), + Parameter(name = "start", description = "페이징 시작Index", example = "0"), + Parameter(name = "size", description = "페이징 조회할 갯수", example = "10") + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "서로 친구 목록", + responseCode = "200", + content = [Content(schema = Schema(implementation = FollowAccountDto::class))] + ) + ], + security = [SecurityRequirement(name = "Bearer Authentication")] + ) + ) + ] + ) fun followRouterFunction(): RouterFunction = router { accept(MediaType.APPLICATION_JSON).nest { diff --git a/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/dto/RegisterInput.kt b/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/dto/RegisterInput.kt index 7515c3f..fdbad7f 100644 --- a/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/dto/RegisterInput.kt +++ b/src/main/kotlin/com/nexters/pimo/follow/adapter/in/web/dto/RegisterInput.kt @@ -1,5 +1,6 @@ package com.nexters.pimo.follow.adapter.`in`.web.dto +import io.swagger.v3.oas.annotations.media.Schema import java.io.Serializable /** @@ -7,5 +8,6 @@ import java.io.Serializable * @since 2023.02.16 */ data class RegisterInput( + @Schema(description = "사용자 ID", example = "admin1", required = true) val userId: String ): Serializable \ No newline at end of file diff --git a/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowAccountDto.kt b/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowAccountDto.kt index 194e1b9..d09efcd 100644 --- a/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowAccountDto.kt +++ b/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowAccountDto.kt @@ -1,5 +1,6 @@ package com.nexters.pimo.follow.application.dto +import io.swagger.v3.oas.annotations.media.Schema import java.io.Serializable /** @@ -7,10 +8,16 @@ import java.io.Serializable * @since 2023.02.18 */ data class FollowAccountDto( + @Schema(description = "친구 사용자ID", example = "admin1") val userId: String, + @Schema(description = "친구 닉네임", example = "admin1") var nickName: String, + @Schema(description = "친구 프로필사진링크", example = "https://localhost:8080") var profileImgUrl: String, + @Schema(description = "친구 계정상태", example = "0") var status: String, + @Schema(description = "계정 업데이트 일자", example = "yyyyMMddHHmmss") var updatedAt: String, + @Schema(description = "계정 생성 일자", example = "yyyyMMddHHmmss") val createdAt: String ): Serializable diff --git a/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowCntDto.kt b/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowCntDto.kt index 80fc889..6d2777d 100644 --- a/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowCntDto.kt +++ b/src/main/kotlin/com/nexters/pimo/follow/application/dto/FollowCntDto.kt @@ -1,5 +1,6 @@ package com.nexters.pimo.follow.application.dto +import io.swagger.v3.oas.annotations.media.Schema import java.io.Serializable /** @@ -7,7 +8,10 @@ import java.io.Serializable * @since 2023.02.16 */ data class FollowCntDto( + @Schema(description = "나만 친구 목록", example = "0") val followerCnt: Long, + @Schema(description = "상대방만 친구 목록", example = "0") val followeeCnt: Long, + @Schema(description = "서로 친구 목록", example = "0") val followForFollowCnt: Long ): Serializable diff --git a/src/main/kotlin/com/nexters/pimo/login/adapter/in/web/LoginRouter.kt b/src/main/kotlin/com/nexters/pimo/login/adapter/in/web/LoginRouter.kt index c4c04a9..5612b1d 100644 --- a/src/main/kotlin/com/nexters/pimo/login/adapter/in/web/LoginRouter.kt +++ b/src/main/kotlin/com/nexters/pimo/login/adapter/in/web/LoginRouter.kt @@ -1,8 +1,25 @@ package com.nexters.pimo.login.adapter.`in`.web +import com.nexters.pimo.common.dto.BaseResponse +import com.nexters.pimo.common.dto.BaseTokenInfo +import com.nexters.pimo.follow.adapter.`in`.web.dto.RegisterInput +import com.nexters.pimo.follow.application.dto.FollowAccountDto +import com.nexters.pimo.follow.application.dto.FollowCntDto +import com.nexters.pimo.login.domain.TokenInfo +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.ExampleObject +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import org.springdoc.core.annotations.RouterOperation +import org.springdoc.core.annotations.RouterOperations import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.RequestMethod import org.springframework.web.reactive.function.server.RouterFunction import org.springframework.web.reactive.function.server.ServerResponse import org.springframework.web.reactive.function.server.router @@ -17,6 +34,70 @@ class LoginRouter( ) { @Bean + @RouterOperations( + value = [ + RouterOperation( + path = "/login/token", + method = [RequestMethod.GET], + beanClass = LoginHandler::class, + beanMethod = "token", + operation = Operation( + tags = ["로그인관리"], + summary = "토큰정보 조회", + operationId = "token", + parameters = [ + Parameter( + name = "state", + description = "소셜로그인 구분 구분", + examples = [ + ExampleObject(name = "kakao", value = "kakao", description = "카카오 로그인"), + ExampleObject(name = "apple", value = "apple", description = "애플 로그인"), + ] + ), + Parameter(name = "code", description = "인가코드(identity_token)", example = "0"), + ], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "획득한 토큰정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = TokenInfo::class))] + ) + ], + ) + ), + RouterOperation( + path = "/login/encode", + method = [RequestMethod.GET], + beanClass = LoginHandler::class, + beanMethod = "encode", + operation = Operation( + tags = ["로그인관리"], + summary = "토큰 인코딩", + operationId = "encode", + parameters = [Parameter(name = "token", description = "토큰", example = "0")], + responses = [ + // 공통 Response(BaseResponse)객체를 노출하기 위해 사용, responseCode는 swagger에서 맨 앞단에 노출시키기 위해 "0" 고정 + ApiResponse( + responseCode = "0", + description = "공통 Response", + content = [Content(schema = Schema(implementation = BaseResponse::class))] + ), + ApiResponse( + description = "인코딩한 토큰정보", + responseCode = "200", + content = [Content(schema = Schema(implementation = BaseTokenInfo::class))] + ) + ], + ) + ), + ] + ) fun loginRouterFunction(): RouterFunction = router { accept(MediaType.APPLICATION_JSON).nest { diff --git a/src/main/kotlin/com/nexters/pimo/login/adapter/out/AppleTokenAdapter.kt b/src/main/kotlin/com/nexters/pimo/login/adapter/out/AppleTokenAdapter.kt index 4fbb4f6..5423746 100644 --- a/src/main/kotlin/com/nexters/pimo/login/adapter/out/AppleTokenAdapter.kt +++ b/src/main/kotlin/com/nexters/pimo/login/adapter/out/AppleTokenAdapter.kt @@ -3,21 +3,17 @@ package com.nexters.pimo.login.adapter.out import com.fasterxml.jackson.databind.ObjectMapper import com.nexters.pimo.common.constants.CommCode import com.nexters.pimo.common.exception.BadRequestException -import com.nexters.pimo.common.exception.NotFoundDataException +import com.nexters.pimo.common.exception.ThirdPartyServerException import com.nexters.pimo.login.application.port.out.JwtTokenPort import com.nexters.pimo.login.domain.TokenInfo import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm -import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value -import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.util.UriComponentsBuilder import reactor.core.publisher.Mono -import reactor.kotlin.core.publisher.switchIfEmpty import java.io.Serializable import java.math.BigInteger import java.security.KeyFactory @@ -32,7 +28,7 @@ import java.util.* */ @Component class AppleTokenAdapter( - private val webClientBuilder: WebClient.Builder + private val defaultWebClient: WebClient ): JwtTokenPort { private val log = LoggerFactory.getLogger(this::class.java) @@ -41,13 +37,6 @@ class AppleTokenAdapter( @Value("\${login.apis.apple}") private lateinit var url: String - private lateinit var webClient: WebClient - - @PostConstruct - fun initWebClient() { - this.webClient = webClientBuilder.build() - } - override fun createToken(code: String): Mono = findBy(code) .flatMap { @@ -88,11 +77,19 @@ class AppleTokenAdapter( val kid = header["kid"].toString() val alg = header["alg"].toString() - return webClient.get() + return defaultWebClient.get() .uri(url + "/auth/keys") .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono(ApplePublicKeys::class.java) + .exchangeToMono { + if(it.statusCode().is2xxSuccessful) { + return@exchangeToMono it.bodyToMono(ApplePublicKeys::class.java) + }else if(it.statusCode().is4xxClientError) { + return@exchangeToMono throw BadRequestException("애플 공개키 정보를 가져올 수 없습니다.") + }else{ + return@exchangeToMono throw ThirdPartyServerException("일시적으로 애플서버를 이용할 수 없습니다.") + } + } + .log() .map { toMatchedKey(it.keys, kid, alg) } } diff --git a/src/main/kotlin/com/nexters/pimo/login/adapter/out/KakaoTokenAdapter.kt b/src/main/kotlin/com/nexters/pimo/login/adapter/out/KakaoTokenAdapter.kt index 6cdedd8..6d22e1c 100644 --- a/src/main/kotlin/com/nexters/pimo/login/adapter/out/KakaoTokenAdapter.kt +++ b/src/main/kotlin/com/nexters/pimo/login/adapter/out/KakaoTokenAdapter.kt @@ -2,10 +2,11 @@ package com.nexters.pimo.login.adapter.out import com.nexters.pimo.common.constants.CommCode import com.nexters.pimo.common.exception.BadRequestException +import com.nexters.pimo.common.exception.ThirdPartyServerException +import com.nexters.pimo.common.exception.UnAuthorizationException import com.nexters.pimo.login.adapter.out.dto.KakaoTokenInfo import com.nexters.pimo.login.application.port.out.JwtTokenPort import com.nexters.pimo.login.domain.TokenInfo -import jakarta.annotation.PostConstruct import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpHeaders @@ -23,7 +24,7 @@ import reactor.core.publisher.Mono */ @Component class KakaoTokenAdapter( - private val webClientBuilder: WebClient.Builder + private val defaultWebClient: WebClient ): JwtTokenPort { private val log = LoggerFactory.getLogger(this::class.java) @@ -34,13 +35,6 @@ class KakaoTokenAdapter( @Value("\${login.apis.kapi}") private lateinit var kapiUrl: String - private lateinit var webClient: WebClient - - @PostConstruct - fun initWebClient() { - this.webClient = webClientBuilder.build() - } - override fun createToken(code: String): Mono { try{ val params: MultiValueMap = LinkedMultiValueMap() @@ -53,11 +47,18 @@ class KakaoTokenAdapter( .queryParams(params) .build(false) - return webClient.get() + return defaultWebClient.get() .uri(uriComponents.toUri()) .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono(TokenInfo::class.java) + .exchangeToMono { + if(it.statusCode().is2xxSuccessful) { + return@exchangeToMono it.bodyToMono(TokenInfo::class.java) + }else if(it.statusCode().is4xxClientError) { + return@exchangeToMono throw BadRequestException("유효한 인가코드가 아닙니다.") + }else{ + return@exchangeToMono throw ThirdPartyServerException("일시적으로 카카오서버를 이용할 수 없습니다.") + } + } .log() }catch (e: Exception) { throw e @@ -74,12 +75,19 @@ class KakaoTokenAdapter( .build(false) log.info(" >>> [authToken] request - token: $token, url: ${uriComponents.toUri()}") - return webClient.get() + return defaultWebClient.get() .uri(uriComponents.toUri()) .header(HttpHeaders.AUTHORIZATION, "Bearer $token") .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono(KakaoTokenInfo::class.java) + .exchangeToMono { + if(it.statusCode().is2xxSuccessful) { + return@exchangeToMono it.bodyToMono(KakaoTokenInfo::class.java) + }else if(it.statusCode().is4xxClientError) { + return@exchangeToMono throw BadRequestException("유효한 토큰이 아닙니다.") + }else{ + return@exchangeToMono throw ThirdPartyServerException("일시적으로 카카오서버를 이용할 수 없습니다.") + } + } .log() .map { toUserId(it) } }catch (e: Throwable){ @@ -89,7 +97,7 @@ class KakaoTokenAdapter( private fun toUserId(response: KakaoTokenInfo): String { if(response.code != null || response.msg != null) { - throw BadRequestException("유효한 토큰이 아닙니다. - code: ${response.code}, msg: ${response.msg}") + throw UnAuthorizationException("유효한 토큰이 아닙니다. - code: ${response.code}, msg: ${response.msg}") } return response.id } diff --git a/src/main/kotlin/com/nexters/pimo/login/domain/TokenInfo.kt b/src/main/kotlin/com/nexters/pimo/login/domain/TokenInfo.kt index d9ad6ae..f430fe5 100644 --- a/src/main/kotlin/com/nexters/pimo/login/domain/TokenInfo.kt +++ b/src/main/kotlin/com/nexters/pimo/login/domain/TokenInfo.kt @@ -1,14 +1,17 @@ package com.nexters.pimo.login.domain import com.fasterxml.jackson.annotation.JsonProperty +import io.swagger.v3.oas.annotations.media.Schema /** * @author yoonho * @since 2023.01.26 */ data class TokenInfo( + @Schema(description = "access_token", example = "0") @JsonProperty("access_token") val accessToken: String, + @Schema(description = "refresh_token", example = "0") @JsonProperty("refresh_token") val refreshToken: String ) \ No newline at end of file diff --git a/src/main/resources/application-deploy.yaml b/src/main/resources/application-deploy.yaml new file mode 100644 index 0000000..115edcb --- /dev/null +++ b/src/main/resources/application-deploy.yaml @@ -0,0 +1,26 @@ +spring: + config: + activate: + on-profile: deploy + +login: + keys: + cipher: parkyoonhosecret + jwt: thisistestusersecretkeyprojectnameisnexterspimooooooooooo + clientId: 9991a02c1253223dd0fa649fab9e0df9 + apis: + kauth: https://kauth.kakao.com + kapi: https://kapi.kakao.com + apple: https://appleid.apple.com + +auth: + url: http://localhost:8080 + conn: + connect-timeout: 3000 + read-timeout: 2000 + user-agent: PIMO + +springdoc: + default-consumes-media-type: application/json + swagger-ui: + path: /swagger-ui.html \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8b6af8a..7f114f0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -6,4 +6,16 @@ login: apis: kauth: https://kauth.kakao.com kapi: https://kapi.kakao.com - apple: https://appleid.apple.com \ No newline at end of file + apple: https://appleid.apple.com + +auth: + url: http://localhost:8080 + conn: + connect-timeout: 3000 + read-timeout: 2000 + user-agent: PIMO + +springdoc: + default-consumes-media-type: application/json + swagger-ui: + path: /swagger-ui.html \ No newline at end of file diff --git a/src/main/resources/h2-schema.sql b/src/main/resources/h2-schema.sql index 7637528..74b1440 100644 --- a/src/main/resources/h2-schema.sql +++ b/src/main/resources/h2-schema.sql @@ -1,3 +1,4 @@ +-- 사용자 계정관리 테이블 create table if not exists UserTB ( id BIGINT NOT NULL AUTO_INCREMENT, @@ -19,4 +20,24 @@ comment on column UserTB.updatedAt is '업데이트 일자'; comment on column UserTB.createdAt is '생성 일자'; insert into UserTB (userId, nickName, profileImgUrl, status) values ('admin1', 'admin1', 'admin1', '0'); -insert into UserTB (userId, nickName, profileImgUrl, status) values ('admin2', 'admin2', 'admin2', '0'); \ No newline at end of file +insert into UserTB (userId, nickName, profileImgUrl, status) values ('admin2', 'admin2', 'admin2', '0'); +insert into UserTB (userId, nickName, profileImgUrl, status) values ('admin3', 'admin3', 'admin3', '0'); +insert into UserTB (userId, nickName, profileImgUrl, status) values ('admin4', 'admin4', 'admin4', '0'); +insert into UserTB (userId, nickName, profileImgUrl, status) values ('admin5', 'admin5', 'admin5', '0'); + + +-- 친구관리 테이블 +create table if not exists FollowTB +( + id BIGINT NOT NULL AUTO_INCREMENT, + followerUserId varchar(127) not null, + followerNickName varchar(127) not null, + followeeUserId varchar(127) not null, + followeeNickName varchar(127) not null, + createdAt timestamp not null default now(), + primary key (Id) +); + +insert into FollowTB(followerUserId, followerNickName, followeeUserId, followeeNickName) values ('admin1', 'admin1', 'admin2', 'admin2'); +insert into FollowTB(followerUserId, followerNickName, followeeUserId, followeeNickName) values ('admin1', 'admin1', 'admin3', 'admin3'); +insert into FollowTB(followerUserId, followerNickName, followeeUserId, followeeNickName) values ('admin1', 'admin1', 'admin4', 'admin4'); \ No newline at end of file diff --git a/src/test/kotlin/com/nexters/pimo/PimoApplicationTests.kt b/src/test/kotlin/com/nexters/pimo/PimoApplicationTests.kt index b829b7f..18de4c9 100644 --- a/src/test/kotlin/com/nexters/pimo/PimoApplicationTests.kt +++ b/src/test/kotlin/com/nexters/pimo/PimoApplicationTests.kt @@ -1,13 +1,11 @@ package com.nexters.pimo -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication -@SpringBootTest -class PimoApplicationTests { - - @Test - fun contextLoads() { - } +@SpringBootApplication +class PimoApplicationTests +fun main(args: Array) { + runApplication(*args) }