Skip to content

Commit

Permalink
[YS-264] �feat: 회원 정보 수정 기능을 위한 contactEmail 검증 API 및 로직 추가 (#81)
Browse files Browse the repository at this point in the history
* feat: add contactEmail validation logic to the existing member update feature.

* refactor: add contactEmail field to MemberResponse

* refactor: add contactEmail field to MemberResponse

* fix: exclude user's existing email from duplicate validation

* feat: add validateContactEmailForUpdate

* test: add ValidateContactEmailForUpdateUseCaseTest

* refactor: refactor URIs for consistency

* refactor: change parameter name

* refactor: rename validate to validation in URI
  • Loading branch information
Ji-soo708 authored Feb 6, 2025
1 parent 2a8c19c commit fd11473
Show file tree
Hide file tree
Showing 22 changed files with 375 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ class MemberService(
private val memberGateway: MemberGateway,
private val createParticipantUseCase: CreateParticipantUseCase,
private val createResearcherUseCase: CreateResearcherUseCase,
private val verifyContactEmailUseCase: VerifyContactEmailUseCase,
private val verifyContactEmailUseCase: ValidateContactEmailForSignUpUseCase,
private val verifyResearcherEmailUseCase: VerifyResearcherEmailUseCase,
private val getResearcherInfoUseCase: GetResearcherInfoUseCase,
private val getParticipantInfoUseCase: GetParticipantInfoUseCase,
private val updateResearcherInfoUseCase: UpdateResearcherInfoUseCase,
private val updateParticipantInfoUseCase: UpdateParticipantInfoUseCase
private val updateParticipantInfoUseCase: UpdateParticipantInfoUseCase,
private val validateContactEmailForUpdateUseCase: ValidateContactEmailForUpdateUseCase
) {
@Transactional
fun participantSignup(input: CreateParticipantUseCase.Input): CreateParticipantUseCase.Output {
Expand All @@ -34,7 +35,7 @@ class MemberService(
}

@Transactional
fun validateDuplicatedContactEmail(input: VerifyContactEmailUseCase.Input) : VerifyContactEmailUseCase.Output{
fun validateContactEmailForSignUp(input: ValidateContactEmailForSignUpUseCase.Input) : ValidateContactEmailForSignUpUseCase.Output{
return verifyContactEmailUseCase.execute(input)
}

Expand All @@ -57,4 +58,8 @@ class MemberService(
fun updateParticipantInfo(input: UpdateParticipantInfoUseCase.Input): UpdateParticipantInfoUseCase.Output {
return updateParticipantInfoUseCase.execute(input)
}

fun validateContactEmailForUpdate(input: ValidateContactEmailForUpdateUseCase.Input): ValidateContactEmailForUpdateUseCase.Output {
return validateContactEmailForUpdateUseCase.execute(input)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class FetchGoogleUserInfoUseCase(
val name: String?,
val oauthEmail: String,
val role: RoleType?,
val provider: ProviderType
val provider: ProviderType,
val contactEmail: String?
)

override fun execute(input: Input): Output {
Expand All @@ -52,7 +53,8 @@ class FetchGoogleUserInfoUseCase(
name = member.name,
oauthEmail = member.oauthEmail,
role = member.role,
provider = ProviderType.GOOGLE
provider = ProviderType.GOOGLE,
contactEmail = member.contactEmail
)
} else {
// 등록된 멤버가 없으면 isRegistered = false, memberId = null
Expand All @@ -64,7 +66,8 @@ class FetchGoogleUserInfoUseCase(
name = null,
oauthEmail = email,
role = null,
provider = ProviderType.GOOGLE
provider = ProviderType.GOOGLE,
contactEmail = null
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class FetchNaverUserInfoUseCase(
val name: String?,
val oauthEmail: String,
val role: RoleType?,
val provider: ProviderType
val provider: ProviderType,
val contactEmail: String?
)

override fun execute(input: Input): Output {
Expand All @@ -53,7 +54,8 @@ class FetchNaverUserInfoUseCase(
name = member.name,
oauthEmail = member.oauthEmail,
role = member.role,
provider = ProviderType.NAVER
provider = ProviderType.NAVER,
contactEmail = member.contactEmail
)
} else {
// 등록된 멤버가 없으면 isRegistered = false, memberId = null
Expand All @@ -65,7 +67,8 @@ class FetchNaverUserInfoUseCase(
name = null,
oauthEmail = email,
role = null,
provider = ProviderType.NAVER
provider = ProviderType.NAVER,
contactEmail = null
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class CreateParticipantUseCase (
val name: String?,
val oauthEmail: String?,
val provider: ProviderType?,
val contactEmail: String?,
val role: RoleType?,
)

Expand All @@ -64,6 +65,7 @@ class CreateParticipantUseCase (
name = newMember.name,
oauthEmail = newMember.oauthEmail,
provider = newMember.provider,
contactEmail = newMember.contactEmail,
role = newMember.role
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class CreateResearcherUseCase(
val name: String?,
val oauthEmail: String?,
val provider: ProviderType?,
val contactEmail: String?,
val role: RoleType?,
)

Expand All @@ -56,6 +57,7 @@ class CreateResearcherUseCase(
name = savedMember.name,
oauthEmail = savedMember.oauthEmail,
provider = savedMember.provider,
contactEmail = savedMember.contactEmail,
role = savedMember.role
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.dobby.backend.application.usecase.member

import com.dobby.backend.application.usecase.UseCase
import com.dobby.backend.domain.exception.ContactEmailDuplicateException
import com.dobby.backend.domain.exception.ParticipantNotFoundException
import com.dobby.backend.domain.gateway.member.MemberGateway
import com.dobby.backend.domain.gateway.member.ParticipantGateway
import com.dobby.backend.domain.model.member.Member
import com.dobby.backend.domain.model.member.Participant
Expand All @@ -10,7 +12,8 @@ import com.dobby.backend.infrastructure.database.entity.enums.MatchType
import java.time.LocalDate

class UpdateParticipantInfoUseCase(
private val participantGateway: ParticipantGateway
private val participantGateway: ParticipantGateway,
private val memberGateway: MemberGateway
) : UseCase<UpdateParticipantInfoUseCase.Input, UpdateParticipantInfoUseCase.Output> {

data class Input(
Expand All @@ -34,6 +37,9 @@ class UpdateParticipantInfoUseCase(
override fun execute(input: Input): Output {
val participant = participantGateway.findByMemberId(input.memberId)
?: throw ParticipantNotFoundException
if (memberGateway.existsByContactEmail(input.contactEmail) && participant.member.contactEmail != input.contactEmail) {
throw ContactEmailDuplicateException
}

val updatedParticipant = participantGateway.save(
participant.updateInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.dobby.backend.application.usecase.member

import com.dobby.backend.application.usecase.UseCase
import com.dobby.backend.domain.exception.ContactEmailDuplicateException
import com.dobby.backend.domain.exception.ResearcherNotFoundException
import com.dobby.backend.domain.gateway.member.MemberGateway
import com.dobby.backend.domain.gateway.member.ResearcherGateway
import com.dobby.backend.domain.model.member.Member

class UpdateResearcherInfoUseCase(
private val researcherGateway: ResearcherGateway
private val researcherGateway: ResearcherGateway,
private val memberGateway: MemberGateway
) : UseCase<UpdateResearcherInfoUseCase.Input, UpdateResearcherInfoUseCase.Output> {
data class Input(
val memberId: String,
Expand All @@ -28,6 +31,9 @@ class UpdateResearcherInfoUseCase(
override fun execute(input: Input): Output {
val researcher = researcherGateway.findByMemberId(input.memberId)
?: throw ResearcherNotFoundException
if (memberGateway.existsByContactEmail(input.contactEmail)&& researcher.member.contactEmail != input.contactEmail) {
throw ContactEmailDuplicateException
}

val updatedResearcher = researcherGateway.save(
researcher.updateInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import com.dobby.backend.application.usecase.UseCase
import com.dobby.backend.domain.exception.ContactEmailDuplicateException
import com.dobby.backend.domain.gateway.member.MemberGateway

class VerifyContactEmailUseCase(
class ValidateContactEmailForSignUpUseCase(
private val memberGateway: MemberGateway
): UseCase<VerifyContactEmailUseCase.Input, VerifyContactEmailUseCase.Output> {
): UseCase<ValidateContactEmailForSignUpUseCase.Input, ValidateContactEmailForSignUpUseCase.Output> {
data class Input(
val contactEmail: String
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.dobby.backend.application.usecase.member

import com.dobby.backend.application.usecase.UseCase
import com.dobby.backend.domain.gateway.member.MemberGateway

class ValidateContactEmailForUpdateUseCase(
private val memberGateway: MemberGateway
) : UseCase<ValidateContactEmailForUpdateUseCase.Input, ValidateContactEmailForUpdateUseCase.Output> {
data class Input(
val memberId: String,
val contactEmail: String
)

data class Output(
val isDuplicate: Boolean
)

override fun execute(input: Input): Output {
val currentContactEmail = memberGateway.findContactEmailByMemberId(input.memberId)
if (currentContactEmail == input.contactEmail) {
return Output(false)
}

val isDuplicate = memberGateway.existsByContactEmail(input.contactEmail)
return Output(isDuplicate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ data object ResearcherNotFoundException : ClientException("ME0003", "Researcher
data object ParticipantNotFoundException : ClientException("ME0004", "Participant Not Found.", HttpStatus.NOT_FOUND)
data object EmailNotValidateException : ClientException("ME0005", "You should validate your school email first", HttpStatus.BAD_REQUEST)
data object SignupOauthEmailDuplicateException : ClientException("ME0006", "You've already joined with requested oauth email", HttpStatus.CONFLICT)
data object ContactEmailDuplicateException: ClientException("ME0007", "This contact email is already in use", HttpStatus.CONFLICT)
data object ContactEmailDuplicateException: ClientException("ME0007", "This contact email is already in use.", HttpStatus.CONFLICT)

/**
* Experiment error codes
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import com.dobby.backend.infrastructure.database.entity.enums.MemberStatus
interface MemberGateway {
fun getById(memberId: String): Member
fun findById(memberId: String): Member?
fun existsByContactEmail(contactEmail: String): Boolean
fun findByOauthEmailAndStatus(email: String, status: MemberStatus): Member?
fun findByOauthEmail(email: String): Member?
fun save(savedMember: Member) : Member
fun existsByContactEmail(contactEmail: String) : Boolean
fun findContactEmailByMemberId(memberId: String): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository

interface MemberRepository : JpaRepository<MemberEntity, String> {
fun findByOauthEmail(oauthEmail: String): MemberEntity?
fun existsByContactEmail(contactEmail: String): Boolean
fun findByOauthEmailAndStatus(oauthEmail: String, status: MemberStatus): MemberEntity?
fun existsByContactEmail(contactEmail: String): Boolean
fun findContactEmailById(memberId: String): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ class MemberGatewayImpl(
.orElse(null)
}

override fun existsByContactEmail(contactEmail: String): Boolean {
return memberRepository.existsByContactEmail(contactEmail)
}

override fun findByOauthEmailAndStatus(email: String, status: MemberStatus): Member? {
return memberRepository
.findByOauthEmailAndStatus(email, status)
Expand All @@ -45,4 +41,12 @@ class MemberGatewayImpl(
.save(MemberEntity.fromDomain(savedMember))
return savedEntity.toDomain()
}

override fun existsByContactEmail(contactEmail: String): Boolean {
return memberRepository.existsByContactEmail(contactEmail)
}

override fun findContactEmailByMemberId(memberId: String): String {
return memberRepository.findContactEmailById(memberId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package com.dobby.backend.presentation.api.controller
import com.dobby.backend.application.service.MemberService
import com.dobby.backend.presentation.api.dto.request.member.*
import com.dobby.backend.presentation.api.dto.response.member.DefaultResponse
import com.dobby.backend.presentation.api.dto.request.member.ParticipantSignupRequest
import com.dobby.backend.presentation.api.dto.request.member.ResearcherSignupRequest
import com.dobby.backend.presentation.api.dto.request.member.UpdateParticipantInfoRequest
import com.dobby.backend.presentation.api.dto.request.member.UpdateResearcherInfoRequest
import com.dobby.backend.presentation.api.dto.response.member.ParticipantInfoResponse
import com.dobby.backend.presentation.api.dto.response.member.ResearcherInfoResponse
import com.dobby.backend.presentation.api.dto.response.member.SignupResponse
Expand Down Expand Up @@ -48,21 +52,21 @@ class MemberController(
return MemberMapper.toResearcherSignupResponse(output)
}

@GetMapping("/signup/validate/contact-email")
@GetMapping("/signup/validation/contact-email")
@Operation(
summary = "연락 받을 이메일 주소 검증 API - 회원가입 시 필수 API",
description = "연락 받을 이메일이 사용 가능한지 검증해주는 API입니다. 사용가능하면 true, 아니면 예외를 던집니다."
)
fun emailAvailableCheck(
fun validateContactEmailForSignUp(
@RequestParam contactEmail: String
) : DefaultResponse {
val input = MemberMapper.toContactEmailVerificationInput(contactEmail)
val output = memberService.validateDuplicatedContactEmail(input)
return MemberMapper.toContactEmailVerificationResponse(output)
val input = MemberMapper.toValidateContactEmailForSignUpInput(contactEmail)
val output = memberService.validateContactEmailForSignUp(input)
return MemberMapper.toValidateContactEmailForSignUpResponse(output)
}

@PreAuthorize("hasRole('RESEARCHER')")
@GetMapping("/researchers/me")
@GetMapping("/me/researchers")
@Operation(
summary = "연구자 회원 정보 렌더링",
description = "연구자의 회원 정보를 반환합니다."
Expand All @@ -74,7 +78,7 @@ class MemberController(
}

@PreAuthorize("hasRole('RESEARCHER')")
@PutMapping("/researchers/me")
@PutMapping("/me/researchers")
@Operation(
summary = "연구자 회원 정보 수정",
description = "연구자의 회원 정보를 수정합니다."
Expand All @@ -88,7 +92,7 @@ class MemberController(
}

@PreAuthorize("hasRole('PARTICIPANT')")
@GetMapping("/participants/me")
@GetMapping("/me/participants")
@Operation(
summary = "참여자 회원 정보 렌더링",
description = "참여자의 회원 정보를 반환합니다."
Expand All @@ -100,7 +104,7 @@ class MemberController(
}

@PreAuthorize("hasRole('PARTICIPANT')")
@PutMapping("/participants/me")
@PutMapping("/me/participants")
@Operation(
summary = "참여자 회원 정보 수정",
description = "참여자의 회원 정보를 수정합니다."
Expand All @@ -112,4 +116,17 @@ class MemberController(
val output = memberService.updateParticipantInfo(input)
return MemberMapper.toParticipantInfoResponse(output)
}

@GetMapping("/me/validation/contact-email")
@Operation(
summary = "연락 받을 이메일 주소 검증 API",
description = "회원 정보 수정 시, 이메일 중복 확인을 위한 API입니다."
)
fun validateContactEmailForUpdate(
@RequestParam contactEmail: String
): DefaultResponse {
val input = MemberMapper.toValidateContactEmailForUpdateUseCaseInput(contactEmail)
val output = memberService.validateContactEmailForUpdate(input)
return DefaultResponse(output.isDuplicate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ data class OauthLoginResponse(

@Schema(description = "사용자 정보")
val memberInfo: MemberResponse
) {
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ data class MemberResponse(
@Schema(description = "OAuth 제공자", example = "GOOGLE")
val provider: ProviderType?,

@Schema(description = "연락받을 이메일", example = "[email protected]")
val contactEmail: String?,

@Schema(description = "역할", example = "RESEARCHER")
val role: RoleType?,
) {
Expand All @@ -29,6 +32,7 @@ data class MemberResponse(
memberId = id,
oauthEmail = oauthEmail,
provider = provider,
contactEmail = contactEmail,
role = role,
name = name
)
Expand Down
Loading

0 comments on commit fd11473

Please sign in to comment.