From 38ca9b596101d0fb2a8d317e732baa23526984a2 Mon Sep 17 00:00:00 2001 From: Sujung Shin <113707388+chock-cho@users.noreply.github.com> Date: Sat, 4 Jan 2025 01:46:57 +0900 Subject: [PATCH] =?UTF-8?q?[YS-67]=20feat:=20=EC=8B=A4=ED=97=98=EC=9E=90?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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` 버그 해결 --- .../application/mapper/OauthUserMapper.kt | 2 +- .../application/mapper/SignupMapper.kt | 43 +++++++++++ .../application/service/OauthService.kt | 2 +- .../application/service/SignupService.kt | 17 +++++ .../usecase/FetchGoogleUserInfoUseCase.kt | 12 +-- .../usecase/ParticipantSignupUseCase.kt | 31 ++++++++ .../backend/domain/exception/ErrorCode.kt | 1 + .../domain/exception/MemberException.kt | 1 + .../database/entity/ParticipantEntity.kt | 58 +++++++------- .../database/entity/ResearcherEntity.kt | 21 ++---- .../feign/GoogleAuthFeignClient.kt | 6 +- .../feign/GoogleUserInfoFeginClient.kt | 5 +- .../api/controller/AuthController.kt | 4 +- .../ParticipantSignupController.kt | 29 +++++++ .../dto/request/ParticipantSignupRequest.kt | 53 +++++++++++++ .../{ => auth}/MemberSignInResponse.kt | 3 +- .../response/{ => auth}/OauthLoginResponse.kt | 3 +- .../{ => auth/google}/GoogleInfoResponse.kt | 2 +- .../{ => auth/google}/GoogleTokenResponse.kt | 2 +- .../signup/ParticipantSignupResponse.kt | 15 ++++ .../application/mapper/SignupMapperTest.kt | 75 +++++++++++++++++++ .../application/service/OauthServiceTest.kt | 2 +- .../usecase/FetchGoogleUserInfoTest.kt | 2 +- .../usecase/ParticipantSignupUseCaseTest.kt | 64 ++++++++++++++++ 24 files changed, 388 insertions(+), 65 deletions(-) create mode 100644 src/main/kotlin/com/dobby/backend/application/mapper/SignupMapper.kt create mode 100644 src/main/kotlin/com/dobby/backend/application/service/SignupService.kt create mode 100644 src/main/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCase.kt create mode 100644 src/main/kotlin/com/dobby/backend/presentation/api/controller/SignupController/ParticipantSignupController.kt create mode 100644 src/main/kotlin/com/dobby/backend/presentation/api/dto/request/ParticipantSignupRequest.kt rename src/main/kotlin/com/dobby/backend/presentation/api/dto/response/{ => auth}/MemberSignInResponse.kt (73%) rename src/main/kotlin/com/dobby/backend/presentation/api/dto/response/{ => auth}/OauthLoginResponse.kt (80%) rename src/main/kotlin/com/dobby/backend/presentation/api/dto/response/{ => auth/google}/GoogleInfoResponse.kt (73%) rename src/main/kotlin/com/dobby/backend/presentation/api/dto/response/{ => auth/google}/GoogleTokenResponse.kt (68%) create mode 100644 src/main/kotlin/com/dobby/backend/presentation/api/dto/response/signup/ParticipantSignupResponse.kt create mode 100644 src/test/kotlin/com/dobby/backend/application/mapper/SignupMapperTest.kt create mode 100644 src/test/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCaseTest.kt diff --git a/src/main/kotlin/com/dobby/backend/application/mapper/OauthUserMapper.kt b/src/main/kotlin/com/dobby/backend/application/mapper/OauthUserMapper.kt index e02cf926..15abb13e 100644 --- a/src/main/kotlin/com/dobby/backend/application/mapper/OauthUserMapper.kt +++ b/src/main/kotlin/com/dobby/backend/application/mapper/OauthUserMapper.kt @@ -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( diff --git a/src/main/kotlin/com/dobby/backend/application/mapper/SignupMapper.kt b/src/main/kotlin/com/dobby/backend/application/mapper/SignupMapper.kt new file mode 100644 index 00000000..aa1e3080 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/application/mapper/SignupMapper.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dobby/backend/application/service/OauthService.kt b/src/main/kotlin/com/dobby/backend/application/service/OauthService.kt index 65e29e38..7d136f1e 100644 --- a/src/main/kotlin/com/dobby/backend/application/service/OauthService.kt +++ b/src/main/kotlin/com/dobby/backend/application/service/OauthService.kt @@ -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 diff --git a/src/main/kotlin/com/dobby/backend/application/service/SignupService.kt b/src/main/kotlin/com/dobby/backend/application/service/SignupService.kt new file mode 100644 index 00000000..3d08f2e0 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/application/service/SignupService.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoUseCase.kt b/src/main/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoUseCase.kt index 183510ae..79f453aa 100644 --- a/src/main/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoUseCase.kt +++ b/src/main/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoUseCase.kt @@ -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 @@ -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, @@ -58,8 +54,8 @@ class FetchGoogleUserInfoUseCase( role = regMember.role ?: throw SignInMemberException(), provider = ProviderType.GOOGLE ) - } catch (e: FeignException) { - throw OAuth2ProviderMissingException() + } catch (e: SignInMemberException) { + throw SignInMemberException() } } diff --git a/src/main/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCase.kt b/src/main/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCase.kt new file mode 100644 index 00000000..69185d76 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCase.kt @@ -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 +{ + 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 + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt b/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt index e7057157..39e9445a 100644 --- a/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt +++ b/src/main/kotlin/com/dobby/backend/domain/exception/ErrorCode.kt @@ -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 diff --git a/src/main/kotlin/com/dobby/backend/domain/exception/MemberException.kt b/src/main/kotlin/com/dobby/backend/domain/exception/MemberException.kt index e50c2251..5d08f637 100644 --- a/src/main/kotlin/com/dobby/backend/domain/exception/MemberException.kt +++ b/src/main/kotlin/com/dobby/backend/domain/exception/MemberException.kt @@ -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) \ No newline at end of file diff --git a/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ParticipantEntity.kt b/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ParticipantEntity.kt index 48725f21..c4737b9a 100644 --- a/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ParticipantEntity.kt +++ b/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ParticipantEntity.kt @@ -12,7 +12,7 @@ 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, @@ -20,38 +20,42 @@ class ParticipantEntity ( @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 +) + diff --git a/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ResearcherEntity.kt b/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ResearcherEntity.kt index 743a6033..a55466b5 100644 --- a/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ResearcherEntity.kt +++ b/src/main/kotlin/com/dobby/backend/infrastructure/database/entity/ResearcherEntity.kt @@ -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, @@ -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 ) diff --git a/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleAuthFeignClient.kt b/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleAuthFeignClient.kt index 940ef241..aa0ed62a 100644 --- a/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleAuthFeignClient.kt +++ b/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleAuthFeignClient.kt @@ -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", @@ -16,4 +14,4 @@ interface GoogleAuthFeignClient { @PostMapping("/token") fun getAccessToken(@RequestBody googleTokenRequest: GoogleTokenRequest ): GoogleTokenResponse -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleUserInfoFeginClient.kt b/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleUserInfoFeginClient.kt index a39b48b9..6c26b6ab 100644 --- a/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleUserInfoFeginClient.kt +++ b/src/main/kotlin/com/dobby/backend/infrastructure/feign/GoogleUserInfoFeginClient.kt @@ -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", @@ -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 -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/controller/AuthController.kt b/src/main/kotlin/com/dobby/backend/presentation/api/controller/AuthController.kt index 3ec3d1af..102d3589 100644 --- a/src/main/kotlin/com/dobby/backend/presentation/api/controller/AuthController.kt +++ b/src/main/kotlin/com/dobby/backend/presentation/api/controller/AuthController.kt @@ -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 @@ -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) diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/controller/SignupController/ParticipantSignupController.kt b/src/main/kotlin/com/dobby/backend/presentation/api/controller/SignupController/ParticipantSignupController.kt new file mode 100644 index 00000000..619b66ff --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/presentation/api/controller/SignupController/ParticipantSignupController.kt @@ -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) + } +} diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/dto/request/ParticipantSignupRequest.kt b/src/main/kotlin/com/dobby/backend/presentation/api/dto/request/ParticipantSignupRequest.kt new file mode 100644 index 00000000..eb453209 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/presentation/api/dto/request/ParticipantSignupRequest.kt @@ -0,0 +1,53 @@ +package com.dobby.backend.presentation.api.dto.request + +import com.dobby.backend.infrastructure.database.entity.enum.GenderType +import com.dobby.backend.infrastructure.database.entity.enum.MatchType +import com.dobby.backend.infrastructure.database.entity.enum.ProviderType +import com.dobby.backend.infrastructure.database.entity.enum.areaInfo.Area +import com.dobby.backend.infrastructure.database.entity.enum.areaInfo.Region +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Past +import org.springframework.format.annotation.DateTimeFormat +import java.time.LocalDate + +data class ParticipantSignupRequest( + @Email(message = "OAuth 이메일이 유효하지 않습니다.") + @NotBlank(message = "OAuth 이메일은 공백일 수 없습니다.") + val oauthEmail: String, + + @NotBlank(message = "OAuth provider은 공백일 수 없습니다.") + val provider: ProviderType, + + @Email(message= "연락 받을 이메일이 유효하지 않습니다.") + @NotBlank(message = "연락 받을 이메일은 공백일 수 없습니다.") + val contactEmail: String, + + @NotBlank(message = "이름은 공백일 수 없습니다.") + val name : String, + + @NotBlank(message = "성별은 공백일 수 없습니다.") + val gender: GenderType, + + @Past @NotNull(message = "생년월일은 공백일 수 없습니다.") + @DateTimeFormat(pattern = "yyyy-MM-dd") + val birthDate: LocalDate, + + @NotBlank(message = "거주 지역은 공백일 수 없습니다.") + var basicAddressInfo: AddressInfo, + + // 추가 활동 정보 + var additionalAddressInfo: AddressInfo?, + + // 선호 실험 진행 방식 + var preferType: MatchType, +) + +data class AddressInfo( + @NotBlank(message = "시/도 정보는 공백일 수 없습니다.") + val region: Region, + + @NotBlank(message = "시/군/구 정보는 공백일 수 없습니다.") + val area: Area +) \ No newline at end of file diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/MemberSignInResponse.kt b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/MemberSignInResponse.kt similarity index 73% rename from src/main/kotlin/com/dobby/backend/presentation/api/dto/response/MemberSignInResponse.kt rename to src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/MemberSignInResponse.kt index 88c93908..72284466 100644 --- a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/MemberSignInResponse.kt +++ b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/MemberSignInResponse.kt @@ -1,5 +1,6 @@ -package com.dobby.backend.presentation.api.dto.response +package com.dobby.backend.presentation.api.dto.response.auth +import com.dobby.backend.presentation.api.dto.response.MemberResponse import io.swagger.v3.oas.annotations.media.Schema @Schema(description = "로그인 결과 DTO") diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/OauthLoginResponse.kt b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/OauthLoginResponse.kt similarity index 80% rename from src/main/kotlin/com/dobby/backend/presentation/api/dto/response/OauthLoginResponse.kt rename to src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/OauthLoginResponse.kt index 1ae224ae..731037f6 100644 --- a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/OauthLoginResponse.kt +++ b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/OauthLoginResponse.kt @@ -1,5 +1,6 @@ -package com.dobby.backend.presentation.api.dto.response +package com.dobby.backend.presentation.api.dto.response.auth +import com.dobby.backend.presentation.api.dto.response.MemberResponse import io.swagger.v3.oas.annotations.media.Schema import lombok.Getter diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/GoogleInfoResponse.kt b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/google/GoogleInfoResponse.kt similarity index 73% rename from src/main/kotlin/com/dobby/backend/presentation/api/dto/response/GoogleInfoResponse.kt rename to src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/google/GoogleInfoResponse.kt index 99da0d88..46652cd6 100644 --- a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/GoogleInfoResponse.kt +++ b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/google/GoogleInfoResponse.kt @@ -1,4 +1,4 @@ -package com.dobby.backend.presentation.api.dto.response +package com.dobby.backend.presentation.api.dto.response.auth.google import com.fasterxml.jackson.annotation.JsonProperty diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/GoogleTokenResponse.kt b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/google/GoogleTokenResponse.kt similarity index 68% rename from src/main/kotlin/com/dobby/backend/presentation/api/dto/response/GoogleTokenResponse.kt rename to src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/google/GoogleTokenResponse.kt index 26ef7b67..59852ce0 100644 --- a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/GoogleTokenResponse.kt +++ b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/auth/google/GoogleTokenResponse.kt @@ -1,4 +1,4 @@ -package com.dobby.backend.presentation.api.dto.response +package com.dobby.backend.presentation.api.dto.response.auth.google import com.fasterxml.jackson.annotation.JsonProperty diff --git a/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/signup/ParticipantSignupResponse.kt b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/signup/ParticipantSignupResponse.kt new file mode 100644 index 00000000..c9c13105 --- /dev/null +++ b/src/main/kotlin/com/dobby/backend/presentation/api/dto/response/signup/ParticipantSignupResponse.kt @@ -0,0 +1,15 @@ +package com.dobby.backend.presentation.api.dto.response.signup + +import com.dobby.backend.presentation.api.dto.response.MemberResponse +import io.swagger.v3.oas.annotations.media.Schema + +data class SignupResponse( + @Schema(description = "실험자 회원가입 후 발급되는 Access Token") + val accessToken: String, + + @Schema(description = "실험자 회원가입 후 발급되는 Refresh Token") + val refreshToken: String, + + @Schema(description = "실험자 회원가입 정보") + val memberInfo: MemberResponse +) diff --git a/src/test/kotlin/com/dobby/backend/application/mapper/SignupMapperTest.kt b/src/test/kotlin/com/dobby/backend/application/mapper/SignupMapperTest.kt new file mode 100644 index 00000000..834b5afc --- /dev/null +++ b/src/test/kotlin/com/dobby/backend/application/mapper/SignupMapperTest.kt @@ -0,0 +1,75 @@ +package com.dobby.backend.application.mapper + +import com.dobby.backend.infrastructure.database.entity.enum.* +import com.dobby.backend.infrastructure.database.entity.enum.areaInfo.Area +import com.dobby.backend.infrastructure.database.entity.enum.areaInfo.Region +import com.dobby.backend.presentation.api.dto.request.ParticipantSignupRequest +import org.springframework.test.context.ActiveProfiles +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDate +import com.dobby.backend.presentation.api.dto.request.AddressInfo as DtoAddressInfo + +@ActiveProfiles("test") +class SignupMapperTest : BehaviorSpec({ + given("주소 dto를 AddressInfo 엔티티로 변환할 때") { + `when`("toAddressInfoDto 메서드가 호출되면") { + val dtoAddressInfo = DtoAddressInfo(region = Region.SEOUL, area = Area.SEOUL_ALL) + val result = SignupMapper.toAddressInfo(dtoAddressInfo) + + then("올바른 AddressInfo 객체가 반환되어야 한다") { + result.region shouldBe Region.SEOUL + result.area shouldBe Area.SEOUL_ALL + } + } + + `when`("toMember 메서드가 호출되면") { + val request = ParticipantSignupRequest( + oauthEmail = "test@example.com", + provider = ProviderType.GOOGLE, + contactEmail = "contact@example.com", + name = "Test User", + birthDate = LocalDate.of(2002, 11, 21), + basicAddressInfo = DtoAddressInfo(region = Region.SEOUL, area = Area.SEOUL_ALL), + additionalAddressInfo = null, + preferType = MatchType.HYBRID, + gender = GenderType.FEMALE + ) + val result = SignupMapper.toMember(request) + + then("올바른 MemberEntity 객체가 반환되어야 한다") { + result.oauthEmail shouldBe "test@example.com" + result.provider shouldBe ProviderType.GOOGLE + result.contactEmail shouldBe "contact@example.com" + result.name shouldBe "Test User" + result.birthDate shouldBe LocalDate.of(2002, 11, 21) + result.role shouldBe RoleType.PARTICIPANT + result.status shouldBe MemberStatus.ACTIVE + } + } + + `when`("toParticipant 메서드가 호출되면") { + val request = ParticipantSignupRequest( + oauthEmail = "test@example.com", + provider = ProviderType.GOOGLE, + contactEmail = "contact@example.com", + name = "Test User", + birthDate = LocalDate.of(2002, 11, 21), + basicAddressInfo = DtoAddressInfo(region = Region.SEOUL, area = Area.SEOUL_ALL), + additionalAddressInfo = DtoAddressInfo(region = Region.GYEONGGI, area = Area.GWANGMYEONGSI), + preferType = MatchType.HYBRID, + gender = GenderType.FEMALE + ) + val member = SignupMapper.toMember(request) + val result = SignupMapper.toParticipant(member, request) + + then("올바른 ParticipantEntity 객체가 반환되어야 한다") { + result.member.oauthEmail shouldBe "test@example.com" + result.basicAddressInfo.region shouldBe Region.SEOUL + result.basicAddressInfo.area shouldBe Area.SEOUL_ALL + result.additionalAddressInfo?.region shouldBe Region.GYEONGGI + result.additionalAddressInfo?.area shouldBe Area.GWANGMYEONGSI + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/com/dobby/backend/application/service/OauthServiceTest.kt b/src/test/kotlin/com/dobby/backend/application/service/OauthServiceTest.kt index 434c91e0..942db323 100644 --- a/src/test/kotlin/com/dobby/backend/application/service/OauthServiceTest.kt +++ b/src/test/kotlin/com/dobby/backend/application/service/OauthServiceTest.kt @@ -4,7 +4,7 @@ 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.request.OauthLoginRequest 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 import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe import io.mockk.every diff --git a/src/test/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoTest.kt b/src/test/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoTest.kt index 12807e58..7558dc3f 100644 --- a/src/test/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoTest.kt +++ b/src/test/kotlin/com/dobby/backend/application/usecase/FetchGoogleUserInfoTest.kt @@ -9,7 +9,7 @@ import com.dobby.backend.infrastructure.feign.GoogleAuthFeignClient import com.dobby.backend.infrastructure.feign.GoogleUserInfoFeginClient import com.dobby.backend.infrastructure.token.JwtTokenProvider 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.auth.google.GoogleTokenResponse import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe import io.mockk.every diff --git a/src/test/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCaseTest.kt b/src/test/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCaseTest.kt new file mode 100644 index 00000000..c25997e6 --- /dev/null +++ b/src/test/kotlin/com/dobby/backend/application/usecase/ParticipantSignupUseCaseTest.kt @@ -0,0 +1,64 @@ +package com.dobby.backend.application.usecase + +import com.dobby.backend.application.mapper.SignupMapper +import com.dobby.backend.infrastructure.database.entity.ParticipantEntity +import com.dobby.backend.infrastructure.database.entity.enum.GenderType +import com.dobby.backend.infrastructure.database.entity.enum.MatchType +import com.dobby.backend.infrastructure.database.entity.enum.areaInfo.Area +import com.dobby.backend.infrastructure.database.entity.enum.areaInfo.Region +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.request.AddressInfo as DtoAddressInfo +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.time.LocalDate + +class ParticipantSignupUseCaseTest : BehaviorSpec({ + + given("유효한 ParticipantSignupRequest가 주어졌을 때") { + val participantRepository = mockk() + val jwtTokenProvider = mockk() + val useCase = ParticipantSignupUseCase(participantRepository, jwtTokenProvider) + + val request = ParticipantSignupRequest( + oauthEmail = "test@example.com", + provider = com.dobby.backend.infrastructure.database.entity.enum.ProviderType.GOOGLE, + contactEmail = "contact@example.com", + name = "Test User", + birthDate = LocalDate.of(2002, 11, 21), + basicAddressInfo = DtoAddressInfo(region = Region.SEOUL, area = Area.SEOUL_ALL), + additionalAddressInfo = null, + preferType = MatchType.HYBRID, + gender = GenderType.FEMALE + ) + + val member = SignupMapper.toMember(request) + val participant = SignupMapper.toParticipant(member, request) + + every { participantRepository.save(any()) } returns participant + every { jwtTokenProvider.generateAccessToken(any()) } returns "mock-access-token" + every { jwtTokenProvider.generateRefreshToken(any()) } returns "mock-refresh-token" + + `when`("ParticipantSignupUseCase가 실행되면") { + val response = useCase.execute(request) + + then("ParticipantRepository에 엔티티가 저장되고, SignupResponse가 반환되어야 한다") { + response.accessToken shouldBe "mock-access-token" + response.refreshToken shouldBe "mock-refresh-token" + response.memberInfo.oauthEmail shouldBe "test@example.com" + response.memberInfo.name shouldBe "Test User" + + // 엔티티 저장 + verify(exactly = 1) { participantRepository.save(any()) } + + // JWT 토큰 발급 + verify(exactly = 1) { jwtTokenProvider.generateAccessToken(any()) } + verify(exactly = 1) { jwtTokenProvider.generateRefreshToken(any()) } + } + } + } +})