Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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 ->
//
// }
// }
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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\"")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CreateUser>) {
body.handleValidationCatch()
.map(userService::createUser)
.awaitSingle()
}
}

/*
[{"error_description": "Invalid Value"}]
* */
Original file line number Diff line number Diff line change
@@ -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<CreateUser>)
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
"""
}
}

This file was deleted.

Loading