Skip to content

Commit

Permalink
[feat #19] 회원가입 API (#20)
Browse files Browse the repository at this point in the history
* feat : Swagger 설

* feat : User nickname 필드 추가

* feat : User 등록 메소드 구현

* feat : 회원가입 로직 구

* feat : application 모듈 회원가입 dto

* feat : 회원가입 API

* feat : in-web 모듈 회원가입 dto

* feat : 로그인 API 명세

* refactor : 오타 수정

* fix : 스웨거 오류 해결

* fix : develop 브랜치 conflict 해결

* feat : Controller 인자 PrincipalUser로 변경

* feat : 요청 dto validation 추가
  • Loading branch information
dlswns2480 authored Jul 19, 2024
1 parent 06b690f commit 2295172
Show file tree
Hide file tree
Showing 21 changed files with 316 additions and 4 deletions.
2 changes: 2 additions & 0 deletions adapters/in-web/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.data:spring-data-commons")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0")

// 테스팅
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.1")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.pokit.auth

import com.pokit.auth.config.ErrorOperation
import com.pokit.auth.port.`in`.AuthUseCase
import com.pokit.token.dto.request.SignInRequest
import com.pokit.user.exception.UserErrorCode
import io.swagger.v3.oas.annotations.Operation
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
Expand All @@ -14,6 +17,8 @@ class AuthController(
private val authUseCase: AuthUseCase,
) {
@PostMapping("/signin")
@Operation(summary = "로그인 API")
@ErrorOperation(UserErrorCode::class)
fun signIn(
@RequestBody request: SignInRequest,
) = ResponseEntity.ok(authUseCase.signIn(request))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.pokit.auth.config

import com.pokit.common.exception.ErrorCode
import kotlin.reflect.KClass

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ErrorOperation(val value: KClass<out ErrorCode>)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ class SecurityConfig(
private val entryPoint: AuthenticationEntryPoint,
) {
companion object {
private val WHITE_LIST = arrayOf("/api/v1/auth/**")
private val WHITE_LIST = arrayOf(
"/api/v1/auth/**",
"/swagger-ui/index.html#/",
"/swagger",
"/swagger-ui.html",
"/swagger-ui/**",
"/api-docs",
"/api-docs/**",
"/v3/api-docs/**"
)
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.pokit.auth.config

import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.media.Schema
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class SwaggerConfig {
@Bean
fun openAPI(): OpenAPI {
return OpenAPI()
.components(
Components()
.addSchemas(
"ErrorResponse", Schema<Any>()
.addProperty("message", Schema<Any>().type("string"))
.addProperty("code", Schema<Any>().type("string"))
)
)
.info(configurationInfo())
}

private fun configurationInfo(): Info {
return Info()
.title("POKIT API")
.description("포킷 api 명세서")
.version("1.0")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ class CustomAuthenticationFilter(
private val userPort: UserPort,
) : OncePerRequestFilter() {
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
val excludePath = arrayOf("/api/v1/auth/signin")
val excludePath = arrayOf(
"/api/v1/auth/signin",
"/swagger-ui/index.html#/",
"/swagger", "/swagger-ui.html",
"/swagger-ui/**",
"/api-docs",
"/api-docs/**",
"/v3/api-docs/**"
)
val path = request.requestURI
val shouldNotFilter =
excludePath
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.pokit.common.exception

import com.pokit.auth.config.ErrorOperation
import io.swagger.v3.oas.models.Operation
import io.swagger.v3.oas.models.examples.Example
import io.swagger.v3.oas.models.media.Content
import io.swagger.v3.oas.models.media.MediaType
import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.oas.models.responses.ApiResponse
import io.swagger.v3.oas.models.responses.ApiResponses
import org.springdoc.core.customizers.OperationCustomizer
import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod

@Component
class ApiErrorOperationCustomizer : OperationCustomizer {
override fun customize(operation: Operation, handlerMethod: HandlerMethod): Operation {
val annotation = handlerMethod.method.getAnnotation(ErrorOperation::class.java)
if (annotation != null) {
val errorCodeClass = annotation.value.java
val errorCodes = errorCodeClass.enumConstants
val apiResponses = operation.responses ?: ApiResponses()

for (errorCode in errorCodes) {
val exampleContent = ErrorResponse(errorCode.message, errorCode.code)

val example = Example().apply {
value = exampleContent
}

val mediaType = MediaType().apply {
addExamples("example", example)
schema = Schema<ErrorResponse>()
}

val content = Content().apply {
addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType)
}

val response = ApiResponse()
.description("${errorCode.code}: ${errorCode.message}")
.content(content)
apiResponses.addApiResponse(errorCode.code, response)
}

operation.responses(apiResponses)
}
return operation
}
}
37 changes: 37 additions & 0 deletions adapters/in-web/src/main/kotlin/com/pokit/user/UserController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.pokit.user

import com.pokit.auth.config.ErrorOperation
import com.pokit.auth.model.PrincipalUser
import com.pokit.auth.model.toDomain
import com.pokit.user.dto.ApiSignUpRequest
import com.pokit.user.dto.response.SignUpResponse
import com.pokit.user.exception.UserErrorCode
import com.pokit.user.model.User
import com.pokit.user.port.`in`.UserUseCase
import io.swagger.v3.oas.annotations.Operation
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1/user")
class UserController(
private val userUseCase: UserUseCase
) {
@PostMapping("/signup")
@Operation(summary = "회원 등록 API")
@ErrorOperation(UserErrorCode::class)
fun signUp(
@AuthenticationPrincipal principalUser: PrincipalUser,
@Valid @RequestBody request: ApiSignUpRequest
): ResponseEntity<SignUpResponse> {
val user = principalUser.toDomain()
val signUpRequest = request.toSignUpRequest()
val response = userUseCase.signUp(user, signUpRequest)
return ResponseEntity.ok(response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.pokit.user.dto

import com.pokit.user.dto.request.SignUpRequest
import com.pokit.user.model.InterestType
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

data class ApiSignUpRequest(
@NotBlank(message = "닉네임은 필수값입니다.")
@Size(max = 10, message = "닉네임은 10자 이하만 가능합니다.")
val nickName: String,
@Size(min = 1, max = 3, message = "최소 하나 이상, 세개 이하만 가능합니다.")
val interests: List<String>
) {
fun toSignUpRequest(): SignUpRequest {
val interestTypes = interests.map {
InterestType.of(it)
}

return SignUpRequest(
nickName = nickName,
interests = interestTypes
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.pokit.out.persistence.user.impl

import com.pokit.out.persistence.user.persist.UserEntity
import com.pokit.out.persistence.user.persist.UserRepository
import com.pokit.out.persistence.user.persist.registerInfo
import com.pokit.out.persistence.user.persist.toDomain
import com.pokit.user.model.User
import com.pokit.user.port.out.UserPort
Expand All @@ -23,4 +24,10 @@ class UserAdapter(

override fun loadById(id: Long) = userRepository.findByIdOrNull(id)
?.run { toDomain() }

override fun register(user: User): User? {
val userEntity = userRepository.findByIdOrNull(user.id)
userEntity?.registerInfo(user)
return userEntity?.toDomain()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class UserEntity(

@Column(name = "role")
val role: Role,

@Column(name = "nickname")
var nickName: String = email,
) {
companion object {
fun of(user: User) =
Expand All @@ -28,3 +31,7 @@ class UserEntity(
}

fun UserEntity.toDomain() = User(id = this.id, email = this.email, role = this.role)

fun UserEntity.registerInfo(user: User) {
this.nickName = user.nickName
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.pokit.user

import com.pokit.user.dto.UserInfo
import com.pokit.user.dto.request.SignUpRequest
import com.pokit.user.model.InterestType
import com.pokit.user.model.Role
import com.pokit.user.model.User

Expand All @@ -9,5 +11,9 @@ class UserFixture {
fun getUser() = User(1L, "[email protected]", Role.USER)

fun getUserInfo() = UserInfo("[email protected]")

fun getInvalidUser() = User(2L, "[email protected]", Role.USER)

fun getSignUpRequest() = SignUpRequest("인주니", listOf(InterestType.SPORTS))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.pokit.user.port.`in`

import com.pokit.user.dto.request.SignUpRequest
import com.pokit.user.dto.response.SignUpResponse
import com.pokit.user.model.User

interface UserUseCase {
fun signUp(user: User, request: SignUpRequest): SignUpResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ interface UserPort {
fun loadByEmail(email: String): User?

fun loadById(id: Long): User?

fun register(user: User): User?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.pokit.user.port.service

import com.pokit.common.exception.NotFoundCustomException
import com.pokit.user.dto.request.SignUpRequest
import com.pokit.user.dto.response.SignUpResponse
import com.pokit.user.exception.UserErrorCode
import com.pokit.user.model.User
import com.pokit.user.port.`in`.UserUseCase
import com.pokit.user.port.out.UserPort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class UserService(
private val userPort: UserPort
) : UserUseCase {
@Transactional
override fun signUp(user: User, request: SignUpRequest): SignUpResponse {
user.modifyUser(
request.nickName,
)

val savedUser = userPort.register(user)
?: throw NotFoundCustomException(UserErrorCode.NOT_FOUND_USER)

return SignUpResponse(savedUser.id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.pokit.user.port.service

import com.pokit.common.exception.NotFoundCustomException
import com.pokit.user.UserFixture
import com.pokit.user.model.User
import com.pokit.user.port.out.UserPort
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk

class UserServiceTest : BehaviorSpec({
val userPort = mockk<UserPort>()
val userService = UserService(userPort)
Given("회원을 등록할 때") {
val user = UserFixture.getUser()
val invalidUser = UserFixture.getInvalidUser()
val request = UserFixture.getSignUpRequest()
val modifieUser = User(user.id, user.email, user.role, request.nickName)

every { userPort.register(user) } returns modifieUser
every { userPort.register(invalidUser) } returns null

When("수정하려는 정보를 받으면") {
val response = userService.signUp(user, request)
Then("회원 정보가 수정되고 수정된 회원의 id가 반환된다.") {
response.userId shouldBe modifieUser.id
user.nickName shouldBe modifieUser.nickName
}
}

When("수정하려는 회원을 찾을 수 없으면") {
Then("예외가 발생한다.") {
shouldThrow<NotFoundCustomException> {
userService.signUp(invalidUser, request)
}
}
}


}

})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.pokit.user.dto.request

import com.pokit.user.model.InterestType

data class SignUpRequest(
val nickName: String,
val interests: List<InterestType>
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.pokit.user.dto.response

class SignUpResponse(
val userId: Long
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ enum class UserErrorCode(
override val code: String,
) : ErrorCode {
INVALID_EMAIL("올바르지 않은 이메일 형식의 유저입니다.", "U_001"),
NOT_FOUND_USER("회원을 찾을 수 없습니다.", "U_002"),
INVALID_INTEREST_TYPE("관심사가 잘못되었습니다.", "U_002"),
NOT_FOUND_USER("존재하지 않는 회원입니다.", "U_003")
}
Loading

0 comments on commit 2295172

Please sign in to comment.