diff --git a/build.gradle.kts b/build.gradle.kts index a23ee62..504c883 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { val kotlinVersion = "1.5.20" - id("org.springframework.boot") version "2.5.2" + id("org.springframework.boot") version "2.5.3" id("io.spring.dependency-management") version "1.0.11.RELEASE" kotlin("jvm") version kotlinVersion kotlin("plugin.spring") version kotlinVersion @@ -37,9 +37,14 @@ repositories { mavenCentral() } +extra["springCloudVersion"] = "2020.0.3" + dependencies { annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") + implementation("io.github.openfeign:feign-okhttp") + implementation("io.github.openfeign:feign-jackson:9.3.1") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") @@ -65,8 +70,6 @@ dependencies { testAnnotationProcessor ("com.querydsl:querydsl-apt:$querydslVersion:jpa") runtimeOnly("mysql:mysql-connector-java") -// runtimeOnly("org.mariadb.jdbc:mariadb-java-client") -// implementation("com.zaxxer:HikariCP:5.0.0") compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") @@ -86,6 +89,12 @@ dependencies { runtimeOnly(group= "io.jsonwebtoken", name= "jjwt-jackson", version= "0.11.2") } +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + val generatedSourcesDir = file("${buildDir}/generated/querydsl") configurations { diff --git a/src/main/kotlin/com/example/attendanceapimono/AttendanceApiMonoApplication.kt b/src/main/kotlin/com/example/attendanceapimono/AttendanceApiMonoApplication.kt index c6a48b1..dd7178c 100644 --- a/src/main/kotlin/com/example/attendanceapimono/AttendanceApiMonoApplication.kt +++ b/src/main/kotlin/com/example/attendanceapimono/AttendanceApiMonoApplication.kt @@ -1,8 +1,12 @@ package com.example.attendanceapimono import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer import org.springframework.boot.runApplication +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +@EnableFeignClients @SpringBootApplication class AttendanceApiMonoApplication diff --git a/src/main/kotlin/com/example/attendanceapimono/adapter/infra/config/FeignDefaultConfig.kt b/src/main/kotlin/com/example/attendanceapimono/adapter/infra/config/FeignDefaultConfig.kt new file mode 100644 index 0000000..3333991 --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/adapter/infra/config/FeignDefaultConfig.kt @@ -0,0 +1,55 @@ +package com.example.attendanceapimono.adapter.infra.config + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import feign.* +import feign.codec.Decoder +import feign.codec.Encoder +import feign.codec.ErrorDecoder +import feign.jackson.JacksonDecoder +import feign.jackson.JacksonEncoder +import org.springframework.cloud.openfeign.FeignFormatterRegistrar +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.format.FormatterRegistry +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar +import org.springframework.http.HttpStatus +import java.util.* + + +@Configuration +class FeignDefaultConfig { + + @Bean + fun decoder(): Decoder { + return JacksonDecoder(jacksonObjectMapper()) + } + + @Bean + fun encoder(): Encoder { + return JacksonEncoder(jacksonObjectMapper()) + } + + @Bean + fun feignLoggerLevel(): Logger.Level { + return Logger.Level.FULL + } + + @Bean + fun retryer() = Retryer.Default() + + @Bean + fun localDateFeignFormatterRegister(): FeignFormatterRegistrar? { + return FeignFormatterRegistrar { registry: FormatterRegistry -> + val registrar = DateTimeFormatterRegistrar() + registrar.setUseIsoFormat(true) + registrar.registerFormatters(registry) + } + } + +// @Bean +// fun decoder(): ErrorDecoder { todo implements +// return ErrorDecoder { methodKey, response -> +// +// } +// } +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/adapter/infra/feign/GoogleAuthClient.kt b/src/main/kotlin/com/example/attendanceapimono/adapter/infra/feign/GoogleAuthClient.kt new file mode 100644 index 0000000..35a4e9d --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/adapter/infra/feign/GoogleAuthClient.kt @@ -0,0 +1,11 @@ +package com.example.attendanceapimono.adapter.infra.feign + +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam + +@FeignClient(value = "googleAuth", url = "https://www.googleapis.com") +interface GoogleAuthClient { + @GetMapping("/oauth2/v3/tokeninfo") + fun getByToken(@RequestParam("id_token") idToken: String): GoogleSocialInfo +} diff --git a/src/main/kotlin/com/example/attendanceapimono/adapter/infra/feign/GoogleSocialAdapterImpl.kt b/src/main/kotlin/com/example/attendanceapimono/adapter/infra/feign/GoogleSocialAdapterImpl.kt new file mode 100644 index 0000000..67fd65f --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/adapter/infra/feign/GoogleSocialAdapterImpl.kt @@ -0,0 +1,35 @@ +package com.example.attendanceapimono.adapter.infra.feign + +import com.example.attendanceapimono.domain.user.SocialAdapter +import com.example.attendanceapimono.domain.user.SocialInfo +import com.example.attendanceapimono.domain.user.SocialType +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import feign.QueryMap +import feign.RequestLine +import kotlinx.coroutines.flow.Flow +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Component +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestHeader +import javax.inject.Qualifier + + +@JsonIgnoreProperties(ignoreUnknown = true) +data class GoogleSocialInfo( + val sub: String, + override val email: String, + val picture: String, +) : SocialInfo { + override val id: String get() = this.sub + override val type: SocialType get() = SocialType.GOOGLE + override val thumb: String get() = this.picture +} + +@Component("googleAdapter") +class GoogleSocialAdapterImpl(private val auth: GoogleAuthClient) : SocialAdapter { + override fun findByToken(token: String): SocialInfo { + return auth.getByToken(token) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/adapter/present/HelloWorldController.kt b/src/main/kotlin/com/example/attendanceapimono/adapter/present/HelloWorldController.kt index 2d55768..469850b 100644 --- a/src/main/kotlin/com/example/attendanceapimono/adapter/present/HelloWorldController.kt +++ b/src/main/kotlin/com/example/attendanceapimono/adapter/present/HelloWorldController.kt @@ -1,26 +1,12 @@ package com.example.attendanceapimono.adapter.present import com.example.attendanceapimono.adapter.present.api.HelloWorldAPI -import com.example.attendanceapimono.application.exception.KotlinTestException -import io.swagger.v3.oas.annotations.Operation -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.responses.ApiResponse -import io.swagger.v3.oas.annotations.tags.Tag -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.reactive.asFlow import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController -import reactor.core.publisher.Mono -import reactor.kotlin.core.publisher.toMono @RestController class HelloWorldController : HelloWorldAPI { - - override suspend fun helloWorld() = ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body("\"hello ddd attendance api mono world\"") diff --git a/src/main/kotlin/com/example/attendanceapimono/adapter/present/UserController.kt b/src/main/kotlin/com/example/attendanceapimono/adapter/present/UserController.kt new file mode 100644 index 0000000..7e95d3a --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/adapter/present/UserController.kt @@ -0,0 +1,26 @@ +package com.example.attendanceapimono.adapter.present + +import com.example.attendanceapimono.adapter.present.api.UserAPI +import com.example.attendanceapimono.application.UserService +import com.example.attendanceapimono.application.dto.user.CreateUser +import com.example.attendanceapimono.application.exception.handleValidationCatch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.reactor.awaitSingle +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Mono + +@RestController +class UserController(private val userService: UserService) : UserAPI { + override suspend fun createUser(body: Mono) { + body.handleValidationCatch() + .map(userService::createUser) + .awaitSingle() + } +} + +/* + [{"error_description": "Invalid Value"}] +* */ \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/adapter/present/api/UserAPI.kt b/src/main/kotlin/com/example/attendanceapimono/adapter/present/api/UserAPI.kt index 80a72dd..a95012f 100644 --- a/src/main/kotlin/com/example/attendanceapimono/adapter/present/api/UserAPI.kt +++ b/src/main/kotlin/com/example/attendanceapimono/adapter/present/api/UserAPI.kt @@ -1,4 +1,39 @@ package com.example.attendanceapimono.adapter.present.api +import com.example.attendanceapimono.application.dto.user.CreateUser +import io.swagger.v3.oas.annotations.Operation +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.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus +import reactor.core.publisher.Mono +import javax.validation.Valid +import io.swagger.v3.oas.annotations.parameters.RequestBody as DocRequestBody + +@Tag(name = "유저 관련 API") interface UserAPI { + + @Operation( + summary = "유저 생성", + requestBody = DocRequestBody(content = [ + Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = Schema(implementation = CreateUser::class), + ) + ]) + ) + @ApiResponse( + responseCode = "201", + description = "일반 참가자 생성", + content = [Content(examples = [ExampleObject("")])], + ) + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/user") + suspend fun createUser(@Valid @RequestBody body: Mono) } \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/application/UserService.kt b/src/main/kotlin/com/example/attendanceapimono/application/UserService.kt index 60a991a..a2681d5 100644 --- a/src/main/kotlin/com/example/attendanceapimono/application/UserService.kt +++ b/src/main/kotlin/com/example/attendanceapimono/application/UserService.kt @@ -1,8 +1,46 @@ package com.example.attendanceapimono.application -import com.example.attendanceapimono.domain.user.UserRepository +import com.example.attendanceapimono.application.dto.user.CreateUser +import com.example.attendanceapimono.domain.user.* +import kotlinx.coroutines.* +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + @Service -class UserService(private val userRepository: UserRepository) { +class UserService( + private val userRepository: UserRepository, + private val socialProviderRepository: SocialProviderRepository, + @Qualifier("googleAdapter") + private val googleAdapter: SocialAdapter +) { + + @Transactional + fun createUser(dto: CreateUser): Unit = runBlocking { + val user = dto.entity() + listOf( + async { userRepository.save(user) }, + async { + val socialInfo = when (dto.type) { + SocialType.GOOGLE->googleAdapter.findByToken(dto.token) + SocialType.APPLE->TODO("not implemented, throw exception") + } + val socialID = SocialProviderID(socialInfo.id, socialInfo.type) + socialProviderRepository + .findByIdOrNull( + socialID + )?.run { + TODO("conflict, exists social provider, throw exception") + } + socialProviderRepository.save( + SocialProvider( + socialID, + user + ) + ) + } + ).awaitAll() + } } \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/application/dto/user/CreateUser.kt b/src/main/kotlin/com/example/attendanceapimono/application/dto/user/CreateUser.kt new file mode 100644 index 0000000..138cd2c --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/application/dto/user/CreateUser.kt @@ -0,0 +1,68 @@ +package com.example.attendanceapimono.application.dto.user + +import com.example.attendanceapimono.domain.user.* +import io.swagger.v3.oas.annotations.media.Schema +import lombok.extern.slf4j.Slf4j +import org.hibernate.validator.constraints.Length +import java.time.LocalDateTime +import java.util.* +import javax.persistence.Column +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.validation.constraints.Email +import javax.validation.constraints.NotEmpty + +@Schema( + title = "회원가입", + description = "", + example = CreateUser.Example, +) +data class CreateUser( + @Schema(description = "소셜 토큰") + @field:NotEmpty + val token: String, + + @Schema(description = "소설 타입") + val type: SocialType, + + @Schema(description = "이메일 주소") + @field:Email + val email: String, + + @Schema(description = "이름(성)") + @field:Length(min = 1, max = 20) + val lastName: String, + + @Schema(description = "이름") + @field:Length(min = 1, max = 20) + val firstName: String, + + @Schema(description = "직군 포지션") + val position: UserPosition, +) { + fun entity() = User( + id = UUID.randomUUID(), + state = UserState.NORMAL, + role = UserRole.MEMBER, + position = this.position, + generationID = 6, // TODO 별도의 제네레이션 테이블로 처리 하는게 좋을 듯함 + firstName = this.firstName, + lastName = this.lastName, + email = this.email, + createdAt = LocalDateTime.now(), + updatedAt = LocalDateTime.now(), + ) + + companion object { + const val Example = """ + { + "token": "social_token", + "type": "GOOGLE", + "email": "dddstudy1@gmail.com", + "lastName": "이", + "firstName": "재성", + "position": "BACKEND" + } + """ + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/application/dto/user/UserTemp.kt b/src/main/kotlin/com/example/attendanceapimono/application/dto/user/UserTemp.kt deleted file mode 100644 index 3839e55..0000000 --- a/src/main/kotlin/com/example/attendanceapimono/application/dto/user/UserTemp.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.attendanceapimono.application.dto.user - -class UserTemp { -} \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/application/exception/BadRequestBodyException.kt b/src/main/kotlin/com/example/attendanceapimono/application/exception/BadRequestBodyException.kt index d794995..8d8b8e1 100644 --- a/src/main/kotlin/com/example/attendanceapimono/application/exception/BadRequestBodyException.kt +++ b/src/main/kotlin/com/example/attendanceapimono/application/exception/BadRequestBodyException.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import org.springframework.web.bind.support.WebExchangeBindException import org.springframework.web.server.ServerWebInputException +import reactor.core.publisher.Mono import java.lang.RuntimeException class BadRequestBodyException : RuntimeException { @@ -15,11 +16,17 @@ class BadRequestBodyException : RuntimeException { } fun Flow.handleValidationCatch(): Flow { - return catch { - when (it) { - is WebExchangeBindException->throw BadRequestBodyException(it) - is ServerWebInputException->throw BadRequestBodyException(it) - else->throw it - } + return catch { handle(it) } +} + +fun Mono.handleValidationCatch(): Mono { + return doOnError(::handle) +} + +private fun handle(it: Throwable) { + when (it) { + is WebExchangeBindException->throw BadRequestBodyException(it) + is ServerWebInputException->throw BadRequestBodyException(it) + else->throw it } } \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialAdapter.kt b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialAdapter.kt new file mode 100644 index 0000000..6a3b88e --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialAdapter.kt @@ -0,0 +1,5 @@ +package com.example.attendanceapimono.domain.user + +interface SocialAdapter { + fun findByToken(token: String): SocialInfo +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialInfo.kt b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialInfo.kt new file mode 100644 index 0000000..e5ae60a --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialInfo.kt @@ -0,0 +1,8 @@ +package com.example.attendanceapimono.domain.user + +interface SocialInfo { + val id: String + val type: SocialType + val email: String + val thumb: String? +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialProvider.kt b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialProvider.kt index be4446f..ed846a0 100644 --- a/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialProvider.kt +++ b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialProvider.kt @@ -4,22 +4,14 @@ import java.io.Serializable import javax.persistence.* enum class SocialType { - GOOGLE + GOOGLE, APPLE } @Entity @Table(name = "social_providers") -@IdClass(SocialProviderID::class) class SocialProvider( - - @Id - @Column(length = 30, nullable = false) - val id: String, - - @Id - @Enumerated(EnumType.STRING) - @Column(length = 10, nullable = false) - val type: SocialType, + @EmbeddedId + val id: SocialProviderID, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) // user.id @@ -27,7 +19,12 @@ class SocialProvider( ) -data class SocialProviderID( +@Embeddable +class SocialProviderID( + @Column(name = "id", length = 30, nullable = false) val id: String, + + @Enumerated(EnumType.STRING) + @Column(name = "type", length = 10, nullable = false) val type: SocialType, -) : Serializable +) : Serializable \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialProviderRepository.kt b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialProviderRepository.kt new file mode 100644 index 0000000..b6c3488 --- /dev/null +++ b/src/main/kotlin/com/example/attendanceapimono/domain/user/SocialProviderRepository.kt @@ -0,0 +1,7 @@ +package com.example.attendanceapimono.domain.user + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface SocialProviderRepository : JpaRepository \ No newline at end of file diff --git a/src/main/kotlin/com/example/attendanceapimono/domain/user/UserRepository.kt b/src/main/kotlin/com/example/attendanceapimono/domain/user/UserRepository.kt index 417ff96..b4a6b0f 100644 --- a/src/main/kotlin/com/example/attendanceapimono/domain/user/UserRepository.kt +++ b/src/main/kotlin/com/example/attendanceapimono/domain/user/UserRepository.kt @@ -5,5 +5,4 @@ import org.springframework.stereotype.Repository import java.util.* @Repository -interface UserRepository : JpaRepository { -} \ No newline at end of file +interface UserRepository : JpaRepository \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 83d824e..fba5072 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -4,4 +4,13 @@ spring: springdoc: swagger-ui: - path: /doc \ No newline at end of file + path: /doc + +feign: + compression: + request: + enabled: true + response: + enabled: true + okhttp: + enabled: true \ No newline at end of file