Skip to content

Commit

Permalink
[YS-67] feat: 실험자 회원가입 API 구현 (#17)
Browse files Browse the repository at this point in the history
* feat: implements participant signup

- 참여자 회원가입 API 구현
- 구글 OAuth 에외처리 로직 일부 수정

* test: add test codes for SignupMapper and ParticipantSignupUseCase

- 테스트 커버리지 충족을 위해 테스트 코드 추가
- `SignupMapperTest` 코드 추가
- `ParticipantSignupUseCase` 코드 추가

* fix: resolve duplicate requested authentication

- 실험자 회원가입 시, Authentication 중복 생성 코드 제거

* refact: move DB Transaction to seperate responsibility to satisfy clean
architecture principal

- DB 트랜잭션 책임 관련: UseCase  → Service 계층으로 위임

* refact: Applay automatic Component Scan for UseCase classes

- UseCase 클래스가 비즈니스 로직의 핵심 계층: 자동으로 컴포넌트 스캔
  대상에 포함되도록 어노테이션 제거
- 관리와 확장성을 고려하여 클린 아키텍처를 적용한 구현 방식

* style: rename API endpoint signup to meet the convention rule

- 회원가입 API endpoint 관련하여 기존 `/join` → `/signup` 으로 개선
- 코드 컨벤션을 위한 API endpoint 개선

* refact: update annotations to validate DateTime Format

- 생년월일 데이터 포맷 관련: 기존: `@NotBlank` → `@NotNull` `@Past`
  `@DateTimeFormat`

* refact: rename mapper function to match its usecase

- dto → entity 변환하는 용례에 따라, 기존: `toAddressInfoDto` →
  `toAddressInfo` 로 리네이밍

* fix: reflect updated codes to test code

- dto 함수 리네이밍 반영한 `SingupMapperTest` 버그 해결
  • Loading branch information
chock-cho authored Jan 3, 2025
1 parent bd5458e commit 38ca9b5
Show file tree
Hide file tree
Showing 24 changed files with 388 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.dobby.backend.application.mapper
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType
import com.dobby.backend.infrastructure.database.entity.enum.RoleType
import com.dobby.backend.presentation.api.dto.response.MemberResponse
import com.dobby.backend.presentation.api.dto.response.OauthLoginResponse
import com.dobby.backend.presentation.api.dto.response.auth.OauthLoginResponse

object OauthUserMapper {
fun toDto(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.dobby.backend.application.mapper

import com.dobby.backend.infrastructure.database.entity.MemberEntity
import com.dobby.backend.infrastructure.database.entity.ParticipantEntity
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus
import com.dobby.backend.infrastructure.database.entity.enum.RoleType
import com.dobby.backend.presentation.api.dto.request.ParticipantSignupRequest
import com.dobby.backend.presentation.api.dto.response.MemberResponse
import com.dobby.backend.infrastructure.database.entity.AddressInfo as AddressInfo
import com.dobby.backend.presentation.api.dto.request.AddressInfo as DtoAddressInfo

object SignupMapper {
fun toAddressInfo(dto: DtoAddressInfo): AddressInfo {
return AddressInfo(
dto.region,
dto.area
)
}
fun toMember(req: ParticipantSignupRequest): MemberEntity {
return MemberEntity(
id = 0, // Auto-generated
oauthEmail = req.oauthEmail,
provider = req.provider,
status = MemberStatus.ACTIVE,
role = RoleType.PARTICIPANT,
contactEmail = req.contactEmail,
name = req.name,
birthDate = req.birthDate
)
}
fun toParticipant(
member: MemberEntity,
req: ParticipantSignupRequest
): ParticipantEntity {
return ParticipantEntity(
member = member,
basicAddressInfo = toAddressInfo(req.basicAddressInfo),
additionalAddressInfo = req.additionalAddressInfo?.let { toAddressInfo(it) },
preferType = req.preferType,
gender = req.gender
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.dobby.backend.application.service

import com.dobby.backend.application.usecase.FetchGoogleUserInfoUseCase
import com.dobby.backend.presentation.api.dto.request.OauthLoginRequest
import com.dobby.backend.presentation.api.dto.response.OauthLoginResponse
import com.dobby.backend.presentation.api.dto.response.auth.OauthLoginResponse
import org.springframework.stereotype.Service

@Service
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.dobby.backend.application.service

import com.dobby.backend.application.usecase.ParticipantSignupUseCase
import com.dobby.backend.presentation.api.dto.request.ParticipantSignupRequest
import com.dobby.backend.presentation.api.dto.response.signup.SignupResponse
import jakarta.transaction.Transactional
import org.springframework.stereotype.Service

@Service
class SignupService(
private val participantSignupUseCase: ParticipantSignupUseCase
) {
@Transactional
fun participantSignup(input: ParticipantSignupRequest): SignupResponse {
return participantSignupUseCase.execute(input)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.dobby.backend.application.usecase

import com.dobby.backend.application.mapper.OauthUserMapper
import com.dobby.backend.domain.exception.OAuth2EmailNotFoundException
import com.dobby.backend.domain.exception.OAuth2ProviderMissingException
import com.dobby.backend.domain.exception.SignInMemberException
import com.dobby.backend.infrastructure.config.properties.GoogleAuthProperties
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus
Expand All @@ -13,13 +12,10 @@ import com.dobby.backend.infrastructure.feign.GoogleUserInfoFeginClient
import com.dobby.backend.infrastructure.token.JwtTokenProvider
import com.dobby.backend.presentation.api.dto.request.GoogleTokenRequest
import com.dobby.backend.presentation.api.dto.request.OauthLoginRequest
import com.dobby.backend.presentation.api.dto.response.GoogleTokenResponse
import com.dobby.backend.presentation.api.dto.response.OauthLoginResponse
import com.dobby.backend.presentation.api.dto.response.auth.google.GoogleTokenResponse
import com.dobby.backend.presentation.api.dto.response.auth.OauthLoginResponse
import com.dobby.backend.util.AuthenticationUtils
import feign.FeignException
import org.springframework.stereotype.Component

@Component
class FetchGoogleUserInfoUseCase(
private val googleAuthFeignClient: GoogleAuthFeignClient,
private val googleUserInfoFeginClient: GoogleUserInfoFeginClient,
Expand Down Expand Up @@ -58,8 +54,8 @@ class FetchGoogleUserInfoUseCase(
role = regMember.role ?: throw SignInMemberException(),
provider = ProviderType.GOOGLE
)
} catch (e: FeignException) {
throw OAuth2ProviderMissingException()
} catch (e: SignInMemberException) {
throw SignInMemberException()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.dobby.backend.application.usecase

import com.dobby.backend.application.mapper.SignupMapper
import com.dobby.backend.infrastructure.database.repository.ParticipantRepository
import com.dobby.backend.infrastructure.token.JwtTokenProvider
import com.dobby.backend.presentation.api.dto.request.ParticipantSignupRequest
import com.dobby.backend.presentation.api.dto.response.MemberResponse
import com.dobby.backend.presentation.api.dto.response.signup.SignupResponse
import com.dobby.backend.util.AuthenticationUtils

class ParticipantSignupUseCase (
private val participantRepository: ParticipantRepository,
private val jwtTokenProvider: JwtTokenProvider
):UseCase<ParticipantSignupRequest, SignupResponse>
{
override fun execute(input: ParticipantSignupRequest): SignupResponse {
val memberEntity = SignupMapper.toMember(input)
val participantEntity = SignupMapper.toParticipant(memberEntity, input)

val newParticipant = participantRepository.save(participantEntity)
val authentication = AuthenticationUtils.createAuthentication(memberEntity)
val accessToken = jwtTokenProvider.generateAccessToken(authentication)
val refreshToken = jwtTokenProvider.generateRefreshToken(authentication)

return SignupResponse(
memberInfo = MemberResponse.fromDomain(newParticipant.member.toDomain()),
accessToken = accessToken,
refreshToken = refreshToken
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ enum class ErrorCode(
* Signup error codes
*/
SIGNUP_ALREADY_MEMBER("SIGN_UP_001", "You've already joined", HttpStatus.CONFLICT),
SIGNUP_UNSUPPORTED_ROLE("SIGN_UP_002", "Requested RoleType does not supported", HttpStatus.BAD_REQUEST),

/**
* Signin error codes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ open class MemberException(
class MemberNotFoundException : MemberException(ErrorCode.MEMBER_NOT_FOUND)
class AlreadyMemberException: MemberException(ErrorCode.SIGNUP_ALREADY_MEMBER)
class SignInMemberException: MemberException(ErrorCode.SIGNIN_MEMBER_NOT_FOUND)
class RoleUnsupportedException: MemberException(ErrorCode.SIGNUP_UNSUPPORTED_ROLE)
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,50 @@ import java.time.LocalDate
@Entity(name = "participant")
@DiscriminatorValue("PARTICIPANT")
class ParticipantEntity (
@OneToOne
@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true)
@JoinColumn(name = "member_id", nullable = false)
val member: MemberEntity,

@Column(name = "gender", nullable = false)
@Enumerated(EnumType.STRING)
val gender: GenderType,

@Column(name = "basic_region", nullable = false)
@Enumerated(EnumType.STRING)
var basicRegion: Region,

@Column(name = "basic_area", nullable = false)
@Enumerated(EnumType.STRING)
var basicArea : Area,
@Embedded
@AttributeOverrides(
AttributeOverride(name = "region", column = Column(name = "basic_region", nullable = false)),
AttributeOverride(name = "area", column = Column(name = "basic_area", nullable = false))
)
val basicAddressInfo: AddressInfo,

@Column(name = "optional_region", nullable = true)
@Enumerated(EnumType.STRING)
var optionalRegion: Region?,

@Column(name = "optional_area", nullable = true)
@Enumerated(EnumType.STRING)
var optionalArea: Area?,
@Embedded
@AttributeOverrides(
AttributeOverride(name = "region", column = Column(name = "optional_region", nullable = true)),
AttributeOverride(name = "area", column = Column(name = "optional_area", nullable = true))
)
val additionalAddressInfo: AddressInfo?,

@Column(name = "prefer_type", nullable = true)
@Enumerated(EnumType.STRING)
var preferType: MatchType?,

id: Long,
oauthEmail: String,
provider: ProviderType,
contactEmail: String,
name: String,
birthDate: LocalDate
) : MemberEntity(
id= id,
oauthEmail = oauthEmail,
provider = provider,
contactEmail= contactEmail,
id= member.id,
oauthEmail = member.oauthEmail,
provider = member.provider,
contactEmail= member.contactEmail,
role = RoleType.PARTICIPANT,
name = name,
birthDate = birthDate
name = member.name,
birthDate = member.birthDate
)

@Embeddable
data class AddressInfo(
@Enumerated(EnumType.STRING)
@Column(name = "region", nullable = false)
val region: Region,

@Enumerated(EnumType.STRING)
@Column(name = "area", nullable = false)
val area: Area
)

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import java.time.LocalDate
@Entity(name = "researcher")
@DiscriminatorValue("RESEARCHER")
class ResearcherEntity (
@OneToOne
@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true)
@JoinColumn(name = "member_id", nullable = false)
val member: MemberEntity,

Expand All @@ -26,19 +26,12 @@ class ResearcherEntity (

@Column(name = "lab_info", length = 100, nullable = true)
val labInfo : String,

id: Long,
oauthEmail: String,
provider: ProviderType,
contactEmail: String,
name: String,
birthDate: LocalDate
) : MemberEntity(
id= id,
oauthEmail = oauthEmail,
provider = provider,
contactEmail= contactEmail,
id= member.id,
oauthEmail = member.oauthEmail,
provider = member.provider,
contactEmail= member.contactEmail,
role = RoleType.RESEARCHER,
name = name,
birthDate = birthDate
name = member.name,
birthDate = member.birthDate
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package com.dobby.backend.infrastructure.feign

import com.dobby.backend.presentation.api.dto.request.GoogleTokenRequest
import com.dobby.backend.presentation.api.dto.response.GoogleTokenResponse
import feign.Headers
import com.dobby.backend.presentation.api.dto.response.auth.google.GoogleTokenResponse
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam

@FeignClient(
name = "google-auth-feign-client",
Expand All @@ -16,4 +14,4 @@ interface GoogleAuthFeignClient {
@PostMapping("/token")
fun getAccessToken(@RequestBody googleTokenRequest: GoogleTokenRequest
): GoogleTokenResponse
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.dobby.backend.infrastructure.feign

import com.dobby.backend.presentation.api.dto.response.GoogleInfoResponse
import com.dobby.backend.presentation.api.dto.response.auth.google.GoogleInfoResponse
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader

@FeignClient(
name= "google-userinfo-feign-client",
Expand All @@ -13,4 +12,4 @@ import org.springframework.web.bind.annotation.RequestHeader
interface GoogleUserInfoFeginClient {
@GetMapping("/oauth2/v3/userinfo?access_token={OAUTH_TOKEN}")
fun getUserInfo(@PathVariable("OAUTH_TOKEN")token: String): GoogleInfoResponse
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package com.dobby.backend.presentation.api.controller
import com.dobby.backend.application.service.OauthService
import com.dobby.backend.application.usecase.GenerateTestToken
import com.dobby.backend.presentation.api.dto.request.OauthLoginRequest
import com.dobby.backend.presentation.api.dto.response.OauthLoginResponse
import com.dobby.backend.presentation.api.dto.response.auth.OauthLoginResponse
import com.dobby.backend.application.usecase.GenerateTokenWithRefreshToken
import com.dobby.backend.application.usecase.GetMemberById
import com.dobby.backend.infrastructure.database.entity.enum.RoleType
import com.dobby.backend.presentation.api.dto.request.MemberRefreshTokenRequest
import com.dobby.backend.presentation.api.dto.response.MemberResponse
import com.dobby.backend.presentation.api.dto.response.TestMemberSignInResponse
Expand Down Expand Up @@ -41,6 +42,7 @@ class AuthController(
@PostMapping("/login/google")
@Operation(summary = "Google OAuth 로그인 API", description = "Google OAuth 로그인 후 인증 정보를 반환합니다")
fun signInWithGoogle(
@RequestParam role : RoleType, // RESEARCHER, PARTICIPANT
@RequestBody @Valid oauthLoginRequest: OauthLoginRequest
): OauthLoginResponse {
return oauthService.getGoogleUserInfo(oauthLoginRequest)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.dobby.backend.presentation.api.controller.SignupController

import com.dobby.backend.application.service.SignupService
import com.dobby.backend.infrastructure.database.entity.enum.RoleType
import com.dobby.backend.infrastructure.database.entity.enum.RoleType.*
import com.dobby.backend.presentation.api.dto.request.ParticipantSignupRequest
import com.dobby.backend.presentation.api.dto.response.signup.SignupResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.*

@Tag(name = "회원가입 API")
@RestController
@RequestMapping("/v1/participants")
class ParticipantSignupController(
private val signupService: SignupService
) {
@PostMapping("/signup")
@Operation(
summary = "참여자 회원가입 API- OAuth 로그인 필수",
description = "참여자 OAuth 로그인 실패 시, 리다이렉팅하여 참여자 회원가입하는 API입니다."
)
fun signup(
@RequestBody @Valid req: ParticipantSignupRequest
): SignupResponse {
return signupService.participantSignup(req)
}
}
Loading

0 comments on commit 38ca9b5

Please sign in to comment.